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 accurate. If something is wrong feel free to PR!
Currently built for the Unreal Engine 5.0 binary from the Epic Games launcher.
Requires git LFS to be installed to clone.
This documentation will be updated often!
- Mass
- Entity Component System
- Sample Project
- Mass Concepts
4.1 Entities
4.2 Fragments
4.3 Tags
4.4 Processors
4.5 Queries
      4.5.1 Access requirements
      4.5.2 Presence requirements
      4.5.3 Iterating Queries
      4.5.4 Mutating entities with Defer()
4.6 Traits
4.7 Shared Fragments
4.8 Observers
      4.8.1 Observing multiple Fragment/Tags- 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 entity spawner that uses a special mass-specific data asset to spawn entities in a circle defined in an Environmental Query System (EQS).
- A Mass-simulated crowd of cones that parades around the level following a ZoneGraph shape with lanes.
- A linetraced projectile simulation example
- Grouped niagara rendering for entities
4.1 Entities
4.2 Fragments
4.3 Tags
4.4 Processors
4.5 Queries
4.6 Traits
4.7 Shared Fragments
Unique identifiers for individual entities.
Data-only UScriptStructs
that entities can own and processors can query on. Stored in chunked archetype arrays for quick processing.
Empty UScriptStructs
employed for filtering.
Just bits on an archetype internally.
The main way fragments are operated on in Mass. Combine one more user-defined queries with functions that operate on the data inside them.
They can also include rules that define in which order they are called in. Automatically registered with Mass by default.
In their constructor they can define rules for their execution order and which types of game client they execute on:
//Using the built-in movement processor group
ExecutionOrder.ExecuteInGroup = UE::Mass::ProcessorGroupNames::Movement;
//This executes only on clients and not the dedicated server
ExecutionFlags = (int32)(EProcessorExecutionFlags::Client | EProcessorExecutionFlags::Standalone);
On initialization, Mass creates a graph of processors using their execution rules so they execute in order. For example, we make sure to move entities before we render them.
Some specific processors that come with Mass that are designed to be derived from to express your own logic. The visualization and LOD modules are both designed to be used this way.
Remember: you create the queries yourself in the header!
Processors use one or more FMassEntityQuery
to select the entities to iterate on.
They are a set of Fragment and Tag types combined with rules to act as a filter for entities in our ECS subsystem we want to change or read the data of.
In processors we add rules to queries by overriding the ConfigureQueries
function and adding rules to the queries we defined in the header.
Queries can exist and be iterated outside of processors but there aren't many usecases for that I am aware of.
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. |
Here are some basic examples in which we add access rules in two Fragments from a FMassEntityQuery MoveEntitiesQuery
:
//Entities must have an FTransformFragment and we are reading and changing it (EMassFragmentAccess::ReadWrite)
MoveEntitiesQuery.AddRequirement<FTransformFragment>(EMassFragmentAccess::ReadWrite);
//Entities must have an FMassForceFragment and we are only reading it (EMassFragmentAccess::ReadOnly)
MoveEntitiesQuery.AddRequirement<FMassForceFragment>(EMassFragmentAccess::ReadOnly);
Note that 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 must be present. Default presence requirement. |
Any | At least one of the fragments marked any must be present. |
None | None of the required fragments can be present. |
Optional | If fragment is present we'll use it, but it does not need to be present. |
Here are some basic examples in which we add presence rules in two Tags from a FMassEntityQuery MoveEntitiesQuery
:
// All entities must have a FMoverTag
MoveEntitiesQuery.AddTagRequirement<FMoverTag>(EMassFragmentPresence::All);
// None of the Entities may have a FStopTag
MoveEntitiesQuery.AddTagRequirement<FStopTag>(EMassFragmentPresence::None);
Fragments can be filtered by presence with an additional EMassFragmentPresence
parameter.
// Don't include entities with a HitLocation fragment
MoveEntitiesQuery.AddRequirement<FHitLocationFragment>(EMassFragmentAccess::ReadOnly, EMassFragmentPresence::None);
EMassFragmentPresence::Optional
can be used to get an Entity to be considered for iteration without the need of actually containing the specified Tag or Fragment. If the Tag or Fragment exists, it will be processed.
// We don't always have a movement speed modifier, but include it if we do
MoveEntitiesQuery.AddRequirement<FMovementSpeedModifier>(EMassFragmentAccess::ReadOnly,EMassFragmentPresence::Optional);
Rarely used but still worth a mention EMassFragmentPresence::Any
filters for entities that must at least one of the fragments marked with Any. Here is a contrived example:
FarmAnimalsQuery.AddTagRequirement<FHorseTag>(EMassFragmentPresence::Any);
FarmAnimalsQuery.AddTagRequirement<FSheepTag>(EMassFragmentPresence::Any);
FarmAnimalsQuery.AddTagRequirement<FGoatTag>(EMassFragmentPresence::Any);
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:
//Note that this is a lambda! If you want extra data you may need to pass something into the []
MovementEntityQuery.ForEachEntityChunk(EntitySubsystem, Context, [](FMassExecutionContext& Context)
{
//Get the length of the entities in our current ExecutionContext
const int32 NumEntities = Context.GetNumEntities();
//These are what let us read and change entity data from the query in the ForEach
const TArrayView<FTransformFragment> TransformList = Context.GetMutableFragmentView<FTransformFragment>();
//This one is readonly, so we don't need Mutable
const TConstArrayView<FMassForceFragment> ForceList = Context.GetFragmentView<FMassForceFragment>();
//Loop over every entity in the current chunk and do stuff!
for (int32 EntityIndex = 0; EntityIndex < NumEntities; ++EntityIndex)
{
FTransform& TransformToChange = TransformList[EntityIndex].GetMutableTransform();
FVector DeltaForce = ForceList[EntityIndex].Value;
//Multiply the amount to move by delta time from the context.
DeltaForce = Context.GetDeltaTimeSeconds() * DeltaForce;\
TransformToChange.AddToTranslation(DeltaForce);
}
});
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>();
}
}
});
The following Listings define the native mutations that you can defer:
Fragments:
Context.Defer().AddFragment<FMyTag>();
Context.Defer().RemoveFragment<FMyTag>();
Tags:
Context.Defer().AddTag<FMyTag>();
Context.Defer().RemoveTag<FMyTag>();
Destroying entities:
Context.Defer().DestroyEntity(MyEntity);
Context.Defer().BatchDestroyEntities(MyEntitiesArray);
It is also possible to create custom mutations by implementing your own commands and passing them through 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 space by sharing which mesh they are displaying, parameters etc.
You can create C++ traits!
Shared Fragments (FMassSharedFragment
) are fragments that multiple entities can point to. This is often used for configuration that won't change for a group of entities at runtime.
The archetype only needs to store one copy for many entities that share it. Hashes are used to find existing shared fragments nad to create new ones.
Adding one to query differs from other fragments:
PositionToNiagaraFragmentQuery.AddSharedRequirement<FSharedNiagaraSystemFragment>(EMassFragmentAccess::ReadWrite);
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.
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.
You should store a pointer to this subsystem in your code.
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
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 crowd entities 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.