Read and follow its variables
Server Objects run on ck-exec.
The variables a definition marks Visible to Players are a Server Object's public face. The SDK keeps a copy of them current on every client that has the Server Object, and tells your code each time the server changes them.
React to changes
WatchValues, a function of UCrowdyServerObject, takes a listener and returns an FDelegateHandle. The beacon watches its Server Object right after acquiring it, and sets its light from the oil and the lit flag:
// VillageBeacon.h
void HandleBeaconChanged(UCrowdyServerObject* Object, TConstArrayView<FString> Fields);
// VillageBeacon.cpp
void AVillageBeacon::BeginPlay()
{
Super::BeginPlay();
FString Error;
Beacon = GetGameInstance()->GetSubsystem<UCrowdyServerObjectSubsystem>()->Acquire(BeaconDefinition, VillageName, this, Error);
if (!Beacon)
{
UE_LOG(LogTemp, Warning, TEXT("The village beacon cannot be used: %s"), *Error);
return;
}
Beacon->WatchValues(FOnCrowdyServerValuesChanged::FDelegate::CreateUObject(this, &AVillageBeacon::HandleBeaconChanged));
}
void AVillageBeacon::HandleBeaconChanged(UCrowdyServerObject* Object, TConstArrayView<FString> Fields)
{
const FVillageBeaconState* State = Object->GetState().GetPtr<FVillageBeaconState>();
if (!State)
{
return;
}
// Fields names what changed by server name; the state holds every watched value.
Light->SetVisibility(State->bLit);
if (Fields.Contains(TEXT("Oil")))
{
Light->SetIntensity(State->Oil * 100.f);
}
}
The listener, an FOnCrowdyServerValuesChanged delegate, receives the Server Object and the names of the fields that changed. It runs:
- once when the values first arrive, naming every variable players can see. This comes just before
OnStatusChangedreports Ready, so by the time a status handler sees Ready the values are already applied; - once when you start watching, if the values are already current, naming every variable players can see. A second beacon that acquires a Server Object the first one already made Ready is set up at once, with no special case in your code. A listener added from inside a Ready status handler is called once, not twice;
- once per change on the server, naming every field that change touched. A Server Function that changes both
OilandbLitproduces one call naming both, never two calls or a moment where one is new and the other old.
UnwatchValues(Handle) removes a listener. A listener bound to a UObject, as above, is skipped once that object is gone, so the beacon does not need to remove its own; a lambda or raw binding does.
Read the values
GetState() returns an FInstancedStruct holding the definition's State struct. Read it with GetPtr and the struct type, as the beacon does, at any time, not only inside the listener.
- The variables Visible to Players hold what the server last sent. Before the first values arrive they hold the struct's defaults.
- The other fields, the Server Only variables, hold the struct's defaults, always. They never leave the server, so
LastFedByreads0on every client. - The copy belongs to the SDK. Changing it changes nothing on the server, and the next change from the server overwrites it. To change a value, call a Server Function.
When the definition's variables are added in the asset rather than taken from a struct, there is no struct to read them into. GetStateList() returns them as an FInstancedPropertyBag to read by name; it is empty when the variables come from a struct:
const FInstancedPropertyBag State = Beacon->GetStateList();
const TValueOrError<int32, EPropertyBagResult> Oil = State.GetValueInt32(TEXT("Oil"));
const TValueOrError<bool, EPropertyBagResult> bLit = State.GetValueBool(TEXT("bLit"));
if (Oil.HasValue() && bLit.HasValue())
{
Light->SetIntensity(bLit.GetValue() ? Oil.GetValue() * 100.f : 0.f);
}
The same rules hold: the variables players do not see keep their default values on every client. The typed Blueprint Get and On Changed nodes read and follow these variables too.
How many players are online
Every app has one root Server Object, which Server Compute deploys along with your types. It counts the players in the app's world: the number the platform's realtime servers report for your app, in which a player who leaves is still counted for a few minutes, until their session ends.
UCrowdyServerObjectLibrary::GetPlayersOnline(Owner, Count) reads it. It returns false, with Count 0, until the count first arrives. The first call starts following the root for Owner, which keeps it followed until it is destroyed, so calling it every frame is fine:
int32 PlayersOnline = 0;
if (UCrowdyServerObjectLibrary::GetPlayersOnline(this, PlayersOnline))
{
PlayersText->SetText(FText::AsNumber(PlayersOnline));
}
To react to each change instead, acquire the root and watch it. GetPlayersOnlineDefinition() gives its definition, CSO_PlayersOnline in the Crowdy SDK's content, and its State struct is FCrowdyPlayersOnlineState. The root has only one instance, so the Instance Id is ignored:
FString Error;
UCrowdyServerObject* Root = UCrowdyServerObjectLibrary::GetServerObject(this, UCrowdyServerObjectLibrary::GetPlayersOnlineDefinition(), FString(), this, Error);
A project with the Game Kit's world starter has world as its root instead. GetPlayersOnline then reads the Online field of the world's FCrowdyKitWorldConditions, from the definition the Game Kit World project setting names; UCrowdyServerObjectLibrary::GetGameKitWorldDefinition() gives that definition, or null when the setting is empty or names no Game Kit starter. FCrowdyKitWorldConditions has a bHasOnline flag beside Online: when the world's snapshot carries no player count, as a world deployed from the platform's own starter pack does not, GetPlayersOnline returns false, with 0, for good, and the log says why once. See The world becomes the app's root.
An app last deployed with an older Crowdy SDK, or never deployed from Server Compute, has no Crowdy SDK root. Deploy once from Server Compute. Until then GetPlayersOnline returns false, and the log says why once for each reason in each game instance (so once per Play In Editor client): "This app has no Crowdy SDK root, so Players Online has no count: deploy the app from Server Compute (an app last deployed with an older Crowdy SDK, or by another tool, has none). It is asked again every minute." The root waits and asks again once a minute instead of every few seconds, and once the app is deployed the count arrives without restarting the game.
Records
A type whose Holds is A Record per Entity keeps the variables once for each entity; see Keep a record per entity. WatchValues and GetState() on its Server Object give the type's defaults, not a record. Use the records instead:
HoldsRecords()says whether the definition keeps records,NumRecords()counts them andGetRecordIds()lists their ids, in no particular order.GetRecord(Id)returns anFCrowdyServerRecord, or null when there is no such record. ItsValuesis anFInstancedStructholding the State struct, itsOwneris the owning player's user id (0 for none) and itsTeamis the team. A variable the player may not receive holds its default. Ids are compared with capitals significant.OnRecordsChanged, anFOnCrowdyServerRecordsChanged, runs after every read, batch or fetch that changed records. It gives the Server Object, the ids that are new or changed, the ids that were removed, and for each changed id the State fields that changed, one bit per field.GetFieldVariableNamesnames the fields by index.WatchRecord(Id, Listener)follows one record with anFOnCrowdyServerValuesChangedlistener that names the variables on each change and when the record is removed, andUnwatchRecord(Id, Handle)stops it.
const FCrowdyServerRecord* Record = Area->GetRecord(RaiderId);
const FRaiderState* Raider = Record ? Record->Values.GetPtr<FRaiderState>() : nullptr;
The component on an actor reads the actor's own record: GetVariables() and OnVariablesChanged give it.
Field names
The names the listener receives are the fields' names on the server: the C++ member name (Oil, bLit), the name of a variable added in the asset, a Blueprint struct field's name with the spaces and other characters removed, or the Server Name you set under Field Names on the definition. Compare them exactly as they are spelled there.
Related
- Create a Server Object type: choosing which variables players see
- Hear its messages: the next page, the messages a type sends to everyone listening
- Call its functions
- Keep a record per entity: a Server Object with a record for each entity of a crowd
- Read and follow its variables, from Blueprint: On Changed, Watch Variables and On Variables Changed
- Troubleshooting: when a value is not what you expect
- The Two Planes: why these values are the server's, not the client's