Skip to content

My understanding of Unreal Engine 5's experimental ECS plugin with a small sample project.

License

Notifications You must be signed in to change notification settings

getnamo/MassCommunitySample

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fork note

Refactored to only include the plugin as a submodule structure.

Community Mass Sample

Note: This project requires Git LFS for it to work properly, zip downloads won't work.

Authors:

Our very WIP understanding of Unreal Engine 5's experimental Entity Component System (ECS) plugin with a small sample project. We are not affiliated with Epic Games and this system is actively being changed often so this information might not be totally accurate.

We are totally open to contributions, If something is wrong or you think it could be improved, feel free to open an issue or submit a pull request.

Currently built for the Unreal Engine 5 latest version binary from the Epic Games launcher. This documentation will be updated often!

Requirements:

Download instructions (Windows):

After installing the requirements from above, follow these steps:

  1. Right-Click where you wish to hold your project, then press Git Bash Here.

  2. Within the terminal, clone the project:

    git clone https://github.com/Megafunk/MassSample.git
  3. Pull LFS:

    git lfs pull
  4. Once LFS finishes, close the terminal.

Table of Contents

  1. Mass
  2. Entity Component System
  3. Sample Project
  4. Mass Concepts
    4.1 Entities
    4.2 Fragments
          4.2.1 Shared Fragments
    4.3 Tags
    4.4 Subsystems
    4.5 The archetype model
          4.5.1 Tags in the archetype model
          4.5.2 Fragments in the archetype model
    4.6 Processors
    4.7 Queries
          4.7.1 Access requirements
          4.7.2 Presence requirements
          4.7.3 Iterating Queries
          4.7.3 Mutating entities with Defer()
    4.8 Traits
    4.9 Observers
          4.9.1 Observers limitations
          4.9.2 Observing multiple Fragment/Tags
    4.10 Multithreading
  5. Common Mass operations
    5.1 Spawning entities
    5.2 Destroying entities
    5.3 Operating Entities
  6. Mass Plugins and Modules
    6.1 MassEntity
    6.2 MassGameplay
    6.3 MassAI
  7. Other Resources

1. Mass

Mass is Unreal's new in-house ECS framework! Technically, Sequencer already used one internally but it wasn't intended for gameplay code. Mass was created by the AI team at Epic Games to facilitate massive crowd simulations, but has grown to include many other features as well. It was featured in the new Matrix demo Epic released recently.

2. Entity Component System

Mass is an archetype-based Entity Componenet System. If you already know what that is you can skip ahead to the next section.

In Mass, some ECS terminology differs from the norm in order to not get confused with existing unreal code:

ECS Mass
Entity Entity
Component Fragment
System Processor

Typical Unreal Engine game code is expressed as actor objects that inherit from parent classes to change their data and functionality based on what they are. In an ECS, an entity is only composed of fragments that get manipulated by processors based on which ECS components they have.

An entity is really just a small unique identifier that points to some fragments. A Processor defines a query that filters only for entities that have specific fragments. For example, a basic "movement" Processor could query for entities that have a transform and velocity component to add the velocity to their current transform position.

Fragments are stored in memory as tightly packed arrays of other identical fragment arrangements called archetypes. Because of this, the aforementioned movement processor can be incredibly high performance because it does a simple operation on a small amount of data all at once. New functionality can easily be added by creating new fragments and processors.

Internally, Mass is similar to the existing Unity DOTS and FLECS archetype-based ECS libraries. There are many more!

3. Sample Project

Currently, the sample features the following:

  • A bare minimum movement processor to show how to set up processors.
  • An example of how to use Mass spawners for zonegraph and EQS.
  • Mass-simulated crowd of cones that parades around the level following a ZoneGraph shape with lanes.
  • Linetraced projectile simulation example.
  • Simple 3d hashgrid for entities.
  • Very basic Mass blueprint integration.
  • Grouped niagara rendering for entities.

4. Mass Concepts

Sections

4.1 Entities
4.2 Fragments
4.3 Tags
4.4 Subsystems
4.5 The archetype model
4.6 Processors
4.7 Queries
4.8 Traits
4.9 Observers

4.1 Entities

Small unique identifiers that point to a combination of fragments and tags in memory. Entities are mainly a simple integer ID. For example, entity 103 might point to a single projectile with transform, velocity, and damage data.

4.2 Fragments

Data-only UScriptStructs that entities can own and processors can query on. To create a fragment, inherit from FMassFragment.

USTRUCT()
struct MASSCOMMUNITYSAMPLE_API FLifeTimeFragment : public FMassFragment
{
	GENERATED_BODY()
	float Time;
};

With FMassFragments each entity gets its own fragment data, however if we wish to share data across all our entities, we have to use a shared fragment.

4.2.1 Shared Fragments

A Shared Fragment is a type of Fragment that multiple entities can point to. This is often used for configuration common to a group of entities, like LOD or replication settings. To create a shared fragment, inherit from FMassSharedFragment.

USTRUCT()
struct MASSCOMMUNITYSAMPLE_API FClockSharedFragment : public FMassSharedFragment
{
	GENERATED_BODY()
	float Clock;
};

In the example above, all the entities containing the FClockSharedFragment will see the same Clock value. If an entity modifies the Clock value, the rest of the entities with this fragment will see the change, as this fragment is shared accross them.

Thanks to this sharing data requirement, the Mass Entity subsystem only needs to store one Shared Fragment for the entities that use it.

4.3 Tags

Empty UScriptStructs that processors can use to filter entities to process based on their presence/absence. To create a tag, inherit from FMassTag.

USTRUCT()
struct MASSCOMMUNITYSAMPLE_API FProjectileTag : public FMassTag
{
	GENERATED_BODY()
};

Note: Tags should never contain member properties.

4.4 Subsystems

Starting in UE 5.1, Mass enhanced its API to support UWorldSubsystems in our Processors. This provides a way to create encapsulated functionality to operate Entities. First, inherit from UWorldSubsystem and define its basic interface alongside your functions and variables:

UCLASS()
class MASSCOMMUNITYSAMPLE_API UMyWorldSubsystem : public UWorldSubsystem
{
	GENERATED_BODY()

public:
	void Write(int32 InNumber);
	int32 Read() const;

protected:
	// UWorldSubsystem begin interface
	virtual void Initialize(FSubsystemCollectionBase& Collection) override;
	virtual void Deinitialize() override;
	// UWorldSubsystem end interface
	
private:
	UE_MT_DECLARE_RW_ACCESS_DETECTOR(AccessDetector);
	int Number = 0;
};

Following next, we present an implementation example of the provided interface above (see MassEntityTestTypes.h):

void UMyWorldSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
	// Initialize dependent subsystems before calling super
	Collection.InitializeDependency(UMyOtherSubsystemOne::StaticClass());
	Collection.InitializeDependency(UMyOtherSubsystemTwo::StaticClass());
	Super::Initialize(Collection);

	// In here you can hook to delegates!
	// ie: OnFireHandle = FExample::OnFireDelegate.AddUObject(this, &UMyWorldSubsystem::OnFire);
}

void UMyWorldSubsystem::Deinitialize()
{
	// In here you can unhook from delegates
	// ie: FExample::OnFireDelegate.Remove(OnFireHandle);
	Super::Deinitialize();
}

void UMyWorldSubsystem::Write(int32 InNumber)
{
	UE_MT_SCOPED_WRITE_ACCESS(AccessDetector);
	Number = InNumber;
}

int32 UMyWorldSubsystem::Read() const
{
	UE_MT_SCOPED_READ_ACCESS(AccessDetector);
	return Number;
}

The code above is multithread-friendly, hence the UE_MT_X tokens.

Finally, to make this world subsystem compatible with Mass, you must define its subsystem traits, which inform Mass about its parallel capabilities. In this case, our subsystem supports parallel reads:

/**
 * Traits describing how a given piece of code can be used by Mass. 
 * We require author or user of a given subsystem to 
 * define its traits. To do it add the following in an accessible location. 
 */
template<>
struct TMassExternalSubsystemTraits<UMyWorldSubsystem> final
{
	enum
	{
		ThreadSafeRead = true,
		ThreadSafeWrite = false,
	};
};
/**
* this will let Mass know it can access UMyWorldSubsystem on any thread.
*
* This information is being used to calculate processor and query 
* dependencies as well as appropriate distribution of
* calculations across threads.
*/

If you want to use a UWorldSubsystem that has not had its traits defined before and you cannot modify its header explicitly, you can add the subsystem trait information in a separate header file (see MassGameplayExternalTraits.h).

4.5 The archetype model

As mentioned previously, an entity is a unique combination of fragments and tags. Mass calls each of these combinations archetypes. For example, given three different combinations used by our entities, we would generate three archetypes:

MassArchetypeDefinition

The FMassArchetypeData struct represents an archetype in Mass internally.

4.5.1 Tags in the archetype model

Each archetype (FMassArchetypeData) holds a bitset (TScriptStructTypeBitSet<FMassTag>) that contains the tag presence information, whereas each bit in the bitset represents whether a tag exists in the archetype or not.

MassArchetypeTags

Following the previous example, Archetype 0 and Archetype 2 contain the tags: TagA, TagC and TagD; while Archetype 1 contains TagC and TagD. Which makes the combination of Fragment A and Fragment B to be split in two different archetypes.

4.5.2 Fragments in the archetype model

At the same time, each archetype holds an array of chunks (FMassArchetypeChunk) with fragment data.

Each chunk contains a subset of the entities included in our archetype where data is organized in a pseudo-struct-of-arrays way:

MassArchetypeChunks

The following Figure represents the archetypes from the example above in memory:

MassArchetypeMemory

By having this pseudo-struct-of-arrays data layout divided in multiple chunks, we are allowing a great number of whole-entities to fit in the CPU cache.

This is thanks to the chunk partitoning, since without it, we wouldn't have as many whole-entities fit in cache, as the following diagram displays:

MassArchetypeCache

In the above example, the Chunked Archetype gets whole-entities in cache, while the Linear Archetype gets all the A Fragments in cache, but cannot fit each fragment of an entity.

The Linear approach would be fast if we would only access the A Fragment when iterating entities, however, this is almost never the case. Usually, when we iterate entities we tend to access multiple fragments, so it is convenient to have them all in cache, which is what the chunk partitioning provides.

The chunk size (UE::Mass::ChunkSize) has been conveniently set based on next-gen cache sizes (128 bytes per line and 1024 cache lines). This means that archetypes with more bits of fragment data will contain less entities per chunk.

Note: It is relevant to note that a cache miss would be produced every time we want to access a fragment that isn't on cache for a given entity.

4.6 Processors

Processors combine multiple user-defined queries with functions that compute entities.

Processors are automatically registered with Mass and added to the EMassProcessingPhase::PrePhsysics processing phase by default. Each EMassProcessingPhase relates to an ETickingGroup, meaning that, by default, processors tick every frame in their given processing phase.

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.

In their constructor, processors can define rules for their execution order, the processing phase and which type of game clients they execute on:

UMyProcessor::UMyProcessor()
{
	// This processor is registered with mass by just existing! This is the default behaviour of all processors.
	bAutoRegisterWithProcessingPhases = true;
	// 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).

The ExecutionFlags variable indicates whether this processor should be executed on Standalone, Server or Client.

By default all processors are multithreaded, however, they can also be configured to run in a single-thread if necessary by setting bRequiresGameThreadExecution to true.

Note: Mass ships with a series of processors that are designed to be inherited and extended with custom logic. ie: The visualization and LOD processors.

4.7 Queries

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()
{
	MyQuery.AddTagRequirement<FMoverTag>(EMassFragmentPresence::All);
	MyQuery.AddRequirement<FHitLocationFragment>(EMassFragmentAccess::ReadOnly, EMassFragmentPresence::Optional);
	MyQuery.AddSubsystemRequirement<UMassDebuggerSubsystem>(EMassFragmentAccess::ReadWrite);
	MyQuery.RegisterWithProcessor(*this);

	ProcessorRequirements.AddSubsystemRequirement<UMassDebuggerSubsystem>(EMassFragmentAccess::ReadWrite);
}

To execute queries on a processor, we must register them by calling RegisterWithProcessor passing the processor as a parameter. FMassEntityQuery also offers a parameter constructor that calls RegisterWithProcessor, which is employed in some processors from various Mass modules (ie: UDebugVisLocationProcessor).

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).

Queries are executed by calling the ForEachEntityChunk member function with a lambda, passing the related FMassEntityManager and FMassExecutionContext.

Processors execute queries within their Execute function:

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.

Note: Queries can also be created and iterated outside processors.

4.7.1 Access requirements

Queries can define read/write access requirements for Fragments and Subsystems:

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.

FMassFragments use AddRequirement to add access and presence requirement to our fragments. While FMassSharedFragments employ AddSharedRequirement. Finally, UWorldSubsystems use AddSubsystemRequirement.

Here are some basic examples in which we add access rules in two Fragments from a FMassEntityQuery MyQuery:

void UMyProcessor::ConfigureQueries()
{
	// Entities must have an FTransformFragment and we are reading and writing it (EMassFragmentAccess::ReadWrite)
	MyQuery.AddRequirement<FTransformFragment>(EMassFragmentAccess::ReadWrite);
		
	// Entities must have an FMassForceFragment and we are only reading it (EMassFragmentAccess::ReadOnly)
	MyQuery.AddRequirement<FMassForceFragment>(EMassFragmentAccess::ReadOnly);

	// Entities must have a common FClockSharedFragment that can be read and written
	MyQuery.AddSharedRequirement<FClockSharedFragment>(EMassFragmentAccess::ReadWrite);

	// Entities must have a UMassDebuggerSubsystem that can be read and written
	MyQuery.AddSubsystemRequirement<UMassDebuggerSubsystem>(EMassFragmentAccess::ReadWrite);

	// Registering the query with UMyProcessor
	MyQuery.RegisterWithProcessor(*this);
}

ForEachEntityChunks can use the following functions to access ReadOnly or ReadWrite data according to the access requirement:

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.

Find below an example:

MyQuery.ForEachEntityChunk(EntityManager, Context, [this, World = EntityManager.GetWorld()](FMassExecutionContext& Context)
{
	UMassDebuggerSubsystem& Debugger = Context.GetMutableSubsystemChecked<UMassDebuggerSubsystem>(World);

	const auto TransformList = Context.GetFragmentView<FTransformFragment>();
	const auto ForceList = Context.GetMutableFragmentView<FMassForceFragment>();

	for (int32 EntityIndex = 0; EntityIndex < Context.GetNumEntities(); ++EntityIndex)
	{
		FTransform& TransformToChange = TransformList[EntityIndex].GetMutableTransform();
		const FVector DeltaForce = Context.GetDeltaTimeSeconds() * ForceList[EntityIndex].Value;
		TransformToChange.AddToTranslation(DeltaForce);
		Debugger.AddShape(EMassEntityDebugShape::Box, TransformToChange.GetLocation(), 10.f);
	}
});

Note: Tags do not have access requirements since they don't contain data.

4.7.2 Presence requirements

Queries can define presence requirements for Fragments and Tags:

EMassFragmentPresence Description
All All of the required fragments/tags must be present. Default presence requirement.
Any At least one of the fragments/tags marked any must be present.
None None of the required fragments/tags can be present.
Optional If fragment/tag is present we'll use it, but it does not need to be present.
4.7.2.1 Presence requirements in Tags

To add presence rules to Tags, use AddTagRequirement.

void UMyProcessor::ConfigureQueries()
{
	// Entities are considered for iteration without the need of containing the specified Tag
	MyQuery.AddTagRequirement<FOptionalTag>(EMassFragmentPresence::Optional);
	// Entities must at least have the FHorseTag or the FSheepTag
	MyQuery.AddTagRequirement<FHorseTag>(EMassFragmentPresence::Any);
	MyQuery.AddTagRequirement<FSheepTag>(EMassFragmentPresence::Any);
	MyQuery.RegisterWithProcessor(*this);
}

ForEachChunks can use DoesArchetypeHaveTag to determine if the current archetype contains the the Tag:

MyQuery.ForEachEntityChunk(EntityManager, Context, [](FMassExecutionContext& Context)
{
	if(Context.DoesArchetypeHaveTag<FOptionalTag>())
	{
		// I do have the FOptionalTag tag!!
	}

	// Same with Tags marked with Any
	if(Context.DoesArchetypeHaveTag<FHorseTag>())
	{
		// I do have the FHorseTag tag!!
	}
	if(Context.DoesArchetypeHaveTag<FSheepTag>())
	{
		// I do have the FSheepTag tag!!
	}
});
4.7.2.2 Presence requirements in Fragments

Fragments and shared fragments can define presence rules in an additional EMassFragmentPresence parameter through AddRequirement and AddSharedRequirement, respectively.

void UMyProcessor::ConfigureQueries()
{
	// Entities are considered for iteration without the need of containing the specified Fragment
	MyQuery.AddRequirement<FMyOptionalFragment>(EMassFragmentAccess::ReadWrite, EMassFragmentPresence::Optional);
	// Entities must at least have the FHorseFragment or the FSheepFragment
	MyQuery.AddRequirement<FHorseFragment>(EMassFragmentAccess::ReadWrite, EMassFragmentPresence::Any);
	MyQuery.AddRequirement<FSheepFragment>(EMassFragmentAccess::ReadWrite, EMassFragmentPresence::Any);
	MyQuery.RegisterWithProcessor(*this);
}

ForEachChunks can use the length of the Optional/Any fragment's TArrayView to determine if the current chunk contains the Fragment before accessing it:

MyQuery.ForEachEntityChunk(EntityManager, Context, [](FMassExecutionContext& Context)
{
	const auto OptionalFragmentList = Context.GetMutableFragmentView<FMyOptionalFragment>();
	const auto HorseFragmentList = Context.GetMutableFragmentView<FHorseFragment>();	
	const auto SheepFragmentList = Context.GetMutableFragmentView<FSheepFragment>();
	for (int32 i = 0; i < Context.GetNumEntities(); ++i)
	{
		// An optional fragment array is present in our current chunk if the OptionalFragmentList isn't empty
		if(OptionalFragmentList.Num() > 0)
		{
			// Now that we know it is safe to do so, we can compute
			OptionalFragmentList[i].DoOptionalStuff();
		}

		// Same with fragments marked with Any
		if(HorseFragmentList.Num() > 0)
		{
			HorseFragmentList[i].DoHorseStuff();
		}
		if(SheepFragmentList.Num() > 0)
		{
			SheepFragmentList[i].DoSheepStuff();
		}		
	}
});

4.7.3 Mutating entities 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();

The following Subsections will employ the keywords EntityHandle and EntityHandleArray when handling singular or plural operations, respectively.

4.7.3.1 Basic mutation operations

The following Listings define the native mutations that you can defer:

Fragments:

Context.Defer().AddFragment<FMyFragment>(EntityHandle);
Context.Defer().RemoveFragment<FMyFragment>(EntityHandle);

Tags:

Context.Defer().AddTag<FMyTag>(EntityHandle);
Context.Defer().RemoveTag<FMyTag>(EntityHandle);

Destroying entities:

Context.Defer().DestroyEntity(EntityHandle);
Context.Defer().DestroyEntities(EntityHandleArray);
4.7.3.2 Advanced mutation operations

There is a set of FCommandBufferEntryBase commands that can be used to defer some more useful entity mutations. The following subsections provide an overview.

4.7.3.2.1 FMassCommandAddFragmentInstanceList

Defers a list of Fragment mutations over an entity using FStructViews and/or FConstStructViews.

In the example below we mutate the FHitResultFragment with HitResult data, and a FSampleColorFragment fragment with a new color.

FConstStructView HitResultStruct = FConstStructView::Make(FHitResultFragment(HitResult));
FStructView ColorStruct = FStructView::Make(FSampleColorFragment(Color));

Context.Defer().PushCommand(FMassCommandAddFragmentInstanceList(EntityHandle, 
	{HitResultStruct, ColorStruct}
));
4.7.3.2.2 FCommandAddFragmentInstance (Singular)

Identical to FMassCommandAddFragmentInstanceList but it takes a single Fragment as input instead of a list.

FConstStructView HitResultStruct = FConstStructView::Make(FHitResultFragment(HitResult));

Context.Defer().PushCommand(FCommandAddFragmentInstance(EntityHandle, HitResultStruct));

4.7.3.2.3 FBuildEntityFromFragmentInstances

Creates an Entity given a list of initialized Fragments using FStructViews and/or FConstStructViews.

In the example below, we inline the FStructViews:

Note: Can also take an optional FMassArchetypeSharedFragmentValues struct as the third function argument.

FSampleColorFragment ColorFragment;
ColorFragment.Color = FColor::Green;

FTransformFragment TransformFragment;
TransformFragment.SetTransform(SpawnTransform);

//Reserve our future entity's ID!
const FMassEntityHandle EntityHandle = EntitySubsystem->ReserveEntity();

Context.Defer().PushCommand(FBuildEntityFromFragmentInstances(EntityHandle,
	{FStructView::Make(ColorFragment),FStructView::Make(ThingyFragment)}
	//you can add a final FMassArchetypeSharedFragmentValues if you like!
));
4.7.3.2.4 FBuildEntityFromFragmentInstance (singular)

Identical to FBuildEntityFromFragmentInstances but it takes a single Fragment as input instead of a list. It can take an optional FMassArchetypeSharedFragmentValues struct as the third function argument as well.

FConstStructView ColorStruct = FConstStructView::Make(FSampleColorFragment(HitResult));

//Reserve our future entity's ID!
const FMassEntityHandle EntityHandle = EntitySubsystem->ReserveEntity();

Context.Defer().PushCommand(FBuildEntityFromFragmentInstance(EntityHandle, ColorStruct));
4.7.3.2.5 FCommandSwapTags

Removes the first tag (FOffTag in this example) and adds the second to the entity. (FOnTag)

Context.Defer().PushCommand(FCommandSwapTags(EntityHandle, 
	FOffTag::StaticStruct(), 
	FOnTag::StaticStruct()
	));
4.7.3.2.6 FCommandRemoveComposition

Removes a collection of Fragments and Tags from an Entity given a FMassArchetypeCompositionDescriptor.

The FMassArchetypeCompositionDescriptor is a struct that defines a set of Fragments and Tags. It is possible to obtain it from a given archetype handle (FMassArchetypeHandle) or Entity template (FMassEntityTemplate).

The following code demonstrates how to get it from an Entity template from a UMassEntityConfigAsset pointer:

const FMassEntityTemplate* EntityTemplate = EntityConfig->GetConfig().GetOrCreateEntityTemplate(*Owner, *EntityConfig);
const FMassArchetypeCompositionDescriptor& Composition = EntityTemplate->GetCompositionDescriptor();

The following code demonstrates how to get it from an archetype handle:

FMassEntityHandle EntityHandle = Context.GetEntity(EntityId);
FMassArchetypeHandle Archetype = EntitySubsystem->GetArchetypeForEntity(EntityHandle);
const FMassArchetypeCompositionDescriptor& Composition = EntitySubsystem->GetArchetypeComposition(Archetype);

Once FMassArchetypeCompositionDescriptor is obtained, we perform the command doing the following:

Context.Defer().PushCommand(FCommandRemoveComposition(EntityHandle, Composition));
4.7.3.2.7 FDeferredCommand Lambda

Command dedicated to execute a captured lambda. This way, you can defer your own arbitrary code to happen when the command buffer is flushed.

This is a smart way to handle Actor mutations, as those usually need to happen on the main thread.

// Don't forget the UMassEntitySubsystem&! It can just be (UMassEntitySubsystem&) if it's unneeded.
Context.Defer().EmplaceCommand<FDeferredCommand>([MyAwesomePointer](const UMassEntitySubsystem& System)
{
		auto World = System.GetWorld();
		MyAwesomePointer->DoStuffWithWorld(World);
});

Note that the commands that mutate Entities change the value of ECommandBufferOperationType in their declaration in order to pass their changes to the relevant observers when the commands are flushed.

They also manually add their changes to the observed changes list by implementing AppendAffectedEntitiesPerType.

Note: Since it defines ECommandBufferOperationType::None this deferred action does not add any entity changes to trigger observers on its own!

4.7.3.2.8 Custom mutation operations

It is possible to create custom mutations by implementing your own commands derived from FCommandBufferEntryBase.

Context.Defer().EmplaceCommand<FMyCustomComand>(...)

The command needs to have a constructor and to override FCommandBufferEntryBase::Execute() but in order to correctly trigger observers two extra steps are required:

  1. Setting the Type definition to either ECommandBufferOperationType::Remove or ECommandBufferOperationType::Add in the header.
enum
{
	Type = ECommandBufferOperationType::Add
};
  1. Implementing (not overriding) AppendAffectedEntitiesPerType and calling functions on the passed in FMassCommandsObservedTypes as needed. Here we are adding a changed Tag and changed Fragment. TargetEntity is a member of the parent struct.
void AppendAffectedEntitiesPerType(FMassCommandsObservedTypes& ObservedTypes)
{
	ObservedTypes.TagAdded(TagType, TargetEntity);	
	ObservedTypes.FragmentAdded(FragmentType, TargetEntity);
}

4.8 Traits

Traits are C++ defined objects that declare a set of Fragments, Tags and data for authoring new entities in a data-driven way.

To start using traits, create a DataAsset that inherits from UMassEntityConfigAsset and add new traits to it. Each trait can be expanded to set properties if it has any.

In addition, it is possible to inherit Fragments from another UMassEntityConfigAsset by setting it in the Parent field.

MassEntityConfigAsset

Between the many built-in traits offered by Mass, we can find the Assorted Fragments trait, which holds an array of FInstancedStruct that enables adding Fragments to this trait from the editor without the need of creating a new C++ Trait.

AssortedFragments

Traits are often used to add Shared Fragments in the form of settings. For example, our visualization traits save memory by sharing which mesh they are displaying, parameters etc. Configs with the same settings will share the same Shared Fragment.

4.8.1 Creating a trait

Traits are created by inheriting UMassEntityTraitBase and overriding BuildTemplate. Here is a very basic example:

UCLASS(meta = (DisplayName = "Debug Printing"))
class MASSCOMMUNITYSAMPLE_API UMSDebugTagTrait : public UMassEntityTraitBase
{
	GENERATED_BODY()
public:
	virtual void BuildTemplate(FMassEntityTemplateBuildContext& BuildContext, UWorld& World) const override
	{
		// Adding a tag
		BuildContext.AddTag<FMassSampleDebuggableTag>();
		
		// Adding a fragment
		BuildContext.AddFragment<FTransformFragment>();

		// _GetRef lets us mutate the fragment
		BuildContext.AddFragment_GetRef<FSampleColorFragment>().Color = UserSetColor;
	};

	// Editable in the editor
	UPROPERTY(EditAnywhere)
	FColor UserSetColor;
};

Note: We recommend looking at the many existing traits in this sample and the mass modules for a better overview. For the most part, they are fairly simple UObjects that occasionally have extra code to make sure the fragments are all valid and set correctly.

Shared Fragments

Here is a partial BuildTemplate example for adding a shared struct, which can do some extra work to see if a shared fragment identical to the new one already exists:

	//Create the actual fragment struct and set up the data for it however you like 
	FMySharedSettings MyFragment;
	MyFragment.MyValue = UserSetValue;

	//Get a hash of a FConstStructView of said fragment and store it
	uint32 MySharedFragmentHash = UE::StructUtils::GetStructCrc32(FConstStructView::Make(MyFragment));
	
	//Search the Mass Entity subsystem for an identical struct with the hash. If there are none, make a new one with the set fragment.
	FSharedStruct MySharedFragment = 
		EntitySubsystem->GetOrCreateSharedFragment<FMySharedSettings>(MySharedFragmentHash, MyFragment);

	//Finally, add the shared fragment to the BuildContext!
	BuildContext.AddSharedFragment(MySharedFragment);

4.8.2 Validating traits

There is also a ValidateTemplate overridable function which appears to just let you create your own validation for the trait that can raise errors or even change the buildcontext if need be. This is called after BuildTemplate is called for all of the traits of the current template.

In this snippet, we check if a field of the trait is null and print an error:

void UMSNiagaraRepresentationTrait::ValidateTemplate(FMassEntityTemplateBuildContext& BuildContext, UWorld& World) const
{
	//If our shared niagara system is null, show an error!
	if (!SharedNiagaraSystem)
	{
		UE_VLOG(&World, LogMass, Error, TEXT("SharedNiagaraSystem is null!"));
		return;
	}
}

4.9 Observers

The UMassObserverProcessor is a type of processor that operates on entities that have just performed a EMassObservedOperation over the Fragment/Tag type observed:

EMassObservedOperation Description
Add The observed Fragment/Tag was added to an entity.
Remove The observed Fragment/Tag was removed from an entity.

Observers do not run every frame, but every time a batch of entities is changed in a way that fulfills the observer requirements.

For example, 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();
		}
	});
}

4.9.1 Observer limitations

At the time of writing, Observers are only triggered explicitely during these specific Entity actions:

  • Entity changes in the Subsystem:
    • UMassEntitySubsystem::BatchCreateEntities: [TODO: Add definition]
    • UMassEntitySubsystem::BatchDestroyEntityChunks: [TODO: Add definition]
    • UMassEntitySubsystem::AddCompositionToEntity_GetDelta: [TODO: Add definition]
    • UMassEntitySubsystem::RemoveCompositionFromEntity: [TODO: Add definition]
  • Any deferred command that adds or removes Fragments/Tags to an entity.

4.9.2 Observing multiple Fragment/Tags

Observers can also be used to observe multiple operations and/or types. For that, override the Register function in UMassObserverProcessor:

// header file
UPROPERTY()
UScriptStruct* MyObserverType = nullptr;

EMassObservedOperation MyOperation = EMassObservedOperation::MAX;


// cpp file 
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());
}

As noted above, it is possible to reuse the same EMassObservedOperation operation for multiple observed types, and vice-versa.

4.10 Multithreading

Out of the box Mass can spread out work to threads in two different ways:

  • Per-Processor threading based on the processor dependency graph by setting the console variable mass.FullyParallel 1

  • Per-query parrallel for calls that spread the job of one query over multiple threads by using the command argument ParallelMassQueries=1 for the given Unreal process. This is currently used nowhere in the Mass modules or sample and currently it seems to break when deferring commands from it multiple times a frame.

5. Common Mass operations

This section is designed to serve as a quick reference for how to perform common operations with Mass. As usual, we are open to ideas on how to organize this stuff!!

As a rule of thumb, most entity mutations (adding/removing components, spawning or removing entities) are generally done by deferring them from inside of processors.

5.1 Spawning entities

In this Section we are going to review different methods to spawn entities. First, we review the Mass Spawner, which is useful to spawn entities with predefined data. Then, we'll move to more complex spawning methods that enable us to have fine grained control over the spawning.

5.1.1 Spawn entities with data predefined in the Editor - Mass Spawner

Mass Spawners (AMassSpawner) are useful to spawn entities with static data in the world (predefined CDO and spawning transform).

Mass Spawners require two things to spawn entities:

  • An array of entity types: Define which entities to spawn through a UMassEntityConfigAsset.
  • An array of Spawn Data Generators (FMassSpawnDataGenerator): Define where to spawn entities (their starting transform).

In the details panel of a AMassSpawner we can find the following: MassSpawnerSettings

In the above image, the MEC_DebugVisualize Entity Config is used to spawn 25 entities on BeginPlay (bAutoSpawnOnBeginPlay is set to true).

The spawning location of these entities is generated by the EQS SpawnPoints Generator, which is a built-in generator that uses the Environmental Query System to find locations in the world to spawn. In this example, we are creating a circle of locations around the spawner actor:

EQSCircle

The result in game on BeginPlay:

SpawnerCircleResult

Mass Spawners are placed in the level and can be queried in runtime to trigger spawns by calling DoSpawning() from C++ or Blueprints:

aa

Mass Spawners provide a minimal API to do spawn related operations, following next we provide some of the user-friendly accessible functions from both, blueprints and C++:

  • 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.

Note: The Matrix demo uses extensively the Mass Spawner system.

5.1.2 Spawn entities with runtime data

In this section we explore more flexible spawn mechanism, in which we are able to spawn entities on demand with runtime data (ie: a passed in location).

These spawning methods can be benefitial when we require to mutate entities on spawn, or when when the spawning data cannot be predefined (ie: the initial transform data for a projectile spawning from a weapon).

5.1.2.1 Batch Spawning

In C++, you can just call BatchCreateEntities() on an instance of a UMassEntitySubSystem by passing in a specific archetype with the number you want. This is actually how AMassSpawner spawns stuff internally! It calls BatchSetEntityFragmentsValues() afterwards to set the initial data on the returned FEntityCreationContext.

5.1.2.2 Single Entity Spawning

Spawning a new Entity only requires asking the Mass Entity Subsystem for a new entity. Here are some common ways to create new entities with data.

Entity with Fragment data

Check out this example with FBuildEntityFromFragmentInstances from the commands section:

We currently recommend not calling UMassEntitySubsystem::BuildEntity directly unless you are sure don't need observers to trigger for the entity. The shared fragments go in there as well as the third function argument!

Entity with Fragment data and Tags

Currently, my best guess is to use FBuildEntityFromFragmentInstances and then defer however many Context.Defer().AddTag<FTagType>(EntityReservedEarlier); you need.

A note on observers

It is very important to remember that Observers are only triggered explicitely in certain functions out of the box. Check out the list here.

5.2 Destroying entities

[TODO]

5.3 Operating Entities

In this Section we are going to explore the most relevant tools Mass offers to operate Entities. This covers all the get and set operations and structures to work with them (fragment, archetype, tags...).

Note: In cases where we need to operate with Entities outside the current processing context (e.g. avoidance between Entity crowds) it is possible to call all of the regular Mass Subsystem functions or deferred actions on them. This is not ideal for cache coherency but it is nearly unavoidable in gameplay code.

5.2.1 FMassEntityView

FMassEntityView is a struct that eases all kinds of Entity operations. It is composed by a FMassEntityHandle and a UMassEntitySubsystem. 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:

  • [TODO]
  • [TODO]
  • [TODO]

In the following example, we check if NearbyEntity is an enemy, if it is, we damage it:

FMassEntityView EntityView(EntitySubsystem, NearbyEntity.Entity);

if (EntityView.HasTag<FEnemyMassTag>())
{
	if(auto DamageOnHitFragment = EntityView.GetFragmentDataPtr<FDamageOnHit>())
	{
		// Do damage to this entity etc...
	}
}

6. Mass Plugins and Modules

This Section overviews the three main Mass plugins and their different modules:

6.1 MassEntity
6.2 MassGameplay
6.3 MassAI

MassEntity is the main plugin that manages everything regarding Entity creation and storage.

6.2 MassGameplay

The MassGameplay plugin compiles a number of useful Fragments and Processors that are used in different parts of the Mass framework. It is divided into the following modules:

6.2.1 MassCommon
6.2.2 MassMovement
6.2.3 MassRepresentation
6.2.4 MassSpawner
6.2.5 MassActors
6.2.6 MassLOD
6.2.7 MassReplication
6.2.8 MassSignals
6.2.9 MassSmartObjects

6.2.1 MassCommon

Basic fragments like FTransformFragment.

6.2.2 MassMovement

Features an important UMassApplyMovementProcessor processor that moves entities based on their velocity and force. Also includes a very basic sample.

6.2.3 MassRepresentation

Processors and fragments for rendering entities in the world. They generally use an ISMC to do so.

6.2.4 MassSpawner

A highly configurable actor type that can spawn specific entities where you want. There are two ways of choosing locations built in, one that uses an Environmental Query System asset and one that uses a ZoneGraph tag-based query. This appears to be intended for things that spawn all at once initially like NPCs,trees etc, rather than dynamicly spawned things like projectiles, for example.

6.2.5 MassActors

A bridge between the general UE5 actor framework and Mass. A type of fragment that turns entities into "Agents" that can exchange data in either direction (or both ways).

6.2.6 MassLOD

LOD Processors that can manage different kinds of levels of detail, from rendering to ticking at different rates based on fragment settings. They are used in visualization and replication currently as well.

6.2.7 MassReplication

Replication support for Mass! Other modules override UMassReplicatorBase to replicate stuff. Entities are given a separate Network ID that gets passed over the network, rather than the EntityHandle. An example showing this is planned for much later.

6.2.8 MassSignals

A system that lets entities send named signals to each other.

Lets entities "claim" SmartObjects to interact with them.

6.3 MassAI

MassAI is a plugin that provides AI features for Mass within a series of modules:

6.3.1 ZoneGraph
6.3.2 StateTree
6.3.3 ...

This section, like the rest of the document, is still work in progress.

6.3.1 ZoneGraph

In-level splines and shapes that use config defined lanes to direct zonegraph pathing things around! Think sidewalks, roads etc.

6.3.2 StateTree

A new lightweight AI statemachine that can work in conjunction with Mass Crowds. One of them is used to give movement targets to the cones in the parade in the sample.

7. Other Resources

7.1 Mass

This section compiles very useful Mass resources to complement this documentation.

Epic Games Official resources:

@quabqi's blog posts (Chinese):

7.2 General Entity Component Systems (ECS)

About

My understanding of Unreal Engine 5's experimental ECS plugin with a small sample project.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • C++ 92.7%
  • C 6.2%
  • C# 1.1%