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
*ParamNameisEditAnywhereon 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+1for 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.
RcsAccel — Vec3 (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. Thesaturateis load-bearing: when the projection is negative this pod stays dark and the opposite pod (its mirrorThrusterAxis) handles that half — each pod owns only one direction. - Orient the arrow/plume mesh along
ThrusterAxisonce (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
ThrusterAxisdiffering 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) — settrueon drive-start,falseon drive-stop. Use to enable/disable an emitter or gate a spawn loop. Note this can betruewhileMoveCommandis0(e.g. a stationary pivot is "driving"/maneuvering but not thrusting) — keep idle-vs-active state onbDrivingand 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
- Add the rig's emitters/audio as sibling components of
UMovementDriveComponenton the rig actor (collected once atBeginPlay). - 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. - Drive your visuals/audio from the parameter; smooth in the graph.
- For RCS thrusters, add a unit
ThrusterAxisuser param (rig-local, per-pod) and scale bysaturate(dot(ThrusterAxis, RcsAccel)); orient the mesh alongThrusterAxisonce. See theRcsAccelrecipe for the sign convention. - Decide how each consumer handles the sign of
MoveCommand/TurnSigned(abs()for symmetric, sign for directional/retro). - 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.