A modifier changes selected properties on an existing camera node class while a non-transient camera is running. Most modifier assets should use the built-in Node Type override workflow; write a custom modifier class only when a static property copy is not expressive enough.

When to write a modifier

Write a modifier when the effect is:

  • Conditional: sprinting, aiming, stunned, photo mode, or a debug camera state.
  • A parameter tweak: it changes fields on nodes that already exist.
  • Targeted at a specific node class: FieldOfViewNode.FieldOfView, ControlRotateNode.HorizontalSpeed, or PivotDampingNode.Interpolator.

If the effect is always on, author it directly in the camera type. If the effect needs new per-frame behavior, write a new node.

Non-transient cameras only

Modifiers are resolved on non-transient cameras only. Short-lived cameras activated with bIsTransient = true skip modifier resolution. If a cinematic needs to be modifier-aware, clear bIsTransient on its activation params.

The authoring model

There are two runtime objects involved:

UComposableCameraNodeModifierDataAsset is the Content Browser asset gameplay code adds and removes. It owns routing metadata: ApplyMode, priority, camera tag query, transition overrides, and the list of modifier entries.

UComposableCameraModifierBase is each entry in the asset's Modifiers array. It either uses Node Type override mode or delegates to a custom modifier class.

Node Type override mode

Use this path first. It is designer-friendly, serializes cleanly, and supports both camera reactivation and in-place value transitions.

  1. Content Browser -> right-click -> Composable Camera System -> Node Modifier Data Asset.
  2. Add an entry to Modifiers.
  3. Leave Use Custom Modifier Class disabled.
  4. Choose the node class in Node Template.
  5. Check the properties to override.
  6. Edit the checked values on the template.
  7. Set Priority and CameraTagQuery.

Only editable, non-transient, non-deprecated top-level node properties are eligible. Properties marked with NoModifierOverride are intentionally hidden.

Apply mode

The asset's ApplyMode controls how an effective-set change reaches the live camera.

ReactivateCamera is the default. Adding or removing the asset reconstructs the current type-asset camera, applies the modifier values during construction, and blends from the old pose to the new pose. Use OverrideEnterTransition and OverrideExitTransition when this modifier should use a different camera-pose blend than the camera type's normal transition chain.

ModifyExistingInstance keeps the same camera and node instances alive. The runtime state writes checked Node Type properties into the live node and can blend supported values through UComposableCameraModifierTransitionBase. Use OverrideEnterValueTransition, OverrideReplaceValueTransition, and OverrideExitValueTransition for these value blends. Leave them unset for immediate changes, except that a null Replace preserves the legacy priority handoff rule.

In-place modification is intended for properties that are safe to retune on a live node. It does not add nodes, remove nodes, rebuild the RuntimeDataBlock, or run arbitrary Blueprint modifier code every frame.

Camera tag queries

Camera types carry CameraTags. Modifier assets carry CameraTagQuery.

  • Empty CameraTagQuery matches every camera.
  • Use ANY, ALL, and NONE query groups for gameplay-specific routing.
  • Legacy modifier CameraTags values migrate to an ANY query during load.

For example, a sprint FOV modifier might match cameras with Gameplay.ThirdPerson, while an ADS weapon modifier might match Gameplay.ADS.

Custom Modifier Class mode

Use a custom class when the effect needs procedural logic instead of a checked property copy.

Blueprint

  1. Create a Blueprint class derived from ComposableCameraModifierBase.
  2. Set NodeClass on the class defaults to the node class you want to mutate.
  3. Add editable variables for the values your modifier needs.
  4. Implement ApplyModifier.
  5. In the data asset entry, enable Use Custom Modifier Class and assign an instanced object of your Blueprint class.

Example Blueprint flow:

Event ApplyModifier(Node)
  -> Cast to FieldOfViewNode
  -> Set FieldOfView from SprintFieldOfView

ApplyModifier runs when the camera is built or reactivated. It is not a per-frame callback.

C++

// 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 add an instance of USprintFOVBumpModifier to a UComposableCameraNodeModifierDataAsset.

Runtime add and remove

From Blueprint or C++, call AddModifier and RemoveModifier on UComposableCameraBlueprintLibrary, or call the same operations directly on an AComposableCameraPlayerCameraManager.

The PCM's UComposableCameraModifierManager handles tag-query matching, priority resolution, reactivation, and in-place value ownership.

Priority and stacking

Two modifier assets can both target the same camera and the same node class. Node Type entries elect winners per checked property, so disjoint properties can compose on one node class while overlapping properties still use priority.

Custom Modifier Class entries stay on a whole-node lane: if a custom modifier wins a node-class bucket, it is not composed with Node Type entries for that class.

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 and Private/Modifiers layout in your project module.

Testing

  1. Create the modifier data asset in the editor.
  2. In PIE, activate a non-transient camera whose CameraTags satisfy the asset's CameraTagQuery.
  3. Call AddModifier and RemoveModifier from a console command, input mapping, or gameplay event.
  4. Check showdebug camera, CCS.Debug.Panel 1, or CCS.Dump.Camera to confirm the effective modifier and live node values.

For C++ custom modifier classes, also write a unit-style test that constructs the modifier, constructs the target node, calls ApplyModifier directly, and asserts on the mutated fields.

Common pitfalls

  • Modifier does not apply. The camera may be transient, the CameraTagQuery may not match, or another active asset may have higher priority for the same checked property.
  • Modifier snaps. A ReactivateCamera asset has no modifier transition and the camera transition chain resolves to null. Set a camera transition or use ModifyExistingInstance with a value transition for property-only tuning.
  • In-place modifier appears ignored. The property may not be eligible for in-place ownership or continuous blending. Use a checked Node Type override on an editable top-level node property.
  • Two modifiers fight. Higher Priority wins per checked property. Split effects across disjoint Node Type properties when they should compose.
  • A cutscene camera changes unexpectedly. Empty CameraTagQuery matches every camera. Add a stricter query or mark the cutscene activation transient.

See Also