Table of Contents
Building Massive Enemy Chasing Behavior with Unreal Mass Framework
Recently I became interested in a game called Vampire Survivors, and I thought it would be fun to make a similar style game with a little bit more action game elements, instead of only moving around and auto-attacking.
The first step is to achieve massive enemies chasing the player with a self-avoidance feature. This feels like a perfect use case for Unreal Engine’s Mass Framework.
Prerequisites
Basic understanding of Mass Framework and its components, such as fragments, tags, processors, and traits.
Some good tutorials:
Basic understanding of C++
Overview
In this post, I am going to create a custom Mass Trait and Processor to make a large number of enemies follow the player.
The general idea is:
- Use a custom Trait to add the required Mass setup.
- Use a Shared Fragment to store the player location.
- Use a Processor to update each entity’s movement target.
- Let Mass Movement and Avoidance handle the actual movement and self-avoidance behavior.
In simple words, the entities will read the player location, set their move target toward the player, and then rely on Mass movement logic to move and avoid each other.
Environment Setup
Enable Plugins

Create Mass Entity Config Asset
In Unreal Editor Content Browser, create a Data Asset.

Select Mass Entity Config Asset.

Add the following traits in the Mass Entity Config Asset. The order does not matter.

For Assorted Fragments, expand it and add 2 fragments and 1 tag:
- Agent Radius Fragment
- Transform Fragment
- Mass Crowd Tag

Create a Custom Trait Using C++
Create a C++ class based on UMassEntityTraitBase.
You can name it whatever makes sense to you. In this example, I named it:
TutorialMovementTrait
What is a Trait?
My understanding is that a Trait is like a container or holder for fragments, processors, and tags.
Of course, you can write your processor in a separate C++ file. But when learning Mass, I find it easier to keep the trait and processor together first.
In simple words:
The Trait defines what data and behavior the Mass Entity should have.
Let’s create a processor in the same .h and .cpp files first.
We do not need to add any logic for the Trait class yet.
Header File
// TutorialMovementTrait.h
#pragma once
#include "CoreMinimal.h"
#include "MassEntityTraitBase.h"
#include "MassProcessor.h"
#include "TutorialMovementTrait.generated.h"
UCLASS()
class SCHOOLSURVIVORS_API UTutorialMovementTrait : public UMassEntityTraitBase
{
GENERATED_BODY()
};
UCLASS()
class SCHOOLSURVIVORS_API UTutorialMovementProcessor : public UMassProcessor
{
GENERATED_BODY()
public:
UTutorialMovementProcessor();
protected:
virtual void ConfigureQueries() override;
virtual void Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context) override;
private:
FMassEntityQuery EntityQuery;
};
CPP File
// TutorialMovementTrait.cpp
#include "TutorialMovementTrait.h"
#include "MassCommonTypes.h"
#include "MassCommonFragments.h"
#include "MassExecutionContext.h"
#include "MassMovementFragments.h"
#include "MassNavigationFragments.h"
UTutorialMovementProcessor::UTutorialMovementProcessor()
: EntityQuery(*this)
{
bAutoRegisterWithProcessingPhases = true;
ExecutionFlags = (int32)EProcessorExecutionFlags::All;
}
void UTutorialMovementProcessor::ConfigureQueries()
{
EntityQuery.AddRequirement<FTransformFragment>(EMassFragmentAccess::ReadWrite);
EntityQuery.AddRequirement<FMassMoveTargetFragment>(EMassFragmentAccess::ReadWrite);
EntityQuery.AddConstSharedRequirement<FMassMovementParameters>(EMassFragmentPresence::All);
EntityQuery.RegisterWithProcessor(*this);
}
void UTutorialMovementProcessor::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context)
{
// We will add the movement logic here later.
}
Create a Shared Fragment to Store Player Location
Remember that we use the Trait as the container of fragments and processors?
Now let’s create a Shared Fragment in the trait .h file.
The reason I use a Shared Fragment here is because all enemies need to know the same player location.
Instead of letting every entity query the player location individually, I can store the player location once and share it with all entities.
// TutorialMovementTrait.h
#pragma once
#include "CoreMinimal.h"
#include "MassEntityTraitBase.h"
#include "MassProcessor.h"
#include "TutorialMovementTrait.generated.h"
USTRUCT()
struct FFollowPlayerShareFragment : public FMassSharedFragment
{
GENERATED_BODY()
FVector PlayerLocation = FVector::ZeroVector;
};
UCLASS()
class SCHOOLSURVIVORS_API UTutorialMovementTrait : public UMassEntityTraitBase
{
GENERATED_BODY()
};
UCLASS()
class SCHOOLSURVIVORS_API UTutorialMovementProcessor : public UMassProcessor
{
GENERATED_BODY()
public:
UTutorialMovementProcessor();
protected:
virtual void ConfigureQueries() override;
virtual void Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context) override;
private:
FMassEntityQuery EntityQuery;
};
This Shared Fragment stores a single FVector value called PlayerLocation.
Initialize the Shared Fragment in the Trait
Now we need to add the Shared Fragment to the entity template.
To do this, we override the Trait’s BuildTemplate function.
Update the Trait class in the header file:
// TutorialMovementTrait.h
#pragma once
#include "CoreMinimal.h"
#include "MassEntityTraitBase.h"
#include "MassProcessor.h"
#include "TutorialMovementTrait.generated.h"
USTRUCT()
struct FFollowPlayerShareFragment : public FMassSharedFragment
{
GENERATED_BODY()
FVector PlayerLocation = FVector::ZeroVector;
};
UCLASS()
class SCHOOLSURVIVORS_API UTutorialMovementTrait : public UMassEntityTraitBase
{
GENERATED_BODY()
public:
virtual void BuildTemplate(
FMassEntityTemplateBuildContext& BuildContext,
const UWorld& World
) const override;
};
UCLASS()
class SCHOOLSURVIVORS_API UTutorialMovementProcessor : public UMassProcessor
{
GENERATED_BODY()
public:
UTutorialMovementProcessor();
protected:
virtual void ConfigureQueries() override;
virtual void Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context) override;
private:
FMassEntityQuery EntityQuery;
};
Then implement BuildTemplate in the .cpp file:
void UTutorialMovementTrait::BuildTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World) const
{
FMassEntityManager& EntityManager = UE::Mass::Utils::GetEntityManagerChecked(World);
// Add Shared Fragment for shared player location
FFollowPlayerShareFragment FollowPlayerFragment;
FollowPlayerFragment.PlayerLocation = FVector::ZeroVector;
uint32 MySharedFragmentHash = UE::StructUtils::GetStructCrc32(
FConstStructView::Make(FollowPlayerFragment)
);
UE_LOG(LogTemp, Log, TEXT("Hash is : %u"), MySharedFragmentHash);
FSharedStruct SharedFragment =
EntityManager.GetOrCreateSharedFragmentByHash<FFollowPlayerShareFragment>(
MySharedFragmentHash,
FollowPlayerFragment
);
bool bIsSharedFragmentValid = SharedFragment.IsValid();
UE_LOG(
LogTemp,
Log,
TEXT("SharedFragment is valid: %s"),
bIsSharedFragmentValid ? TEXT("true") : TEXT("false")
);
BuildContext.AddSharedFragment(SharedFragment);
}
Here, I first create an instance of FFollowPlayerShareFragment, then generate a hash for it.
After that, I use:
EntityManager.GetOrCreateSharedFragmentByHash<FFollowPlayerShareFragment>()
to create or retrieve the Shared Fragment.
Finally, I add it to the Mass entity template with:
BuildContext.AddSharedFragment(SharedFragment);
Configure the Processor Query
Now we need to tell the processor what data it needs.
In ConfigureQueries, we add the required fragments and shared fragments.
void UTutorialMovementProcessor::ConfigureQueries()
{
EntityQuery.AddRequirement<FTransformFragment>(EMassFragmentAccess::ReadWrite);
EntityQuery.AddRequirement<FMassMoveTargetFragment>(EMassFragmentAccess::ReadWrite);
EntityQuery.AddConstSharedRequirement<FMassMovementParameters>(EMassFragmentPresence::All);
EntityQuery.AddSharedRequirement<FFollowPlayerShareFragment>(
EMassFragmentAccess::ReadWrite,
EMassFragmentPresence::All
);
EntityQuery.RegisterWithProcessor(*this);
}
The important part here is:
EntityQuery.AddSharedRequirement<FFollowPlayerShareFragment>(
EMassFragmentAccess::ReadWrite,
EMassFragmentPresence::All
);
Because I want to update the player location inside the Shared Fragment, I use ReadWrite access.
If you only need to read the Shared Fragment, you can use const shared access instead.
Important Note About Accessing Shared Fragments
This was one of the confusing parts for me.
In the Execute function, Shared Fragments should be accessed inside the EntityQuery.ForEachEntityChunk block.
At first, I wrote the Shared Fragment access code outside of ForEachEntityChunk, thinking that code outside the entity loop would simply run once per processor execution.
However, that is not the correct way to access Shared Fragments in Mass.
Inside ForEachEntityChunk, Mass provides the correct FMassExecutionContext, and this is where we can safely access Shared Fragments through functions like:
Context.GetMutableSharedFragment<FFollowPlayerShareFragment>();
It is also important to understand that ForEachEntityChunk is not the same as the per-entity loop.
Inside each chunk, we can still loop through every entity manually:
for (int32 EntityIndex = 0; EntityIndex < Context.GetNumEntities(); ++EntityIndex)
{
// Per-entity logic here
}
So the general structure is:
EntityQuery.ForEachEntityChunk(EntityManager, Context, ([this](FMassExecutionContext& Context)
{
// Per-chunk logic here
// Shared Fragment access should happen here
for (int32 EntityIndex = 0; EntityIndex < Context.GetNumEntities(); ++EntityIndex)
{
// Per-entity logic here
}
}));
In simple words:
Access Shared Fragments inside
ForEachEntityChunk, then put the per-entity logic inside the entity loop.
Add Movement Logic in Execute
Now we can add the actual logic to make enemies follow the player.
In this example, I get the player location from a custom world subsystem called UMyWorldSubsystem.
You can replace this part with your own way of getting the player location.
void UTutorialMovementProcessor::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context)
{
EntityQuery.ForEachEntityChunk(EntityManager, Context, ([this](FMassExecutionContext& Context)
{
UE_LOG(LogTemp, Log, TEXT("Ready to get shared fragment!"));
FFollowPlayerShareFragment& FollowPlayerSharedFragment =
Context.GetMutableSharedFragment<FFollowPlayerShareFragment>();
UE_LOG(LogTemp, Log, TEXT("Got shared fragment!"));
UWorld* World = Context.GetWorld();
float DeltaTime = Context.GetDeltaTimeSeconds();
UMyWorldSubsystem* MySubsystem = nullptr;
if (World)
{
MySubsystem = World->GetSubsystem<UMyWorldSubsystem>();
if (MySubsystem)
{
FollowPlayerSharedFragment.PlayerLocation = MySubsystem->GetPlayerLocation();
}
}
const TArrayView<FTransformFragment> TransformsList =
Context.GetMutableFragmentView<FTransformFragment>();
const TArrayView<FMassMoveTargetFragment> NavTargetsList =
Context.GetMutableFragmentView<FMassMoveTargetFragment>();
const FMassMovementParameters& MovementParams =
Context.GetConstSharedFragment<FMassMovementParameters>();
for (int32 EntityIndex = 0; EntityIndex < Context.GetNumEntities(); ++EntityIndex)
{
FTransform& Transform = TransformsList[EntityIndex].GetMutableTransform();
FMassMoveTargetFragment& MoveTarget = NavTargetsList[EntityIndex];
FVector CurrentLocation = Transform.GetLocation();
MoveTarget.Center = FVector(
FollowPlayerSharedFragment.PlayerLocation.X,
FollowPlayerSharedFragment.PlayerLocation.Y,
0.f
);
FVector TargetVector = MoveTarget.Center - CurrentLocation;
TargetVector.Z = 0.f;
MoveTarget.DistanceToGoal = TargetVector.Size();
MoveTarget.Forward = TargetVector.GetSafeNormal();
Transform.SetScale3D(FVector(1.f));
FRotator LookAtRotator =
UKismetMathLibrary::FindLookAtRotation(CurrentLocation, MoveTarget.Center);
FRotator LerpedRotator =
UKismetMathLibrary::RLerp(
Transform.GetRotation().Rotator(),
LookAtRotator,
DeltaTime * 2.f,
true
);
Transform.SetRotation(FQuat::MakeFromRotator(LerpedRotator));
}
}));
}
One small improvement here is that the player location is only updated once per chunk:
FollowPlayerSharedFragment.PlayerLocation = MySubsystem->GetPlayerLocation();
Then each entity inside that chunk reads the same shared player location:
MoveTarget.Center = FVector(
FollowPlayerSharedFragment.PlayerLocation.X,
FollowPlayerSharedFragment.PlayerLocation.Y,
0.f
);
This is cleaner than calling GetPlayerLocation() for every entity.
Full Processor Example
Here is the full processor setup after adding the Shared Fragment requirement and movement logic.
UTutorialMovementProcessor::UTutorialMovementProcessor()
: EntityQuery(*this)
{
bAutoRegisterWithProcessingPhases = true;
ExecutionFlags = (int32)EProcessorExecutionFlags::All;
ExecutionOrder.ExecuteBefore.Add(UE::Mass::ProcessorGroupNames::Avoidance);
}
void UTutorialMovementProcessor::ConfigureQueries()
{
EntityQuery.AddRequirement<FTransformFragment>(EMassFragmentAccess::ReadWrite);
EntityQuery.AddRequirement<FMassMoveTargetFragment>(EMassFragmentAccess::ReadWrite);
EntityQuery.AddConstSharedRequirement<FMassMovementParameters>(EMassFragmentPresence::All);
EntityQuery.AddSharedRequirement<FFollowPlayerShareFragment>(
EMassFragmentAccess::ReadWrite,
EMassFragmentPresence::All
);
EntityQuery.RegisterWithProcessor(*this);
}
void UTutorialMovementProcessor::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context)
{
EntityQuery.ForEachEntityChunk(EntityManager, Context, ([this](FMassExecutionContext& Context)
{
FFollowPlayerShareFragment& FollowPlayerSharedFragment =
Context.GetMutableSharedFragment<FFollowPlayerShareFragment>();
UWorld* World = Context.GetWorld();
float DeltaTime = Context.GetDeltaTimeSeconds();
UMyWorldSubsystem* MySubsystem = nullptr;
if (World)
{
MySubsystem = World->GetSubsystem<UMyWorldSubsystem>();
if (MySubsystem)
{
FollowPlayerSharedFragment.PlayerLocation = MySubsystem->GetPlayerLocation();
}
}
const TArrayView<FTransformFragment> TransformsList =
Context.GetMutableFragmentView<FTransformFragment>();
const TArrayView<FMassMoveTargetFragment> NavTargetsList =
Context.GetMutableFragmentView<FMassMoveTargetFragment>();
const FMassMovementParameters& MovementParams =
Context.GetConstSharedFragment<FMassMovementParameters>();
for (int32 EntityIndex = 0; EntityIndex < Context.GetNumEntities(); ++EntityIndex)
{
FTransform& Transform = TransformsList[EntityIndex].GetMutableTransform();
FMassMoveTargetFragment& MoveTarget = NavTargetsList[EntityIndex];
FVector CurrentLocation = Transform.GetLocation();
MoveTarget.Center = FVector(
FollowPlayerSharedFragment.PlayerLocation.X,
FollowPlayerSharedFragment.PlayerLocation.Y,
0.f
);
FVector TargetVector = MoveTarget.Center - CurrentLocation;
TargetVector.Z = 0.f;
MoveTarget.DistanceToGoal = TargetVector.Size();
MoveTarget.Forward = TargetVector.GetSafeNormal();
Transform.SetScale3D(FVector(1.f));
FRotator LookAtRotator =
UKismetMathLibrary::FindLookAtRotation(CurrentLocation, MoveTarget.Center);
FRotator LerpedRotator =
UKismetMathLibrary::RLerp(
Transform.GetRotation().Rotator(),
LookAtRotator,
DeltaTime * 2.f,
true
);
Transform.SetRotation(FQuat::MakeFromRotator(LerpedRotator));
}
}));
}
Summary
In this post, I created a simple Mass Framework setup for massive enemies chasing the player.
The main idea is:
- Use a Trait to define the Mass setup.
- Use a Shared Fragment to store the player location.
- Use a Processor to update each enemy’s movement target.
- Access Shared Fragments inside
EntityQuery.ForEachEntityChunk. - Run per-entity movement logic inside the chunk loop.
The most important lesson I learned is:
Shared Fragments should be accessed inside
ForEachEntityChunk, not outside of it.
This helps keep the processor structure cleaner and makes it easier to separate per-chunk logic from per-entity logic.
For this type of game, where many enemies need to follow the same player target, Shared Fragments are a good way to store shared data and avoid repeating the same query for every entity.
Notes for UE 5.4
When following the tutorial, I found two small issues in UE 5.4.
The first issue happened around 6:57 in the video.
In UE 5.4, I needed to add const before UWorld& to match the function definition in UMassEntityTraitBase.
Otherwise, I got a C3668 error.
virtual void BuildTemplate(
FMassEntityTemplateBuildContext& BuildContext,
const UWorld& World
) const override;
The second issue happened around 8:21 in the video.
I needed to initialize EntityQuery in the processor constructor like this:
USimpleRandomMovementProcessor::USimpleRandomMovementProcessor()
: EntityQuery(*this)
{
bAutoRegisterWithProcessingPhases = true;
ExecutionFlags = (int32)EProcessorExecutionFlags::All;
}
Without : EntityQuery(*this), the project could compile, but the entities did not move.
Here was the comment I posted under the tutorial:

(WIP…)