Sequence Player
One generic async engine for every ordered multi-step flow — tutorial prompts, boss intros, phase changes — played by the same service, declarative instead of nested coroutines.
What the system is for
Most Unity projects need to orchestrate ordered multi-step flows: show a prompt, wait, highlight something, wait again, hand control back. Whether it is a tutorial sequence, a boss introduction or a scripted gameplay moment, the underlying problem is the same — run a list of stages in order and await completion before returning control to the caller.
Serenity solves this once with ISequencePlayerService, a generic async orchestrator in the Serenity.SequencePlayer namespace. It knows nothing about cutscenes, dialog or any other domain — it only knows how to play an ordered list of stages and await the result, leaving every vocabulary decision to whatever vertical sits on top of it.
The Unity problem
Without a shared engine, each choreographed flow becomes its own coroutine or state machine. A tutorial step, a boss intro and a scripted event are implemented three different ways, tested three different ways and debugged three different ways. Cancellation, error handling and logging are copy-pasted or omitted. Call sites depend on concrete MonoBehaviours instead of stable interfaces.
The real cost is maintenance. A single design change — add a step, reorder a phase — touches multiple implementations. An engine that hard-codes its stage vocabulary makes this worse: every new kind of flow either forces a new enum member into a shared type or gets rejected outright.
How Serenity approaches it
ISequencePlayerService exposes two PlayAsync overloads: one that accepts a string key and resolves the definition at runtime, and one that accepts an ISequenceDefinition instance directly. Both return an awaitable Task so the caller can schedule a sequence, await it, and continue with game logic in a single line. Each ISequenceDefinition holds an ordered IReadOnlyList of ISequenceStage units; stages that need explicit duration implement ITimedSequenceStage, which adds a DurationSeconds property.
ISequenceStage no longer carries a closed, engine-owned stage enum. Its Kind member is a plain string that the engine never interprets — it exists purely so a vertical built on top of the engine can define its own stage vocabulary and branch on it. A tutorial system can use Kind values like "Highlight" or "WaitForInput" without ever touching the engine's code, and the Cutscene Player — the cinematic vertical built on this same engine — does the same with its own cinematic stage names.
How it fits into Serenity
The SequencePlayer namespace follows Serenity's layered structure. The Application layer holds ISequencePlayerService, the factory interface ISequencePlayerServiceFactory, ISequencePlayerSettings and the ISequenceDefinition and ISequenceStage contracts — deliberately free of any domain-specific stage type. The Infrastructure layer provides the Unity implementation; the Installation layer wires everything through Serenity's initialization pipeline so consumers see only the interfaces.
Sequence Player cooperates with the Feedback system for screen effects, with the Music Player to start or stop tracks at specific stages, with Game Mode to signal that a scripted sequence is active so other systems can react, and with the Event Dispatcher to publish stage lifecycle signals that UI or analytics may observe.
Practical workflow
- Define your sequence as a ScriptableObject implementing ISequenceDefinition and register its key in ISequencePlayerSettings.
- Implement each stage as a class implementing ISequenceStage or ITimedSequenceStage, giving Kind a string value meaningful to your own system.
- Let the installer register ISequencePlayerService through the initialization pipeline.
- Inject ISequencePlayerService at any call site that needs to orchestrate a multi-step flow.
- Call PlayAsync with a string key or a definition instance and await the result — the engine plays the ordered stages and returns control when the last stage completes.
- Use CancellationToken to abort mid-sequence on scene unload or game state change without leaving the engine in an inconsistent state.
What you get
- ISequencePlayerService with two PlayAsync overloads — play by string key or by ISequenceDefinition instance
- ISequenceDefinition holding an ordered IReadOnlyList of ISequenceStage units for runtime orchestration
- ITimedSequenceStage extending ISequenceStage with DurationSeconds for time-based stage execution
- ISequenceStage.Kind as a plain, engine-agnostic string, so verticals define their own stage vocabulary instead of extending a shared enum
- ISequencePlayerSettings exposing DefinitionIds for inspector-driven registration
- Factory interface ISequencePlayerServiceFactory for swappable implementations
- CancellationToken support throughout so sequences abort cleanly on scene transitions or state changes
- A stable foundation for domain verticals — the Cutscene Player ships as a cinematic vertical built on this same engine
When to use this
- Tutorial flows that must execute an ordered list of prompts, highlights and pauses before returning control.
- Boss introductions, phase changes or scripted gameplay moments that need to run as an ordered list of steps.
- Games where the same multi-step choreography pattern repeats across different contexts and you want one engine, not N coroutines.
- Codebases that need to await a sequence completion before triggering the next game-state transition, without tying the caller to a fixed set of stage kinds.
Related systems
Use Serenity when you want a single async orchestration engine for every ordered flow in your project, with a stage vocabulary that stays yours to define instead of one baked into the framework.
English
Español
Català