Skip to main content
Technical Deep Dive

Animating a Space Salvager: UE5 Animation Architecture

Linked anim graphs, motion warping, and the solo dev animation pipeline

Aaron - Sweet Pine Studios | | 1776 words

MEMO: Animation System Training Materials
Salvage Solutions Inc. is pleased to provide educational content on vessel crew animation protocols. Associates who understand how their movements are calculated may experience improved spatial awareness. This is statistically correlated with extended career duration. Training is optional but recommended.

  • S.A.L.L.I.

Building a character animation system for a multiplayer horror game presents unique challenges. Your local player only sees first-person arms, but everyone else sees your third-person body sliding around like you're ice skating. That's the problem I needed to solve for Deep Haul.

This post walks through the animation architecture I built for Deep Haul's salvagers: a GAS-driven system using UE5's Animation Layer Interface, motion warping, and a simplified version of Lyra's cardinal direction approach. All with minimal C++ and maximum editor flexibility.

The Multiplayer Animation Problem

Deep Haul is a 2-4 player co-op extraction game. The local player sees first-person arms. But crucially, every other player sees your third-person mesh. Characters face wherever they aim (camera yaw) but can move in any direction relative to that facing - strafing, backpedaling, diagonal movement.

With a traditional 1D blend space driven only by speed, a strafing player plays the forward run animation while sliding sideways. It looks terrible and is immediately noticeable from another player's perspective.

The solution: A 2D blend space using GroundSpeed and Direction to select directional animations... except that's what I thought I'd do. Then I looked at how Lyra actually handles this.

What Lyra Actually Does (And Why)

Epic's Lyra starter game uses cardinal direction selection with Orientation Warping, not blend spaces. From Epic's own AnimBP comments:

"We only author Forward/Back/Left/Right directions and rely on warping to fill in the gaps."

Here's why that's genius:

  1. Only need 4 animations (per movement speed) instead of 8+ for a full 2D blend space
  2. Orientation Warping rotates the lower body to match actual movement direction
  3. Stride Warping adjusts leg stride length to match actual speed
  4. The warping only needs to cover ≤45° from the nearest cardinal, well within its comfort range

Lyra calculates CardinalDirectionFromVelocity by mapping movement angle to 90° quadrants:

  • Forward: -45° to +45°
  • Right: +45° to +135°
  • Backward: >+135° or <-135°
  • Left: -135° to -45°

For Deep Haul, I'm using a simplified version of this approach. No Start/Stop states with distance matching (yet), no Pivot states, no additive leans. Just: cardinal selection + orientation warping + stride warping. That's the beta scope.

Architecture Overview

The system has three layers working together:

1. C++ Base Class: UDeepHaulAnimInstance

Extends UAnimInstance and handles:

  • GAS tag callbacks that automatically update animation properties when gameplay tags change
  • Locomotion math in NativeUpdateAnimation (ground speed, direction, cardinal mapping)
  • Zero Blueprint event graph pollution. All data flows through properties

Key properties exposed to Blueprint:

// Locomotion (updated in NativeUpdateAnimation)
UPROPERTY(BlueprintReadOnly, Category = "DeepHaul|Locomotion")
float GroundSpeed;

UPROPERTY(BlueprintReadOnly, Category = "DeepHaul|Locomotion")
float Direction; // -180 to 180, movement angle relative to facing

UPROPERTY(BlueprintReadOnly, Category = "DeepHaul|Locomotion")
ECardinalDirection CardinalDirection; // Forward=0, Right=1, Back=2, Left=3

UPROPERTY(BlueprintReadOnly, Category = "DeepHaul|Locomotion")
bool bShouldJog; // true when speed > jog threshold

// GAS-driven state (updated via tag callbacks)
UPROPERTY(BlueprintReadOnly, Category = "DeepHaul|GAS")
bool bIsDead;

UPROPERTY(BlueprintReadOnly, Category = "DeepHaul|GAS")
bool bIsSprinting;

UPROPERTY(BlueprintReadOnly, Category = "DeepHaul|GAS")
bool bIsCarrying;

The tag bindings are registered directly in C++ via ASC->RegisterGameplayTagEvent() callbacks in InitializeTagBindings(). When State.Dead appears on the player's Ability System Component, bIsDead automatically becomes true. No Blueprint setup needed.

2. Blueprint AnimBP: ABP_Drifter_Base

Parent class set to UDeepHaulAnimInstance. Contains the main AnimGraph:

Locomotion State Machine
  ├─ Idle: Play MM_Unarmed_Idle_Ready
  ├─ Moving:
  │    Blend Poses by Int (CardinalDirection: 0=Fwd, 1=Right, 2=Bwd, 3=Left)
  │      each pin → Blend Poses by Bool (bShouldJog: walk or jog variant)
  │         ↓
  │    Orientation Warping  ← Direction
  │         ↓
  │    Stride Warping  ← GroundSpeed
  └─ Falling: Play MM_Unarmed_Fall_Loop
       ↓
Layered Blend Per Bone #1 (spine_01)  ← weight: bHasActiveItem
  Base:  Locomotion output (full body)
  Layer: UpperBody_ItemHold (ALI) → Slot (DefaultGroup.UpperBody)
       ↓
Layered Blend Per Bone #2 (spine_01)  ← weight: bIsCarrying
  Base:  output from above
  Layer: Carry pose (two-handed heavy object)
       ↓
Output Pose

Key design decisions:

Why cardinal selection comes before warping: The Blend Poses by Int node selects one of four cardinal animations based on CardinalDirection. Each cardinal pin then has a Blend Poses by Bool that picks walk vs. jog based on bShouldJog. This gives 8 total animations (4 cardinals × 2 speeds). The output goes to Orientation Warping, which smoothly rotates the lower body for diagonal movement.

Why the slot sits between ALI and Layered Blend: Item action montages (like the crowbar swing) need to play through the upper body item hold layer, not under it. The slot is placed after the UpperBody_ItemHold ALI call so montages correctly override the base hold pose.

Why carry override comes last: When carrying a heavy object (corpse, salvage crate), the two-handed carry pose must override any item hold animation. The bIsCarrying blend happens after the item layer blend, giving carry poses final say over the upper body.

3. Animation Layer Interface + Linked AnimBPs

The ALI defines two functions:

  • UpperBody_ItemHold: Looping hold pose for equipped items
  • FullBody_ItemAction: Reserved for potential full-body item actions

Each item type has a linked AnimBP:

  • ABP_Layer_Unarmed (default, empty upper body)
  • ABP_Layer_Crowbar (two-handed grip hold pose)
  • ABP_Layer_Scanner (one-handed device hold)
  • etc.

When the player equips an item, ADeepHaulCharacter::UpdateAnimLayers() reads the AnimLayerClass from the item's UItemDataAsset and calls LinkAnimClassLayers() to swap in the correct linked AnimBP.

Motion Warping Setup

Orientation Warping and Stride Warping are nodes from UE5's Animation Warping plugin (enabled by default). They sit in the AnimGraph after the cardinal selection:

Orientation Warping settings:

  • Locomotion Angle: connected to Direction property
  • Mode: OrientationWarping (rotates lower body, counter-rotates spine)
  • IK Foot Root Bone: root (Lyra Manny skeleton convention)
  • IK Foot Bones: foot_l, foot_r

Stride Warping settings:

  • Locomotion Speed: connected to GroundSpeed property
  • Min/Max Speed: matches authored animation speeds (walk ~200, jog ~500)
  • Stride Direction: Auto

That's it. The warping nodes do the heavy lifting. I just feed them the right data from C++.

Why No Death State in the State Machine?

Deep Haul uses full ragdoll on death. Once EnableRagdoll activates, physics drives the mesh and the AnimBP output is irrelevant. A "Death" state would play for a split-second before ragdoll kicks in, if it's visible at all.

Instead: bIsDead tag → ragdoll activates → AnimBP stops mattering. No wasted state machine complexity.

The Hand Grip Offset System

Separate from animation layers but complementary: each UItemDataAsset has a HandGripOffset transform property. When an item spawns and attaches to the HandGrip_R socket, this offset is applied.

This solves the "tool doesn't sit in hand correctly" problem without requiring per-item socket definitions on the skeleton. The crowbar needs a different grip point than the scanner. Just set the offset in the data asset.

// In ADeepHaulCharacter::UpdateHeldItem()
if (ItemData->HandGripOffset.GetLocation() != FVector::ZeroVector ||
    ItemData->HandGripOffset.GetRotation() != FQuat::Identity)
{
    FTransform SocketTransform = GetMesh()->GetSocketTransform(TEXT("HandGrip_R"));
    FTransform OffsetTransform = ItemData->HandGripOffset * SocketTransform;
    WorldItem->SetActorTransform(OffsetTransform);
}

Animation layers control what the arms do. Hand grip offsets control where the tool sits. Together, they make items look correct in hand.

Solo Dev Animation Pipeline

Here's my actual workflow for getting animations into the game:

  1. Retarget Lyra animations to Drifter skeleton in UE5's IK Retargeter (done, 10 locomotion animations retargeted)
  2. Set Force Root Lock on all locomotion sequences to prevent root motion sliding
  3. Place AN_Footstep anim notifies at foot contact frames for footstep audio
  4. Author custom item hold poses in Cascadeur on Manny skeleton (in progress)
  5. Export FBX → import to UE targeting Drifter skeleton via retargeter
  6. Create linked AnimBPs for each item, set hold pose in UpperBody_ItemHold
  7. Create action montages for item use (crowbar swing, scanner pulse, etc.) with slot DefaultGroup.UpperBody

For beta, I'm focusing on:

  • ✅ Locomotion (walk/jog × 4 cardinals + idle + fall) - DONE
  • ⏳ 5 item hold poses (crowbar, scanner, O2 canister, flare, noisemaker) - IN PROGRESS
  • ⏳ 6 action montages (crowbar swing/pry, scanner pulse, O2 inhale, flare/noisemaker throws) - TODO
  • ⏳ 1 carry pose (heavy two-handed) - TODO

Post-beta polish:

  • Start/Stop states with distance matching
  • Pivot state for sharp direction reversals
  • Turn-in-place for idle foot correction
  • Additive lean blend space for turning

Performance Notes

The GAS tag callback approach has zero per-frame cost until tags actually change. NativeUpdateAnimation runs every frame but only does basic math (velocity decomposition, cardinal quadrant check). The AnimGraph is straightforward - no complex blend trees or heavy per-bone manipulation.

In multiplayer testing with 4 players, animation system performance is not a concern. The bottleneck is elsewhere (networked physics, AI pathfinding).

Lessons Learned

Start with Lyra's architecture, then simplify: I initially tried to design from scratch, then realized Lyra already solved these problems. Understanding why Lyra uses cardinal selection + warping (fewer animations, cleaner warping, easier authoring) made the path clear.

Tag-driven state is cleaner than Blueprint events: The RegisterGameplayTagEvent callback pattern eliminates AnimBP event graph spaghetti. When State.Carrying appears, bIsCarrying just... becomes true. No custom events, no casts, no GetGameplayTagCount checks.

Motion warping is magic when fed good data: Orientation Warping and Stride Warping "just work" if you give them accurate Direction and GroundSpeed. The hard part is calculating those values correctly in C++ - once you do, the nodes handle the rest.

Linked AnimBPs are underrated: The ability to hot-swap an entire animation sub-graph when equipping an item is powerful. No giant select-by-enum nodes, no duplicate AnimBP variants. One base AnimBP, multiple linked layers.

What's Next

Immediate priorities:

  1. Finish item hold poses in Cascadeur (crowbar grip is proving tricky, two-handed spacing matters)
  2. Create action montages and wire them to item use abilities
  3. Test multiplayer synchronization of montage playback (should "just work" via GAS ability replication, but needs verification)

Future polish:

  • Implement Lyra-style Start/Stop states when distance matching makes sense (post-beta)
  • Add procedural head look-at for awareness feedback (currently head just follows camera yaw)
  • Explore Control Rig for dynamic hand grip adjustments (might eliminate need for per-item offsets)

The animation system is now robust enough to support beta. Players can move naturally, items look correct when held, and the architecture is flexible enough to add new items without touching C++.


"Animation system training complete. Associates are reminded that correct posture during salvage operations reduces Workers' Compensation claims by 3%. Salvage Solutions Inc. appreciates your commitment to efficient movement protocols."

  • S.A.L.L.I.
ue5 animation blueprint unreal engine linked anim graph motion warping ue5 indie game animation pipeline ue5 multiplayer animation