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 classFieldOfViewNode.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.

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, New Value: 95.0)

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_SprintFOVBump modifier to the Modifiers array.
  • 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 / OverrideExitTransition for 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 FieldOfViewNode and lose on ControlRotateNode simultaneously.
  • 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.ThirdPerson applies to any camera whose tag set contains Gameplay.ThirdPerson, regardless of what other tags the camera carries.
  • Empty CameraTags means "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:

  1. If adding → OverrideEnterTransition on the incoming group, if set.
  2. If removing → OverrideExitTransition on the outgoing group, if set.
  3. 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

  1. Construct the modifier asset in the editor and add it to a simple modifier data asset.
  2. In PIE, activate a camera whose tag set matches the modifier's CameraTags.
  3. Call AddModifier / RemoveModifier from a console command or input mapping.
  4. 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 showdebug

Open showdebug composablecamera 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

When the modifier is removed, the entry disappears. This is the quickest way to confirm tag matching and priority resolution are working as expected.

Worked example: sprint FOV bump

This section walks through the full end-to-end modifier authoring flow — a sprint FOV bump that widens the active gameplay camera's field of view to 95° when the player sprints, blending smoothly in both directions.

Prerequisites

You need a gameplay camera with a FieldOfViewNode in its chain (the Follow Camera tutorial produces one). The camera's type asset must carry a CameraTag of Gameplay.ThirdPerson.

Step 1 — create the modifier class (Blueprint)

Content Browser → right-click → Blueprint Class → search for ComposableCameraModifierBase. Name it BP_SprintFOVBump.

Open it. In the Class Defaults panel, set NodeClass to ComposableCameraFieldOfViewNode. Override Apply Modifier in the Event Graph:

Event ApplyModifier (Node)
  → Cast to ComposableCameraFieldOfViewNode
      → Set FieldOfView = 95.0

Compile and save.

Parameterize the FOV

For a more reusable modifier, add a UPROPERTY(EditAnywhere) variable SprintFOV (default 95.0) on the Blueprint and use it instead of a hardcoded value. Then each data asset that references the modifier can set a different sprint FOV without duplicating the class.

Step 2 — create the data asset wrapper

Content Browser → right-click → Composable Camera System → Node Modifier Data Asset. Name it DA_SprintFOVModifier.

Field Value Why
Modifiers One entry: BP_SprintFOVBump The modifier class from step 1
Priority 10 Wins over any lower-priority group also targeting FieldOfViewNode. Typical gameplay modifiers live in the 1–20 range.
CameraTags Gameplay.ThirdPerson Only applies to cameras whose type asset carries this tag. Keeps cutscene cameras unaffected.
OverrideEnterTransition (optional) An InertializedTransition with TransitionDuration = 0.25 A quick blend in when the modifier activates.
OverrideExitTransition (optional) An InertializedTransition with TransitionDuration = 0.35 A slightly longer blend out — the return to normal FOV feels smoother if it's slower than the snap in.

Step 3 — wire up sprint input

Open your character (or player controller) Blueprint:

On Sprint Started
  └─> Get Composable Camera Player Camera Manager (Index 0) ─┐
  └─> Add Modifier (PCM: ↑, Modifier Asset: DA_SprintFOVModifier)

On Sprint Ended
  └─> Get Composable Camera Player Camera Manager (Index 0) ─┐
  └─> Remove Modifier (PCM: ↑, Modifier Asset: DA_SprintFOVModifier)

Step 4 — play and verify

Enter PIE and sprint. You should see a smooth inertialized blend from the default FOV to 95° as the modifier kicks in, the wider FOV hold for the duration of the sprint, and a smooth blend back when sprint ends.

Common pitfalls

  • Modifier doesn't apply — camera has no matching tag. Check CameraTag on the type asset and CameraTags on the data asset. Empty CameraTags means "all cameras" (risky); a specific tag means the camera must carry it.
  • Modifier applies but snaps instead of blends. The camera's EnterTransition is null, and the data asset's OverrideEnterTransition is also null — so the reactivation is a hard cut. Set one of them.
  • Two modifiers fight — wrong one wins. Higher Priority wins per (camera, node class). There is no stacking. If you need additive effects on the same node class, compose them inside a single ApplyModifier.
  • Modifier applies to the cutscene camera. CameraTags is empty, so it matches everything. Set it to a specific gameplay tag.
  • Transient camera ignores the modifier. Transient cameras skip modifier resolution entirely. Clear bIsTransient on the activation params if the camera needs to be modifier-aware.

See also: Modifiers Catalog for the exact field semantics; Concepts → Modifiers for the full lifecycle and resolution model; Custom Nodes if the effect needs per-frame work rather than a parameter tweak.