Skip to content
Website logo
YawLighthouse

Unreal Engine's Mass: Processors (Systems)

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

A description of Processors within Unreal Engine's Mass.

This blog post was written in reference to Unreal Engine 5.8

Overview

Processors are UObject’s that execute every frame and are defaulted to non-game thread execution, to have a processor execute on the game thread, enable bRequiresGameThreadExecution.

You can also specify the network context’s that a Processor can execute in by setting ExecutionFlags in the constructor of the Processor via a bitmask of EProcessorExecutionFlags. This allows you to build editor tooling functionality using Mass and/or runtime functionality fairly seamlessly using ExecutionFlags.

Execution Flags Table

Flag

When it’s active

Description

None

N/A

No execution context.

Unset values.

Standalone

  • NM_Standalone

A running game application without networking, single-player/standalone/PIE/packaged game.

Server

  • NM_DedicatedServer
  • NM_ListenServer

World is a server either dedicated server or listen server.

Client

  • NM_Client
  • NM_ListenServer

The world is acting as a networking client.

Listen Servers are also Clients.

Editor

No world available and GEditor exists.

Editor level with no game world. Intended for tooling and CDO setup, runs when the Processor is being considered for the editor only.

EditorWorld

When the world is considered an editor world and NOT a game world.

World.IsEditorWorld() && !World.IsGameWorld()

A live editor preview world is being used, not PIE.

AllNetModes

  • Standalone
  • Server
  • Client

Any actual gameplay world regardless of net role.

AllWorldModes

  • Standalone
  • Server
  • Client
  • EditorWorld

Any type of world except for the editor itself which isn’t a world.

All

  • Standalone
  • Server
  • Client
  • Editor
  • EditorWorld

All other flags.

You can also specify the execution order in the constructor using the ExecutionOrder which allows for setting the execution group and what execution groups to execute Before/After as FName values.


Processors can also declare the Archetypes they will create, via FMassEntityCreationRequirements (see GetProcessorEntityCreationRequirements, HasEntityCreationRequirements, ResolveRequestedArchetypes and ExportArchetypesCreated in MassProcessor.h). These get resolved once during ::CallInitialize(), and again by FMassProcessorDependencySolver(in MassProcessorDependencySolver.h) when it builds the processing graph, so the declared Archetypes are created and validated initially rather than rediscovered at spawn time.

Processors have these override points, listed in the order ::CallInitialize() runs them:

  • ::ConfigureQueries(): Declare the requirements for this Processor’s Queries. This runs before ::InitializeInternal(), so don’t rely here on anything that function sets up. The requirements it produces feed both the game thread execution decision and the dependency solver.
  • ::InitializeInternal(): Custom initialization functionality for this Processor, called from ::CallInitialize().
  • ::Execute(): The actual core functionality of this Processor.
Types of Base Processors

Class

Executes when?

Work is done via

Description/Notes

UMassProcessor

Every frame within its phase.

By overriding Execute function.

Regular Processors.

UMassObserverProcessor

Only on an observed event.

By overriding Execute function.

Semi-one time usage Processor based on the event its observing.

UMassCompositeProcessor

As a container and does not technically execute.

Handles grouping child processors.

Primarily a container processor that handle’s grouping and building a prallel graph.

Back to Table of Contents

Mass Entity Queries

Typically, you would just override ::ConfigureQueries() and ::Execute() and have a constructor initializing a FMassEntityQuery. But you are not restricted to a single query container per Processor. Since each Query operates on a basis of requirements for it to properly execute, what’s inputted to query is up to you.

When configuring a Query you have to specify the type with the appropriate function and the Access/Presence requirement for that type.

Query Access Options

Option

Description

None

No binding, invalid/not setup.

ReadOnly

Only reading data from the Fragment.

ReadWrite

Being able to read from and write to data on the Fragment.

Query Presence Options

Option

Description

All

This Fragment MUST be present on the Entity.

Essentially an AND evaluation.

Any

One of the Fragments must be present on the Entity.

Essentially an OR evaluation.

None

This Fragment must NOT be present on the Entity.

This is a NOT evaluation.

Optional

If this Fragment is present on the Entity, then we’ll use that Fragment/Data. This is not needed for this Query to consider it successful, but this is important for multithreading and task organization.

Add Requirement Functions

Object Type

Query Function

Example

USubsystem

::AddSubsystemRequirement()

Query.AddSubsystemRequirement<UMySubsystem>(EMassFragmentAccess::ReadOnly);

FMassTag

::AddTagRequirement()

Query.AddTagRequirement<FMyTag>(EMassFragmentPresence::All);

FMassFragment

::AddRequirement()

Query.AddRequirement<FMyFragment>(EMassFragmentAccess::ReadWrite, EMassFragmentPresence::All);

FMassSparseFragment

and

FMassSparseTag

::AddSparseRequirement()

Query.AddSparseRequirement<FMySparseFragment>(EMassFragmentPresence::All);

or

Query.AddSparseRequirement<FMySparseTag>(EMassFragmentPresence::All);

FMassChunkFragment

::AddChunkRequirement()

Query.AddChunkRequirement<FMyChunkFragment>(EMassFragmentAccess::ReadWrite, EMassFragmentPresence::All);

FMassConstSharedFragment

::AddConstSharedRequirement()

Query.AddConstSharedRequirement<FMyConstSharedFragment>(EMassFragmentPresence::All);

FMassSharedFragment

::AddSharedRequirement()

Query.AddSharedRequirement<FMySharedFragment>(EMassFragmentAccess::ReadOnly, EMassFragmentPresence::All);

Provided by Epic’s official documentation.
Processor Query Diagram

Queries operate by iterating over chunks of Entities, typically used with a ForEachEntityChunk that inputs the Processor’s Context(FMassExecutionContext) and a lambda for the actual chunk iteration, which has a parameter for another FMassExecutionContext, DO NOT input the Processor’s execution context into the lambda capture as this is a different instance of that type and will not work with the code executing within the lambda.

For iterating over each Entity in that chunk you have to manually use the iterator: FMassExecutionContext::FEntityIterator(I recommend making a macro or alias for shorthand usage). This iterator provides the index as an int32 OR you can use the context’s ::ForEachEntityInChunk() function to input a lambda and execute that loop inside the lambda.

This is an example of iterating over Entities within a Processors ::Execute() function.

Example: Iterating over Entities
c++
// .h --------------------

UCLASS()
class UMyProcessor : public UMassProcessor
{
    GENERATED_BODY()
public:
    UMyProcessor();
protected:
    // UMassProcessor overrides
    virtual void ConfigureQueries(const TSharedRef<FMassEntityManager>& EntityManager) override;
    virtual void Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context) override;
    // ~UMassProcessor overrides

    FMassEntityQuery Query;
};

// .cpp --------------------

UMyProcessor::UMyProcessor()
    : Query(*this) // Initialize the query so it knows which processor owns it.
{
    // Configure defaults like execution phase,
    // if its game thread only,
    // execution order, etc.
}

void UMyProcessor::ConfigureQueries(const TSharedRef<FMassEntityManager>& EntityManager)
{
    Query.AddRequirement<FTransformFragment>(EMassFragmentAccess::ReadOnly);
    
    Query.AddTagRequirement<FMyTag>(EMassFragmentPresence::All);

    // This processor will only run if an entity has a FTransformFragment
    // that can be read, and has FMyTag,
    // Otherwise it is considerd Pruned.
}

void UMyProcessor::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context)
{
    // ForEachEntityChunk is what actually binds the per chunk state.
    // The fragment views, the archetype composition check, and the entity iterator
    // are only valid INSIDE this lambda.
    // The lambda's Context is a different instance than the outer one,
    // so do NOT capture the outer Context.
    Query.ForEachEntityChunk(Context, [](FMassExecutionContext& Context)
    {
        // This is how you check if an archetype has a tag.
        const bool HasTag = Context.DoesArchetypeHaveTag<FMyTag>();

        // Get the transform's to iterate over.
        // GetFragmentView returns a TConstArrayView, which matches the ReadOnly requirement
        // we declared. If you need to write, declare EMassFragmentAccess::ReadWrite and use
        // GetMutableFragmentView, which returns a TArrayView instead.
        const TConstArrayView<FTransformFragment> TransformView = Context.GetFragmentView<FTransformFragment>();

        for (FMassExecutionContext::FEntityIterator Itr = Context.CreateEntityIterator(); Itr; ++Itr)
        {
            // We technically already checked it with the archetype check and this shouldn't fail because of our Query.
            // This is just to show how to check for a tag on a single entity.
            if (!FMassEntityView(Context.GetEntityManagerChecked(), Context.GetEntity(Itr)).HasTag<FMyTag>())
            {
                continue;
            }

            const FTransformFragment& TransformFragment = TransformView[Itr];

            // Do stuff with transform fragment...

            // If you wanted to destroy this entity,
            // you have to defer the operation since
            // we're iterating the list of entities right now.
            Context.Defer().DestroyEntity(Context.GetEntity(Itr));
        }

        // OR you can use the function version for iterating over each entity.
        // Note: the lambda IS the per entity body, the loop lives inside
        // ForEachEntityInChunk. So use `return` to skip an entity, not `continue`.
        Context.ForEachEntityInChunk([&TransformView](FMassExecutionContext& Context, int32 EntityIndex)
        {
            // We technically already checked it with the archetype check and this shouldn't fail because of our Query.
            // This is just to show how to check for a tag on a single entity.
            if (!FMassEntityView(Context.GetEntityManagerChecked(), Context.GetEntity(EntityIndex)).HasTag<FMyTag>())
            {
                return;
            }

            const FTransformFragment& TransformFragment = TransformView[EntityIndex];

            // Do stuff with transform fragment...

            // If you wanted to destroy this entity,
            // you have to defer the operation since
            // we're iterating the list of entities right now.
            Context.Defer().DestroyEntity(Context.GetEntity(EntityIndex));
        });
    });
}

Back to Table of Contents

Observer Processors

Observer’s execute only when a Fragment or Tag is added/removed or the owning Entity is created/destroyed. Useful as delegate Processors in a way, for initialization, or cleanup functionality.

A large portion of UAFMass operates on Observer Processors.

Types of Observer Flags

Flag

Description

AddElement

A Fragment/Tag was added to the entity.

RemoveElement

A Fragment/Tag was removed from the Entity.

CreateEntity

The whole Entity was just created.

DestroyEntity

The whole Entity was destroyed.

Add

Both AddElement or CreateEntity.

Remove

Both RemoveElement or DestroyEntity.

All

Any type of event where the Entity is Created/Destroyed, a Fragment/Tag is added/removed from it.

Back to Table of Contents

Composite Processors

Composite’s(UMassCompositeProcessor) will own a ChildPipeline and build a flat graph that drives multithreaded execution of multiple Processors.

// MassProcessor.h

struct FDependencyNode
{
    FName Name;
    UMassProcessor* Processor;
    TArray<int32> Dependencies;
};

// ---------------------------------------------------

// Handles sorting and building the graph.
virtual void BuildFlatProcessingGraph(TConstArrayView<FMassProcessorOrderInfo> SortedProcessors);

// Allow for nesting "A.B.C"
void AddGroupedProcessor(FName RequestedGroupName, UMassProcessor& SubProcessor);

This also allows for setting up a ExecuteBefore/ExecuteAfter and ExecutionPriority dependency graph sorting.

Back to Table of Contents

Auto/Dynamic Processors

Processors have two different registration paths based on if bAutoRegisterWithProcessingPhases is true/false.

What this flag does is it will automatically register the Processor with the Phase Manager. When a Processor is registered with the Phase Manager, that means it will be automatically polled and Executed every frame as each Processing Phase is executed.

The two contextual types of Processors:

  • Auto(default): Requires bAutoRegisterWithProcessingPhases = true. This also does not allow for multiple instances of this Processor class due to logical conflicts.
  • Dynamic: Requires bAutoRegisterWithProcessingPhases = false, and has to be manually added via UMassSimulationSubsystem::RegisterDynamicProcessor(). Because of this pattern, it allows for multiple instances of this Processor class since it can be spawned manually at runtime.
    • This is typically intended for Runtime-spawned Processors(like Initializer Processors ), Abstract Processors, Processors that are intended to be extended(IE: UMassCrowdLODCollectorProcessor, explained in more detail within Mass Actor's ).

You can also setup a Processor to be Active, Inactive, or a One-Shot type of Processor via an already established enum:

enum class EActivationState : uint8
{
    Inactive,
    Active,
    /** Auto-disables after the next ::CallExecute() */
    OneShot
};

Project Settings Configuration

You can override default engine configurations for Processors at the project level(incase engine behavior conflicts with project level code). This is done via Project Settings ini configuration.

Processor Project Settings Config
Found within Project Settings>Engine>Mass>Module Settings>Mass Entity>Processor CDOs
Processor Project Settings Config

Multi-Threading

(Still need to do further investigations, so this will be sharing basic info for now.)

TLDR: Mass automatically supports multi-threading and has lots of nice safeguards to help avoid doing something you shouldn’t, such as how in a Processor’s Query, you HAVE to specify if you are reading/writing data. Plus the usage of any Fragments just being structs of data that arean’t UObject’s meaning they’re not bound to the game thread makes it easy to modify data across different threads.

If you want to run only on the game thread, there is a flag you can enable bRequiresGameThreadExecution otherwise it will automatically try to run the Processor via Unreal’s Job system.


The way Mass operates across multiple threads is using Unreal’s Task Graph framework as an order of dependencies in parallel with the game thread. The dependency graph is generated for all Processors at the beginning of the processing phases and never the end.

It organizes this with its own processing queue that by default supports these restrictions:

  • Maximum Processor Workers: 8
  • Maximum Chunk Workers: 16

With this, you could, in theory, run animation operations off the game thread(with some effort).

Megafunk attempted this outside of Mass and recorded their findings here: MegafunkUtils (GitHub)

Manually running Processors

It is possible to manually force Processors to run regardless of the current Processor phase. You can see an example of this within UMassSpawnerSubsystem::DoSpawning(), where it will manually call an Initializer Processor.

When manually running Processors, you can provide context 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) to be used within the Processor during the scope of the Processing Context(FMassProcessingContext).

// .h

UCLASS()
class AMyActor : public AActor
{
    GENERATED_BODY()

    void RunSpecialProcessors(FMassEntityManager& EntityManager, 
        const TArray<TSubclassOf<UMassProcessor>>& ProcessorTypes,
        const TArray<FMassArchetypeEntityCollection>& EntityCollections,
        FMyPayloadData PayloadData);

    // A common practice is to cache any Processors you create.
    UPROPERTY(Transient)
    TArray<TObjectPtr<UMassProcessor>> CachedProcessors;
};

// ~.h
//////////////////////////////////////////////
// .cpp

void AMyActor::RunSpecialProcessors(TSharedPtr<FMassEntityManager> EntityManager, 
    const TArray<TSubclassOf<UMassProcessor>>& ProcessorTypes,
    const TArray<FMassArchetypeEntityCollection>& EntityCollections,
    const FMyPayloadData& PayloadData)
{ 
    if (!EntityManager.IsValid())
    {
        UE_LOG(LogTemp, Error, TEXT("Inputted NULL EntityManager to AMyActor(%s)"), *GetName());
        return;
    }

    // First lets collect the Processors to run based on our inputted types
    TArray<UMassProcessor*> Processors;
    for (const TSubclassOf<UMassProcessor>& Type : ProcessorTypes)
    {
        if (!Type)
        {
            continue;
        }

        // Check if we already made a Processor, and can just reuse it
        TObjectPtr<UMassProcessor>* const FoundProcessor = CachedProcessors.FindByPredicate[&Type](const UMassProcessor* Processor)
        {
            return Processor && Processor->GetClass() == Type;
        });
        
        // If we failed to find it then lets make one and cache it for later
        if (!FoundProcessor)
        {
            UMassProcessor* NewProcessor = NewObject<UMassProcessor>(this, Type);

            // Handles internally initializing it and setting it up properly for Mass usage
            NewProcessor->CallInitialize(this, EntityManager.ToSharedRef());

            CachedProcessors.Add(NewProcessor);
        } 

        Processors.Add(NewProcessor);
    }

    // Setting up our context data to pass into the Processors
    FMassProcessingContext ProcessingContext(EntityManager, 
        /*DeltaTime*/ 0.0f, 
        /*bInFlushCommandBuffer*/ false);
    
    // AuxData is probably renamed to PayloadData by the time your reading this.
    // Providing optional data for these Processors.
    ProcessingContext.AuxData = FConstStructView::Make(PayloadData);

    // Run our Processors on the inputted Entities(EntityCollections) so it has that context.
    UE::Mass::Executor::RunProcessorsView(MakeArrayView(Processors), ProcessingContext, EntityCollections);
}

// ~.cpp

Share this post

Other Mass Blog Posts