About Mass Framework
Mass is Unreal's in-house ECS (Entity Component System archetype-based) framework that use an efficient data-oriented design. It's created by the AI team at Epic Games to facilitate massive crowd & traffic simulations. It was designed for and featured in the Matrix Awakens demo Epic released in 2021, while it can be used also for other gameplay purposes. Since that time, MassFramework has grown to include many powerful features. As it's generally written, it may be used for many use-cases.
Difference between OOP and ECS
- In object-oriented programming (OOP), game code is expressed as an
Actor
object that inherit from parent classes to change their data and functionality based on what they are. - In an Entity Component System (ECS), an
entity
is only composed offragments
that get manipulated byprocessors
based on which ECS components they have.
Development of Mass Framework is still in progress (WIP) and as such it's meant to be used for specific stuff only right now. Anyway, in a time, it will get closer to full fledged ECS framework such as Apparatus in Unreal Engine or other solutions such as Unity Dots, FLECS and other archetype-based ECS libraries.
Mass Plugins and Modules
Plugin name | Plugin description |
---|---|
MassEntity |
|
MassGameplay |
Located at |
MassAI |
Located at |
MassCrowd |
Plugin initially written for Crowd and Traffic behavior in CitySample project. It extends |
Meshes / Visuals in Mass
Mass framework works with Skeletal
as well as static meshes
. As so, there's not visual nor technological difference regarding the Meshes itself. There are instanced entities with Static Meshes
as well as standard Actors
(extended for MassAgent component
used in the Mass) with a Mesh of your wish. What's the difference is, that¨s the general form of presence in the scene and behaviour controls.
- Regular Actor - Standard actor without Mass implementation. The actor is just commonly placed in the scene, with its own logic.
- Mass Controlled Actor -
BP_Actor
(s) spawned to the scene byAMassSpawner
(or through its derived class). There's an individualBP_Actor
representation for each member. TheBP_Actor
itself has no own functionality, instead, it's controlled through attachedMassAgent
component (part ofMassEntity
plugin), specifically MassEntity Config
file (DataAsset).As it's a standard actor in a scene, just with attached
MassAgent
and other components, it may be based onStatic Mesh
as well asSkeletal Mesh
. - Mass Entity - All spawned objects by
are placed in the scene within one object -AMassSpawner
(or through its derived class)MassVisualizer
actor (MassRepresentationSubsystemVisualizer), but still can move quite independently.
Usually it works so, that the closest crowd characters are fully rigged and animated MetaHuman characters (Mass Controlled Actor
), and the more distant ones are custom generated, vertex-animated Static Meshes generated from the MetaHumans (Mass Entities
).
Mass Architecture Logic (MassAI plugin)
The visualization below shows a logic behind MassAI
plugin use.
- Whole scene behavior is configured through
MassEntityConfig
asset. - Based on
MassEntityConfig
asset,Mass Spawner
spawnsMass entities
andMass controlled Actors
(Mass Agents) to the scene. Mass agents
move within a specifiedZoneGraph
and behave in a base ofStateTree
, including interaction withSmartObjects
placed in the scene.
Mass Framework Entity Component System (ECS):
In Mass, some ECS terminology differs from the norm. Partly, because there is an existing patent for ECS framework (idea documented decades prior the patent itself) by Unity Engine. In the Mass framework it looks like follow:
norm ECS | Mass ECS | User's perspective ECS explanation |
---|---|---|
Entity | Entity | Entities = Indexes of data |
Component | Fragment | Components = the Data itself |
System | Processor | Systems = Behaviors definition |
Beside the shown above, there are also Traits
, Tags
and Subsystems
.
Data-Oriented design architecture of Mass framework
As it's data-oriented, we work with a "joining" various custom data each other, see an example of Entity
id10 linking at 2 Traits
that are composed of Fragments
and Tag
.
Mass ECS data scheme
- Mass manages different agents called
Entities
.Entities
are something like lightweightactors
Entities
have a collection ofTraits
.Traits
are something like lightweightcomponents
Since neither
entities
nortraits
have an option to attach any functionality, the comparison toActors
andComponents
are not 100% true, but it helps to understand these components a bit more.Entities
defining identity onlyTraits
defining data only
Entities
are going to be defined by a list oftraits
, and that definition is called anarchetype
.- Real data are stored in
fragments
that are smallUStructs
.Fragments
define the state of a given functionality (it's something like the data of a component). - The actual functionality, the evaluation, and the alteration of each
fragmented
state is implemented inprocessors
.Processors
filter the entities that use the data they can operate on and will be served thefragments
of those entities packed in chunks all of the same type at the same time, facilitating the manipulation of the same kind of data in sequential and contiguous blocks, making cache misses less likely to happen.
Fragment
Fragment represents an atomic piece of data (Structs
) used in calculations. It may be imagined as of an TArray of elements that each entity access own index from the TArray. Common examples for Fragments include Transform
, Velocity
and LOD Index
.
Fragments can be grouped into collections, and an instance of such collection can be associated with an ID. This collection instance is called an Entity
.
As mentioned, Fragments are Data-only UStructs
. As so, they are defined in .h
file only and has no .cpp
file in general. Fragments can be own by entities
and processed with processors
based on specified query.
Fragment types in Mass (to derive from on creation)
FMassFragment
represents UStruct
for single Entity
- Each entity gets its own fragment
data.
Creating a Mass fragment FMassFragment
USTRUCT(BlueprintType)
struct EXMPLPR_API FOccupantTrajectoryFragment : public FMassFragment
{
GENERATED_BODY()
UPROPERTY(EditAnywhere)
float Time;
};
FMassSharedFragment
allows to share data across many entities - it's a type of fragment
that multiple entities
can point to. The typical usage is for the configuration of a group of entitities
for LODs or replication settings.
Creating a shared Mass fragment FMassSharedFragment
USTRUCT()
struct MASSCOMMUNITYSAMPLE_API FVisibilityDistanceSharedFragment : public FMassSharedFragment
{
GENERATED_BODY()
UPROPERTY()
float Distance;
};
In the FVisibilityDistanceSharedFragment
example above, all the entities
containing the FVisibilityDistanceSharedFragment
will see the same Distance
value. If an entity
modifies the Distance
value, the rest of the entities with this fragment will see the change as they share it through the archetype. Shared fragments are generally added from Mass Traits
.
Shared fragments must be Crc hashable, otherwise there's not possible to create a new instance with GetOrCreateSharedFragmentByHash
call. With that call, there's also possible to pass in your own hash which helps if there's a preference to control what makes each one unique.
Thanks to this sharing data requirement, the Mass entity manager only needs to store one Shared Fragment
for the entities
that use it.
Attaching custom Fragment to Mass
Custom created Fragments
can be attached to Traits through Assorted Fragments
trait in Mass Entity Config
data asset file, see below.
Selected Built-in Mass Fragments
- Transform Fragment
MassGameplay
(MassCommonFragments.h) - Mass Actor Fragment
MassGameplay
(MassActorSubsystem.h)
Fragment to save the actor pointer of a mass entity if it exist - Agent Radius Fragment
MassGameplay
(MassCommonFragments.h) - Other fragments used in the source code
- FMassViewerInfoFragment (
MassGameplay
) - FMassRepresentationFragment (
MassGameplay
) - FMassRepresentationLODFragment (
MassGameplay
) - FMassVisualizationChunkFragment (
MassGameplay
) - FMassRepresentationSubsystemSharedFragment (
MassGameplay
) - FMassVelocityFragment (
MassGameplay
) - FMassActorFragment (
MassGameplay
)
- FMassViewerInfoFragment (
Other code-based operations with fragments
MontageFragment.Clear();
Clear fragment - When Clear function is defined as of:void FMassMontageFragment::Clear() { *this = FMassMontageFragment(); }
Context.Defer().PushCommand<FMassCommandRemoveFragments<FMassMontageFragment>>(Context.GetEntity(EntityIdx));
- Add data to fragment
Trait
A Trait is a collective name for Fragments
and Processors
supplying a given functionality. From here, Traits ~= Features.
For now, we can simplify is as follow:
Creating a custom Trait
Custom trait is a standard C++ class inherited from UMassEntityTraitBase
class.
#include "CoreMinimal.h"
#include "MassEntityTraitBase.h"
#include "UObject/Object.h"
#include "InstancedStruct.h"
#include "OccupantMovementTrait.generated.h"
UCLASS()
class PATHFINDERIMPORTER_API UOccupantMovementTrait : public UMassEntityTraitBase
{
GENERATED_BODY()
protected:
virtual void BuildTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World) const override;
// Other Optional properties
UPROPERTY(EditAnywhere, Category = "Mass")
FVector StartingForce = {0,0,100.0f};
UPROPERTY(EditAnywhere, meta = (BaseStruct = "/Script/MassEntity.MassTag", ExcludeBaseStruct))
TArray<FInstancedStruct> Tags;
};
#include "Mass/Traits/OccupantMovementTrait.h"
#include "MassCommonFragments.h"
#include "MassEntityTemplateRegistry.h"
#include "Mass/Fragments/OccupantFragments.h"
void UOccupantMovementTrait::BuildTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World) const
{
// Add Fragment
BuildContext.AddFragment<FOccupantTrajectoryFragment>();
// Require Fragment
BuildContext.RequireFragment<FTransformFragment>();
// Get Fragment Ref
BuildContext.AddFragment_GetRef<FMassForceFragment>().Value = StartingForce;
// Add Single Tag
BuildContext.AddTag<FMSBasicMovement>();
// Add Tags
for (const FInstancedStruct& Tag : Tags)
{
if(!Tag.IsValid()) continue;
const UScriptStruct* StructType = Tag.GetScriptStruct();
if (StructType->IsChildOf(FMassTag::StaticStruct())) BuildContext.AddTag(*StructType);
}
}
PrivateDependencyModuleNames.AddRange(
new string[] {
"MassEntity",
"StructUtils",
"MassCommon",
"MassMovement",
"MassActors",
"MassSpawner",
"MassGameplayDebug",
"MassSignals",
"MassCrowd",
"MassActors",
"MassRepresentation",
"MassReplication",
"MassNavigation",
"MassSimulation",
//needed for replication setup
"NetCore",
"AIModule",
"ZoneGraph",
"MassGameplayDebug",
"MassZoneGraphNavigation",
"Niagara",
"DeveloperSettings",
"GeometryCore",
"MassAIBehavior",
"StateTreeModule",
"MassLOD",
"NavigationSystem",
"Chaos",
"PhysicsCore",
"ChaosCore",
"ChaosSolverEngine", "CADKernel",
"RHI"
}
);
"Plugins": [
{
"Name": "MassEntity",
"Enabled": true
},
{
"Name": "MassGameplay",
"Enabled": true
},
{
"Name": "MassAI",
"Enabled": true
},
{
"Name": "MassCrowd",
"Enabled": true
},
{
"Name": "CodeView",
"Enabled": true
}
]
Attaching Traits to Mass
Custom written trait
from above example as well as any Mass defualt Trait can be attached to the scene through Mass Entity Config
file attached in Mass Spawner
as follow.
Mass Entity Config Data Asset has an array of traits
through which there can be attached numerous traits instances. Each Trait
instance is then responsible for adding and configuring Fragments
in a way that results in the Entity's
exhibiting the behavior supplied by the Trait
.
Default Mass Traits to use
-
Visualization
MassCrowd
- Crowd Visualization
Take into notice, that the
MassCrowdAgentConfig_MetaHuman
extendsMassCrowdAgentConfig
with ortherTraits
associated.
- Crowd Visualization
-
Assorted Fragments
MassGameplay
Assorted Fragments allows to attach general as well as custom-made
Fragments
to theMass Entity Config
data asset file. -
Replication
MassGameplay
-
State Tree
MassAI
-
Crowd Server Representation
MassCrowd
-
Look At
MassAI
Trait without custom configuration
-
CrowdMember
MassCrowd
Trait without custom configuration
-
ZoneGraph Annotation
MassAI
Trait without custom configuration
-
SmartObject User
MassGameplay
Smart Objects are a collection of Actors placed in a level that the Mass AI agents and players can interact with, see Smart Objects section. The system is configurable and adds an unprecedented level of interactivity to the scene. See more at Smart Objects documentation.
-
SimulationLOD
MassGameplay
-
LODCollector
MassGameplay
Trait without custom configuration
-
Movement
MassGameplay
-
ZoneGraph Navigation
MassAI
-
Steering
MassAI
-
Smooth Orientation
MassAI
Proper orientation related to the movement.
-
Avoidance
MassAI
-
Navigation Obstacle
MassAI
Trait without custom configuration
-
Debug Visualization
renders the entities in the scene while in Editor.MassGameplay
This
Trail
allows to set custom options, such as culling distances, mesh, material or wire shape. Agent Feet Location Sync
indicates sync direction, in othe words the priority actor of movement, see:- Actor To Mass: Mass Controlled Actor is driven by its character movement component based on environment (physics) and respects (is affected based on) its collision settings
- Mass To Actor: Mass Controlled Actor is driven completely by data fileld in Mass Processor, such as
Transform
, animation data and so on.
Agent Movement Sync
indicates sync direction for movement.
Entity
An Entity
is a data-only element (it does not contain any logic), usually in the form of small unique identifiers that points to a combination of fragments and tags in memory. Entities are mainly a simple integer ID. For example, entity with id 10 might point to a single actor with properties such as transform, velocity, and damage data.
Creating an Entity is similar to class instancing in object-oriented programming. However, instead of strictly declaring a class and its functionality, Entities
are built by Traits
composition. These compositions can be changed at runtime. For example, an Entity's
composition can include two Traits
linked to Fragments
such as a Transform
and a Velocity
.
Entities are created and placed in the scene based on Entity Config Asset
by Mass Spawner
.
Processors
Processors take care about behavior. They combine multiple user-defined queries with functions that compute entities.
By default, all crated processors run automatically, it's not needed to attach / define them anywhere for running.
UMassProcessor
Processor Constructor setup
UMyProcessor::UMyProcessor()
{
// This processor is registered with mass by just existing! This is the default behaviour of all processors.
bAutoRegisterWithProcessingPhases = true;
// Setting the processing phase explicitly
ProcessingPhase = EMassProcessingPhase::PrePhysics;
// 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);
// This processor should not be multithreaded
bRequiresGameThreadExecution = true;
}
On initialization, Mass creates a dependency graph of processors using their execution rules so they execute in order (ie: In the example above we make sure to move our entities with MSMovementProcessor
before we call Execute
in UMyProcessor
).
-
Processing phases
Unreal classes deriving from UMassProcessor are automatically registered with Mass and added to the EMassProcessingPhase::PrePhsysics processing phase by default. Each EMassProcessingPhase relates to an ETickingGroup, meaning that, by default, processors tick every frame in their given processing phase. They can also be created and registered with the UMassSimulationSubsystem but the common case is to create a new type. Users can configure to which processing phase their processor belongs by modifying the ProcessingPhase variable included in UMassProcessor:
EMassProcessingPhase
Related ETickingGroup
Description PrePhysics
TG_PrePhysics
Executes before physics simulation starts. StartPhysics
TG_StartPhysics
Special tick group that starts physics simulation. DuringPhysics
TG_DuringPhysics
Executes in parallel with the physics simulation work. EndPhysics
TG_EndPhysics
Special tick group that ends physics simulation. PostPhysics
TG_PostPhysics
Executes after rigid body and cloth simulation. FrameEnd
TG_LastDemotable
Catchall for anything demoted to the end. -
Execution flags
Flag
Description Standalone
The processor should be executed on Standalone Server
The processor should be executed on Server Client
The processor should be executed on Client -
Threads settings
By default all processors are multithreaded. To run in a single-thread, set
bRequiresGameThreadExecution
to true.
Processors Queries configuration
Queries (FMassEntityQuery
) filter and iterate entities given a series of rules based on Fragment
and Tag
presence.
Processors can define multiple FMassEntityQuerys and should override the ConfigureQueries to add rules to the different queries defined in the processor's header:
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);
// Entities must have a UMassDebuggerSubsystem that can be read and written
MyQuery.AddSubsystemRequirement<UMassDebuggerSubsystem>(EMassFragmentAccess::ReadWrite);
MyQuery.AddTagRequirement<FMoverTag>(EMassFragmentPresence::All);
MyQuery.AddRequirement<FHitLocationFragment>(EMassFragmentAccess::ReadOnly, EMassFragmentPresence::Optional);
MyQuery.AddSubsystemRequirement<UMassDebuggerSubsystem>(EMassFragmentAccess::ReadWrite);
// Register Queries by calling RegisterWithProcessor passing the processor as a parameter.
MyQuery.RegisterWithProcessor(*this);
ProcessorRequirements.AddSubsystemRequirement<UMassDebuggerSubsystem>(EMassFragmentAccess::ReadWrite);
}
ProcessorRequirements
is a special query part of UMassProcessor
that holds all the UWorldSubsystems
that get accessed in the Execute
function outside the queries scope. In the example above, UMassDebuggerSubsystem
gets accessed within MyQuery
's scope (MyQuery.AddSubsystemRequirement
) and in the Execution
function scope (ProcessorRequirements.AddSubsystemRequirement
).
Note: Queries can also be created and iterated outside processors.
-
Access requirements
EMassFragmentAccess
Description None
No binding required. ReadOnly
We want to read the data for the fragment/subsystem. ReadWrite
We want to read and write the data for the fragment/subsystem. -
Presence requirements
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.
Processors Execution
Processors execute queries within their Execute
function:
Queries are executed by calling the ForEachEntityChunk
member function with a lambda, passing the related FMassEntityManager
and FMassExecutionContext
.
void UMyProcessor::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context)
{
//Note that this is a lambda! If you want extra data you may need to pass it in the [] operator
MyQuery.ForEachEntityChunk(EntityManager, Context, [](FMassExecutionContext& Context)
{
//Loop over every entity in the current chunk and do stuff!
for (int32 EntityIndex = 0; EntityIndex < Context.GetNumEntities(); ++EntityIndex)
{
// ...
}
});
}
Be aware that the index we employ to iterate entities, in this case EntityIndex
, doesn't identify uniquely your entities along time, since chunks' disposition may change and an entity that has an index this frame, may be in a different chunk with a different index in the next frame.
Access Requirement based on Query Conf
EMassFragmentAccess |
Type | Function | Description |
---|---|---|---|
ReadOnly |
Fragment | GetFragmentView |
Returns a read only TConstArrayView containing the data of our ReadOnly fragment. |
ReadWrite |
Fragment | GetMutableFragmentView |
Returns a writable TArrayView containing de data of our ReadWrite fragment. |
ReadOnly |
Shared Fragment | GetConstSharedFragment |
Returns a constant reference to our read only shared fragment. |
ReadWrite |
Shared Fragment | GetMutableSharedFragment |
Returns a reference of our writable shared fragment. |
ReadOnly |
Subsystem | GetSubsystemChecked |
Returns a read only constant reference to our world subsystem. |
ReadWrite |
Subsystem | GetMutableSubsystemChecked |
Returns a reference of our writable shared world subsystem. |
Notes
Tags
do not have access requirements since they don't contain data.
Example use - updating location
// .h file
UCLASS()
class UAdvancedRandomMovementProcessor : public UMassProcessor
{
GENERATED_BODY()
public:
UAdvancedRandomMovementProcessor();
protected:
virtual void ConfigureQueries() override;
virtual void Execute(UMassEntitySubsystem& EntitySubsystem, FMassExecutionContext& Context) override;
private:
FMassEntityQuery EntityQuery;
// .cpp file
// Constructor
UAdvancedRandomMovementProcessor::UAdvancedRandomMovementProcessor()
{
bAutoRegisterProcessingPhases = true;
ExecutionFlags = (int32)EProcessorExecutionFlags::All;
ExecutionOrder.ExecutionBefore.Add(UE::Mass::ProcessorGroupNames::Avoidance);
}
void UAdvancedRandomMovementProcessor::ConfigurateQueries()
{
EntityQuery.AddRequirement<FTransformFragment>(EMassFragmentAccess::ReadOnly);
EntityQuery.AddRequirement<FMassMoveTargetFragment>(EMassFragmentAccess::ReadWrite);
EntityQuery.AddConstSharedRequirement<FMassMovementParameters>(EMassFragmentPresence::All);
}
// Execute is going to be executed just one time at the beginning.
void UAdvancedRandomMovementProcessor::Execute(UMassEntitySubsystem& EntitySubsystem, FMassExecutionContext& Context)
{
EntityQuery.ForEachEntityChunk(EntitySubsystem, Context, ([this](FMassExecutionContext& Context)
{
// Attaching variables with ReadWrite permissions
const TArrayView<FMassMoveTargetFragment> NavTargetsList = Context.GetMutableFragmentView<FMassMoveTargetFragment>();
// Attaching variables with ReadOnly permissions (no need od Mutable)
const TConstArrayView<FOccupantTrajectoryFragment> TrajectoryDataArr = Context.GetFragmentView<FOccupantTrajectoryFragment>();
const TConstArrayView<FTransformFragment> TransformsList = Context.GetFragmentView<FTransformFragment>();
const FMassMovementParameters& MovementParams = Context.GetConstSharedFragment<FMassMovementParameters>();
for (int32 EntityIndex = 0; EntityIndex < Context.GetNumEntities(); ++EntityIndex)
{
const FTransform& Transform = TransformsList[EntityIndex].GetTransform();
FMassMoveTargetFragment& MoveTarget = NavTargetsList[EntityIndex];
FVector CurrentLocation = Transform.GetLocation();
FVector TargetVector = MoveTarget.Center - CurrentLocation;
TargetVector.Z = 0.f;
MoveTarget.DistanceToGoal = (TargetVector).Size();
MoveTarget.Forward = (TTargetVector).GetSafeNormal();
if(MoveTarget.DistanceToGoal <= 20.f || MoveTarget.Center == FVector::ZeroVector)
{
MoveTarget.Center = FVector(FMath::RandRange(-1.f, 1.f) * 1000.f, FMath::RandRange(-1.f, 1.f) * 1000.f, CurrentLocation.Z);
MoveTarget.DistanceToGoal = (MoveTarget.Center - Transform.GetLocation()).Size();
MoveTarget.Forward = (MoveTarget.Center - Transform.GetLocation()).GetSafeNormal();
MoveTarget.DesiredSpeed = FMassInt16Real(MovementParams.DefaultDesiredSpeed);
}
}
}));
}
}
/** Move target. */
USTRUCT()
struct MASSNAVIGATION_API FMassMoveTargetFragment : public FMassFragment
{
GENERATED_BODY()
// ...
public:
FVector Center = FVector::ZeroVector; // Target location to reach
FVector Forward = FVector::ZeroVector; // desired facing vector
float DistanceToGoal = 0.0f; // Distance to center
float SlackRadius = 0.0f;
FMassInt16Real DesiredSpeed = FMassInt16Real(0.0f);
EMassMovementAction IntentAtGoal = EMassMovementAction::Move;
// ...
}
Other examples:
UMassObserverProcessor
Processor
helps initialize fragments
that depend on other fragments' information or other systems that may not be initialized at construction time..
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. |
Exdamples of use
Updating Radius value in Fragment
.hUCLASS() class BUILDYMASS_API UBuildyMassAgentRadiusInitializerProcessor : public UMassObserverProcessor { GENERATED_BODY() public: // Constructor UBuildyMassAgentRadiusInitializerProcessor(); protected: virtual void ConfigureQueries() override; virtual void Execute(UMassEntitySubsystem& EntitySubsystem, FMassExecutionContext& Context) override; FMassEntityQuery EntityQuery; }
.cpp// adjust the radius (FAgentRadiusFragment) for the avoidance of some of entities in the building simulation. Change it for the FBuildyMassBuilderTag and only for the FBuildyMassBuilderTag from default 40 to 20 units // Constructor UBuildyMassAgentRadiusInitializerProcessor::UBuildyMassAgentRadiusInitializerProcessor() { // Registering process - Instead of a state in execution order or registration, define the type observed. // Use FAgentRqadiusFragment tag as requirement ObservedType = FAgentRqadiusFragment::StaticStruct(); Operation = EMassObservedOpration::Add; } void UBuildyMassAgentRadiusInitializerProcessor::ConfigurateQueries() { EntityQuery.AddRequirement<FAgentRqadiusFragment>(EMassFragmentAccess::ReadWrite); // Filtering definition EntityQuery.AddTagRequirement<FBuildyMassBuilderTag>(EMassFragmentPresence::All); /* EMassFragmentPresence ::All - all entities with that tag will pass ::Known - no entity with that tag will not pass */ } // Execute is going to be executed just one time at the beginning. void UBuildyMassAgentRadiusInitializerProcessor::Execute(UMassEntitySubsystem& EntitySubsystem, FMassExecutionContext& Context) { EntityQuery.ForEachEntityChunk(EntitySubsystem, Context, ([this](FMassExecutionContext& Context) { // Attaching variables with ReadWrite permissions const TArrayView<FAgentRqadiusFragment> RadiusList = Context.GetMutableFragmentView<FAgentRqadiusFragment>(); static uint32 CurrentId = 0; for(FAgentRqadiusFragment& RadiusFragment : RadiusList) { RadiusFragment.Radius = 20.f } })); }
.Build.cs-
Updating Color
This observer changes the color to the entities that just had an FColorFragment added:
UMSObserverOnAdd::UMSObserverOnAdd() { ObservedType = FSampleColorFragment::StaticStruct(); Operation = EMassObservedOperation::Add; ExecutionFlags = (int32)(EProcessorExecutionFlags::All); } void UMSObserverOnAdd::ConfigureQueries() { EntityQuery.AddRequirement<FSampleColorFragment>(EMassFragmentAccess::ReadWrite); } void UMSObserverOnAdd::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context) { EntityQuery.ForEachEntityChunk(EntityManager, 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(); } }); }
Use cases:
- Change default value (from 40 to 20) like in the example above
- An observer processor could have been used in the simple movement example to assign an initial movement target instead of having the origin as the initial target. Like, for example, the current location. So in that way, the movement processor will have branched to finding a new random target automatically.
Entity Manager Observer calls
At the time of writing, Observers are only triggered by the Mass Manager directly during these specific Entity actions. This mainly comes up due to some of the specific single-entity modifying functions like addfragmenttoentity
- Entity changes in the entity manager:
FMassEntityManager::BatchBuildEntities
FMassEntityManager::BatchCreateEntities
FMassEntityManager::BatchDestroyEntityChunks
FMassEntityManager::AddCompositionToEntity_GetDelta
FMassEntityManager::RemoveCompositionFromEntity
FMassEntityManager::BatchChangeTagsForEntities
FMassEntityManager::BatchChangeFragmentCompositionForEntities
FMassEntityManager::BatchAddFragmentInstancesForEntities
- The deferred commands that change entity should all call one of the above.
Observing multiple Fragment/Tags
Observers can also be used to observe multiple operations and/or types. For that, override the Register
function in UMassObserverProcessor
.
UPROPERTY()
UScriptStruct* MyObserverType = nullptr;
EMassObservedOperation MyOperation = EMassObservedOperation::MAX;
UMyMassObserverProcessor::UMyMassObserverProcessor()
{
ObservedType = FSampleColorFragment::StaticStruct();
Operation = EMassObservedOperation::Add;
ExecutionFlags = (int32)(EProcessorExecutionFlags::All);
MyObserverType = FSampleMaterialFragment::StaticStruct();
MyOperation = EMassObservedOperation::Add;
}
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());
}
Tags
Tags
are empty UScriptStructs
that processors
can use to filter entities
in Processor
to process based on their presence/absence.
Tags require #include "MassEntityTypes.h"
and can be defined in any file.
Creating a tag in any file
#include "MassEntityTypes.h"
/** Special tag to differentiate the TrafficVehicle from the rest of the other entities */
USTRUCT()
struct MASSTRAFFIC_API FMassTrafficVehicleTag : public FMassTag
{
GENERATED_BODY()
};
Example of Creating a Mass tag in Trait file and attaching it to the Trait
#include "CoreMinimal.h"
#include "MassEntityTypes.h"
#include "MassEntityTraitBase.h"
#include "SomeTrait.generated.h"
/** Tag that represents ... */
USTRUCT()
struct PATHFINDERIMPORTER_API FSomeMyTag : public FMassTag
{
GENERATED_BODY()
};
UCLASS()
class PATHFINDERIMPORTER_API USomeTrait : public UMassEntityTraitBase
{
GENERATED_BODY()
protected:
virtual void BuildTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World) const override;
};
#include "Mass/Traits/SomeTrait.h"
//#include "MassActorSubsystem.h"
#include "MassEntityTemplateRegistry.h"
void USomeTrait::BuildTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World) const
{
// ..
BuildContext.AddTag<FSomeMyTag>();
}
- A good example of the use of tags to exclude certain entities (filtering application) is e.g. the LOD system
Filtration based on Tag
Processor
Tag filtration definition can be specified for each processor in its ConfigureQueries
function, see:
void USomeProcessor::ConfigureQueries()
{
EntityQuery.AddTagRequirement<FMSInOctreeGridTag>(EMassFragmentPresence::All);
}
Dynamical adding / Removal tags furing execution
Context.Defer().AddTag<FSomeTag>(Context.GetEntity(i));
Context.Defer().RemoveTag<FSomeTag>(Context.GetEntity(i));
Other methods are defined in Plugins/Runtime/MassEntity/Source/MassEntity/Public/MassCommandBuffer.h
file.
Example: Muting entities (adding to tag filtration) with Defer()
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 FDead to any entity that has a health fragment with its Health
variable less or equal to 0, at the same time, as we define in ConfigureQueries, after the FDead tag is added, the entity won't be considered for iteration (EMassFragmentPresence::None
):
void UDeathProcessor::ConfigureQueries()
{
// All the entities processed in this query must have the FHealthFragment fragment
DeclareDeathQuery.AddRequirement<FHealthFragment>(EMassFragmentAccess::ReadOnly, EMassFragmentPresence::All);
// Entities processed by this queries shouldn't have the FDead tag, as this query adds the FDead tag
DeclareDeathQuery.AddTagRequirement<FDead>(EMassFragmentPresence::None);
DeclareDeathQuery.RegisterWithProcessor(*this);
}
void UDeathProcessor::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context)
{
DeclareDeathQuery.ForEachEntityChunk(EntityManager, Context, [&,this](FMassExecutionContext& Context)
{
auto HealthList = Context.GetFragmentView<FHealthFragment>();
for (int32 EntityIndex = 0; EntityIndex < Context.GetNumEntities(); ++EntityIndex)
{
if(HealthList[EntityIndex].Health <= 0.f)
{
// Adding a tag to this entity when the deferred commands get flushed
FMassEntityHandle EntityHandle = Context.GetEntity(EntityIndex);
Context.Defer().AddTag<FDead>(EntityHandle);
}
}
});
}
In order to Defer Entity mutations we require to obtain the handle (FMassEntityHandle) of the Entities we wish to modify. FMassExecutionContext holds an array with all the Entity handles. We can access it through two different methods:
Plurality | Code |
---|---|
Singular | FMassEntityHandle EntityHandle = Context.GetEntity(EntityIndex); |
Plural | auto EntityHandleArray = Context.GetEntities(); |
Source files structure
Entities
, Fragments
, Tags
, Processors
and other Mass classes can be defined and declared within one file, see the example below:
#include "CoreMinimal.h"
#include "CrowdCharacterDefinition.h"
#include "MassEntityTypes.h"
#include "MassObserverProcessor.h"
#include "CrowdVisualizationFragment.generated.h"
struct FMassEntityQuery;
USTRUCT()
struct CITYSAMPLE_API FCitySampleCrowdVisualizationFragment : public FMassFragment
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, Category = "")
FCrowdVisualizationID VisualizationID;
UPROPERTY(EditAnywhere, Category = "")
uint32 TopColor = 0;
UPROPERTY(EditAnywhere, Category = "")
uint32 BottomColor = 0;
UPROPERTY(EditAnywhere, Category = "")
uint32 ShoesColor = 0;
UPROPERTY(EditAnywhere, Category = "")
uint8 SkinAtlasIndex = 0;
};
UCLASS()
class CITYSAMPLE_API UCitySampleCrowdVisualizationFragmentInitializer : public UMassObserverProcessor
{
GENERATED_BODY()
public:
UCitySampleCrowdVisualizationFragmentInitializer();
protected:
virtual void ConfigureQueries() override;
virtual void Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context) override;
protected:
FMassEntityQuery EntityQuery;
uint32 FindColorOverride(FCrowdCharacterDefinition& CharacterDefinition, USkeletalMesh* SkelMesh);
UAnimToTextureDataAsset* GetAnimToTextureDataAsset(TSoftObjectPtr<UAnimToTextureDataAsset> SoftPtr);
};
// Others
#include "CrowdVisualizationFragment.h"
#include "MassExecutionContext.h"
#include "CrowdCharacterDataAsset.h"
#include "CrowdPresetDataAsset.h"
#include "MassCrowdRepresentationSubsystem.h"
#include "MassRepresentationTypes.h"
#include "MassRepresentationFragments.h"
#include "MassCrowdAnimationTypes.h"
#include "CitySampleCrowdSettings.h"
#include "CrowdCharacterActor.h"
UCitySampleCrowdVisualizationFragmentInitializer::UCitySampleCrowdVisualizationFragmentInitializer()
: EntityQuery(*this)
{
ObservedType = FCitySampleCrowdVisualizationFragment::StaticStruct();
Operation = EMassObservedOperation::Add;
}
void UCitySampleCrowdVisualizationFragmentInitializer::ConfigureQueries()
{
EntityQuery.AddRequirement<FCitySampleCrowdVisualizationFragment>(EMassFragmentAccess::ReadWrite);
EntityQuery.AddRequirement<FMassRepresentationFragment>(EMassFragmentAccess::ReadWrite);
EntityQuery.AddRequirement<FCrowdAnimationFragment>(EMassFragmentAccess::ReadWrite);
}
uint32 UCitySampleCrowdVisualizationFragmentInitializer::FindColorOverride(FCrowdCharacterDefinition& CharacterDefinition, USkeletalMesh* SkelMesh)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_CitySampleCrowdVisualizationFragmentInitializer_FindColorOverride);
if (SkelMesh == nullptr) return FColor::White.ToPackedRGBA();
// ...
}
void UCitySampleCrowdVisualizationFragmentInitializer::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context)
{
// ...
};
UAnimToTextureDataAsset* UCitySampleCrowdVisualizationFragmentInitializer::GetAnimToTextureDataAsset(TSoftObjectPtr<UAnimToTextureDataAsset> SoftPtr)
{
if (SoftPtr.IsNull()) return nullptr;
if (SoftPtr.IsValid()) return SoftPtr.Get();
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_CitySampleCrowdVisualizationFragmentInitializer_GetAnimToTextureDataAsset_LoadSync);
return SoftPtr.LoadSynchronous();
}
}
Mass Character BP class
As mentioned, Mass controlled actors are just a Blueprint derived actors of C++ class with MassAgent
component.
Mass Agents
MassAgent Component is a way to comunicate with Mass Gameplay Framework - it provides a nice way of adding entities to actors, so that they are considered as such.
In the entity config, we can add agent specific traits and decide if the information is going to be synchronized from the actor to the Mass system, from the Mass system to the actor, both ways, or only for initialization.
Setting Character as Mass agents
In a case of human based crowds, usually the MassAgent
component is palced to ACharacter
BP class.
MassCharacter
class is a custom class derived from ACharacter
. City Sample Crowd Character
class is derived from ACitySampleCharacter
(inherited from ACharacter
), IMassCrowdActorInterface
and IMassActorPoolableInterface
.
MassCharacter example
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "AnimToTextureDataAsset.h"
#include "MassCharacter.generated.h"
UCLASS()
class PROJECTM_API AMassCharacter : public ACharacter
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, Category = "Mass")
TObjectPtr<UAnimToTextureDataAsset> AnimToTextureDataAsset;
UFUNCTION(BlueprintImplementableEvent)
void BP_OnCharacterDeath();
};
#include "Character/MassCharacter.h"
// Empty class
MetaHuman Mass example
Smart Objects in Mass (MassAI)
Smart objects are type of objects with which AI agents can interact with (usually based on StateTree
). An example of such object is a bench.
Smart object is a common BP_Actor
with attached Mass's SmartObject
component.
Smart Object Definition File
Trait attachment
Interaction with Smart Objects is made through Smart Object User
trait.
UWorldSubsystem
The MassEntity framework is divided into several subsystems for encapsulation and code organization. All of the subsystems are World subsystems, which means their lifetime is bound to that of the World where they were created.
UWorldSubsystem
serves for handling positioning for the workers and for the different elements, like resources or buildings.
SubsystemExample
A grid (plain two-dimensional array that lives within a subsystem) representing the world, where each cell is going to be 2 by 2 meters and can contain either an empty ground, a building with the builder's ID associated, resources, either a tree or a boulder with a number of slots associated to the current gatherer's ID.
UCLASS()
class BUILDYMASS_API UBuildyMassGridSubsystem : public UWorldSubsystem
{
GENERATED_BODY()
public:
typedef FBuildyMassGridCellContent BuildyMassGrid[GridDimensions][GridDimensions];
private:
BuildyMassGrid Grid;
};
Subsystems are automatically instanced classes whose lifetimes are managed for us and are easily accessible from almost anywhere - so also from the processors, that can ask for the location of resources or the height of the current cell.
UCLASS()
class BUILDYMASS_API UBuildyMassBuilderProcessor : public UMassProcessor
{
GENERATED_BODY()
TObjectPtr<UBuildyMassGridSubsystem> GridSubsystem;
TObjectPtr<UBuildyMassBuildingSubsystem> BuildingSubsystem;
};
There is a cached pointer to the grid subsystem in the relevant processors and grabbing the actual system in the initialized virtual method.
There's a need to be very cautious about memory in this regard, since accessing every frame to this grid for each entity would cause a big impact, because it will likely mean random memory accesses, meaning that we will have to ask the main RAM to fetch the data for us, because it can't live altogether in the cache, along with the fragments that we are currently iterating on.
To palliate that, an example below show storing a subgrid of 3 by 3 cells in the MovementTargetFragment
. And only when an entity is outside its cached grid, we'll request new data to the grid subsystem. That will save us many cycles every frame, since knowing if an entity is out or not from its cached grid doesn't need any data from the grid subsystem itself.
typedef FBuildyMassGridCellContent BuildyMassGrid[3][3];
USTRUCT()
struct BUILDYMASS_API FBuildyMassMovementTargetFragment : public FMassFragment
{
GENERATED_BODY()
FVector TargetLocation;
uint32 CachedGridX;
uint32 CachedGridY;
UBuildyMassGridSubsystem::BuildyMassSubGrid CachedGrid;
bool bStartGoing;
}
The exact way of caching is used in the Mass NavSubsystem, see default struct FMassMoveTargetFragment
inherited from FMassFragment
. The built-in movement system works by updating the values in FMassMoveTargetFragment
. And the already mentioned processors will work out the displacement.
/** Move target. */
USTRUCT()
struct MASSNAVIGATION_API FMassMoveTargetFragment : public FMassFragment
{
GENERATED_BODY()
// ...
public:
FVector Center = FVector::ZeroVector; // Target location to reach
FVector Forward = FVector::ZeroVector; // desired facing vector
float DistanceToGoal = 0.0f; // Distance to center
float SlackRadius = 0.0f;
FMassInt16Real DesiredSpeed = FMassInt16Real(0.0f);
EMassMovementAction IntentAtGoal = EMassMovementAction::Move;
// ...
}
UBuildyMassBuildingSubsystem
Mass supports also entities that don't move (trees, boulders, and buildings...) but have the navigation obstacle, So every entity using avoidance will consider them for the calculations.
UCLASS()
class BUILDYMASS_API UBuildyMassBuildingSubsystem : public UWorldSubsystem
{
GENERATED_BODY()
public:
// At some point, we are going to want to interact among entities or with the external systems. In this example, the builders generate new instance static meshes when adding parts to their buildings. And for that, they are communicating with their subsystem, dropping messages in a queue. That queue is a basic array, where I'm adding those messages.
UFUNCTION(BlueprintCallable, Category = Mass)
bool AddBuildingMessage(const unit8& Floor, const int32& CellX, const int32& CellY, const bool& bFinished, const uint8& Orientation);
UFUNCTION(BlueprintCallable, Category = Mass)
bool IsBuildingInboxEmpty() const;
UFUNCTION(BlueprintCallable, Category = Mass)
// the subsystem will be processing all those petitions and adding instances to the proper instance static mesh components.
FBuildyMassBuildingMessage ConsumeBuildingMessage();
private:
TArray<FBuildyMassBuildingMessage, TInlineAllocator><32768> BuildingMessages;
}
Proper instanced Static Mesh components
FBuildyMassBuildingMessage ConsumeBuildingMessage();
is linked to following
void UBuildyMassBuildingsComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
while(!BuildingSubsystem->IsBuildingInboxEmpty())
{
// Spawn Building Parts and Particles
}
}
Spawning entities to the world
Spawn entities with runtime data
Mass Spawner (MassGameplay
)
The Mass Spawner is an entry point to bring Mass Entities into the world. The Mass Spawner dictates two things: what type of entity is spawned and where it is spawned. As so, Mass Spawner is useful to spawn entities with predefined data. Mass Spawner can be added through Quick add to project
button.
Mass Spawners require set a few things to spawn entities:
- Count - number of entities to spawn
- EntityConfig - An array of entity types: Specifies what type of entity to spawn. It's made with
UMassEntityConfigAsset
. - Spawn Data Generators - An array of (
FMassSpawnDataGenerator
), that specifies where to spawn (transform) the entities in the world.
Mass Spawners are placed in the level and they can beset to start spawn automatically (Auto Spawn On Begin Play
) or be queried in runtime based on the request. AMassSpawner
class contain a bunch of public blueprint collable functions to do that, such as:
Function | Description |
---|---|
DoSpawning() |
Performs the spawning of all the agent types of this spawner. |
DoDespawning() |
Despawns all mass agents spawned by this spawner. |
ScaleSpawningCount(float Scale) |
Scales the spawning counts. Scale is the number to multiply the all counts of each agent types. |
GetCount() |
Returns the unscaled count of entities to spawn. |
GetSpawningCountScale() |
Returns the number to multiply the all counts of each agent types. |
Mass Spawners are useful to spawn entities with static data in the world (predefined CDO and spawning transform).
The best practice of using Mass spawners is so to use each Mass Spawner
for entities of a certain property, see an example from City Sample below:
Spawning various entities
Through a single Mass Spawner (or it's BP_ derived class in this case), there's possible to set various entities to spawn, see 7 types of cars in the example below:
As shown, there's set 3844 entities to spawn, while these entities are of 7 types defined by Entity Data Asset files. Exact the same files are palced also in the Array of 7 called "Entity Type to Parking T..." in the Generator Instance
.
Parking spots themselves are defined by DataAsset "CitySampleSmallCityParkingSpaces" in this case. The DataAsset file links to Point Cloud data asset "CITY cars parked", see the image below.
BP Mass Traffic Parked Vehicle Spawn Data Generator
placed in the Generator Instance
is a part of MASSTRAFFIC_API plugin (it's derived class of UMassEntitySpawnDataGeneratorBase
).
Mass Spawner
allows supports various types of Spawn Data Generators
, see more at Spawn Data Generators section.
Mass Entity Config Asset
Mass Entity Definition
asset is used to specify what should be spawned by the Mass Spawner
. It dictates Traits
for that entity
being spawned, such as visuals, level of detail, behaviors and more.
It can be created by clicking with right mouse button at Content Drawer
→ Miscellaneous
→ Data Asset
and selecting the data asset of Mass Entity Config Asset
(requires enabled MassEntity plugin
). Name the newly created data asset with DA_
prefix.
Spawn Data Generators
Spawn data generators define where to spawn entities by Mass Spawner
. It provides 3 types of Generator Instance
:
-
EQS SpawnPoints Generator
EQS SpawnPoints Generator
works with EQSRequest query template, see below:Environment Query
The easies way of creating
Environment Query
if from theEQS SpawnPoint Generator
component once there's necessaty to attachEnvironment Query
. Press create new (as shown above) and name it withEQ_
prefix.Environment Query
allows placing various nodes, such asPoints: Grid
. This basically defines the size of grid where the entities should be placed and how. You can select one of followings Generators:Actors of class
Composite
- Allows placing multiple Generator types at onceCurrent Location
Perceived Actors
Points: Circle
Points: Cone
Points: Donut
Points: Grid
Points: Pathing Grid
Smart Objects
-
Zone Graph SpawnPoints Generator
The ZoneGraph is a lightweight design-driven flow for AI that follows a point-by-point corridor structure. It can store meaningful tags (static and dynamic) that can be leveraged for AI behaviors.
Zone Graph SpawnPoints Generator
workws with build Zone Graphs in the project, see following screenshot and video.
When deciding where to spawn entities in City Sample demo, distribution along a ZoneGraph provided through the procedural data from Houdini is used to generate spawn points along the ZoneGraph for the Crowd and Traffic Systems to use.
-
Custom Written Generator
There's possible to write own generators, such as
BP Mass Traffic Parked Vehicle Spawn Data Generator
linking to Cloud Point data, as shown below.
Destroying entities
-
Deferred
The preferred way of destroying entities is to defer it (especially when processing, to stay safe.)
EntityManager->Defer().DestroyEntities(Entities); EntityManager->Defer().DestroyEntity(Entity);
-
Directly
BatchDestroyEntityChunks
is preferred as it calls the observer manager for you. This is only truly safe to call outside of processing on the main thread, like other direct composition changes.UMassSpawnerSubsystem::DestroyEntities
calls this as well.EntityManager->BatchDestroyEntityChunks(Collection)
LODs in Mass
LODs cannot be sued just for setting textures quality, but for the behaviour as well. E.g. activating / disabling avoidance based on camera distance.
Internally, the LOD uses tags to filter out the farther levels or to select simpler functionality for them.
void USampleProcessor::ConfigurateQueries()
{
HighLODQuery.AddTagRequirement<FMassHighLODTag>(EMassFragmentPresence::All);
MediumLODQuery.AddTagRequirement<FMassMediumLODTag>(EMassFragmentPresence::All);
// Similar for LowLOD and OffLOD
}
void USampleProcessor::Execute(UMassEntitySubsystem& EntitySubsystem, FMassExecutionContext& Context)
{
HighLODQuery.ForEachEntityChunk(EntitySubsystem, Context, ([this](FMassExecutionContext& Context)
{
// More complex code
});
MediumLODQuery.ForEachEntityChunk(EntitySubsystem, Context, ([this](FMassExecutionContext& Context)
{
// Simpler code
});
// Even simpler code for LowLOD and OffLOD
}
void USampleProcessor::ConfigurateQueries()
{
EntityQuery.AddTagRequirement<FMassLowLODTag>(EMassFragmentPresence::None);
EntityQuery.AddTagRequirement<FMassOffLODTag>(EMassFragmentPresence::None);
}
void USampleProcessor::Execute(UMassEntitySubsystem& EntitySubsystem, FMassExecutionContext& Context)
{
EntityQuery.ForEachEntityChunk(EntitySubsystem, Context, ([this](FMassExecutionContext& Context)
{
// Code that will run only for HighLOD and MediumLOD
});
}
As shown, there's possible to set up different functionality in the processors for different levels using different queries with different requirements
This system is the other key to scalability, since it will allow us to leverage the amount of code we run. It allows to reach almost 100,000 units processing different amounts of code at different ticking rates.
To make it work in entities, there is an additional trait
called LODCollector
that can be added to Mass Entity Config Asset. Take into notice, that LODCollector
requires registration in the Project
settings. Go to Project Settings
→ Mass
→ Module Settings / Mass Entitz / ProcessorsCDOs
and search for NassLODCollectorProcessor
. In it, check Auto Register with Processing Phases
box.
FMassEntityView
FMassEntityView
is a struct that eases all kinds of Entity operations. One can be constructed with a FMassEntityHandle
and a FMassEntityManager
. On construction, the FMassEntityView
caches the Entity's archetype data, which will later reduce repeated work needed to retrieve information about the Entity.
Following next, we expose some of the relevant functions of FMassEntityView
. In the following contrived processor example, we check if NearbyEntity
is an enemy, if it is, we damage it:
FMassEntityView EntityView(Manager, NearbyEntity.Entity);
//Check if we have a tag
if (EntityView.HasTag<FEnemyTag>())
{
if(auto DamageOnHitFragment = EntityView.GetFragmentDataPtr<FDamageOnHit>())
{
// Now we defer something to do to the other entity!
FDamageFragment DamageFragment;
DamageFragment.Damage = DamageOnHitFragment.Damage * AttackPower;
Context.Defer().PushCommand<FMassCommandAddFragmentInstances>(EntityView.GetEntity, DamageFragment);
}
}
Debugging Mass behaviour
Mass Framework offers a buncg of console commands to debug the Mass function.
EnableGDT
VisLog
mass.Debug
More to know
There is a MassCrowd plugin in the AI folder that is specific for creating crowds in your projects.
Also state trees have been introduced into Unreal Engine 5. And they connect pretty easily to the Mass system. Just by creating one state tree asset and selecting it as it's schema Mass Behavior, we are going to be able to control entities' states through different tasks.
Tasks are going to be as processors, but that output a result to the processing of the fragments. So that the new transition to a state can be evaluated. This is a cleaner way to handle a state for our entities.
And also, it can work in a level of abstraction where less technical team members can feel more comfortable. Tasks for the Mass system still have to be defined in C++. But tasks for actors can be created in Blueprint in the same way that we can create tasks for behavior trees.
Other resources
- Your First 60 Minutes with Mass
- CitySample project
- Crowds documentation for City Sample project
- MassSample Github
MassSample Github page comes with a great explanation of the ECS used for the Mass on that page (why it's incredibly high performance) and Mass settings and options. Best of last, a sample project included!
- On All Fronts game GitHub
- MassAI framework explanation in Japanese
- Chinesse articles about MassSystem