Unreal Engine·Programming·Mass Framework

Understanding Entities and Fragments in Unreal Engine's Mass Framework

How entity handles, fragments and archetypes work in Unreal's Mass framework, and why an 8 byte handle is only part of the memory an entity needs.

I have been wanting to learn Mass properly for a while, but kept getting stuck. The official documentation is quite sparse, so this time I went to the engine source, starting with Engine/Source/Runtime/MassEntity, and took notes.

Mass is Unreal Engine’s entity component system ( ECS ). Epic built it to simulate tens of thousands of things at once, including the crowds and traffic in City Sample. Running two thousand enemies as Actors is the kind of workload where a system like this becomes useful.

This is the first article from those notes. It covers the data side: entities, the five element types and the archetypes that store them. Queries and processors will come later.

Everything here is checked against Unreal Engine 5.8. Mass has been changing quite a bit between versions, so if you are on 5.5 or older some of these names will not match. The community MassSample repo is also worth having open while you read, it is the best unofficial resource out there.

What is an entity

In Mass, you refer to an entity through an FMassEntityHandle. The handle holds no gameplay data or behavior. Position, health and velocity live in fragments that you give the entity. Trimmed to its data, the handle looks like this:

USTRUCT(BlueprintType)
struct alignas(8) FMassEntityHandle
{
    GENERATED_BODY()

    UPROPERTY(VisibleAnywhere, Category = "Mass|Debug", Transient)
    int32 Index = 0;

    UPROPERTY(VisibleAnywhere, Category = "Mass|Debug", Transient)
    int32 SerialNumber = 0;
};

The handle is two int32, 8 bytes in total, so it is cheap to copy and store. That is the size of the handle itself. Mass also allocates storage to track entities and their current serial numbers, plus the archetypes, chunks and fragment data. All of that adds to the memory used.

I originally described an entity as being 8 bytes in a post, and Sander Mertens pointed out this distinction. His ECS storage article has an illustrated explanation in the entity index section. It describes Flecs, but it is useful for understanding why an ECS needs storage behind the IDs we pass around.

I find it useful to picture a spreadsheet. A fragment type is a column, and the data is in the cells. The handle lets Mass look up the entity’s current row, but it does not contain that row’s data. Mass has to keep the information needed for that lookup somewhere too.

Index is a slot in the entity storage. Index 0 is reserved, so a zero-initialized handle is always invalid. SerialNumber is there for recycled slots: when an entity dies, its slot will eventually be reused by a new entity with a fresh serial number. An old handle you kept around still points at the slot, but the serial numbers no longer match, so the storage can tell that the entity is gone.

The handle can not check this by itself. Its IsSet() only checks the fields in the handle, without asking the storage. It can still return true for an entity that died five minutes ago. To check whether the handle still refers to an existing entity, ask the manager:

Handle.IsSet();                       // local handle fields only
EntityManager.IsEntityValid(Handle);  // check against the manager's entity storage

That validation needs a non-null handle, an index that exists in the storage, and a stored serial number that matches the handle’s serial number. The storage API describes the index and serial comparison. Even before we add gameplay data, there is memory involved in keeping track of which handles are still valid.

Speaking of the manager, everything in Mass belongs to FMassEntityManager. It owns the entity storage, the archetypes, and pretty much every API in this post. Each world has one, hosted by a subsystem:

UMassEntitySubsystem& EntitySubsystem = *World->GetSubsystem<UMassEntitySubsystem>();
FMassEntityManager& EntityManager = EntitySubsystem.GetMutableEntityManager();

Fragments

A fragment is a plain USTRUCT deriving from FMassFragment. This is the “component” of classic ECS, if an entity has health, a velocity, a target location… a fragment is what holds it.

USTRUCT()
struct FEnemyHealthFragment : public FMassFragment
{
    GENERATED_BODY()

    UPROPERTY()
    float Health = 100.f;
};

Fragments are plain data. Mass moves entities around in memory with raw memcpy style operations, so fragment types must be trivially copyable. That rules out FString, TArray, TMap and anything else that allocates on the heap. Prefer FName over FString, and fixed size arrays over TArray. The rule is enforced at compile time with a pretty readable error. There is an escape hatch, a trait with a field literally called AuthorAcceptsItsNotTriviallyCopyable, and the name already tells you that the copy cost is your problem then.

By the way, FTransformFragment, the most used fragment in all of Mass, already ships with the engine.

Also, keep fragments small and split by access pattern. Every byte multiplies across thousands of entities, and data that changes every frame and data that rarely changes want to be in separate fragments. Mass handles tons of small fragment types incredibly well.

Tags and the other element types

A tag is an element with no data at all:

USTRUCT()
struct FPanickedTag : public FMassTag
{
    GENERATED_BODY()
    // tags must stay empty: their presence IS the data
};

A tag occupies zero bytes per entity. It only exists as a bit in the bookkeeping of the archetype, and it represents boolean facts as presence or absence. Ex: Dead, Panicked, SelectedAsTarget.

Queries filter by tag before touching any memory. A query for panicked enemies skips the calm ones, without iterating over them to check a bool inside a fragment. We will look at this in the queries article.

These are the element types:

TypeStoredUse for
FMassFragmentper entityThe bulk of your data: position, health, velocity
FMassTagnowhere ( zero size )Boolean facts as presence/absence
FMassChunkFragmentonce per chunkWorking data for a batch of entities processed together
FMassSharedFragmentonce per unique valueMutable data shared by many entities
FMassConstSharedFragmentonce per unique valueImmutable config shared by many entities

The shared kinds solve the “same value repeated thousands of times” problem. If every grunt in the level uses the same movement tuning, storing a MaxSpeed of 600 in each entity is a waste, so a const shared fragment stores one instance and every grunt references it. You do not create these values yourself, you ask the manager for them, and equal values collapse into the same instance:

FMovementConfigConstSharedFragment ConfigTemplate;
ConfigTemplate.MaxSpeed = 600.f;
const FConstSharedStruct SharedConfig = EntityManager.GetOrCreateConstSharedFragment(ConfigTemplate);

Registered values stay for the lifetime of the manager. Do not use shared fragments for per-entity values like health, as every unique value keeps a slot. It also splits the storage into small chunks, which we will look at below.

For now, fragments and tags will probably be enough. Chunk fragments are mainly internal machinery, used by the engine for things like per-chunk LOD bookkeeping.

Archetypes

The set of element types an entity carries is called its composition. Ex: Transform + EnemyHealth + Panicked. An archetype is the storage for all the entities that share one composition, and there is one archetype per unique composition. Order does not matter, { Health, Transform } and { Transform, Health } are the same archetype.

Inside an archetype, entities live in chunks, memory blocks of 128 KB by default ( configurable in Project Settings -> Mass -> Mass Entity ). Within a chunk, each fragment type gets its own tightly packed array, laid side by side, plus one array with the entity handles. Our enemy from before, a transform at ~96 bytes, a health at 4 bytes and the 8 byte handle, fits a bit over a thousand entities per chunk. Rough math, alignment and chunk overhead affect the capacity. This only estimates how many entities fit in a chunk; the manager’s entity storage and other allocations add to the total memory cost.

This layout is why Mass is fast. When a query walks a chunk reading transforms it is doing a linear read over a packed array, which is what CPU prefetchers like, and the fragment types the query did not ask for are not touched at all.

Take into account that iteration order is not stable. When an entity is removed, the last entity of the chunk is swapped into its slot to keep the arrays dense, and the engine also merges half empty chunks in the background. So do not put gameplay meaning into the order entities are visited.

Changing an entity’s composition moves it to a different archetype. Adding a fragment or removing a tag copies the entity’s data into a chunk of the destination archetype. This is cheaper than spawning an Actor, but more expensive than flipping a bool.

I keep state that changes every frame as values inside fragments. State that changes occasionally, like panicked or dead, can be a tag. Fragment additions are for structural changes.

UE 5.8 also added sparse fragments and tags, which can be added and removed without this archetype move. I will leave those for another article.

This is also why unique shared fragment values hurt: entities pointing at different shared values can not live in the same chunk, so one value per entity means one entity per chunk, and the linear read advantage is gone.

How to create an entity

The friendliest way is the entity builder, a fluent API on the manager:

FMassEntityHandle Enemy = EntityManager.MakeEntityBuilder()
    .Add<FTransformFragment>(FTransform(SpawnLocation))
    .Add<FEnemyHealthFragment>()
    .Add<FPanickedTag>()
    .Commit();

The entity is created at Commit(). The builder works out the composition, finds or creates the archetype, and applies the initial values. I like how little setup this API needs.

That being said, creating entities like this is only allowed while Mass is not running its processors, inside the simulation changes go into a command buffer and are applied later. And in a real game you will mostly not create entities by hand at all, spawners and traits build them from data assets. We will go over both in their own articles.

Which element type should I use then?

Use a fragment for values such as health, position and timers. For an on/off state that changes occasionally, use a tag.

Configuration shared by an entity type goes in a const shared fragment, while mutable state shared by a group goes in a shared fragment. Working data for a batch belongs in a chunk fragment, though you probably do not need one yet.

My recommendation is to start with fragments and tags only. The shared kinds are an optimization, and you can adopt them later without redesigning anything.

Recap

The entity handle is 8 bytes: an index and a serial number. Tracking the entity takes additional memory, before counting any fragment data. Check IsEntityValid() on the manager so it can compare the handle against its storage. The handle’s IsSet() can not tell you whether the entity still exists.

Keep fragments small and trivially copyable, without FString or TArray, and separate frequently changed data from rarely touched data. Tags take zero bytes per entity, but adding or removing them moves the entity between archetypes. Per-frame state belongs in fragment values.

Each composition has an archetype, with data in packed arrays inside chunks. This gives Mass its fast iteration, but the iteration order is not stable.

Keep reading

Dashes and Knockbacks with Root Motion Sources

Dashes and Knockbacks with Root Motion Sources

No root motion in the clip. The task builds the motion from plain numbers and hands it to the movement component, the animation is just cosmetic.
PlayMontageAndWait in GAS: Montage Replication

PlayMontageAndWait in GAS: Montage Replication

Montage_Play works on your screen and nowhere else. The ASC is the one that tells everybody else.
Building Souls-Style Input Buffering on Top of GAS

Building Souls-Style Input Buffering on Top of GAS

A buffered press is simply a struct, a gate tag and one drain function that an anim notify state calls.