Unreal Engine's Mass: Processors (Systems)
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.
Flag | When it’s active | Description |
|---|---|---|
None | N/A | No execution context. Unset values. |
Standalone |
| A running game application without networking, single-player/standalone/PIE/packaged game. |
Server |
| World is a server either dedicated server or listen server. |
Client |
| 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.
| A live editor preview world is being used, not PIE. |
AllNetModes |
| Any actual gameplay world regardless of net role. |
AllWorldModes |
| Any type of world except for the editor itself which isn’t a world. |
All |
| 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.
Class | Executes when? | Work is done via | Description/Notes |
|---|---|---|---|
UMassProcessor | Every frame within its phase. | By overriding | Regular Processors. |
UMassObserverProcessor | Only on an observed event. | By overriding | 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. |
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.
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.
// .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));
});
});
}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.
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. |
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.
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 viaUMassSimulationSubsystem::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 ).
- This is typically intended for Runtime-spawned Processors(like
Initializer Processors
),
Abstract Processors, Processors that are intended to be extended(IE:
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.
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