Read and write its fields
This page has a Blueprint twin: Read and write its fields, from Blueprint.
A crowd row keeps the values your game draws it with, such as the brightness of a torch, in fragments, which are small records of data that Mass owns. A field is a name that points at one UPROPERTY of one fragment. You register it once, and then UCrowdyMassTargetLibrary reads and writes it by name for an FCrowdyMassEntityTarget. The same calls are Blueprint nodes. A field is a visual: it holds what this machine draws, and it can differ from machine to machine.
| You need | Include |
|---|---|
CrowdyMassFields::RegisterField, EAccessResult | Target/CrowdyMassFieldAccess.h |
UCrowdyMassTargetLibrary | Target/CrowdyMassTargetLibrary.h |
UCrowdyMassEntitySubsystem::RegisterStateFragmentField | Subsystems/CrowdyMassEntitySubsystem.h |
When you need this
- To show something on the screen about the player you are looking at, from a value the row already holds.
- To make a row do something visible from code that has a target, such as flaring its torch.
For a value that has to be true, such as health, use Read its server values.
Before you start: the name must be registered
No field is registered for you. Registering is two calls under one name: what an arriving property does, and where it lives. In the Oakford square, ULanternCrowdSubsystem registers both when the world's subsystems initialize. It lands the TorchBrightness property of ALanternPlayer on TargetBrightness of FLanternTorchFragment, and it makes the name TorchBrightness read and write that same member. A read returns the target. A write sets a new target, which ULanternTorchFadeProcessor eases the torch toward until the next replicated value replaces it.
// LanternCrowd.h
#include "CoreMinimal.h"
#include "Subsystems/WorldSubsystem.h"
#include "LanternCrowd.generated.h"
UCLASS()
class ULanternCrowdSubsystem : public UWorldSubsystem
{
GENERATED_BODY()
public:
virtual bool ShouldCreateSubsystem(UObject* Outer) const override;
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void OnWorldBeginPlay(UWorld& InWorld) override;
};
// LanternPlayer.h
UPROPERTY(BlueprintReadOnly, Category = "Lantern", meta = (CrowdyState, CrowdyHeartbeat))
float TorchBrightness = 1.f;
// LanternCrowd.cpp
#include "LanternCrowd.h"
#include "Subsystems/CrowdyMassEntitySubsystem.h"
#include "Target/CrowdyMassFieldAccess.h"
bool ULanternCrowdSubsystem::ShouldCreateSubsystem(UObject* Outer) const
{
// Only where the Mass subsystem is created: a play world whose map profile selects the Mass backend.
return Super::ShouldCreateSubsystem(Outer) && GetDefault<UCrowdyMassEntitySubsystem>()->ShouldCreateSubsystem(Outer);
}
void ULanternCrowdSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
// The transient world at engine start has no game instance yet.
if (!GetWorld()->GetGameInstance())
{
return;
}
UCrowdyMassEntitySubsystem* Mass = Collection.InitializeDependency<UCrowdyMassEntitySubsystem>();
if (!Mass || Mass->IsStateFragmentFieldRegistered(TEXT("TorchBrightness")))
{
return;
}
Mass->RegisterStateFragmentField(TEXT("TorchBrightness"), ECrowdyMassStateFieldType::Float,
[](const void* PropertyValuePtr, const FCrowdyMassViewStateContext& Context)
{
FLanternTorchFragment* Torch = Context.GetFragment<FLanternTorchFragment>();
if (!Torch || !PropertyValuePtr)
{
return;
}
Torch->TargetBrightness = *static_cast<const float*>(PropertyValuePtr);
});
CrowdyMassFields::RegisterField<FLanternTorchFragment>(TEXT("TorchBrightness"), TEXT("TargetBrightness"));
}
RegisterStateFragmentFieldsays what an arrivingCrowdyStateproperty does to the row. See Add your own crowd data.CrowdyMassFields::RegisterField<FLanternTorchFragment>(TEXT("TorchBrightness"), TEXT("TargetBrightness"))says whichUPROPERTYof which fragment the name reads and writes. The non-template form takes theUScriptStruct*instead. Use the same name as the matchingRegisterStateFragmentField, so a graph asking for the name and the ingestor landing it cannot drift apart.IsStateFragmentFieldRegisteredandCrowdyMassFields::IsFieldRegisteredanswer whether each registration ran. An unregistered name fails every read and write, and the log warnsNothing is registered as '<name>'at most once every five seconds, so check once at startup.
The rules for RegisterField:
- The property must be a
UPROPERTY. A plain C++ member has no reflection entry and is refused. - The fragment must derive from
FMassFragment. FCrowdyRemoteIdentityFragmentis always refused. It carries the entity's id, owner and role.- Registering the same name with the same binding again succeeds and does nothing, which is what makes it safe to call once per world. The same name with a different binding is refused, and the first one is kept.
- A name from
RegisterFieldis claimed across the whole process. A name fromRegisterStateFragmentFieldis claimed per world, across every class. - Registering grants a graph write access to the field, so register only fields that are yours and safe to draw from.
- In editor builds a native recompile re-resolves the bindings by itself.
CrowdyMassFields::RebindRegisteredFields()does it by hand.
The functions
All of them are static members of UCrowdyMassTargetLibrary, game thread only, and return a bool.
| Function | The field's type |
|---|---|
GetFieldByte, SetFieldByte | uint8, and an enum member that is one byte wide |
GetFieldInt, SetFieldInt | int32 exactly |
GetFieldFloat, SetFieldFloat | float or double; the value is a double |
GetFieldBool, SetFieldBool | bool, including a bitfield |
GetFieldVector, SetFieldVector | FVector |
IsFieldRegistered | takes a name only |
IsTargetLive | takes a target only |
Their Blueprint display names are Get Crowdy Mass Field (Float), Set Crowdy Mass Field (Float) and so on, with Is Crowdy Mass Field Registered and Is Crowdy Mass Target Live.
Read a field
A getter takes the target, the field name and an out value: static bool GetFieldFloat(const FCrowdyMassEntityTarget& Target, FName FieldName, double& OutValue). Check the target with IsTargetLive first, and use the out value only when the call returns true.
Write a field
A setter takes the target, the field name and the value: static bool SetFieldFloat(const FCrowdyMassEntityTarget& Target, FName FieldName, double Value). It returns true when the write happened.
When a read answers false
The call returns false for any of these:
- The name is not registered.
- The entity is gone, or its target is stale.
- The entity has no fragment of the registered type. An entity built from a different config can lack a fragment another one has.
- The field is not of the type the call reads.
- Mass is processing right now.
CrowdyMassFields::AccessField(Target, FieldName, bWrite, Visitor) is the lower-level door, and it says which reason it was. It returns an EAccessResult: Ok, NoWorld, InvalidEntity, UnknownField, NoFragment, TypeMismatch, DuringMassProcessing or OffGameThread. CrowdyMassFields::DescribeAccessResult turns one into text. The visitor gets the field's FProperty and a pointer to its value, and the pointer is valid only during the call.
What a write means
The same field on the same entity holds different values on two machines. Nothing mechanical may be decided from one. Anything a peer must not be able to influence belongs in a Server Object; see Keep a record per entity.
If an ingestor or a processor writes the same member, your write lasts only until it next runs. In the Oakford square the ingestor writes TargetBrightness whenever a TorchBrightness arrives from the player, so a write there holds until the next value lands.
Code reached from a delegate broadcast inside a Mass chunk loop would race the processors that read the same fragment. So would an ordinary tick that overlaps a Mass phase, because Mass phases run alongside the game thread. The access is refused and logged: an error for a write, a warning for a read, at most once every five seconds for a field name. Doing the work from a phase-end delegate, or from a broadcast deferred to the end of the frame, puts it outside the schedule and takes the refusal away.
Set a pose by name
The pose of a row has its own call, because the pose fragment is not a plain field. UCrowdyMassTargetLibrary::SetEntityPose(Target, PoseName) puts the row in the pose of that name from its class's anim set.
NAME_Noneclears the pose.- A name the anim set does not author also clears the pose, and says so in the log.
- It returns false, and does nothing, if the entity is gone, has no pose fragment, Mass is processing, or you named a pose for an entity whose class this client cannot resolve yet. Add the class to Preloaded Entity Classes under Project Settings, Plugins, Crowdy SDK, Replication; see Entities and Spawning.
- The pose is a visual. An arriving pose replaces one you set.
See Play poses and one-shot actions.
Gotchas
- A name is claimed once. Two different fragment fields cannot share a name.
- Reads are values, not references. A function hands back a copy, never a pointer into the row, because Mass moves an entity's data when its archetype changes.
Related
- Target an entity: get the target these functions take
- Add your own crowd data: the fragment, trait and processor behind a field
- Read its server values: the values that decide what is true
- Read and write its fields, from Blueprint: the same calls as nodes