Zone Traversal System

This is an activity-driven sandbox game where I took ownership of the Zone Traversal System. This system enables players to travel seamlessly across playable areas by integrating multiple gameplay aspects including NavMesh, Animation, Timeline, VFX, player input, and camera movement.

System Architecture

The game features various types of traversals, each with unique characteristics. I designed a unified script to manage all traversal scenarios, which ensures consistency and maintainability throughout the system.

Data-Driven Design

The system is fully data-driven. Since some traversals are bidirectional, I exposed origin and destination data directly in the editor. When a player interacts with either the starting or ending point, the system receives the appropriate data and executes the traversal.

Timeline Integration

I leveraged Unity's Timeline system to maintain complete control over traversal sequences. This approach delivers:

  • Real-time preview capabilities in the editor
  • Accelerated integration workflow
  • Comprehensive sequence control

Each Timeline contains:

  • Character animations
  • Character movement
  • Traversal device animations
  • Voice-over triggers
  • VFX triggers

Technical Decisions

Animation Clips vs. Animation Triggers in Timeline

Using animation triggers in Timeline introduces a one-frame delay due to the animation state machine architecture. This causes desynchronization with other animation clips. I evaluated both approaches and made the following comparison:

Animation Clips (Selected Approach):

  • Pros: Clips exist outside the character's Animator, reducing memory overhead. Clips only load when the traversal becomes active
  • Cons: Requires scripting to connect animation tracks to the character and provide the Animator component to the track. Timeline overrides animator animations and lacks support for additive animation layers. Root motion animations present additional challenges: they can manipulate the skeleton root but cannot apply position changes to the Animator GameObject

After thorough evaluation, I selected Timeline animation clips over Animator triggers to achieve smooth integration and minimize memory overhead.

Character Movement Challenges

Issue: Static Animation Clips for Movement

Initially, I recorded positions for each traversal directly into animation clips. This approach created problems when adjusting traversal positions in the scene. Any repositioning required re-recording and re-baking data into the clips, resulting in an inefficient workflow.

Solution: Constraint Position with Dynamic Weight Control

I implemented a constraint-based system with the following features:

  • Anchors positioned on each traversal device (such as jumping pads)
  • Position Constraint components added to the character
  • Constraint weights adjusted dynamically through Timeline
  • Dynamic mapping established between Timeline and scene anchors

This solution removes the need to bake movement positions into animation clips. When traversal devices are repositioned in the editor, Timeline anchors update automatically, which significantly streamlines the integration workflow.

Root Motion Challenges

To provide context for the Root Motion issues, let me first explain the prefab hierarchy

Character Hierarchy

  • Character: Character root anchor
  • Visual: Visual component root anchor, containing animator and VFX
  • CH_Character_Default: Animator component holder
  • Root_skin: Character skeleton root anchor where root motion applies

Issue 1: Double Movement Problem

Root Cause: In a standard rigged character hierarchy, gameplay logic (collision and movement) depends on the outermost Character Root, while the animation's root motion affects the innermost Skeleton Root (Root_Skin).


When the animation moves the visual component but the Character Root remains stationary, the gameplay collider stays behind. Simply moving the Character Root by the animation delta creates double movement because the visuals (Skin Root) are already being translated forward by the animation system.

Solution: RootMotionController (Inverse Decomposition)

I developed a custom RootMotionController script to manage this hierarchy with the following approach:

  • Mechanism: The controller executes in LateUpdate after the animation system updates bone positions
  • Movement: Measures the frame-to-frame horizontal displacement (delta) of the Skin Root, then applies this delta to the Character Root's local position
  • Cancellation: Subtracts the same delta from the Visual Transform's local position using inverse decomposition

This achieves the following result: Character Root Position += Delta and Visual Root Position -= Delta. This method stabilizes the visuals while advancing the gameplay root forward.

public void LateUpdate()
{
    if (!_isTimelineRootMotionEnabled)
    {
        return;
    }
    
    // Update animator localposition
    Transform animatorTransform = _animator.gameObject.transform;
    animatorTransform.localPosition = _startAnimatorRootLocalPosition;
    
    // Calculate delta
    Vector3 deltaWorld = _skeletonRoot.position - _lastSkinRootWorldPosition;
    Vector3 deltaLocal = _characterRoot.InverseTransformDirection(deltaWorld);
    Vector3 horizontalDeltaLocal = new Vector3(deltaLocal.x, 0, deltaLocal.z);

    // Update character root localposition
    _characterRoot.localPosition += horizontalDeltaLocal;

    _visualRoot.localPosition -= horizontalDeltaLocal;

    _lastSkinRootWorldPosition = _skeletonRoot.position;
}

Issue 2: Animation Rewind and Desynchronization

Root Cause: When chaining multiple animation clips (e.g., transitioning from the first jump to the second jump in an 8-stone traversal), a rewind occurs.


The RootMotionController relies on the previous frame's position for delta calculation. When Timeline transitions between clips:

  • Animation Clip 1 ends
  • Animation Clip 2 starts
  • The animation system instantly snaps the Skin Root to the starting pose of Clip 2 (relative to its parent, the Visual Root)
  • This instantaneous snap causes the Visual Transform's accumulated inverse offset to be lost or miscalculated, leading to the visual component rewinding back to the original starting position

Solution: Signal-Based Movement Transfer

To maintain continuity, I implemented the UpdateRootMotionPosition signal to transfer accumulated distance at transition points:

  • Transfer: The method reads the total accumulated inverse offset stored on the Visual Transform's local position
  • Apply to Root: Applies this total distance to the Character Root's localPosition
  • Cleanup: Resets the Visual Transform's local position to Vector3.zero, clearing the inverse buffer for the next animation segment
  • Re-Initialize: Re-records the Skin Root's position to ensure frame-to-frame delta calculation for the second jump starts correctly

Technologies Used

  • Unity & C#
  • Unity Timeline
  • Animation Rigging & Constraints
  • Root Motion System
  • NavMesh
  • Cinemachine