0/0
CRADL // DOCUMENTATION
PORTAL DEV WIKI MOVEMENT_DRIVE_CHANNELS
UTC 00:00:00
◀ RETURN
MOVEMENT_DRIVE_CHANNELS.md 1532 words ~7 min read Updated 2026-07-03

CRADL Movement-Drive Channels

How to wire a cosmetic rig's Niagara and Audio (MetaSound) components to consume the values produced by UMovementDriveComponent. This is the authoring contract for FX/audio artists: create user parameters / MetaSound inputs with the names below, and the component blanket-pushes the movement signals into them every throttled tick. Nothing else is required — the component finds your emitters automatically.

Code-side companions: MovementDriveComponent.h, the two provider interfaces MovementStateProvider.h (result) and MovementCommandProvider.h (intent), and the producers UCradlCharacterMovementComponent + UMovementIntentComponent.

How it works (30 seconds)

Drop UMovementDriveComponent onto the rig BP (the AVisualsRigActor spawned with the pawn) alongside your Niagara + Audio components. On BeginPlay it walks up to the owning pawn, resolves the movement providers, collects every sibling Niagara/Audio component once, and each tick (~30 Hz, dead-banded) pushes the channels into all of them.

  • You opt in by name. Expose a Niagara user parameter or MetaSound input whose name matches a channel below. An unknown/absent name is a harmless no-op, so unrelated rig FX (combat beats, idle glow) are untouched.
  • All channel names are editable per rig. Every *ParamName is EditAnywhere on the component instance — rename them in the rig BP if a system needs different names; the defaults below are what ships.
  • Smoothing is yours. The component pushes coarse ~30 Hz samples and dead-bands repeats. Slew/interpolate in the Niagara or MetaSound graph, not in C++.
  • No replication, ever. Every value is derived per-peer from already-replicated velocity/rotation (or local-only input). Each client rebuilds its own rig. No-ops on a dedicated server.

Channel Quick Reference

Param (default name) Type Range Niagara Audio Source Peers
MoveSpeed float [0,1] SetVariableFloat SetFloatParameter result speed magnitude every peer
TurnRate float [0,1] result yaw rate, unsigned every peer
MoveCommand float [-1,1] primary engine throttle — intent (local) / forward movement (remote) every peer
TurnSigned float [-1,1] rotational RCS — signed yaw rate (handedness) every peer
RcsAccel Vec3 unit-clamped SetVariableVec3 (no vector param) translational RCS — velocity change, rig-local every peer
bDriving bool SetVariableBool drive on/off edge every peer
OnStart trigger SetTriggerParameter drive-started pulse every peer
OnStop trigger SetTriggerParameter drive-stopped pulse every peer

The continuous channels are pushed in TickComponent; the edge channels (bDriving / OnStart / OnStop) fire once on the moving↔idle (or commanding↔not) transition.


Continuous channels

MoveSpeed — float [0,1]

Normalized translation speed: |horizontal velocity| / live max speed, so 1.0 is this pawn's current full speed regardless of loadout. The continuous "how much am I translating" intensity. Always available (computed from replicated velocity on every peer). Use for main-engine glow/plume length, rolling-thrust audio level, dust/wake intensity.

TurnRate — float [0,1] (unsigned)

Normalized yaw rate: |yaw rate| / ReferenceMaxTurnRateDeg. Unsigned on purpose — a turn in either direction reads the same. Use for a symmetric turn cue (banking dust both sides, a turn-strain whine). For which way the pawn is turning, use TurnSigned.

MoveCommand — float [-1,1]the primary engine throttle

Signed forward thrust: +1 full forward, -1 full reverse, 0 no thrust. This is the channel the primary engine reads. It is sourced by locality, exactly like the drive edge:

  • Local player → movement intent (IMovementCommandProvider::GetCommandThrottle): the signed WASD forward axis, or +1 for a point command (click-to-move / chase / drag-steer — always full-forward intent). Leads the actual motion, so the local pilot's engine responds the instant they ask — including straining at full while shoved against a wall.
  • Any other peer → resolved forward movement (IMovementStateProvider::GetForwardSpeedFraction, velocity·forward / maxSpeed). Tracks realized motion. This is why a remote ship still shows forward/reverse thrust without replicating intent.

A pure turn (A/D, no W/S) reads 0 here — it's a maneuver, not thrust. Map negative values to a reverse/retro plume if you have one; otherwise the engine author can use abs(MoveCommand) for symmetric intensity and ignore the sign.

TurnSigned — float [-1,1]rotational RCS

Signed yaw rate (same magnitude as TurnRate, sign preserved): positive/negative encode turn handedness. This is what drives the lateral reaction-control pods so the correct side fires for the turn direction. It keys on yaw rate (lit while turning), not on change — so a sustained turn keeps the pods firing. Use the sign to pick which pod and/or to flip the plume direction.

RcsAccelVec3 (unit-clamped) — translational RCS

The actual velocity change (d(velocity)/dt, normalized by ReferenceMaxAccel and clamped to unit length), expressed in the rig's local frame: X = forward, Y = right, Z = up. This is what the translational reaction-control thrusters fire against — it is result-only and ignores command intent entirely, so it captures all motion changes (thrusting, braking, course-correction, knockback, collisions) and is quiet during steady cruise.

Authoring recipe (one reusable asset, configured per pod): a thruster has a fixed orientation — it doesn't swing to follow the acceleration; what changes is how hard it fires. Give the Niagara asset a second user parameter, ThrusterAxis (Vector, unit length, rig-local), set per placed instance to the direction that pod points (e.g. (0,-1,0) for a left pod). Fire intensity is the projection of the acceleration onto that fixed axis:

fire = saturate( dot( ThrusterAxis, RcsAccel ) )
  • Scale the emitter by fire. The saturate is load-bearing: when the projection is negative this pod stays dark and the opposite pod (its mirror ThrusterAxis) handles that half — each pod owns only one direction.
  • Orient the arrow/plume mesh along ThrusterAxis once (Initialize Particle, or model/rotate it pre-pointed) — it's the pod's static mounting, not runtime-driven.
  • Reuse: one Niagara system, dropped per pod, with only ThrusterAxis differing in the component's override parameters.

Sign convention: the recipe above fires the pod when acceleration points along ThrusterAxis. If you instead author ThrusterAxis as the plume direction (plume opposes thrust, like real RCS), negate one side — dot(ThrusterAxis, -RcsAccel) or flip the axis. Pick one convention and keep it consistent across pods; it's obvious which you want the moment you watch it fire.

RcsAccel is Niagara-only: UAudioComponent has no vector parameter, so audio components skip this channel. If you want RCS audio, drive a scalar from your own per-thruster dot in the graph, or expose a separate scalar channel.

Frame note: the vector is rotated into the rig actor's local space before it's pushed (the rig can be yaw-offset from the pawn, e.g. attached to a rotated mesh). So author your thruster firing axes relative to the rig, not the pawn.


Edge channels (drive on/off)

A single on/off edge, sourced by locality the same way MoveCommand is: the command edge (intent) for the local player, the movement edge (velocity, hysteresis + settle) otherwise. On the transition the component fires:

  • bDriving (Niagara bool) — set true on drive-start, false on drive-stop. Use to enable/disable an emitter or gate a spawn loop. Note this can be true while MoveCommand is 0 (e.g. a stationary pivot is "driving"/maneuvering but not thrusting) — keep idle-vs-active state on bDriving and plume length on the throttle.
  • OnStart / OnStop (MetaSound triggers) — one-shot pulses for the engine spool-up / spool-down. Wire these to MetaSound trigger inputs; the ramp itself lives in the MetaSound graph.

The edge is debounced in the producer (hysteresis + a settle window for the movement source), so a transient velocity dip won't machine-gun OnStart/OnStop.


Tunables

On UMovementDriveComponent (per rig instance):

Property Default Effect
ChannelDeadband 0.01 Skip a push when within this of the last pushed value (vector channels use it as a delta length). Higher = fewer pushes, coarser steps.
*ParamName see table Rename any channel to match a system's own param naming. None disables that channel.

On UCradlCharacterMovementComponent (governs the normalization of the produced values):

Property Default Effect
ReferenceMaxTurnRateDeg 180 Yaw rate (deg/s) that maps to 1.0 for TurnRate / TurnSigned.
ReferenceMaxAccel 2048 Acceleration (cm/s²) that maps to unit length for RcsAccel. Tune so a normal full-thrust start/stop reads near 1; collisions clamp.
FallbackMaxSpeed 600 Denominator for MoveSpeed / MoveCommand when live max speed is ~0.

Authoring checklist

  1. Add the rig's emitters/audio as sibling components of UMovementDriveComponent on the rig actor (collected once at BeginPlay).
  2. On each emitter, create a user parameter matching the channel name and type (float, or Vector for RcsAccel). On MetaSound sources, create matching float inputs and trigger inputs.
  3. Drive your visuals/audio from the parameter; smooth in the graph.
  4. For RCS thrusters, add a unit ThrusterAxis user param (rig-local, per-pod) and scale by saturate(dot(ThrusterAxis, RcsAccel)); orient the mesh along ThrusterAxis once. See the RcsAccel recipe for the sign convention.
  5. Decide how each consumer handles the sign of MoveCommand / TurnSigned (abs() for symmetric, sign for directional/retro).
  6. Mismatched or absent names are silent no-ops — safe to leave channels unconsumed.

Replication

None of these channels replicate. The result signals (MoveSpeed, TurnRate, MoveCommand on remote peers, TurnSigned, RcsAccel) are derived per-peer from velocity/rotation that already replicate via bReplicateMovement; the intent source (MoveCommand on the local player) is local-only by design. The only thing a remote observer does not see is the intent/result divergence (e.g. thrusters straining into a wall) — deliberately left local.