Data-Driven Status Effects in UE5 C++
Build a modular status effect system using UDataAsset, subsystems, and interfaces.
Introduction
30 Second Summary
Think about video games where your character gets poisoned, speeds up from a power-up, or gains a temporary shield. Behind every one of those effects is a system that tracks what's active, counts down timers, and tells the rest of the game to react.
In this project, you will build a complete status effect system in Unreal Engine 5 C++ that applies poison, speed boost, and shield effects to any actor. The system is entirely data-driven, meaning designers can create new effects without writing a single line of code.
What You'll Build
A modular status effect architecture using Data Assets for configuration, a World Subsystem for lifecycle management, and an interface for decoupled effect application.
By the end of this project, you'll have:
- A UStatusEffectDataAsset class that lets designers define effect behavior (damage, duration, tick interval, immunity) purely from the editor.
- A UTickableWorldSubsystem that automatically manages all active effects, ticking them each frame and cleaning up expired ones.
- An IEffectable interface that any actor can implement to receive status effects, keeping the system fully decoupled from specific actor types.
- Multicast delegates that broadcast effect lifecycle events so UI, audio, and VFX systems can react without hard references.
- Secret Mission: Add stacking rules so effects can reject duplicates, refresh their duration, or stack intensity, all configured from a Data Asset enum.
Are there any prerequisites?
You should be familiar with Unreal Engine C++ fundamentals, including creating classes and using UPROPERTY/UFUNCTION macros. You also need Unreal Engine 5.7 and JetBrains Rider already installed on your machine.
Before We Start
Before We Start...
Set Up the Project and Configure Build.cs
Before you can build a status effect system, you need a working Unreal Engine project with the right module dependencies.
In this step, you will create a C++ project from the Third Person template, add the GameplayTags module to your build configuration, and define a set of Native Gameplay Tags that the rest of the system will reference.
In this step, get ready to:
- Create a Third Person C++ project named StatusEffects and open it in JetBrains Rider.
- Add the GameplayTags module dependency to your Build.cs file.
- Define native gameplay tags for effect types and immunities.
Create the StatusEffects project in Unreal Engine
Unreal Engine's Third Person template gives you a character with movement and a camera already wired up. This saves you from writing boilerplate so you can jump straight into building the effect system.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Epic Games Launcher and press Enter to open it.
- In the Epic Games Launcher, click Unreal Engine in the left sidebar.
- Click Launch next to your Unreal Engine 5.7 installation. The Unreal Project Browser window opens.
- Select the Games category on the left if it is not already selected.
- Click the Third Person template tile.
- In the Project Defaults section on the right, select C++ (not Blueprint).
- In the Project Name field, type StatusEffects.
- Click Create. The editor opens with the default Third Person level.
What just happened?
Unreal generated an entire C++ project under the hood, including a .sln solution file, a Build.cs build configuration, and a character class pre-wired with movement and camera logic.
Now open the project in JetBrains Rider so you can edit the C++ source.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Rider and press Enter to open JetBrains Rider.
- In Rider, click Open on the welcome screen.
- Navigate to your StatusEffects project folder and select the StatusEffects.sln file in the project root.
- Click Open. Rider loads the solution and indexes the project files.
Don't see the .sln file?
If the .sln file is missing, return to the Unreal Editor and go to Tools then Generate Visual Studio Project Files. This regenerates the solution file for your IDE.
Help me find my .sln file for the StatusEffects project.
Add the GameplayTags module to Build.cs
Unreal organizes features into modules. To use FGameplayTag and FGameplayTagContainer in your code, you must explicitly declare a dependency on the GameplayTags module in your build file.
- In Rider's Solution Explorer, expand Games then StatusEffects then Source then StatusEffects.
- Open StatusEffects.Build.cs.
- Find this line:
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput" });
- Add "GameplayTags" to the end of the array, so the line now looks like this:
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "GameplayTags" });
- Save StatusEffects.Build.cs.
Why add GameplayTags here?
Unreal's build system only links modules you explicitly declare. Without this line, any #include referencing gameplay tag headers would fail at link time with unresolved symbol errors.
Create native gameplay tag declarations
Native Gameplay Tags register tag names at engine startup. Unlike editor-only tags, they are always available in C++ and survive content migrations.
You will create two files: a header that declares the tags for other files to reference, and a source file that defines (registers) them with the engine.
- In Rider's Solution Explorer, right-click the StatusEffects source folder (Source/StatusEffects).
- Select Add then C++ Header File.
- Name the file StatusEffectTags and press Enter.
- Replace the file contents with the following code:
#pragma once
#include "NativeGameplayTags.h"
namespace StatusEffectTags
{
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Effect_Poison);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Effect_SpeedBoost);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Effect_Shield);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Immunity_Poison);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Immunity_Fire);
}
What does this code do?
UE_DECLARE_GAMEPLAY_TAG_EXTERN tells the compiler that each tag exists somewhere in the project. Other files can #include "StatusEffectTags.h" and reference these tags without needing to know the string values.
Wrapping them in a namespace keeps them organized and prevents naming collisions with tags from other systems.
- Save StatusEffectTags.h.
You will not see a change yet. The corresponding .cpp file provides the actual tag definitions.
- Right-click the same Source/StatusEffects folder again.
- Select Add then C++ Source File.
- Name the file StatusEffectTags and press Enter.
- Replace the file contents with the following code:
#include "StatusEffectTags.h"
namespace StatusEffectTags
{
UE_DEFINE_GAMEPLAY_TAG(Effect_Poison, "Effect.Poison");
UE_DEFINE_GAMEPLAY_TAG(Effect_SpeedBoost, "Effect.SpeedBoost");
UE_DEFINE_GAMEPLAY_TAG(Effect_Shield, "Effect.Shield");
UE_DEFINE_GAMEPLAY_TAG(Immunity_Poison, "Immunity.Poison");
UE_DEFINE_GAMEPLAY_TAG(Immunity_Fire, "Immunity.Fire");
}
What does this code do?
UE_DEFINE_GAMEPLAY_TAG creates each tag and registers it with the engine's tag system at startup. The first argument is the C++ symbol name, and the second is the hierarchical tag string (like "Effect.Poison").
Once registered, these tags appear in the editor's tag picker and can be referenced anywhere in C++ via the StatusEffectTags namespace.
- Save StatusEffectTags.cpp.
- Build the project by pressing Ctrl+Shift+B (Windows) or Cmd+Shift+B (macOS) in Rider.
- In the Build Output window at the bottom of Rider, you should see a successful build with zero errors.
Seeing linker errors?
If you see unresolved external symbol errors mentioning gameplay tags, double check that you added "GameplayTags" to PublicDependencyModuleNames in StatusEffects.Build.cs and saved the file.
If you see include errors for NativeGameplayTags.h, confirm the GameplayTags module is listed in Build.cs. The header lives inside that module.
Help me fix linker errors in my StatusEffects project.
- Return to the Unreal Editor and wait for the hot-reload notification, or close and reopen the project.
- Open Project Settings then search for Gameplay Tags in the search bar.
- Click Manage Gameplay Tags. You should see your tags listed under Effect and Immunity hierarchies.
✔️ Awesome, I've got everything!
Great. Double check you have saved all three files: StatusEffects.Build.cs, StatusEffectTags.h, and StatusEffectTags.cpp.
ⓧ I'd like to double check the full code
Here is the complete content of each file for comparison.
StatusEffectTags.h:
#pragma once
#include "NativeGameplayTags.h"
namespace StatusEffectTags
{
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Effect_Poison);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Effect_SpeedBoost);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Effect_Shield);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Immunity_Poison);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Immunity_Fire);
}
StatusEffectTags.cpp:
#include "StatusEffectTags.h"
namespace StatusEffectTags
{
UE_DEFINE_GAMEPLAY_TAG(Effect_Poison, "Effect.Poison");
UE_DEFINE_GAMEPLAY_TAG(Effect_SpeedBoost, "Effect.SpeedBoost");
UE_DEFINE_GAMEPLAY_TAG(Effect_Shield, "Effect.Shield");
UE_DEFINE_GAMEPLAY_TAG(Immunity_Poison, "Immunity.Poison");
UE_DEFINE_GAMEPLAY_TAG(Immunity_Fire, "Immunity.Fire");
}
StatusEffects.Build.cs (the modified line):
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "GameplayTags" });
Your project is configured and your tag vocabulary is locked in. Next up, you will create the Data Asset that designers use to configure each effect's behavior without touching code.
Create the Data Asset and Effect Instance
Your native tags are compiled and ready. Now you need a way for designers to define what each status effect actually does. How long does poison last? How much damage does it deal per tick? What immunities block it?
In this step, you'll create two classes that separate configuration from runtime behavior. A Data Asset stores the designer-facing settings (duration, tick rate, damage). A separate Instance class holds the live state of an effect while it's active on an actor.
In this step, get ready to:
- Create the UStatusEffectDataAsset class with all configuration fields.
- Create the UStatusEffectInstance class with tick and expiration logic.
- Create three Data Asset instances in the editor with unique configurations.
Create the Data Asset class
A UDataAsset subclass is a lightweight container for read-only data that you create and edit inside the Unreal Editor. Each instance becomes a standalone asset in the Content Browser, perfect for letting designers tweak effect values without touching code.
- In Rider, right-click the Source/StatusEffects folder in the Explorer panel.
- Select Add > Unreal Class.
- Choose DataAsset as the parent class and name it StatusEffectDataAsset.
- Replace the generated content of StatusEffectDataAsset.h with the following:
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "GameplayTagContainer.h"
#include "StatusEffectDataAsset.generated.h"
UCLASS()
class STATUSEFFECTS_API UStatusEffectDataAsset : public UDataAsset
{
GENERATED_BODY()
public:
// Which gameplay tag identifies this effect
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Effect")
FGameplayTag EffectTag;
// How long the effect lasts in seconds
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Effect")
float Duration = 5.0f;
// Seconds between each tick
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Effect")
float TickInterval = 1.0f;
// The magnitude applied each tick (damage, speed multiplier, shield amount)
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Effect")
float ValuePerTick = 10.0f;
// Tags that grant immunity to this effect
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Effect")
FGameplayTagContainer ImmunityTags;
};
What does this code do?
Each UPROPERTY(EditAnywhere, BlueprintReadOnly) field becomes an editable slot in the editor's Details panel when you select a Data Asset instance. Designers can tune values there without recompiling.
The FGameplayTag EffectTag field links this asset to one of the native tags you defined earlier. The FGameplayTagContainer ImmunityTags holds any tags that should block this effect from applying. You'll check those in the subsystem later.
- Delete the generated StatusEffectDataAsset.cpp file if Rider created one. This class is header-only since it has no implementation logic.
- Save StatusEffectDataAsset.h.
Seeing a compile error about FGameplayTag?
Make sure your StatusEffects.Build.cs includes "GameplayTags" in PublicDependencyModuleNames. Without that module dependency, the compiler can't find the tag types.
Help me fix a GameplayTag compile error in my StatusEffectDataAsset.h
✔️ Awesome, I've got everything!
Great. Make sure StatusEffectDataAsset.h is saved before moving on.
ⓧ I'd like to double check the full code
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "GameplayTagContainer.h"
#include "StatusEffectDataAsset.generated.h"
UCLASS()
class STATUSEFFECTS_API UStatusEffectDataAsset : public UDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Effect")
FGameplayTag EffectTag;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Effect")
float Duration = 5.0f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Effect")
float TickInterval = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Effect")
float ValuePerTick = 10.0f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Effect")
FGameplayTagContainer ImmunityTags;
};
Create the effect instance class
The Data Asset defines what an effect is. Now you need a class that represents a single active effect on an actor at runtime. This instance tracks how much time has elapsed, when the last tick fired, and which actor it targets.
- Create a new C++ class in Rider under Source/StatusEffects. Choose UObject as the parent and name it StatusEffectInstance.
- Replace the generated content of StatusEffectInstance.h with the following:
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "StatusEffectInstance.generated.h"
class UStatusEffectDataAsset;
UCLASS()
class STATUSEFFECTS_API UStatusEffectInstance : public UObject
{
GENERATED_BODY()
public:
// Reference to the data asset driving this effect's configuration
UPROPERTY()
TObjectPtr<UStatusEffectDataAsset> DataAsset;
// Weak reference to the actor this effect is applied to
TWeakObjectPtr<AActor> TargetActor;
// Tracks total time this effect has been active
float ElapsedTime = 0.0f;
// Tracks time since the last tick fired
float TimeSinceLastTick = 0.0f;
// Returns true when the effect has expired
bool Tick(float DeltaTime);
};
Why TWeakObjectPtr for the target?
If the target actor is destroyed while an effect is still active (e.g. an enemy dies mid-poison), a raw pointer would dangle. TWeakObjectPtr lets you check IsValid() before using the reference, avoiding crashes without preventing garbage collection.
The DataAsset field uses UPROPERTY() to keep the garbage collector from destroying the referenced asset while the instance is alive. TargetActor intentionally omits UPROPERTY() so it does not prevent actor cleanup.
- Save StatusEffectInstance.h.
- You won't see a change yet. The Tick method implementation comes next.
- Open StatusEffectInstance.cpp and replace its contents with the following:
#include "StatusEffectInstance.h"
#include "StatusEffectDataAsset.h"
bool UStatusEffectInstance::Tick(float DeltaTime)
{
// If the data asset is missing or the target actor was destroyed, expire immediately
if (!DataAsset || !TargetActor.IsValid()) return true;
ElapsedTime += DeltaTime;
TimeSinceLastTick += DeltaTime;
// Fire the tick logic when enough time has accumulated
if (TimeSinceLastTick >= DataAsset->TickInterval)
{
TimeSinceLastTick -= DataAsset->TickInterval;
UE_LOG(LogTemp, Log, TEXT("Effect %s ticked on %s for %f"),
*DataAsset->EffectTag.ToString(),
*TargetActor->GetName(),
DataAsset->ValuePerTick);
}
// Return true when the total elapsed time exceeds the configured duration
return ElapsedTime >= DataAsset->Duration;
}
What does this code do?
The Tick method is called every frame by the subsystem you'll build in the next step. It accumulates delta time into two trackers.
When TimeSinceLastTick exceeds TickInterval, the effect fires its per-tick logic (currently a log message, later you'll swap this for actual gameplay like applying damage). The method returns true when the effect's total elapsed time exceeds its duration, signalling the subsystem to remove it.
- Save StatusEffectInstance.cpp.
- Compile the project in Rider (Ctrl+Shift+B on Windows, Cmd+Shift+B on macOS). You should see a clean build with no errors.
Seeing unresolved symbol errors?
If the linker reports unresolved symbols for FGameplayTag::ToString, double-check that "GameplayTags" is in your Build.cs PublicDependencyModuleNames. Also confirm StatusEffectInstance.cpp includes StatusEffectDataAsset.h so the compiler can see the full definition of the data asset class.
Help me fix linker errors in my StatusEffectInstance
✔️ Awesome, I've got everything!
Great. Both files should be saved and the project should compile cleanly.
ⓧ I'd like to double check the full code
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "StatusEffectInstance.generated.h"
class UStatusEffectDataAsset;
UCLASS()
class STATUSEFFECTS_API UStatusEffectInstance : public UObject
{
GENERATED_BODY()
public:
UPROPERTY()
TObjectPtr<UStatusEffectDataAsset> DataAsset;
TWeakObjectPtr<AActor> TargetActor;
float ElapsedTime = 0.0f;
float TimeSinceLastTick = 0.0f;
// Returns true when the effect has expired
bool Tick(float DeltaTime);
};
#include "StatusEffectInstance.h"
#include "StatusEffectDataAsset.h"
bool UStatusEffectInstance::Tick(float DeltaTime)
{
if (!DataAsset || !TargetActor.IsValid()) return true;
ElapsedTime += DeltaTime;
TimeSinceLastTick += DeltaTime;
if (TimeSinceLastTick >= DataAsset->TickInterval)
{
TimeSinceLastTick -= DataAsset->TickInterval;
UE_LOG(LogTemp, Log, TEXT("Effect %s ticked on %s for %f"),
*DataAsset->EffectTag.ToString(),
*TargetActor->GetName(),
DataAsset->ValuePerTick);
}
return ElapsedTime >= DataAsset->Duration;
}
Create three Data Asset instances in the editor
The classes are compiled. Now you'll create actual asset files that designers would hand-edit to tune each effect's behavior. Each Data Asset instance stores a unique combination of duration, tick rate, and intensity.
- Switch to the Unreal Editor.
- In the Content Browser, right-click in an empty area.
- Select Miscellaneous > Data Asset.
- In the class picker that appears, select StatusEffectDataAsset and click Select.
- Name the new asset DA_Poison.
- Double-click DA_Poison to open it in the Details panel.
- Set EffectTag to Effect.Poison using the tag dropdown.
- Set Duration to 8.0.
- Set TickInterval to 1.0.
- Set ValuePerTick to 5.0.
- Leave ImmunityTags empty.
- Save the asset with Ctrl+S.
- Repeat the same process to create DA_SpeedBoost with these values:
- EffectTag: Effect.SpeedBoost.
- Duration: 5.0.
- TickInterval: 1.0.
- ValuePerTick: 50.0.
- ImmunityTags: empty.
- Create one more Data Asset named DA_Shield with these values:
- EffectTag: Effect.Shield.
- Duration: 10.0.
- TickInterval: 2.0.
- ValuePerTick: 20.0.
- ImmunityTags: empty.
- Save all three assets.
- Verify that all three appear in the Content Browser with their configured values visible in the Details panel when selected.
Why three different configurations?
Poison deals low damage (5.0) every second over a long duration (8.0 seconds). SpeedBoost applies a large value (50.0) frequently but wears off quickly (5.0 seconds). Shield grants moderate protection (20.0) at a slower tick (every 2.0 seconds) over the longest duration (10.0 seconds).
These variations let you test that the subsystem handles different tick rates and durations correctly in the next step.
StatusEffectDataAsset not showing in the class picker?
The class picker only shows compiled C++ classes. If StatusEffectDataAsset doesn't appear, compile the project again from Rider and wait for the editor to hot-reload. You may see a notification in the bottom-right corner of the editor when the reload finishes.
Help me get my Data Asset class to show up in the Unreal Editor class picker
Your data layer is complete. You have a reusable configuration class and a runtime instance that knows how to tick and expire. Next, you'll build the subsystem that manages these instances and makes the whole system run automatically every frame.
Build the IEffectable Interface and World Subsystem
Your Data Assets define what each effect does. Now you need a system that answers two questions: who can receive effects, and who manages their lifecycle.
In this step, you will build both pieces. An interface declares the contract for "effectable" actors. A World Subsystem owns every active effect instance, ticks them each frame, and removes them when they expire.
In this step, get ready to:
- Create the IEffectable interface so any actor can opt-in to receiving effects.
- Build a tickable World Subsystem that manages active effect lifecycles.
- Add an ApplyEffect method that checks immunity before creating instances.
Create the IEffectable interface
An interface in Unreal Engine is a contract. Any actor that implements IEffectable promises it can receive and remove effects and report its owned tags (for immunity checks).
- In Rider, create a new header file at Source/StatusEffects/EffectableInterface.h and paste the following code:
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "GameplayTagContainer.h"
#include "EffectableInterface.generated.h"
class UStatusEffectDataAsset;
// UInterface stub required by UE reflection
UINTERFACE(MinimalAPI, meta=(CannotImplementInterfaceInBlueprint))
class UEffectable : public UInterface
{
GENERATED_BODY()
};
// The actual interface contract actors implement
class STATUSEFFECTS_API IEffectable
{
GENERATED_BODY()
public:
// Called when an effect is applied to this actor
virtual void ApplyEffect(UStatusEffectDataAsset* Effect) = 0;
// Called when an effect is removed from this actor
virtual void RemoveEffect(FGameplayTag EffectTag) = 0;
// Returns tags this actor owns (used for immunity checks)
virtual FGameplayTagContainer GetOwnedTags() const = 0;
};
What does this code do?
The UINTERFACE macro with CannotImplementInterfaceInBlueprint tells UE this is a pure C++ interface. No Blueprint class can implement it, which means you can use standard C++ virtual dispatch.
The three pure virtual methods define the contract. ApplyEffect lets the actor react when it receives an effect. RemoveEffect handles cleanup. GetOwnedTags exposes the actor's current gameplay tags so the subsystem can check immunity before applying anything.
- Save EffectableInterface.h.
Seeing include errors in Rider?
Make sure GameplayTagContainer.h resolves correctly. You added GameplayTags to your Build.cs in Step 1. If the include still shows red, try building the project once to refresh Rider's indexer.
help me fix include errors in my UE5 interface header
Create the UStatusEffectSubsystem
A UTickableWorldSubsystem is automatically created by the engine when a world loads. It ticks every frame without you needing to register a tick component. This makes it ideal for managing effect durations.
- Create Source/StatusEffects/StatusEffectSubsystem.h and paste:
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/WorldSubsystem.h"
#include "StatusEffectSubsystem.generated.h"
class UStatusEffectDataAsset;
class UStatusEffectInstance;
UCLASS()
class STATUSEFFECTS_API UStatusEffectSubsystem : public UTickableWorldSubsystem
{
GENERATED_BODY()
public:
// Lifecycle overrides
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
virtual void Tick(float DeltaTime) override;
virtual TStatId GetStatId() const override;
virtual bool IsTickable() const override;
// Applies an effect to a target actor after checking interface and immunity
void ApplyEffect(AActor* Target, UStatusEffectDataAsset* EffectData);
private:
// All currently active effect instances
UPROPERTY()
TArray<TObjectPtr<UStatusEffectInstance>> ActiveEffects;
};
What does this code do?
This header declares the full API of the subsystem. UTickableWorldSubsystem provides built-in per-frame ticking. The engine instantiates it automatically when any world loads.
The UPROPERTY() on ActiveEffects is critical. Without it, garbage collection would destroy your UStatusEffectInstance objects because nothing else holds a strong reference to them.
- Save StatusEffectSubsystem.h.
- Now create Source/StatusEffects/StatusEffectSubsystem.cpp and paste the lifecycle methods:
#include "StatusEffectSubsystem.h"
#include "StatusEffectInstance.h"
#include "StatusEffectDataAsset.h"
#include "EffectableInterface.h"
void UStatusEffectSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
UE_LOG(LogTemp, Log, TEXT("StatusEffectSubsystem initialized."));
}
void UStatusEffectSubsystem::Deinitialize()
{
ActiveEffects.Empty();
Super::Deinitialize();
}
void UStatusEffectSubsystem::Tick(float DeltaTime)
{
// Iterate in reverse so removing elements doesn't skip entries
for (int32 i = ActiveEffects.Num() - 1; i >= 0; --i)
{
if (!ActiveEffects[i] || ActiveEffects[i]->Tick(DeltaTime))
{
ActiveEffects.RemoveAt(i);
}
}
}
TStatId UStatusEffectSubsystem::GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(UStatusEffectSubsystem, STATGROUP_Tickables);
}
bool UStatusEffectSubsystem::IsTickable() const
{
return !IsTemplate();
}
Why iterate in reverse?
When you remove an element from a TArray mid-iteration, all subsequent indices shift down by one. Iterating from the end avoids skipping elements after a removal.
Each instance's Tick returns true when the effect has expired. The subsystem uses that signal to clean up.
Why call Super::Initialize?
The base UTickableWorldSubsystem::Initialize sets an internal bInitialized flag. Without calling Super::Initialize(Collection), the engine's IsAllowedToTick check will permanently return false and your Tick method will never fire.
- Save StatusEffectSubsystem.cpp.
- Compile the project in Rider. You should see a clean build with no errors.
Compile error about missing GetStatId?
If you see a linker error mentioning GetStatId, make sure you included the RETURN_QUICK_DECLARE_CYCLE_STAT macro call in the .cpp file exactly as shown above. This macro both declares and returns the stat identifier in one expression.
help me fix GetStatId compile errors in my UTickableWorldSubsystem
Add the ApplyEffect method
The subsystem needs a public entry point that other systems call to apply effects. This method gates application behind two checks: does the target implement IEffectable, and is the target immune.
- In StatusEffectSubsystem.cpp, add the following method at the bottom of the file:
void UStatusEffectSubsystem::ApplyEffect(AActor* Target, UStatusEffectDataAsset* EffectData)
{
if (!Target || !EffectData) return;
if (!Target->Implements<UEffectable>()) return;
FGameplayTagContainer OwnedTags = IEffectable::Execute_GetOwnedTags(Target);
// Check immunity
if (OwnedTags.HasAny(EffectData->ImmunityTags)) return;
UStatusEffectInstance* Instance = NewObject<UStatusEffectInstance>(this);
Instance->DataAsset = EffectData;
Instance->TargetActor = Target;
ActiveEffects.Add(Instance);
IEffectable::Execute_ApplyEffect(Target, EffectData);
}
What does this code do?
- Null checks guard against invalid pointers.
- Target->Implements<UEffectable>() verifies the actor has opted into the effect system by implementing the interface.
- The immunity check compares the target's owned tags against the effect's ImmunityTags container. If any tag overlaps, the effect is rejected.
- NewObject<UStatusEffectInstance>(this) creates the instance with the subsystem as its Outer. This ensures the garbage collector tracks it correctly.
- Finally, it notifies the target actor that an effect was applied so it can react (play VFX, update UI, etc).
Why use GetWorld()->GetSubsystem<UStatusEffectSubsystem>()?
Any actor can access this subsystem via GetWorld()->GetSubsystem<UStatusEffectSubsystem>(). You never construct it manually. The engine creates exactly one instance per world, and that single instance manages all active effects across the entire game.
- Save StatusEffectSubsystem.cpp.
- Compile the project in Rider. The build should succeed with no errors.
- Open the Unreal Editor and click **Play** to start a PIE session.
- Open the **Output Log** panel (Window > Developer Tools > Output Log).
- You should see the message StatusEffectSubsystem initialized. in the log. This confirms the engine created and initialized your subsystem automatically.
Don't see the log message?
Make sure the Output Log filter is set to show LogTemp messages. Also verify you called Super::Initialize(Collection) before the UE_LOG line. Without the Super call, the subsystem never enables ticking and may not appear in the log.
help me debug why my UTickableWorldSubsystem isn't initializing
✔️ Awesome, I've got everything!
Great. Make sure all three files are saved before moving on.
ⓧ I'd like to double check the full code
Here is the complete code for all three files created in this step.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "GameplayTagContainer.h"
#include "EffectableInterface.generated.h"
class UStatusEffectDataAsset;
UINTERFACE(MinimalAPI, meta=(CannotImplementInterfaceInBlueprint))
class UEffectable : public UInterface
{
GENERATED_BODY()
};
class STATUSEFFECTS_API IEffectable
{
GENERATED_BODY()
public:
virtual void ApplyEffect(UStatusEffectDataAsset* Effect) = 0;
virtual void RemoveEffect(FGameplayTag EffectTag) = 0;
virtual FGameplayTagContainer GetOwnedTags() const = 0;
};
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/WorldSubsystem.h"
#include "StatusEffectSubsystem.generated.h"
class UStatusEffectDataAsset;
class UStatusEffectInstance;
UCLASS()
class STATUSEFFECTS_API UStatusEffectSubsystem : public UTickableWorldSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
virtual void Tick(float DeltaTime) override;
virtual TStatId GetStatId() const override;
virtual bool IsTickable() const override;
void ApplyEffect(AActor* Target, UStatusEffectDataAsset* EffectData);
private:
UPROPERTY()
TArray<TObjectPtr<UStatusEffectInstance>> ActiveEffects;
};
#include "StatusEffectSubsystem.h"
#include "StatusEffectInstance.h"
#include "StatusEffectDataAsset.h"
#include "EffectableInterface.h"
void UStatusEffectSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
UE_LOG(LogTemp, Log, TEXT("StatusEffectSubsystem initialized."));
}
void UStatusEffectSubsystem::Deinitialize()
{
ActiveEffects.Empty();
Super::Deinitialize();
}
void UStatusEffectSubsystem::Tick(float DeltaTime)
{
for (int32 i = ActiveEffects.Num() - 1; i >= 0; --i)
{
if (!ActiveEffects[i] || ActiveEffects[i]->Tick(DeltaTime))
{
ActiveEffects.RemoveAt(i);
}
}
}
TStatId UStatusEffectSubsystem::GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(UStatusEffectSubsystem, STATGROUP_Tickables);
}
bool UStatusEffectSubsystem::IsTickable() const
{
return !IsTemplate();
}
void UStatusEffectSubsystem::ApplyEffect(AActor* Target, UStatusEffectDataAsset* EffectData)
{
if (!Target || !EffectData) return;
if (!Target->Implements<UEffectable>()) return;
FGameplayTagContainer OwnedTags = IEffectable::Execute_GetOwnedTags(Target);
// Check immunity
if (OwnedTags.HasAny(EffectData->ImmunityTags)) return;
UStatusEffectInstance* Instance = NewObject<UStatusEffectInstance>(this);
Instance->DataAsset = EffectData;
Instance->TargetActor = Target;
ActiveEffects.Add(Instance);
IEffectable::Execute_ApplyEffect(Target, EffectData);
}
Your subsystem is alive, ticking, and ready to manage effects. Next up, you will wire up multicast delegates for event-driven communication and create a test actor that triggers effects on overlap.
Wire Up Delegates and Test with an Overlap Actor
Your subsystem ticks effects and cleans up expired ones automatically. The problem is that nothing outside the subsystem knows when an effect starts or ends.
In this step, you'll add multicast delegates so any system (UI, audio, VFX) can subscribe to effect lifecycle events without coupling to the subsystem's internals. Then you'll implement the IEffectable interface on the character and build a test actor that applies effects on overlap.
In this step, get ready to:
- Add multicast delegates to the subsystem and broadcast them on apply and expire.
- Implement IEffectable on the Third Person Character.
- Create an overlap actor that applies effects from a Data Asset.
Add multicast delegates to the subsystem
Right now the subsystem silently creates and removes effect instances. Other systems have no way to react to those events. Multicast delegates solve this by letting any number of listeners subscribe to a broadcast without the subsystem knowing who they are.
- In Rider, open StatusEffectSubsystem.h and replace its entire contents with the following code:
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/WorldSubsystem.h"
#include "StatusEffectSubsystem.generated.h"
class UStatusEffectDataAsset;
class UStatusEffectInstance;
// Delegate types for effect lifecycle events
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnEffectApplied, AActor*, UStatusEffectDataAsset*);
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnEffectExpired, AActor*, UStatusEffectDataAsset*);
UCLASS()
class STATUSEFFECTS_API UStatusEffectSubsystem : public UTickableWorldSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
virtual void Tick(float DeltaTime) override;
virtual TStatId GetStatId() const override;
virtual bool IsTickable() const override;
void ApplyEffect(AActor* Target, UStatusEffectDataAsset* EffectData);
// Public delegates that any system can subscribe to
FOnEffectApplied OnEffectApplied;
FOnEffectExpired OnEffectExpired;
private:
UPROPERTY()
TArray<TObjectPtr<UStatusEffectInstance>> ActiveEffects;
};
What does this code do?
DECLARE_MULTICAST_DELEGATE_TwoParams creates a delegate type that accepts two parameters. You declare two types: FOnEffectApplied and FOnEffectExpired.
The public OnEffectApplied and OnEffectExpired members are actual instances of those delegate types. Any class can call AddUObject on them to subscribe.
- Now open StatusEffectSubsystem.cpp and replace its contents with this updated implementation:
#include "StatusEffectSubsystem.h"
#include "StatusEffectInstance.h"
#include "StatusEffectDataAsset.h"
#include "EffectableInterface.h"
void UStatusEffectSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
UE_LOG(LogTemp, Log, TEXT("StatusEffectSubsystem initialized."));
}
void UStatusEffectSubsystem::Deinitialize()
{
ActiveEffects.Empty();
Super::Deinitialize();
}
void UStatusEffectSubsystem::Tick(float DeltaTime)
{
// Iterate backwards so removing elements doesn't skip entries
for (int32 i = ActiveEffects.Num() - 1; i >= 0; --i)
{
if (!ActiveEffects[i] || ActiveEffects[i]->Tick(DeltaTime))
{
// Broadcast expiration before removing
if (ActiveEffects[i])
{
OnEffectExpired.Broadcast(ActiveEffects[i]->TargetActor.Get(), ActiveEffects[i]->DataAsset);
}
ActiveEffects.RemoveAt(i);
}
}
}
TStatId UStatusEffectSubsystem::GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(UStatusEffectSubsystem, STATGROUP_Tickables);
}
bool UStatusEffectSubsystem::IsTickable() const
{
return !IsTemplate();
}
void UStatusEffectSubsystem::ApplyEffect(AActor* Target, UStatusEffectDataAsset* EffectData)
{
if (!Target || !EffectData) return;
if (!Target->Implements<UEffectable>()) return;
// Check immunity tags on the target
FGameplayTagContainer OwnedTags = IEffectable::Execute_GetOwnedTags(Target);
if (OwnedTags.HasAny(EffectData->ImmunityTags)) return;
// Create the runtime instance owned by this subsystem
UStatusEffectInstance* Instance = NewObject<UStatusEffectInstance>(this);
Instance->DataAsset = EffectData;
Instance->TargetActor = Target;
ActiveEffects.Add(Instance);
// Notify the target and broadcast to listeners
IEffectable::Execute_ApplyEffect(Target, EffectData);
OnEffectApplied.Broadcast(Target, EffectData);
}
What changed in the .cpp?
The Tick loop now broadcasts OnEffectExpired right before removing an expired instance from the array. This gives listeners the actor pointer and the Data Asset so they know which effect ended on whom.
At the end of ApplyEffect, the new OnEffectApplied.Broadcast(Target, EffectData) line fires after the instance is created and the target is notified. A UI widget could bind to this to show a status icon.
- Save both files.
- Compile in Rider (Ctrl+Shift+B). The build should succeed with no errors.
Seeing linker errors about FOnEffectApplied?
Make sure the DECLARE_MULTICAST_DELEGATE_TwoParams macros are placed outside the class body, between the forward declarations and the UCLASS() macro. They define types, not members.
help me fix delegate declaration errors in my UStatusEffectSubsystem header
Implement IEffectable on the Third Person Character
The subsystem already checks whether a target implements IEffectable before applying effects. Your character needs to fulfill that contract so the subsystem can interact with it.
- Open StatusEffectsCharacter.h in Rider.
- Add the following include near the top of the file, below the existing includes:
#include "EffectableInterface.h"
- Find the class declaration line. It looks like this:
class AStatusEffectsCharacter : public ACharacter
- Add , public IEffectable to the end of that line so it reads:
class AStatusEffectsCharacter : public ACharacter, public IEffectable
- Inside the class body (in the public section), add the three interface method declarations:
// IEffectable interface
virtual void ApplyEffect(UStatusEffectDataAsset* Effect) override;
virtual void RemoveEffect(FGameplayTag EffectTag) override;
virtual FGameplayTagContainer GetOwnedTags() const override;
- Now open StatusEffectsCharacter.cpp and add the following include at the top:
#include "StatusEffectDataAsset.h"
- Add the three method implementations at the bottom of the file:
void AStatusEffectsCharacter::ApplyEffect(UStatusEffectDataAsset* Effect)
{
// Log which effect was applied
UE_LOG(LogTemp, Log, TEXT("Character received effect: %s"), *Effect->EffectTag.ToString());
}
void AStatusEffectsCharacter::RemoveEffect(FGameplayTag EffectTag)
{
UE_LOG(LogTemp, Log, TEXT("Character lost effect: %s"), *EffectTag.ToString());
}
FGameplayTagContainer AStatusEffectsCharacter::GetOwnedTags() const
{
// Return empty container - no immunities for now
return FGameplayTagContainer();
}
What does this code do?
ApplyEffect logs the effect tag so you can confirm the subsystem successfully reached the character. RemoveEffect does the same for removals.
GetOwnedTags returns an empty tag container. This means the character currently has no immunities. In a real game, you'd populate this container dynamically based on equipped items or active buffs.
- Save both files and compile again. The build should succeed.
Compile error about missing override?
Check that the method signatures in your .h file match the interface exactly. GetOwnedTags must be marked const. ApplyEffect takes a raw pointer UStatusEffectDataAsset*, not a const reference.
help me fix IEffectable override errors on my character class
Create an AEffectZone overlap actor and test in PIE
You need a way to trigger effects in the level. An overlap actor with a sphere component is the simplest approach. When the character walks into the sphere, it asks the subsystem to apply a configurable effect.
- Create a new C++ class in Rider. Right-click Source/StatusEffects and select Add > Unreal Class, then choose Actor as the parent. Name it EffectZone.
- Replace the generated EffectZone.h with the following:
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "EffectZone.generated.h"
class USphereComponent;
class UStatusEffectDataAsset;
UCLASS()
class STATUSEFFECTS_API AEffectZone : public AActor
{
GENERATED_BODY()
public:
AEffectZone();
// The Data Asset this zone applies on overlap
UPROPERTY(EditAnywhere, Category="Effect")
TObjectPtr<UStatusEffectDataAsset> EffectToApply;
protected:
UPROPERTY(VisibleAnywhere)
TObjectPtr<USphereComponent> SphereComponent;
UFUNCTION()
void OnBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
bool bFromSweep, const FHitResult& SweepResult);
};
What does this header define?
EffectToApply is an EditAnywhere property so level designers can assign any Data Asset (Poison, SpeedBoost, Shield) per zone instance in the editor. No code changes needed per effect.
The UFUNCTION() macro on OnBeginOverlap is required for AddDynamic to bind correctly. Without it the delegate binding silently fails at runtime.
- Replace the generated EffectZone.cpp with this implementation:
#include "EffectZone.h"
#include "Components/SphereComponent.h"
#include "StatusEffectSubsystem.h"
AEffectZone::AEffectZone()
{
// Create the overlap sphere and set it as root
SphereComponent = CreateDefaultSubobject<USphereComponent>(TEXT("SphereComponent"));
RootComponent = SphereComponent;
SphereComponent->OnComponentBeginOverlap.AddDynamic(this, &AEffectZone::OnBeginOverlap);
}
void AEffectZone::OnBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
bool bFromSweep, const FHitResult& SweepResult)
{
if (!EffectToApply) return;
// Ask the subsystem to apply the configured effect
if (UStatusEffectSubsystem* Subsystem = GetWorld()->GetSubsystem<UStatusEffectSubsystem>())
{
Subsystem->ApplyEffect(OtherActor, EffectToApply);
}
}
What does this code do?
The constructor creates a USphereComponent and binds its overlap event to OnBeginOverlap. When any actor enters the sphere, the handler retrieves the UStatusEffectSubsystem from the world and calls ApplyEffect with the overlapping actor and the configured Data Asset.
The subsystem handles all the logic from here. It checks whether the actor implements IEffectable, verifies immunity tags, creates the instance, and broadcasts the delegate. The zone knows nothing about effect internals.
- Save both files and compile. Confirm no errors.
Unresolved external symbol for USphereComponent?
Make sure you have #include "Components/SphereComponent.h" at the top of EffectZone.cpp. Unreal Engine uses Include What You Use, so the header is not implicitly available.
help me fix linker errors in my EffectZone actor
- Switch to the Unreal Editor. It should prompt you to hot-reload or you can press Ctrl+Alt+F11 to trigger Live Coding.
- In the Content Browser, find AEffectZone under C++ Classes. Drag it into the level three times to create three instances.
- Select the first instance. In the Details panel, find the Effect category and set EffectToApply to DA_Poison.
- Set the second instance's EffectToApply to DA_SpeedBoost.
- Set the third instance's EffectToApply to DA_Shield.
- For each instance, select the SphereComponent in the Details panel. Set Sphere Radius to 200 so the character can walk into it easily.
- Under the Collision section of the SphereComponent, confirm that Generate Overlap Events is checked.
- Space the three zones apart in the level so you can walk into each one separately.
- Press Alt+P to Play in Editor.
- Walk the character into the Poison zone. Open the Output Log (Window > Output Log) and look for log messages.
You should see three types of log messages: "Character received effect: Effect.Poison" (from the character's ApplyEffect), periodic tick logs showing damage values, and eventually the effect expires after 8 seconds.
Walk into the SpeedBoost and Shield zones to confirm all three Data Assets trigger their respective effects with correct tick intervals and durations.
No log messages when walking into the zone?
- Select the SphereComponent on your EffectZone instance in the level. Under Collision, make sure Generate Overlap Events is enabled. Both the sphere and the character's capsule need this enabled for overlaps to fire.
- Verify the EffectToApply field is not empty on the zone instance. A null Data Asset causes the early return.
- Make sure your character class is actually AStatusEffectsCharacter (the one you added IEffectable to). Check World Settings > Game Mode to confirm the correct character class is in use.
help me debug why my EffectZone overlap isn't triggering in PIE
✔️ Awesome, I've got everything!
Great. Double check you've saved all your files and the build is clean.
ⓧ I'd like to double check the full code
Here are the final versions of the files you created or modified in this step.
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/WorldSubsystem.h"
#include "StatusEffectSubsystem.generated.h"
class UStatusEffectDataAsset;
class UStatusEffectInstance;
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnEffectApplied, AActor*, UStatusEffectDataAsset*);
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnEffectExpired, AActor*, UStatusEffectDataAsset*);
UCLASS()
class STATUSEFFECTS_API UStatusEffectSubsystem : public UTickableWorldSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
virtual void Tick(float DeltaTime) override;
virtual TStatId GetStatId() const override;
virtual bool IsTickable() const override;
void ApplyEffect(AActor* Target, UStatusEffectDataAsset* EffectData);
FOnEffectApplied OnEffectApplied;
FOnEffectExpired OnEffectExpired;
private:
UPROPERTY()
TArray<TObjectPtr<UStatusEffectInstance>> ActiveEffects;
};
#include "StatusEffectSubsystem.h"
#include "StatusEffectInstance.h"
#include "StatusEffectDataAsset.h"
#include "EffectableInterface.h"
void UStatusEffectSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
UE_LOG(LogTemp, Log, TEXT("StatusEffectSubsystem initialized."));
}
void UStatusEffectSubsystem::Deinitialize()
{
ActiveEffects.Empty();
Super::Deinitialize();
}
void UStatusEffectSubsystem::Tick(float DeltaTime)
{
for (int32 i = ActiveEffects.Num() - 1; i >= 0; --i)
{
if (!ActiveEffects[i] || ActiveEffects[i]->Tick(DeltaTime))
{
if (ActiveEffects[i])
{
OnEffectExpired.Broadcast(ActiveEffects[i]->TargetActor.Get(), ActiveEffects[i]->DataAsset);
}
ActiveEffects.RemoveAt(i);
}
}
}
TStatId UStatusEffectSubsystem::GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(UStatusEffectSubsystem, STATGROUP_Tickables);
}
bool UStatusEffectSubsystem::IsTickable() const
{
return !IsTemplate();
}
void UStatusEffectSubsystem::ApplyEffect(AActor* Target, UStatusEffectDataAsset* EffectData)
{
if (!Target || !EffectData) return;
if (!Target->Implements<UEffectable>()) return;
FGameplayTagContainer OwnedTags = IEffectable::Execute_GetOwnedTags(Target);
if (OwnedTags.HasAny(EffectData->ImmunityTags)) return;
UStatusEffectInstance* Instance = NewObject<UStatusEffectInstance>(this);
Instance->DataAsset = EffectData;
Instance->TargetActor = Target;
ActiveEffects.Add(Instance);
IEffectable::Execute_ApplyEffect(Target, EffectData);
OnEffectApplied.Broadcast(Target, EffectData);
}
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "EffectZone.generated.h"
class USphereComponent;
class UStatusEffectDataAsset;
UCLASS()
class STATUSEFFECTS_API AEffectZone : public AActor
{
GENERATED_BODY()
public:
AEffectZone();
UPROPERTY(EditAnywhere, Category="Effect")
TObjectPtr<UStatusEffectDataAsset> EffectToApply;
protected:
UPROPERTY(VisibleAnywhere)
TObjectPtr<USphereComponent> SphereComponent;
UFUNCTION()
void OnBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
bool bFromSweep, const FHitResult& SweepResult);
};
#include "EffectZone.h"
#include "Components/SphereComponent.h"
#include "StatusEffectSubsystem.h"
AEffectZone::AEffectZone()
{
SphereComponent = CreateDefaultSubobject<USphereComponent>(TEXT("SphereComponent"));
RootComponent = SphereComponent;
SphereComponent->OnComponentBeginOverlap.AddDynamic(this, &AEffectZone::OnBeginOverlap);
}
void AEffectZone::OnBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
bool bFromSweep, const FHitResult& SweepResult)
{
if (!EffectToApply) return;
if (UStatusEffectSubsystem* Subsystem = GetWorld()->GetSubsystem<UStatusEffectSubsystem>())
{
Subsystem->ApplyEffect(OtherActor, EffectToApply);
}
}
Your status effect system now broadcasts lifecycle events that any system can listen to. Next up, the Secret Mission challenges you to add stacking rules so effects can refresh, stack, or reject duplicates based purely on Data Asset configuration.
Secret mission
Add Effect Stacking Rules
Right now, walking into the same effect zone twice creates duplicate instances every time. Real games need smarter behavior. Can you add data-driven stacking rules so designers choose whether an effect rejects duplicates, refreshes its duration, or stacks intensity?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs or cloud resources to worry about.
Decide whether to keep your project files for future expansion or delete them entirely.
Resources you used:
- The StatusEffects Unreal Engine project folder on your Desktop (contains all source files, Data Assets, and level content).
Keep everything running
No action needed. Choose this if you plan to keep building on the status effect system.
- Your project is ready to extend with new Data Assets for additional effects (stun, slow, regeneration) without writing new code.
- You could integrate this system with Unreal's Gameplay Ability System (GAS) or add a UI widget that listens to your delegates.
- Consider adding networked replication by bridging the subsystem with a replicated actor for multiplayer support.
Pause - I'll come back to this later
Close Unreal Engine and JetBrains Rider to free up system memory. Your project files stay on disk and are ready to reopen later.
- Close the Unreal Editor.
- Close JetBrains Rider.
- Your StatusEffects folder remains on your Desktop. Reopen the .uproject file to pick up where you left off.
Delete - I don't want to use this again
Remove the entire project folder to reclaim disk space. This deletes all source code, Data Assets, compiled binaries, and level content.
- Close Unreal Engine and JetBrains Rider if they are still open.
- Delete the project folder by running the command for your operating system:
macOS
rm -rf ~/Desktop/StatusEffects
Windows
rmdir /s /q %USERPROFILE%\Desktop\StatusEffects
- Confirm the folder is gone by checking your Desktop.
Nice Work!
Nice Work!
You just built a production-ready status effect system in Unreal Engine 5 C++. Your architecture is data-driven, decoupled, and extensible without touching a single line of code.
You've learned how to:
- Build a UTickableWorldSubsystem that manages gameplay object lifecycles automatically, ticking active effects each frame and cleaning up expired ones.
- Create a UDataAsset-driven configuration system that designers can extend with new effects purely from the editor.
- Implement a UINTERFACE to decouple "who can receive effects" from "how effects work," and use multicast delegates so other systems can react to effect lifecycle events without hard references.
- (Secret Mission) Implemented data-driven stacking rules that change effect behavior purely from an enum in the Data Asset.
Ready to quiz yourself?