Skip to content
Website logo
YawLighthouse

Unreal Engine's Mass: Entities

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 · 6 min read

A description of Entities within Unreal Engine's Mass.

This blog post was written in reference to Unreal Engine 5.8

Overview

Entities are essentially row numbers for contextually tracking an object and has no spatial reasoning, this could be an object in the world or an invisible manager that is available at all times, whatever but basically it is an instance of something. Entities just exists as an ID that is tracked and associated with other data to combine together to create a result of functionality. Other than an ID as a serial number essentially, it holds no other data.

In Unreal it is very common to use the type FMassEntityHandle as a generic/core wrapper for tracking/finding the entity, and FMassEntityView for accessing that entity. Here is an example from an Actor’s BeginPlay:

Simple FMassEntityHandle and FMassEntityView usage.
cpp
void AMyActor::BeginPlay()
{
    Super::BeginPlay();
     
    const UWorld* const World = GetWorld();

    FMassEntityHandle MyEntityHandle;

    // Just using this to showcase how to get an actor's associated entity handle
    // otherwise I would just make up a function that gives me a valid handle. 
    // There is a lot more details involved with using Mass Actors.
    if (UMassActorSubsystem* const Subsystem = World->GetSubsystem<UMassActorSubsystem>())
    {
        MyEntityHandle = Subsystem->GetEntityHandleFromActor(this);
    }
 
    if (MyEntityHandle.IsValid())
    {
        // Epic added UE::Mass::Utils::GetEntityManager and UE::Mass::Utils::GetEntityManagerChecked
        // as helpful utility functions, I HIGHLY recommend you use them
        // Otherwise you have to manually get the entity manager subsystem -> get entity manager
        // and it is tedious.
        if (FMassEntityManager* Manager = UE::Mass::Utils::GetEntityManager(World))
        {
            // FMassEntityHandle::IsValid() only forwards to IsSet(), which just checks that
            // Index and SerialNumber are non-zero. It does NOT tell you the entity still
            // exists, only the manager can answer that via FMassEntityManager::IsEntityValid.
            // We're safe here because GetEntityHandleFromActor already validated against the
            // manager for us and returns an invalid handle otherwise.
            // For a handle from any other source(stored across frames, replicated, or spawned
            // earlier in the same frame) check it against the manager first.
            FMassEntityView MyEntityView(*Manager, MyEntityHandle);

            // Doing stuff with the entity like read/write fragments, check tags, etc.
            if (MyEntityView.HasTag<FMyMassTag>())
            {
                // Do stuff...
            }
        }
    }
}

Spawning Entities

One thing that you may want to do is setup an entity’s fragments and other data before the entity is fully spawned/created.

At the spawner level, Entities are created immediately: UMassSpawnerSubsystem::SpawnEntities() has no deferred Pre Spawn/Finish Spawn pair that you manually call. What it defers is Observer notification, using a Scope Lock behavior via the returned Creation Context.

Mass does have an Actor style two-phase construction though, just one level down on FMassEntityManager. The convenient form is FMassEntityManager::MakeEntityBuilder(), which hands you a UE::Mass::FEntityBuilder: the entity handle is reserved up initially, you add Fragments and Tags to it, and the Entity is only actually created when you call ::Commit().

You can spawn entities using the UMassSpawnerSubsystem(a World Subsystem) by calling ::SpawnEntities() which is a synchronous call. This will create a Creation Context(FMassEntityManager::FEntityCreationContext) which is used as the scope lock for processors that run on Entity creation. When the context’s destructor executes it will release the scope lock and then run processors that have been waiting for the entities creation or fragments added. This is useful for deferred spawning of Entities to setup fragments and tags before they’re fully ready for usage in the greater Mass system’s.

There is also a Processor Observer lock that creation context uses but it can also be used independently if you wanted to take advantage of that.

Spawning Apple Entities Example
cpp
void AMyActor::SpawnApples(UMassEntityConfigAsset& AppleEntityType, const int32 AmountToSpawn)
{
    const UWorld* const World = GetWorld();
    if (!World)
    {
        return;
    }

    UMassSpawnerSubsystem* Spawner = World->GetSubsystem<UMassSpawnerSubsystem>();
    if (!Spawner)
    {
        return;
    }

    // GetOrCreateEntityTemplate takes a const UWorld&, so dereference the world pointer.
    const FMassEntityTemplate& AppleTemplate = AppleEntityType.GetOrCreateEntityTemplate(*World);
    TArray<FMassEntityHandle> SpawnedEntities;

    // If we didn't make a local variable then it would immediately destruct the outputted context,
    // causing scope lock to end, which causes the processors to run for these entities.
    TSharedPtr<FMassEntityManager::FEntityCreationContext> CreationContext = Spawner->SpawnEntities(AppleTemplate, AmountToSpawn, SpawnedEntities);

    {
        // Do stuff with SpawnedEntities...
    }
    
    // This is the manual way of running the destructor,
    // or you can just let the scope end and that will destruct the context for us.
    CreationContext.Reset();
    // Processors will now run for our spawned entities now that the context has destructed.
}

Initializer Processors

You can specify a Processor that runs when spawning Entities and providing Payload Data (at the time of writing, there is a TODO to rename AuxData to Payload Data. So I’m using Payload for future proofing reasons).

When the Initializer Processor is run, it will execute only on the spawned Entities (because they’re the only ones relevant to this context of initialization).

There are two parts to how this can be handled with both being optional;

  • You can optionally specify an Initializer class(TSubclassOf<UMassProcessor>).
  • You can optionally include Payload Data, which is an Instanced Struct(FInstancedStruct), this does require an Initializer Class as it is only passed to that class.

Currently, the engine only has one Processor(really two, the other is a test class) setup that uses Payload Data: UMassSpawnLocationProcessor. This class handles setting FTransformFragment using FMassTransformsSpawnData.

I don’t really understand why this supports randomizing the inputted transforms but it is available to you if you want to use it.

Spawning Entities at Transform’s Example
cpp
TArray<FMassEntityHandle> AMyActor::SpawnMyEntitiesAtTransforms(const FMassEntityTemplate& EntityTemplate, 
    const TArray<FTransform>& SpawnTransforms, 
    const int32& Amount)
{
    TArray<FMassEntityHandle> Result;

    UMassSpawnerSubsystem* SpawnerSubsystem = UWorld::GetSubsystem<UMassSpawnerSubsystem>(GetWorld());
    if (!SpawnerSubsystem)
    {
        UE_LOG(LogTemp, Error, TEXT("AMyActor::SpawnMyEntitiesAtTransforms: NULL SpawnerSubsystem from World(%s)", *GetNameSafe(GetWorld()));
        return Result;
    }

    FMassTransformsSpawnData SpawnData; 
    // I don't need this, but you might.
    SpawnData.bRandomize = false;
    SpawnData.Transforms = SpawnTransforms;
 
    SpawnerSubsystem->SpawnEntities(Template.GetTemplateID(), static_cast<uint32>(Amount),
        // Provide our SpawnData and convert it to something for Instanced Struct's to use.
        FConstStructView::Make(SpawnData),
        // Specify what Processor to run for these Entities.
        UMassSpawnLocationProcessor::StaticClass(),
        // Output our now spawned and initialized Entities.
        Result);
    
    return Result;
}

Creating your own Initializer Processor

When creating your own Initializer Processor, you can set it up like any other Processor but you have to do an additional check to confirm that the correct Payload Data was provided to this Processor (if it’s required).

You can confirm the Payload Data by calling ::ValidateAuxDataType() on the Execution Context that was inputted into the ::Execute() function.

UMyInitializationProcessor::UMyInitializationProcessor()
    : EntityQuery(*this)
{   
    // IMPORTANT, make sure this is a Dynamic Processor.
    // Initializer Processors are intended to be spawned and cached at runtime by the Entity Manager.
    bAutoRegisterWithProcessingPhases = false;
}

void UMyInitializationProcessor::ConfigureQueries(const TSharedRef<FMassEntityManager>& EntityManager)
{
    EntityQuery.AddRequirement<FTransformFragment>(EMassFragmentAccess::ReadWrite);
}

void UMyInitializationProcessor::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context)
{
    // You can add an ensure here if you want, up to you. 
    if (!Context.ValidateAuxDataType<FMySpawnData>())
    {
        UE_VLOG_UELOG(this, LogMass, Error, TEXT("Execution Context does not have FMySpawnData Payload Data. Cannot Initialize."));
        return;
    }

    FMySpawnData& SpawnData = Context.GetMutableAuxData().GetMutable<FMySpawnData>();

    // Input the spawn data to our lambda for iterating over each transform
    EntityQuery.ForEachEntityChunk(Context, [&SpawnData](const FMassExecutionContext& Context)
    {
        // Grab our current soon to be modified, transforms chunk
        const TArrayView<FTransformFragment> ViewTransforms = Context.GetMutableFragmentView<FTransformFragment>();

        for (FMassExecutionContext::FEntityIterator Itr = Context.CreateEntityIterator(); Itr; ++Itr)
        {
            FTransformFragment& TransformFrag = ViewTransforms[Itr];

            // Do stuff...
        }
    });
}

Share this post

Other Mass Blog Posts