Our very WIP understanding of Unreal Engine 5's experimental Entity Component System (ECS) plugin with a small sample project. We are not affiliated with Epic Games and this system is actively being changed often so this information might not be totally accurate.
We are totally open to contributions, If something is wrong or you think it could be improved, feel free to open an issue or submit a pull request.
Currently built for the Unreal Engine 5 latest version binary from the Epic Games launcher. This documentation will be updated often!
- Unreal Engine 5 latest version from the Epic Games launcher
- Git Large File Storage
- Mass
- Entity Component System
- Sample Project
- Mass Concepts
4.1 Entities
4.2 Fragments
      4.2.1 Shared Fragments
4.3 Tags
4.4 The archetype model
      4.4.1 Tags in the archetype model
      4.4.2 Fragments in the archetype model
4.5 Processors
4.6 Queries
      4.6.1 Access requirements
      4.6.2 Presence requirements
      4.6.3 Iterating Queries
      4.6.3 Mutating entities with Defer()
4.7 Traits
4.8 Observers
      4.8.1 Observing multiple Fragment/Tags 4.10 Mulitthreading- Mass Plugins and Modules
5.1 MassEntity
5.2 MassGameplay
5.3 MassAI
Mass is Unreal's new in-house ECS framework! Technically, Sequencer already used one internally but it wasn't intended for gameplay code. Mass was created by the AI team at Epic Games to facilitate massive crowd simulations, but has grown to include many other features as well. It was featured in the new Matrix demo Epic released recently.
Mass is an archetype-based Entity Componenet System. If you already know what that is you can skip ahead to the next section.
In Mass, some ECS terminology differs from the norm in order to not get confused with existing unreal code:
ECS | Mass |
---|---|
Entity | Entity |
Component | Fragment |
System | Processor |
Typical Unreal Engine game code is expressed as actor objects that inherit from parent classes to change their data and functionality based on what they are. In an ECS, an entity is only composed of fragments that get manipulated by processors based on which ECS components they have.
An entity is really just a small unique identifier that points to some fragments. A Processor defines a query that filters only for entities that have specific fragments. For example, a basic "movement" Processor could query for entities that have a transform and velocity component to add the velocity to their current transform position.
Fragments are stored in memory as tightly packed arrays of other identical fragment arrangements called archetypes. Because of this, the aforementioned movement processor can be incredibly high performance because it does a simple operation on a small amount of data all at once. New functionality can easily be added by creating new fragments and processors.
Internally, Mass is similar to the existing Unity DOTS and FLECS archetype-based ECS libraries. There are many more!
Currently, the sample features the following:
- A bare minimum movement processor to show how to set up processors.
- An example of how to use Mass spawners for zonegraph and EQS.
- Mass-simulated crowd of cones that parades around the level following a ZoneGraph shape with lanes.
- Linetraced projectile simulation example.
- Simple 3d hashgrid for entities.
- Very basic Mass blueprint integration.
- Grouped niagara rendering for entities.
4.1 Entities
4.2 Fragments
4.3 Tags
4.4 The archetype model
4.5 Processors
4.6 Queries
4.7 Traits
4.8 Observers
Small unique identifiers that point to a combination of fragments and tags in memory. Entities are mainly a simple integer ID. For example, entity 103 might point to a single projectile with transform, velocity, and damage data.
Data-only UScriptStructs
that entities can own and processors can query on. To create a fragment, inherit from FMassFragment
.
USTRUCT()
struct MASSSAMPLE_API FLifeTimeFragment : public FMassFragment
{
GENERATED_BODY()
float Time;
};
With FMassFragment
s each entity gets its own fragment data, however if we wish to share data across all our entities, we have to use a shared fragment.
A Shared Fragment is a type of Fragment that multiple entities can point to. This is often used for configuration common to a group of entities, like LOD or replication settings. To create a shared fragment, inherit from FMassSharedFragment
.
USTRUCT()
struct MASSSAMPLE_API FClockSharedFragment : public FMassSharedFragment
{
GENERATED_BODY()
float Clock;
};
In the example above, all the entities containing the FClockSharedFragment
will see the same Clock
value. If an entity modifies the Clock
value, the rest of the entities with this fragment will see the change, as this fragment is shared accross them.
Thanks to this sharing data requirement, the Mass Entity subsystem only needs to store one Shared Fragment for the entities that use it.
Empty UScriptStructs
that processors can use to filter entities to process based on their presence/absence. To create a tag, inherit from FMassTag
.
USTRUCT()
struct MASSSAMPLE_API FProjectileTag : public FMassTag
{
GENERATED_BODY()
};
Note: Tags should never contain any member properties.
As mentioned previously, an entity is a unique combination of fragments and tags. Mass calls each of these combinations archetypes. For example, given three different combinations used by our entities, we would generate three archetypes:
The FMassArchetypeData
struct represents an archetype in Mass internally.
Each archetype (FMassArchetypeData
) holds a bitset (TScriptStructTypeBitSet<FMassTag>
) that constains the tag presence information, whereas each bit in the bitset represents whether a tag exists in the archetype or not.
Following the previous example, Archetype 0 and Archetype 2 contain the tags: TagA, TagC and TagD; while Archetype 1 contains TagC and TagD. Which makes the combination of Fragment A and Fragment B to be split in two different archetypes.
At the same time, each archetype holds an array of chunks (FMassArchetypeChunk
) with fragment data.
Each chunk contains a subset of the entities included in our archetype where data is organized in a pseudo-struct-of-arrays way:
The following Figure represents the archetypes from the example above in memory:
By having this pseudo-struct-of-arrays data layout divided in multiple chunks, we are allowing a great number of whole-entities to fit in the CPU cache.
This is thanks to the chunk partitoning, since without it, we wouldn't have as many whole-entities fit in cache, as the following diagram displays:
In the above example, the Chunked Archetype gets whole-entities in cache, while the Linear Archetype gets all the A Fragments in cache, but cannot fit each fragment of an entity.
The Linear approach would be fast if we would only access the A Fragment when iterating entities, however, this is almost never the case. Usually, when we iterate entities we tend to access multiple fragments, so it is convenient to have them all in cache, which is what the chunk partitioning provides.
The chunk size (UE::MassEntity::ChunkSize
) has been conveniently set based on next-gen cache sizes (128 bytes per line and 1024 cache lines). This means that archetypes with more bits of fragment data will contain less entities per chunk.
Note: It is relevant to note that a cache miss would be produced every time we want to access a fragment that isn't on cache for a given entity.
Processors combine multiple user-defined queries with functions that compute entities.
Processors are automatically registered with Mass and added to the EMassProcessingPhase::PrePhsysics
processing phase by default. The MassProcessingPhaseManager
owns separate FMassProcessingPhase
instances for every ETickingGroup
, mapped to EMassProcessingPhase
. Users can configure to which FMassProcessingPhase
their processor belongs by modifying the ProcessingPhase
variable included in UMassProcessor
.
In their constructor they can define rules for their execution order and which type of game clients they execute on:
UMyProcessor::UMyProcessor()
{
//This processor is registered with mass by just existing! This is the default behaviour of all processors.
bAutoRegisterWithProcessingPhases = true;
//Using the built-in movement processor group
ExecutionOrder.ExecuteInGroup = UE::Mass::ProcessorGroupNames::Movement;
//You can also define other processors that require to run before or after this one
ExecutionOrder.ExecuteAfter.Add(TEXT("MSMovementProcessor"));
//This executes only on Clients and Standalone
ExecutionFlags = (int32)(EProcessorExecutionFlags::Client | EProcessorExecutionFlags::Standalone);
}
On initialization, Mass creates a graph of processors using their execution rules so they execute in order (ie: In the example above we make sure to move our entities before we call Execute in UMyProcessor
).
Note: Mass ships with a series of processors that are designed to be inherited and extended with custom logic. ie: The visualization and LOD modules are both designed to be used this way.
Queries (FMassEntityQuery
) filter and iterate entities given a series of rules based on fragment and tag presence.
Processors can define multiple FMassEntityQuery
s and should override the ConfigureQueries
function in order to add rules to the different queries defined in the processor's header:
void UMyProcessor::ConfigureQueries()
{
MyQuery.AddTagRequirement<FMoverTag>(EMassFragmentPresence::All);
MyQuery.AddRequirement<FHitLocationFragment>(EMassFragmentAccess::ReadOnly, EMassFragmentPresence::Optional);
}
Queries are executed by calling ForEachEntityChunk
member function with a lambda, passing the related UMassEntitySubsystem
and FMassExecutionContext
. The following example code lies inside the Execute
function of a processor:
void UMyProcessor::Execute(UMassEntitySubsystem& EntitySubsystem, FMassExecutionContext& Context)
{
//Note that this is a lambda! If you want extra data you may need to pass it in the [] operator
MyQuery.ForEachEntityChunk(EntitySubsystem, Context, [](FMassExecutionContext& Context)
{
//Loop over every entity in the current chunk and do stuff!
for (int32 EntityIndex = 0; EntityIndex < Context.GetNumEntities(); ++EntityIndex)
{
// ...
}
});
}
Note: Queries can also be created and iterated outside processors.
Queries can define read/write access requirements for Fragments:
EMassFragmentAccess |
Description |
---|---|
None |
No binding required. |
ReadOnly |
We want to read the data for the fragment. |
ReadWrite |
We want to read and write the data for the fragment. |
FMassFragment
s use AddRequirement
to add access and presence requirement to our fragments. While FMassSharedFragment
s employ AddSharedRequirement
.
Here are some basic examples in which we add access rules in two Fragments from a FMassEntityQuery MyQuery
:
void UMyProcessor::ConfigureQueries()
{
// Entities must have an FTransformFragment and we are reading and writing it (EMassFragmentAccess::ReadWrite)
MyQuery.AddRequirement<FTransformFragment>(EMassFragmentAccess::ReadWrite);
// Entities must have an FMassForceFragment and we are only reading it (EMassFragmentAccess::ReadOnly)
MyQuery.AddRequirement<FMassForceFragment>(EMassFragmentAccess::ReadOnly);
// Entities must have a common FClockSharedFragment that can be read and written
MyQuery.AddSharedRequirement<FClockSharedFragment>(EMassFragmentAccess::ReadWrite);
}
ForEachEntityChunk
s can use the following two functions to access ReadOnly
or ReadWrite
fragment data according to the access requirement:
EMassFragmentAccess |
Function | Description |
---|---|---|
ReadOnly |
GetFragmentView |
Returns a read only TConstArrayView containing the data of our ReadOnly fragment. |
ReadWrite |
GetMutableFragmentView |
Returns a writable TArrayView containing de data of our ReadWrite fragment. |
Find below the following two functions employed in context:
MyQuery.ForEachEntityChunk(EntitySubsystem, Context, [](FMassExecutionContext& Context)
{
const auto TransformList = Context.GetMutableFragmentView<FTransformFragment>();
const auto ForceList = Context.GetMutableFragmentView<FMassForceFragment>();
for (int32 EntityIndex = 0; EntityIndex < Context.GetNumEntities(); ++EntityIndex)
{
FTransform& TransformToChange = TransformList[EntityIndex].GetMutableTransform();
const FVector DeltaForce = Context.GetDeltaTimeSeconds() * ForceList[EntityIndex].Value;
TransformToChange.AddToTranslation(DeltaForce);
}
});
Note: Tags do not have access requirements since they don't contain data.
Queries can define presence requirements for Fragments and Tags:
EMassFragmentPresence |
Description |
---|---|
All | All of the required fragments/tags must be present. Default presence requirement. |
Any | At least one of the fragments/tags marked any must be present. |
None | None of the required fragments/tags can be present. |
Optional | If fragment/tag is present we'll use it, but it does not need to be present. |
To add presence rules to Tags, use AddTagRequirement
.
void UMyProcessor::ConfigureQueries()
{
// Entities are considered for iteration without the need of containing the specified Tag
MyQuery.AddTagRequirement<FOptionalTag>(EMassFragmentPresence::Optional);
// Entities must at least have the FHorseTag or the FSheepTag
MyQuery.AddTagRequirement<FHorseTag>(EMassFragmentPresence::Any);
MyQuery.AddTagRequirement<FSheepTag>(EMassFragmentPresence::Any);
}
ForEachChunk
s can use DoesArchetypeHaveTag
to determine if the current archetype contains the the Tag:
MyQuery.ForEachEntityChunk(EntitySubsystem, Context, [](FMassExecutionContext& Context)
{
if(Context.DoesArchetypeHaveTag<FOptionalTag>())
{
// I do have the FOptionalTag tag!!
}
// Same with Tags marked with Any
if(Context.DoesArchetypeHaveTag<FHorseTag>())
{
// I do have the FHorseTag tag!!
}
if(Context.DoesArchetypeHaveTag<FSheepTag>())
{
// I do have the FSheepTag tag!!
}
});
Fragments and shared fragments can define presence rules in an additional EMassFragmentPresence
parameter through AddRequirement
and AddSharedRequirement
, respectively.
void UMyProcessor::ConfigureQueries()
{
// Entities are considered for iteration without the need of containing the specified Fragment
MyQuery.AddRequirement<FMyOptionalFragment>(EMassFragmentAccess::ReadWrite, EMassFragmentPresence::Optional);
// Entities must at least have the FHorseFragment or the FSheepFragment
MyQuery.AddRequirement<FHorseFragment>(EMassFragmentAccess::ReadWrite, EMassFragmentPresence::Any);
MyQuery.AddRequirement<FSheepFragment>(EMassFragmentAccess::ReadWrite, EMassFragmentPresence::Any);
}
ForEachChunk
s can use the length of the Optional
/Any
fragment's TArrayView
to determine if the current chunk contains the Fragment before accessing it:
MyQuery.ForEachEntityChunk(EntitySubsystem, Context, [](FMassExecutionContext& Context)
{
const auto OptionalFragmentList = Context.GetMutableFragmentView<FMyOptionalFragment>();
const auto HorseFragmentList = Context.GetMutableFragmentView<FHorseFragment>();
const auto SheepFragmentList = Context.GetMutableFragmentView<FSheepFragment>();
for (int32 i = 0; i < Context.GetNumEntities(); ++i)
{
// An optional fragment array is present in our current chunk if the OptionalFragmentList isn't empty
if(OptionalFragmentList.Num() > 0)
{
// Now that we know it is safe to do so, we can compute
OptionalFragmentList[i].DoOptionalStuff();
}
// Same with fragments marked with Any
if(HorseFragmentList.Num() > 0)
{
HorseFragmentList[i].DoHorseStuff();
}
if(SheepFragmentList.Num() > 0)
{
SheepFragmentList[i].DoSheepStuff();
}
}
});
Within the ForEachEntityChunk
we have access to the current execution context. FMassExecutionContext
enables us to get entity data and mutate their composition. The following code adds the tag FIsRedTag
to any entity that has a color fragment with its Color
property set to Red
:
EntityQuery.ForEachEntityChunk(EntitySubsystem, Context, [&,this](FMassExecutionContext& Context)
{
auto ColorList = Context.GetFragmentView<FSampleColorFragment>();
for (int32 EntityIndex = 0; EntityIndex < Context.GetNumEntities(); ++EntityIndex)
{
if(ColorList[EntityIndex].Color == FColor::Red)
{
//Using the context, defer adding a tag to this entity after done processing!
Context.Defer().AddTag<FIsRedTag>(Context.GetEntity(EntityIndex));
}
}
});
The following Listings define the native mutations that you can defer:
Fragments:
Context.Defer().AddFragment<FMyTag>(Entity);
Context.Defer().RemoveFragment<FMyTag>(Entity);
Tags:
Context.Defer().AddTag<FMyTag>(Entity);
Context.Defer().RemoveTag<FMyTag>(Entity);
Destroying entities:
Context.Defer().DestroyEntity(MyEntity);
Context.Defer().BatchDestroyEntities(MyEntitiesArray);
There is a built in set of FCommandBufferEntryBase
derived commands that you can use to defer some more useful entity mutations. Here is a list with some short examples using different styles.
Adds a list of instanced struct fragments with data you can make in FConstStructView
s or FStructView
s. Here's an example with a new FHitResultFragment
with HitResult data and an FSampleColorFragment
fragment with a new color.
FConstStructView HitResulStruct = FConstStructView::Make(FHitResultFragment(HitResult));
FStructView ColorStruct = FStructView::Make(FSampleColorFragment(Color));
Context.Defer().PushCommand(FMassCommandAddFragmentInstanceList(Entity,
{HitResulStruct,ColorStruct}
));
Identical to FMassCommandAddFragmentInstanceList
besides only taking a single fragment as input instead of a list.
Similar to AddFragmentInstances
, this uses a list of FConstStructView
s to create a whole new entity.
Here we make the FStructView
s inline.
FSampleColorFragment ColorFragment;
ColorFragment.Color = FColor::Green;
FTransformFragment TransformFragment;
TransformFragment.SetTransform(SpawnTransform);
Context.Defer().PushCommand(FBuildEntityFromFragmentInstances(Entity,
{FStructView::Make(ColorFragment),FStructView::Make(ThingyFragment)}
));
Identical to FBuildEntityFromFragmentInstances besides only taking a single fragment as input instead of a list.
Removes the first tag (FOffTag
in this example) and adds the second to the entity. (FOnTag
)
Context.Defer().PushCommand(FCommandSwapTags(Entity,
FOffTag::StaticStruct(),
FOnTag::StaticStruct()
));
The FMassArchetypeCompositionDescriptor
is a struct that defines a set of fragments and tags that make up an archetype. For example, we can get one from a given archetype handle or template. In this example we get one from a UMassEntityConfigAsset
pointer.
const FMassEntityTemplate* EntityTemplate =
EntityConfig->GetConfig().GetOrCreateEntityTemplate(*Owner, *EntityConfig);
const FMassArchetypeCompositionDescriptor& Composition = EntityTemplate->GetCompositionDescriptor();
Context.Defer().PushCommand(FCommandRemoveComposition(Entity, Composition));
Note that the commands that mutate entities change the value of ECommandBufferOperationType in their decleration in order to pass their changes to relevant observers when commands are flushed. They also manually add their changes to the observed changes list by implementing AppendAffectedEntitiesPerType
.
It is possible to create custom mutations by implementing your own commands derived from FCommandBufferEntryBase
.
Context.Defer().EmplaceCommand<FMyCustomComand>(...)
Traits are C++ defined objects that declare a set of Fragments, Tags and data for authoring new entities in a data-driven way.
To start using traits, create a DataAsset
that inherits from
MassEntityConfigAsset
and add new traits to it. Each trait can be expanded to set properties if it has any.
Between the many built-in traits offered by Mass, we can find the Assorted Fragments
trait, which holds an array of FInstancedStruct
that enables adding fragments to this trait from the editor without the need of creating a new C++ Trait.
You can also define a parent MassEntityConfigAsset to inherit the fragments from another DataAsset
.
Traits are often used to add Shared Fragments in the form of settings. For example, our visualization traits save memory by sharing which mesh they are displaying, parameters etc. Configs with the same settings will share the same Shared Fragment.
Traits are created by inheriting UMassEntityTraitBase
and overriding BuildTemplate
. Here is a very basic example:
UCLASS(meta = (DisplayName = "Debug Printing"))
class MASSSAMPLE_API UMSDebugTagTrait : public UMassEntityTraitBase
{
GENERATED_BODY()
public:
virtual void BuildTemplate(FMassEntityTemplateBuildContext& BuildContext, UWorld& World) const override
{
// Adding a tag
BuildContext.AddTag<FMassSampleDebuggableTag>();
// Adding a fragment
BuildContext.AddFragment<FTransformFragment>();
// _GetRef lets us mutate the fragment
BuildContext.AddFragment_GetRef<FSampleColorFragment>().Color = UserSetColor;
};
// Editable in the editor
UPROPERTY(EditAnywhere)
FColor UserSetColor;
};
Note: We recommend looking at the many existing traits in this sample and the mass modules for a better overview. For the most part, they are fairly simple UObjects that occasionally call subsystems for bookkeeping.
There is also a ValidateTemplate
overridable function which appears to just let you create your own validation for the trait that just raises errors.
[TODO]
Here is a partial BuildTemplate
example for a shared struct, which requires some extra work on your part to see if a shared fragment identical to the new one already exists:
//Create the actual fragment struct and set up the data for it however you like
FMySharedSettings MyFragment;
MyFragment.MyValue = UserSetValue;
//Get a hash of a FConstStructView of said fragment and store it
uint32 MySharedFragmentHash = UE::StructUtils::GetStructCrc32(FConstStructView::Make(MyFragment));
//Search the Mass Entity subsystem for an identical struct with the hash. If there are none, make a new one with the set fragment.
FSharedStruct MySharedFragment =
EntitySubsystem->GetOrCreateSharedFragment<FMySharedSettings>(MySharedFragmentHash, MyFragment);
//Finally, add the shared fragment to the BuildContext!
BuildContext.AddSharedFragment(MySharedFragment);
The UMassObserverProcessor
is a type of processor that operates on entities that have just performed a EMassObservedOperation
over the Fragment/Tag type observed:
EMassObservedOperation |
Description |
---|---|
Add | The observed Fragment/Tag was added to an entity. |
Remove | The observed Fragment/Tag was removed from an entity. |
Observers do not run every frame, but every time a batch of entities is changed in a way that fulfills the observer requirements.
For example, you could create an observer that handles entities that just had an FColorFragment
added to change their color:
UMSObserverOnAdd::UMSObserverOnAdd()
{
ObservedType = FSampleColorFragment::StaticStruct();
Operation = EMassObservedOperation::Add;
ExecutionFlags = (int32)(EProcessorExecutionFlags::All);
}
void UMSObserverOnAdd::ConfigureQueries()
{
EntityQuery.AddRequirement<FSampleColorFragment>(EMassFragmentAccess::ReadWrite);
}
void UMSObserverOnAdd::Execute(UMassEntitySubsystem& EntitySubsystem, FMassExecutionContext& Context)
{
EntityQuery.ForEachEntityChunk(EntitySubsystem, Context, [&,this](FMassExecutionContext& Context)
{
auto Colors = Context.GetMutableFragmentView<FSampleColorFragment>();
for (int32 EntityIndex = 0; EntityIndex < Context.GetNumEntities(); ++EntityIndex)
{
// When a color is added, make it random!
Colors[EntityIndex].Color = FColor::MakeRandomColor();
}
});
}
It is also possible to create queries to use during the execution process regardless the observed Fragment/Tag.
Note: Currently observers are only called during batched entity actions. This covers processors and spawners but not single entity changes from C++.
Observers can also be used to observe multiple operations and/or types. For that, override the Register
function in UMassObserverProcessor
:
void UMyMassObserverProcessor::Register()
{
check(ObservedType);
check(MyObservedType);
UMassObserverRegistry::GetMutable().RegisterObserver(*ObservedType, Operation, GetClass());
UMassObserverRegistry::GetMutable().RegisterObserver(*ObservedType, MyOperation, GetClass());
UMassObserverRegistry::GetMutable().RegisterObserver(*MyObservedType, MyOperation, GetClass());
UMassObserverRegistry::GetMutable().RegisterObserver(*MyObservedType, Operation, GetClass());
UMassObserverRegistry::GetMutable().RegisterObserver(*MyObservedType, EMassObservedOperation::Add, GetClass());
}
As noted above, it is possible to reuse the same EMassObservedOperation
operation for multiple observed types, and vice-versa.
Out of the box Mass can spread out work to threads in two different ways:
-
Per-Processor threading based on the processor dependency graph by setting the console variable
mass.FullyParallel 1
-
Per-query parrallel for calls that spread the job of one query over multiple threads by using the command argument
ParallelMassQueries=1
for the given Unreal process. This is currently used nowhere in the Mass modules or sample and currently it seems to break when deferring commands from it multiple times a frame.
This Section overviews the three main Mass plugins and their different modules:
5.1
MassEntity
5.2MassGameplay
5.3MassAI
MassEntity
is the main plugin that manages everything regarding Entity creation and storage.
The MassGameplay
plugin compiles a number of useful Fragments and Processors that are used in different parts of the Mass framework. It is divided into the following modules:
5.2.1
MassCommon
5.2.2MassMovement
5.2.3MassRepresentation
5.2.4MassSpawner
5.2.5MassActors
5.2.6MassLOD
5.2.7MassReplication
5.2.8MassSignals
5.2.9MassSmartObjects
Basic fragments like FTransformFragment
.
Features an important UMassApplyMovementProcessor
processor that moves entities based on their velocity and force. Also includes a very basic sample.
Processors and fragments for rendering entities in the world. They generally use an ISMC to do so.
A highly configurable actor type that can spawn specific entities where you want.
A bridge between the general UE5 actor framework and Mass. A type of fragment that turns entities into "Agents" that can exchange data in either direction (or both).
LOD Processors that can manage different kinds of levels of detail, from rendering to ticking at different rates based on fragment settings.
Replication support for Mass! Other modules override UMassReplicatorBase
to replicate stuff. Entities are given a separate Network ID that gets passed over the network, rather than the EntityHandle. An example showing this is planned for much later.
A system that lets entities send named signals to each other.
Lets entities "claim" SmartObjects to interact with them.
MassAI
is a plugin that provides AI features for Mass within a series of modules:
This Section, as the rest of the document is still work in progress.
In-level splines and shapes that use config defined lanes to direct zonegraph pathing things around! Think sidewalks, roads etc.
A new lightweight AI statemachine that can work in conjunction with Mass Crowds. One of them is used to give movement targets to the cones in the parade in the sample.