Mass Framework for crowds & traffic simulation in Unreal Engine

Mass Framework for crowds and traffic simulations. Available in Unreal 5.0 and above.

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

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

MassEntity is the the main plugin that manages everything regarding Entity creation and storage. It's a gameplay-focused framework for data-oriented calculations. It also ensures entities inside the world are persistent. Check more at MassEntity Unreal page.

  • Framework Fundamentals
  • Scheduling
  • Composition tools (Traits)
  • Basic subsystems
  • ...
MassGameplay

MassGameplay plugin extends MassEntity for compilation of a number of useful Fragments and Processors that are used in different parts of the Mass framework.

  • In World Representation
  • Movement
  • LOD Mechanisms
  • StateTree
  • Spawning
  • Replication
  • ...

Located at UE5/Engine/Plugins/Runtime/MassGameplay

MassAI

MassAI is a plugin that provides AI features for Mass. It extends MassGameplay plugin.

  • Navigation
  • AI Behaviors
  • ZoneGraph interface
  • SmartObjects
  • Animation
  • ...

Located at UE5/Engine/Plugins/AI/MassAI

MassCrowd

Plugin initially written for Crowd and Traffic behavior in CitySample project. It extends MassAI plugin

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 by AMassSpawner (or through its derived class). There's an individual BP_Actor representation for each member. The BP_Actor itself has no own functionality, instead, it's controlled through attached MassAgent component (part of MassEntity plugin), specifically Mass Entity Config file (DataAsset).

    As it's a standard actor in a scene, just with attached MassAgent and other components, it may be based on Static Mesh as well as Skeletal Mesh.

  • Mass Entity - All spawned objects by AMassSpawner (or through its derived class) are placed in the scene within one object - 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).

Crowd Simulation sample form CitySample project

Mass Architecture Logic (MassAI plugin)

The visualization below shows a logic behind MassAI plugin use.

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 ECSMass ECSUser's perspective ECS explanation
EntityEntityEntities = Indexes of data
ComponentFragmentComponents = the Data itself
SystemProcessorSystems = 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 Schema. Source: Unreal Engine Docs
  • Mass manages different agents called Entities. Entities are something like lightweight actors
  • Entities have a collection of Traits. Traits are something like lightweight components

    Since neither entities nor traits have an option to attach any functionality, the comparison to Actors and Components are not 100% true, but it helps to understand these components a bit more.

    • Entities defining identity only
    • Traits defining data only
  • Entities are going to be defined by a list of traits, and that definition is called an archetype.
  • Real data are stored in fragments that are small UStructs. 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 in processors. Processors filter the entities that use the data they can operate on and will be served the fragments 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)

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 extends MassCrowdAgentConfig with orther Traits associated.

  • Assorted Fragments MassGameplay

    Assorted Fragments allows to attach general as well as custom-made Fragments to the Mass 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 MassGameplay

    renders the entities in the scene while in Editor.

    This Trail allows to set custom options, such as culling distances, mesh, material or wire shape.

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

    UCLASS()
    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;
    }
    // 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
    		}
    	}));
    }
    
    
    
  • 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

Default structure / Structure after filtration by 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.

Mass Character actor | MetaHuman Mass actor

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:

FunctionDescription
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 DrawerMiscellaneousData 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 the EQS SpawnPoint Generator component once there's necessaty to attach Environment Query. Press create new (as shown above) and name it with EQ_ prefix.

    Environment Query allows placing various nodes, such as Points: 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 once
    • Current 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.

Option 1 of writing

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
}

Option 2 of writing


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 SettingsMassModule 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