A custom modifier is a class derived from UComposableCameraModifierBase that mutates a specific node class's parameters before evaluation. Modifiers are authored in Blueprint most of the time — the base class is Blueprintable and most modifier effects are small enough that the BP overhead is irrelevant. This page covers both BP and C++ flows, and when to reach for either.
When to write a modifier¶
Write a modifier when the effect is:
- Conditional — only applied in certain gameplay states (sprinting, aiming, stunned, in debug camera).
- A parameter tweak — it mutates an existing node's fields rather than adding new per-frame work.
- Targeted at a specific node class —
FieldOfViewNode.FOV,ControlRotateNode.HorizontalSpeed,PivotDampingNode.Interpolator.Speed.
If the effect is always on, author it as a node configuration. If the effect needs new per-frame logic that no existing node provides, write a new node.
Non-transient cameras only
Modifiers are resolved on non-transient cameras only. Cinematic intros and short-lived overlays activated with bIsTransient = true skip modifier resolution entirely. If a cinematic needs to be modifier-aware, clear bIsTransient on its activation params.
The authoring model¶
There are two classes you'll work with:
UComposableCameraModifierBase — the thing that does the work. Declares NodeClass and ApplyModifier(Node). Abstract, Blueprintable.
UComposableCameraNodeModifierDataAsset — the Content-Browser wrapper that groups modifiers and adds routing metadata (priority, camera tags, enter/exit transition overrides). This is the asset gameplay code adds and removes.
You almost always author both: a modifier class (BP or C++) that defines the effect, and a data asset that references one or more modifier instances and ties them to a tag + priority.
Blueprint authoring — the common path¶
Step 1: create a modifier Blueprint.
Content Browser → right-click → Blueprint Class → search for ComposableCameraModifierBase. Name it something like BP_SprintFOVBump.
Step 2: set NodeClass on the Class Default Object.
Open the BP, find NodeClass in the Details panel, set it to the node class you want to target (e.g. FieldOfViewNode). This is a CDO-level configuration — not something you set per-instance. Also create an editable float variable FieldOfView as the new value.
Step 3: implement ApplyModifier as a BP event.
The event gets a Node reference typed as UComposableCameraCameraNodeBase. Cast it to your concrete node class and mutate its parameters:
Event ApplyModifier (Node)
→ Cast to FieldOfViewNode
→ Set FieldOfView (Target: Cast result, Wire the pin of FieldOfView variable)
That's the entire modifier. Compile and save.
Step 4: create the data asset wrapper.
Content Browser → right-click → Miscellaneous → Data Asset → pick ComposableCameraNodeModifierDataAsset. Or use the direct entry under Composable Camera System → Node Modifier Data Asset (purple thumbnail).
In the asset:
- Add your
BP_SprintFOVBumpmodifier to theModifiersarray. - Set
FieldOfViewvalue (e.g. 100). - Set
Priority(higher wins when two groups target the same node class on the same camera). - Set
CameraTags(e.g.Gameplay.ThirdPerson— the modifier applies to any camera whose type asset carries this tag). - Optionally set
OverrideEnterTransition/OverrideExitTransitionfor the reactivation blend when the modifier is added/removed.
Step 5: add and remove at runtime.
From gameplay Blueprint or C++:
AComposableCameraPlayerCameraManager* PCM =
UComposableCameraBlueprintLibrary::GetComposableCameraPlayerCameraManager(WorldContext, /*PlayerIndex*/ 0);
UComposableCameraBlueprintLibrary::AddModifier(WorldContext, PCM, SprintModifierAsset);
// ... later:
UComposableCameraBlueprintLibrary::RemoveModifier(WorldContext, PCM, SprintModifierAsset);
The PCM's UComposableCameraModifierManager handles tag matching, priority resolution, and any reactivation blend.
C++ authoring — when to reach for it¶
Most modifiers do not need C++. Reach for it when:
- The effect is performance-sensitive and runs on many cameras (the BP VM overhead per activation is not per-frame, but you still want C++ if you're allocating in the effect).
- The effect is reusable across projects and you want it shipped in a module rather than as a Blueprint asset.
- The effect needs non-trivial math that's painful to express in BP graph form.
The C++ pattern:
// SprintFOVBumpModifier.h
#pragma once
#include "CoreMinimal.h"
#include "Modifiers/ComposableCameraModifierBase.h"
#include "SprintFOVBumpModifier.generated.h"
UCLASS(meta = (DisplayName = "Sprint FOV Bump"))
class MYPROJECT_API USprintFOVBumpModifier : public UComposableCameraModifierBase
{
GENERATED_BODY()
public:
USprintFOVBumpModifier();
UPROPERTY(EditAnywhere, Category = "Sprint")
float SprintFOV = 95.0f;
protected:
virtual void ApplyModifier_Implementation(
UComposableCameraCameraNodeBase* Node) override;
};
// SprintFOVBumpModifier.cpp
#include "SprintFOVBumpModifier.h"
#include "Nodes/ComposableCameraFieldOfViewNode.h"
USprintFOVBumpModifier::USprintFOVBumpModifier()
{
NodeClass = UComposableCameraFieldOfViewNode::StaticClass();
}
void USprintFOVBumpModifier::ApplyModifier_Implementation(
UComposableCameraCameraNodeBase* Node)
{
if (UComposableCameraFieldOfViewNode* FOVNode =
Cast<UComposableCameraFieldOfViewNode>(Node))
{
FOVNode->FieldOfView = SprintFOV;
}
}
Then author a UComposableCameraNodeModifierDataAsset the same way as the BP flow, adding a default-constructed USprintFOVBumpModifier to its Modifiers array.
Priority and tag semantics (restated — these bite)¶
Two modifier groups can both target the same camera and the same node class. When that happens:
- Only the higher-priority group's modifier fires for that node class on that camera.
- Priority is per-(camera, node class) pair — a group can win on
FieldOfViewNodeand lose onControlRotateNodesimultaneously. - There is no stacking within a single
NodeClass.
If you want additive effects on the same node class (sprint +10° FOV and zoom −5° FOV), compose them inside a single modifier's ApplyModifier — don't rely on the manager to stack.
Camera tag matching uses FGameplayTagContainer::HasAny, not HasAll:
- A group tagged
Gameplay.ThirdPersonapplies to any camera whose tag set containsGameplay.ThirdPerson, regardless of what other tags the camera carries. - Empty
CameraTagsmeans "applies to all cameras" — use sparingly, it's an easy way to accidentally tweak a cutscene camera.
Reactivation transition overrides¶
Adding or removing a modifier group can cause the running camera to reactivate (because its effective modifier for some node class changed). That reactivation picks a transition through this chain:
- If adding →
OverrideEnterTransitionon the incoming group, if set. - If removing →
OverrideExitTransitionon the outgoing group, if set. - Otherwise → the five-tier resolution chain, starting from the target camera's
EnterTransition.
Use the overrides surgically — they exist so a specific modifier effect can blend differently from the camera's default, without forcing a transition table entry or a caller-supplied override at every add/remove site.
Folder placement¶
| File | Location |
|---|---|
| Modifier class header | Source/ComposableCameraSystem/Public/Modifiers/MyModifier.h |
| Modifier class source | Source/ComposableCameraSystem/Private/Modifiers/MyModifier.cpp |
For project-side modifiers, mirror the Public/Modifiers / Private/Modifiers layout in your project module.
Hot-path rule¶
ApplyModifier runs at activation time, not every frame. Allocations are allowed here — cache a reference, allocate a buffer, whatever the effect requires. But the node you mutate will be ticked every frame afterward, so don't set it up in a way that causes the node's tick to allocate. Prefer scalar assignments over array reallocations on the target node's fields.
Testing¶
- Construct the modifier asset in the editor and add it to a simple modifier data asset.
- In PIE, activate a camera whose tag set matches the modifier's
CameraTags. - Call
AddModifier/RemoveModifierfrom a console command or input mapping. - Visually verify the effect, and (optionally) assert on the node's parameter values via a Gameplay Debugger extension.
For C++ modifiers, also write a unit-style test that constructs the modifier, constructs the target node, calls ApplyModifier directly, and asserts on the mutated fields. No PCM required for that path.
Verifying with debug tools¶
Open showdebug camera during PIE while the modifier is active. Under Effective Modifiers, you should see your modifier listed:
[Camera Node] ComposableCameraFieldOfViewNode:
[Modifier] BP_SprintFOVBump_C from [Asset] DA_SprintFOVModifier with priority 10
You can also use CCS.Dump.Camera to copy the running camera's full state — including all modifier-affected node parameters — to the clipboard as plain text, which is more convenient when cross-checking parameter values against expected output. When the modifier is removed, the entry disappears from both showdebug camera and CCS.Dump.Camera. This is the quickest way to confirm tag matching and priority resolution are working as expected.
Common pitfalls¶
- Modifier doesn't apply — camera has no matching tag. Check
CameraTagon the type asset andCameraTagson the data asset. EmptyCameraTagsmeans "all cameras" (risky); a specific tag means the camera must carry it. - Modifier applies but snaps instead of blends. The camera's
EnterTransitionis null, and the data asset'sOverrideEnterTransitionis also null — so the reactivation is a hard cut. Set one of them. - Two modifiers fight — wrong one wins. Higher
Prioritywins per(camera, node class). There is no stacking. If you need additive effects on the same node class, compose them inside a singleApplyModifier. - Modifier applies to the cutscene camera.
CameraTagsis empty, so it matches everything. Set it to a specific gameplay tag. - Transient camera ignores the modifier. Transient cameras skip modifier resolution entirely. Clear
bIsTransienton the activation params if the camera needs to be modifier-aware.
*S


