On-Rails Movement System
A passive, snapshot-based rail service that tracks normalized 0–1 progress along named node paths, keeping movement, AI and camera systems fully decoupled.
What the system is for
On-rails movement sounds simple until you need the camera, the player controller, the AI director and the cutscene sequencer all reading the same path position without stepping on each other. A MonoBehaviour that owns the transform and broadcasts events seems to work at first, then collapses the moment two systems need to write or read progress in the same frame.
The Game Rail module in Serenity solves that by separating path state from path visualization. One service owns the normalized progress for every named rail. Anything that needs to know where something is calls GetSnapshot and reads an immutable value. Nothing fights over ownership.
The Unity problem
Rail movement in Unity typically ends up as a single component that moves a transform, fires events at waypoints and tries to be the source of truth for position, progress and completion. When a second consumer appears — a camera rig, an enemy that mirrors the player path, a UI progress bar, a cutscene trigger — the component grows conditionals, exposed fields and tight references that make it impossible to reuse across scenes.
The deeper problem is coupling state to visualization. The moment a MonoBehaviour owns both the progress value and the transform it moves, testing and reuse become impractical. Game mode changes, spawner restarts and scene transitions all need to reset or inspect progress from the outside, and a self-contained component has no clean surface for that.
How Serenity approaches it
Serenity exposes rail state through IGameRailService, which supports ConfigureRail and RegisterRail to define named paths from RailPathConfig or raw RailNodePosition arrays, AdvanceProgress and SetProgress to write normalized movement deltas, GetProgress to read a RailProgress value object and GetSnapshot to retrieve an immutable RailSnapshot containing the rail identity, current progress and node count.
The service is intentionally passive. Nothing inside it moves a transform or fires events. External systems — the game loop, an AI controller, a sequence player — call AdvanceProgress each frame. Rendering is handled by UnityRailFollowerView, a MonoBehaviour that reads GetSnapshot each update and applies a sampled pose to any target transform with optional look-ahead orientation and exponential smoothing.
How it fits into Serenity
Game Rail lives in the Serenity.GameRail namespace and follows the foundation's layered structure. The Domain layer defines value objects RailId, RailProgress, RailNodePosition and RailSnapshot, the RailPath entity and the RailPathConfig configuration struct — all pure C# with no engine dependencies. The Application layer exposes IGameRailService. The Infrastructure layer provides UnityGameRailService, the MonoBehaviour implementation, and UnityRailFollowerView for transform application. Installation is handled by UnityGameRailInstaller.
Game Rail cooperates with Game Mode so the active mode can register and reset rails at stage or wave boundaries, with the Spawner so spawned actors can be bound to a rail on creation, with Game Session for stage and wave on-rails sequences, and with sequence or cutscene playback systems that need precise, externally driven progress along a camera or cinematic path.
Practical workflow
- Define your rail paths as RailPathConfig structs with a RailId and an ordered array of RailNodePosition waypoints.
- Call ConfigureRail on IGameRailService at initialization — typically from the installer or a level setup script.
- Drive progress each frame by calling AdvanceProgress with a time-scaled delta from your game loop, AI controller or sequence player.
- Add UnityRailFollowerView to any GameObject that should follow the rail — bind the target RailId and configure orientation sampling and smoothing in the inspector.
- Call GetSnapshot from any other system — camera rig, UI progress bar, event trigger — to read immutable rail state without coupling to the mover.
- Call ResetProgress or SetProgress from Game Mode or the Spawner to restart or jump to a specific point when a stage resets.
What you get
- IGameRailService with ConfigureRail, RegisterRail, AdvanceProgress, SetProgress, GetProgress, GetSnapshot and ResetProgress
- RailPathConfig immutable configuration struct combining RailId and ordered RailNodePosition arrays
- RailProgress normalized 0–1 value object with clamped Advance, IsAtStart and IsAtEnd helpers
- RailSnapshot immutable read model exposing RailId, RailProgress and NodeCount for zero-coupling reads
- RailId named identity that decouples registration from consumption across systems
- UnityRailFollowerView MonoBehaviour with configurable update phase, look-ahead orientation and exponential smoothing
- Intentionally passive design — progress is always driven externally, never internally ticked
- Integration with Game Mode, Spawner and Game Session for stage and wave on-rails sequences
When to use this
- On-rails shooter or runner projects where the player, enemies and camera all need to share the same path position.
- Cinematic camera systems that follow a scripted path driven by sequence or cutscene playback rather than a self-animating component.
- Stage or wave designs where Game Mode or the Spawner needs to reset or jump to a specific rail position at runtime.
- Any project where more than one system needs to read rail progress without ownership conflicts or tight cross-component references.
Related systems
Use Serenity when you need on-rails movement that is readable by any system, writable by one, and reusable across scenes — without a single MonoBehaviour trying to own the transform, the state and the events all at once.
English
Español
Català