Skip to content
Website logo
YawLighthouse

Unreal Engine's Mass: Fragments & Tags (Components)

Author:
Nicholas Helish

Nicholas Helish

Game Developer

You already read the About Me  page! I’m Nicholas Helish, the author for this article, website, cool stuff, and fan of cats!

July 02, 2026 · 9 min read

A description of Fragments within Unreal Engine's Mass.

This blog post was written in reference to Unreal Engine 5.8

Overview

Fragments are intended to be just plain ol data with no functionality, it only should hold properties that will be modified by external system’s and Processors. Fragments are allowed to have functions but that contradicts the point of an ECS design so my recommendation is to keep this light.

When an entity is migrated between chunks they are memcpy’d, so keeping a Fragment trivially copyable is important!

You can have non-trivially copyable variables within Fragments but be prepared for handling your use cases appropriately(IE: There will be dragons).

Array containers automatically fall into this camp of non-trivially copyable due to their dynamic sizing, if it’s a static array(the size doesn’t change), then it’s trivially copyable.

Types of Fragments

Fragment Type

Typename

Where is it stored?

Modifiable at Runtime?

Designer editable?

Description/Notes

Default / Regular Fragment

FMassFragment

One per entity.

Yes

Yes but only via Trait’s or external systems that expose it directly.

It’s a per-Entity element of state.

Constant Shared Fragment

FMassConstSharedFragment

One copy, deduplicated by value.

No, Read Only.

Yes, same rules as FMassFragment.

Intended as a read only configuration for a Fragment that is shared by multiple Entities.

Essentially the “Read only DataAsset” Fragment’s of Mass.

Shared Fragment

FMassSharedFragment

One copy per Shared Instance.

Yes

Yes, same rules as FMassFragment

Like the constant shared Fragment, but not constant.

Useful for things like team score, team ID’s, a global mesh, really any shared data that a group needs quickly and we only need to set it in one place.

Chunk Fragment

FMassChunkFragment

One per memory chunk.

Yes

No

Intended as an aggregate of what a Processor computes for a single chunk of Entities.

Useful for LOD’s, tick bookkeeping, etc.

Trivial Copy Safe Types

Some trivially copyable types that work with Fragments:

  • FVector
  • FQuat
  • TWeakObjectPtr
  • float

These types are not trivially copyable and cause a Fragment to be not copy safe:

  • TInstancedStruct
  • Dynamic Array containers, usually (as a rule of thumb) it’s also good to mark it non-trivial when using TArray with a static size since it can be forcibly resized.

This is enforced at compile time. If your Fragment isn’t trivially copyable you’ll hit a static_assert telling you to either fix it or opt out by specializing TMassFragmentTraits(declared in Mass/ExternalSubsystemTraits.h). Opting out is deliberate documentation to Mass that you accepted the cost:

This will mark the entire Fragment as a non-trivial copy type, so it is recommended to make Fragments with only the non-trivial types you intend to use to avoid the least amount of memory impact.

USTRUCT()
struct FMyInstancedStructFragment : public FMassFragment
{
    GENERATED_BODY()
public:
    UPROPERTY(Transient)
    TInstancedStruct<FMyInstancedStruct> Struct;
};

// -SEMI-OPTIONAL-
// This is not necessary for all non-trivial Fragments, but some do require this.
// Makes sure it can't be trivially copied.
template<>
struct TStructOpsTypeTraits<FMyInstancedStructFragment> : public TStructOpsTypeTraitsBase2<FMyInstancedStructFragment>
{
    enum
    {
        WithCopy = false
    };
};

// Notify Mass that it's not a trivial copy fragment.
template<>
struct TMassFragmentTraits<FMyInstancedStructFragment> final
{
    enum
    {
        AuthorAcceptsItsNotTriviallyCopyable = true
    };
};

You can see an example of this with FMassUAFFragment, which holds a TInstancedStruct and uses both that opt-out and TStructOpsTypeTraits<FMassUAFFragment> { WithCopy = false }. Otherwise, you can use your IDE’s search everywhere functionality for examples using TMassFragmentTraits since there are numerous that Epic uses it with.

Read/Write Access

You can also specify thread access rules using C++ traits (you can see an example of this in MassRepresentationFragments.h). These traits live on Shared fragments (TMassSharedFragmentTraits) and external Subsystems (TMassExternalSubsystemTraits), not on a plain FMassFragment. For a Shared Fragment, the engine only honors GameThreadOnly; ThreadSafeWrite takes effect on external Subsystems.

USTRUCT()
struct FMySharedFragment : public FMassSharedFragment
{
    GENERATED_BODY()
    // ...
};

// For shared fragments the engine only reads GameThreadOnly.
// (ThreadSafeWrite is honored on TMassExternalSubsystemTraits, for subsystems.)
template<>
struct TMassSharedFragmentTraits<FMySharedFragment> final
{
    enum 
    { 
        GameThreadOnly = true 
    };
};

Tags

There are also Fragments called Tags(FMassTag) which operate in a different space:

  • They are not stored per entity but instead per Archetype for filtering.
  • They do not hold any data, its essentially a filtering tag applied to a set of entities for quick and easy queries that require little to no data evaluation.
  • They are not modifiable or usable in the editor since they don’t have a concept of state other than basic USTRUCT type filtering checking.

Since they are used per Archetype, adding/removing a Tag from an entity will cause the entity to be moved into a new or already existing chunk of allocated memory.

Tags vs Fragments tradeoffs

Type

Memory Usage per Entity

What Changes?

Cost

Tag

0 Bytes

Change the Archetype, causing an entity migration.

Can be expensive operation due to the move to a newly allocated chunk.

Fragment(with a boolean)

+1 Bytes

No Archetype Change, all in the same chunk.

Cheap operation, just writing to a value. No new memory allocations.

Uses more memory overall.

Additional Notes

The cost of a structural change is documented in MassCommands.h

// MassCommands.h
// - Sparse elements are handled in-place (no archetype move). Non-sparse types: single entity move.

So adding 3 Fragments and 1 Tag at once is considered 1 move operation and not four. But modifying Fragments without any Tag add/remove operations is move free.


Ideally it’s recommended to use Tag’s in these general scenarios:

  • For infrequent state changes. IE: Stunned state for 3 seconds.
  • Skipping whole chunks during queries. IE: Active only query vs dead only query since they’re in different Archetypes.
  • Scale of lots of Entities where we want to have less memory for certain state.

For Fragments:

  • The state is changing every frame or almost every frame.
  • Needing to read values but rarely branching the Archetype.


Sparse Fragments

There are two provided Sparse Fragments:

  • FMassSparseFragment: Per-Entity Fragment data, but held in separate side storage (FSparseElementsStorage) rather than in the Archetype chunk like a regular Fragment.
  • FMassSparseTag: Presence is tracked per Archetype chunk in a per-Entity bitmask.

The defining trait of both is that they are not part of the Archetype’s composition, so adding or removing one never causes an Archetype change (no chunk migration).

To make your own type sparse, derive it from FMassSparseFragment (Fragments) or FMassSparseTag (Tags). The runtime check for whether a type is already sparse is ::IsSparse() in Mass/EntityElementTypes.h.

Adding/Removing Tags/Fragments at Runtime

It is very often necessary to add/remove a Tag and/or Fragment(or multiple in a batch) at runtime. Currently, there are three common ways to achieve this with additional sub-pathways and approaches not mentioned (this is a “many ways to achieve this goal” situation):

Which approach to use

Scope

Outside a Processor (external / game thread)

Inside a Processor’s Execute()

Single Entity

FMassEntityManager per Entity ops (see the table below)

Context.Defer().AddX<T>(Entity)

Multiple Entities

FMassEntityManager Batch* ops (or the Composition approach above)

Context.Defer() commands over Context.GetEntities()

Behavior

Mutates the Archetype immediately. Asserts (checkf) if a processing phase is running.

Queued on the Command Buffer and flushed after the phase, so it is safe mid-iteration.

Composition

This approach is the most flexible when operating outside of a Processor where you specify the bitset to add/remove and then call a function on the Entity Manager to update it based on that bitset using a composition wrapper struct.

Currently, the composition struct has some boilerplate, so there may be some improvements that can be done regarding this flow.

FMassFragmentBitSet FragmentsToRemove;
FragmentsToRemove.Add<FMyFragment>();

FMassTagBitSet TagsToRemove;
TagsToRemove.Add<FMyTag>();
TagsToRemove.Add<FMyOtherTag>();

FMassArchetypeCompositionDescriptor Composition(MoveTemp(FragmentsToRemove), MoveTemp(TagsToRemove),
    FMassChunkFragmentBitSet(), FMassSharedFragmentBitSet(), FMassConstSharedFragmentBitSet());

EntityManager.RemoveCompositionFromEntity(Entity, Composition);
Entity Manager's Operations

The Entity manager has a bunch of functions that can be called to handle Add/Removal of Tags/Fragments, you can do this for a single Entity or for multiple.

These functions are synchronous: they change the Archetype immediately and will assert if called during a processing phase (use the Mass Commands path for that). They support single Entities and batches.

Entity Manager: Single Entity (Synchronous)

What you’re changing

Add

Remove

Notes

Fragment (no value)

AddFragmentToEntity(E, T::StaticStruct())

RemoveFragmentFromEntity(E, T::StaticStruct())

Archetype move. Add no-ops if the Entity already has it.

Fragment (with a value)

AddFragmentToEntity(E, T, Initializer) or AddFragmentInstanceListToEntity(E, {FInstancedStruct::Make(...)})

(same as above)

Sets the value in the same move. Safer than add-then-write.

Several Fragments at once

AddFragmentListToEntity(E, {A, B})

RemoveFragmentListFromEntity(E, {A, B})

One move for the whole list.

Tag

AddTagToEntity(E, T)

RemoveTagFromEntity(E, T)

SwapTagsForEntity(E, Old, New) does remove + add in one move.

Shared Fragment

AddSharedFragmentToEntity(E, Shared)

RemoveSharedFragmentFromEntity(E, T)

Call GetOrCreateSharedFragment(...) first. Returns bool. Add cannot change an existing value.

Const Shared fragment

AddConstSharedFragmentToEntity(E, CS) (use SwapConstSharedFragmentForEntity to replace)

RemoveConstSharedFragmentFromEntity(E, T)

Swap... is the only way to overwrite an existing value.

Any Type

AddElementToEntity(E, T)

RemoveElementFromEntity(E, T)

Figures out Fragment / Tag / Sparse for you.

Whole Composition

AddCompositionToEntity_GetDelta(E, Desc, *Shared)

RemoveCompositionFromEntity(E, Desc)

The Composition approach above, expressed as Entity Manager calls.

Most batched operations work on FMassArchetypeEntityCollections. You can build them from loose handles with UE::Mass::Utils::CreateEntityCollections(EntityManager, Entities, DuplicatesHandling, OutCollections).

The exception is BatchAddFragmentInstancesForEntities, which takes FMassArchetypeEntityCollectionWithPayload instead. Build those with FMassArchetypeEntityCollectionWithPayload::CreateEntityRangesWithPayload(EntityManager, Entities, DuplicatesHandling, FMassGenericPayloadView(GenericPayload), OutCollections). The payload is what carries the per-Entity Fragment values, which is why that call needs its own collection type.

Entity Manager: Multiple Entities / batch (Synchronous)

What you’re changing

Add

Remove

Notes

Tags (bitset)

BatchChangeTagsForEntities(Colls, Add, Remove)

(same call)

One move per Archetype group.

Fragments (bitset)

BatchChangeFragmentCompositionForEntities(Colls, Add, Remove)

(same call)

Fragment only convenience wrapper.

Fragments (with values)

BatchAddFragmentInstancesForEntities(CollsWithPayload, Affected)

(add only)

Takes FMassArchetypeEntityCollectionWithPayload(built via CreateEntityRangesWithPayload). The payload carries the per-Entity values. Overloads also take tags and shared values.

Any element by Type

BatchAddElementToEntities(Entities, T)

BatchRemoveElementFromEntities(Entities, T)

Take raw handle arrays; duplicates folded internally.

Shared

BatchAddSharedFragmentsForEntities(Colls, Values)

BatchRemoveSharedFragmentFromEntities(Colls, BitSet)

Add handles const + non-const shared together.

Const Shared

(via BatchAddSharedFragmentsForEntities)

BatchRemoveConstSharedFragmentFromEntities(Colls, BitSet)

Removal via bitset.

Whole Composition

BatchChangeCompositionForEntities(Colls, Add, Remove)

(same call)

An overload adds shared values in the same move.

Mass Commands

When working within a Processor you HAVE to use the ::Defer() function to access the Mass Command Buffer to defer those add/remove operations until after the Processor is done to avoid race conditions, this is done using the provided FMassExecutionContext struct.

Context.Defer().AddTag<FMyTag>(Entity);
Context.Defer().RemoveFragment<FMyLastFragment>(Entity);
Context.Defer().PushCommand<FMassCommandAddFragments<FMyFragment, FMyOtherFragment>>(Entity);
Mass Commands: During Processing (Deferred)

Operation

Deferred call

Notes

Add / Remove Fragment

Defer().AddFragment<T>(E) / Defer().RemoveFragment<T>(E)

Compile-time checked.

Add / Remove Tag

Defer().AddTag<T>(E) / Defer().RemoveTag<T>(E)

Overloads take TConstArrayView<FMassEntityHandle> for many entities.

Swap Tags

Defer().SwapTags<TOld, TNew>(E)

One move.

Add several at once

Defer().AddElements<A, B, FMyTag>(E)

Mix fragments + tags in a single move.

Add Fragment (with value)

Defer().PushCommand<FMassCommandAddFragmentInstances>(E, FMyFrag{42})

Deferred analog of AddFragmentInstanceListToEntity.

Add Shared Fragment

Defer().AddElementsWithSharedFragments<...>(E, MoveTemp(Values))

Plain AddElements with a shared type is a compile error; use this.

Runtime Types

Defer().AddElements(E, TConstArrayView<const UScriptStruct*>)

When you only have UScriptStruct*, not compile-time types.

Arbitrary logic

Defer().PushCommand<FMassDeferredSetCommand>([](FMassEntityManager& M){ ... })

Runs your lambda at flush time.

Commands flush in a fixed order regardless of push order: Create -> Add -> ChangeComposition -> Set -> Remove -> Destroy (Remove and Destroy share the last slot).

Share this post

Other Mass Blog Posts