Repository navigation
Cinematic Sequencer
SparkEngine includes a timeline-based cinematic sequencer for creating in-game cutscenes and scripted events.
Source: SparkEngine/Source/Engine/Cinematic/Sequencer.h
The sequencer is compiled as part of the engine; there is no separate CMake toggle.
The sequencer provides a non-linear timeline editor for:
- Camera movements and cuts
- Entity animations and transformations
- Audio cue playback
- Event triggers
- UI transitions
- Subtitle display
- Screen fades
SequencerManager (singleton)
|
+-- Sequence "IntroSequence"
| +-- CameraPathTrack (camera keyframes with spline interpolation)
| +-- EntityTransformTrack (position/rotation/scale keyframes)
| +-- EntityPropertyTrack (float property animation, e.g. light intensity)
| +-- AudioCueTrack (timed sound effect triggers)
| +-- EventTrack (script/code callback triggers)
| +-- SubtitleTrack (timed subtitle display)
| +-- FadeTrack (screen fade in/out keyframes)
|
+-- Sequence "OutroSequence"
| +-- ...
|
+-- Update(deltaTime) (advances all playing sequences)
All classes reside in the Spark::Cinematic namespace.
A Sequence is a named collection of tracks that play together over a defined time range. Each sequence has independent playback controls (play, pause, stop, seek, speed, looping) and fires event callbacks at specific times.
Each track controls a specific aspect of the scene:
| Track Type | Class | Controls |
|---|---|---|
| Camera | CameraPathTrack |
Camera position, rotation, FOV, roll over time |
| Transform | EntityTransformTrack |
Entity position, rotation, scale over time |
| Property | EntityPropertyTrack |
Arbitrary float property (e.g. light.intensity, material.opacity) |
| Audio | AudioCueTrack |
Sound effect and music cue timing |
| Event | EventTrack |
Custom event triggers at specific times |
| Subtitle | SubtitleTrack |
Timed subtitle/dialogue text display |
| Fade | FadeTrack |
Screen fade in/out with color control |
All tracks inherit from SequencerTrack:
class SequencerTrack
{
public:
virtual ~SequencerTrack() = default;
virtual TrackType GetType() const = 0;
virtual const char* GetName() const = 0;
virtual float GetDuration() const = 0;
bool enabled = true;
std::string name;
};enum class TrackType
{
CameraPath, // Camera animation track
EntityProperty, // Animate a float property on an entity
EntityTransform, // Animate entity position/rotation/scale
AudioCue, // Trigger audio at specific times
Event, // Fire script/code callbacks at specific times
Subtitle, // Display subtitle text
Fade // Screen fade in/out
};Keyframes define values at specific points in time. The sequencer interpolates between keyframes using one of four modes:
enum class InterpolationMode
{
Step, // No interpolation -- snap to value instantly
Linear, // Linear interpolation between keyframes
CubicBezier, // Cubic bezier curve for eased motion
CatmullRom // Catmull-Rom spline for smooth camera paths
};struct CameraKeyframe
{
float time; // Time in seconds
XMFLOAT3 position; // World-space camera position
XMFLOAT3 lookAt; // World-space look-at target
float fov; // Field of view in degrees
float roll; // Roll angle in degrees
InterpolationMode interpolation = InterpolationMode::CatmullRom;
};struct VectorKeyframe
{
float time; // Time in seconds
XMFLOAT3 value; // XYZ vector value
InterpolationMode interpolation = InterpolationMode::Linear;
};struct PropertyKeyframe
{
float time; // Time in seconds
float value; // Scalar float value
InterpolationMode interpolation = InterpolationMode::Linear;
};struct FadeKeyframe
{
float time; // Time in seconds
float opacity; // 0.0 = transparent, 1.0 = fully opaque
XMFLOAT3 color{0, 0, 0}; // Fade color (default: black)
InterpolationMode interpolation = InterpolationMode::Linear;
};struct AudioCue
{
float time; // Trigger time in seconds
std::string soundName; // Asset name of the sound
float volume = 1.0f; // Playback volume [0, 1]
bool is3D = false; // Spatialized audio
XMFLOAT3 position{0, 0, 0}; // 3D position (if is3D)
};struct EventCue
{
float time; // Trigger time in seconds
std::string eventName; // Event identifier string
std::string parameters; // JSON or key=value parameters
};struct SubtitleCue
{
float startTime; // Display start time
float endTime; // Display end time
std::string text; // Subtitle text
std::string speaker; // Speaker name (for attribution)
XMFLOAT4 color{1.0f, 1.0f, 1.0f, 1.0f}; // Text color (RGBA)
};class CameraPathTrack : public SequencerTrack
{
public:
void AddKeyframe(const CameraKeyframe& kf);
void RemoveKeyframe(int index);
CameraKeyframe Evaluate(float time) const;
const std::vector<CameraKeyframe>& GetKeyframes() const;
};The camera track uses Catmull-Rom spline interpolation by default, producing smooth dolly/crane-style camera paths. For hard cuts, set keyframes to InterpolationMode::Step.
class EntityTransformTrack : public SequencerTrack
{
public:
uint32_t targetEntityID = 0;
void AddPositionKeyframe(const VectorKeyframe& kf);
void AddRotationKeyframe(const VectorKeyframe& kf);
void AddScaleKeyframe(const VectorKeyframe& kf);
XMFLOAT3 EvaluatePosition(float time) const;
XMFLOAT3 EvaluateRotation(float time) const;
XMFLOAT3 EvaluateScale(float time) const;
};class EntityPropertyTrack : public SequencerTrack
{
public:
uint32_t targetEntityID = 0;
std::string propertyName; // E.g., "light.intensity", "material.opacity"
void AddKeyframe(const PropertyKeyframe& kf);
float Evaluate(float time) const;
const std::vector<PropertyKeyframe>& GetKeyframes() const;
};class AudioCueTrack : public SequencerTrack
{
public:
void AddCue(const AudioCue& cue);
const std::vector<AudioCue>& GetCues() const;
std::vector<const AudioCue*> GetTriggeredCues(float prevTime, float currentTime) const;
};The GetTriggeredCues() method returns cues whose trigger time falls in the (prevTime, currentTime] window, ensuring each cue fires exactly once during playback.
class EventTrack : public SequencerTrack
{
public:
void AddCue(const EventCue& cue);
const std::vector<EventCue>& GetCues() const;
std::vector<const EventCue*> GetTriggeredCues(float prevTime, float currentTime) const;
};class SubtitleTrack : public SequencerTrack
{
public:
void AddSubtitle(const SubtitleCue& cue);
const SubtitleCue* GetActiveSubtitle(float time) const;
const std::vector<SubtitleCue>& GetSubtitles() const;
};class FadeTrack : public SequencerTrack
{
public:
void AddKeyframe(const FadeKeyframe& kf);
FadeKeyframe Evaluate(float time) const;
};enum class SequencePlayState { Stopped, Playing, Paused };
using EventCallback = std::function<void(const std::string& eventName, const std::string& params)>;
class Sequence
{
public:
explicit Sequence(const std::string& name);
// Track management
CameraPathTrack* AddCameraTrack(const std::string& trackName);
EntityTransformTrack* AddEntityTransformTrack(const std::string& trackName, uint32_t entityID);
EntityPropertyTrack* AddEntityPropertyTrack(const std::string& trackName, uint32_t entityID,
const std::string& property);
AudioCueTrack* AddAudioCueTrack(const std::string& trackName);
EventTrack* AddEventTrack(const std::string& trackName);
SubtitleTrack* AddSubtitleTrack(const std::string& trackName);
FadeTrack* AddFadeTrack(const std::string& trackName);
// Playback control
void Play();
void Pause();
void Stop();
void SetTime(float time);
void SetPlaybackSpeed(float speed); // 1.0 = normal, 0.5 = half, 2.0 = double
void SetLooping(bool loop);
// Per-frame update
void Update(float deltaTime);
// Event callback
void SetEventCallback(EventCallback callback);
// State queries
const std::string& GetName() const;
float GetCurrentTime() const;
float GetDuration() const; // Max duration across all tracks
SequencePlayState GetPlayState() const;
float GetPlaybackSpeed() const;
bool IsLooping() const;
// Current evaluated state (for rendering)
const CameraKeyframe* GetCurrentCameraState() const;
const SubtitleCue* GetCurrentSubtitle() const;
FadeKeyframe GetCurrentFade() const;
// Track access
const std::vector<std::unique_ptr<SequencerTrack>>& GetTracks() const;
};auto& seqMgr = SequencerManager::GetInstance();
// Create a new sequence
Sequence* intro = seqMgr.CreateSequence("IntroSequence");
// Add a camera track with keyframes
auto* camTrack = intro->AddCameraTrack("MainCamera");
camTrack->AddKeyframe(CameraKeyframe{
0.0f, // time
{0.0f, 10.0f, -20.0f}, // position
{0.0f, 0.0f, 0.0f}, // lookAt
60.0f, // FOV
0.0f, // roll
InterpolationMode::CubicBezier // smooth interpolation
});
camTrack->AddKeyframe(CameraKeyframe{
3.0f, // time
{5.0f, 3.0f, -5.0f}, // position
{0.0f, 1.0f, 0.0f}, // lookAt
45.0f, // FOV (zoom in)
0.0f,
InterpolationMode::CubicBezier
});
// Add an entity transform track (move an NPC during the cutscene)
auto* npcTrack = intro->AddEntityTransformTrack("NPC_Walk", npcEntityId);
npcTrack->AddPositionKeyframe(VectorKeyframe{0.0f, {10.0f, 0.0f, 0.0f}, InterpolationMode::Linear});
npcTrack->AddPositionKeyframe(VectorKeyframe{2.5f, {5.0f, 0.0f, 3.0f}, InterpolationMode::Linear});
npcTrack->AddRotationKeyframe(VectorKeyframe{2.5f, {0.0f, 90.0f, 0.0f}, InterpolationMode::Linear});
// Add a property track (fade a light in)
auto* lightTrack = intro->AddEntityPropertyTrack("SpotLight", lightEntityId, "light.intensity");
lightTrack->AddKeyframe(PropertyKeyframe{0.0f, 0.0f, InterpolationMode::Linear});
lightTrack->AddKeyframe(PropertyKeyframe{2.0f, 1.0f, InterpolationMode::CubicBezier});
// Add audio cues
auto* audioTrack = intro->AddAudioCueTrack("Music");
audioTrack->AddCue(AudioCue{0.0f, "cinematic_music", 0.8f, false, {}});
audioTrack->AddCue(AudioCue{2.0f, "footsteps", 1.0f, true, {5.0f, 0.0f, 3.0f}});
// Add subtitles
auto* subTrack = intro->AddSubtitleTrack("Dialogue");
subTrack->AddSubtitle(SubtitleCue{1.0f, 3.5f, "Welcome to SparkCity.", "Narrator",
{1.0f, 1.0f, 1.0f, 1.0f}});
subTrack->AddSubtitle(SubtitleCue{4.0f, 6.0f, "Your mission begins now.", "Commander",
{1.0f, 1.0f, 0.0f, 1.0f}});
// Add event triggers
auto* eventTrack = intro->AddEventTrack("GameEvents");
eventTrack->AddCue(EventCue{5.0f, "enable_player_control", ""});
eventTrack->AddCue(EventCue{5.0f, "start_music", "combat_theme"});
// Add a screen fade
auto* fadeTrack = intro->AddFadeTrack("ScreenFade");
fadeTrack->AddKeyframe(FadeKeyframe{0.0f, 1.0f, {0,0,0}, InterpolationMode::Linear}); // fade from black
fadeTrack->AddKeyframe(FadeKeyframe{1.0f, 0.0f, {0,0,0}, InterpolationMode::Linear}); // fully visible
fadeTrack->AddKeyframe(FadeKeyframe{5.5f, 0.0f, {0,0,0}, InterpolationMode::Linear}); // still visible
fadeTrack->AddKeyframe(FadeKeyframe{6.0f, 1.0f, {0,0,0}, InterpolationMode::Linear}); // fade to black
// Handle sequence events
intro->SetEventCallback([](const std::string& eventName, const std::string& params) {
if (eventName == "enable_player_control") {
playerController.SetEnabled(true);
} else if (eventName == "start_music") {
audioSystem.PlayMusic(params);
}
});auto& seqMgr = SequencerManager::GetInstance();
// Play a sequence by name
seqMgr.PlaySequence("IntroSequence");
// Control active sequences
seqMgr.PauseSequence("IntroSequence");
seqMgr.StopSequence("IntroSequence");
seqMgr.StopAll();
// Direct sequence control
Sequence* seq = seqMgr.GetSequence("IntroSequence");
seq->Play();
seq->Pause();
seq->Stop();
seq->SetTime(5.0f); // Seek to 5 seconds
seq->SetPlaybackSpeed(0.5f); // Half speed
seq->SetLooping(true); // Loop the sequence
// Query state
SequencePlayState state = seq->GetPlayState(); // Stopped, Playing, or Paused
float currentTime = seq->GetCurrentTime();
float duration = seq->GetDuration();
bool looping = seq->IsLooping();
// Read current cinematic state for rendering
const CameraKeyframe* cam = seq->GetCurrentCameraState();
const SubtitleCue* subtitle = seq->GetCurrentSubtitle();
FadeKeyframe fade = seq->GetCurrentFade();
// Check if any cutscene is blocking gameplay
if (seqMgr.IsAnyCutscenePlaying()) {
DisablePlayerInput();
}
// Update each frame
seqMgr.Update(deltaTime);class SequencerManager
{
public:
static SequencerManager& GetInstance();
Sequence* CreateSequence(const std::string& name);
Sequence* GetSequence(const std::string& name);
void RemoveSequence(const std::string& name);
bool PlaySequence(const std::string& name);
void StopSequence(const std::string& name);
void PauseSequence(const std::string& name);
void StopAll();
void Update(float deltaTime);
bool IsAnyCutscenePlaying() const;
Sequence* GetActiveSequence();
// Console integration
std::string Console_ListSequences() const;
std::string Console_GetSequenceInfo(const std::string& name) const;
void Console_PlaySequence(const std::string& name);
void Console_StopSequence(const std::string& name);
void Console_SetTime(const std::string& name, float time);
};During Sequence::Update(deltaTime):
- Advance
m_currentTimebydeltaTime * m_playbackSpeed. - For each
CameraPathTrack, evaluate the camera state atm_currentTime. - For each
EntityTransformTrack, evaluate and apply position/rotation/scale. - For each
EntityPropertyTrack, evaluate and apply the float value. - For each
AudioCueTrack, callGetTriggeredCues(m_previousTime, m_currentTime)and play triggered sounds. - For each
EventTrack, callGetTriggeredCues(m_previousTime, m_currentTime)and dispatch viam_eventCallback. - For each
SubtitleTrack, queryGetActiveSubtitle(m_currentTime). - For each
FadeTrack, evaluate the current fade state. - If
m_currentTime >= GetDuration(), either loop (reset to 0) or stop. - Store
m_previousTime = m_currentTimefor the next frame's cue detection.
- Step: Returns the value of the keyframe at or before the current time.
-
Linear: Computes
lerp(a, b, t)wheretis the normalized time between two keyframes. - CubicBezier: Uses cubic Hermite interpolation with automatically computed tangents.
-
CatmullRom: Uses the
CatmullRomInterpolate()helper inCameraPathTrack, which requires four control points (P0, P1, P2, P3) to produce a smooth path through P1 and P2.
The sequencer supports multiple camera techniques:
| Technique | Implementation |
|---|---|
| Smooth transition | Multiple keyframes with CatmullRom or CubicBezier interpolation |
| Hard cut | Two keyframes at the same time or adjacent times with Step interpolation |
| Dolly/crane shot | Closely spaced CatmullRom keyframes along a path |
| Zoom | Keyframes with different FOV values |
| Dutch angle | Keyframes with non-zero roll values |
| Follow shot | Entity transform track + camera track with matching movement |
Cinematics can be triggered from:
- Game module code (C++)
- AngelScript scripts
- Trigger volumes in scenes
- Event System callbacks
The SparkEditor includes a visual timeline editor for creating and editing cinematic sequences. See SparkEditor for details on the Animation Timeline panel.
| Command | Description |
|---|---|
seq_list |
List all registered sequences |
seq_info <name> |
Show detailed info about a sequence |
seq_play <name> |
Play a sequence by name |
seq_stop <name> |
Stop a sequence by name |
seq_time <name> <seconds> |
Seek to a specific time |
- Keyframes are stored in sorted vectors; evaluation uses binary search for O(log N) lookup.
- Camera spline interpolation is computed per-frame only for actively playing sequences.
- Audio and event cue detection uses the
(prevTime, currentTime]window to ensure exactly-once triggering. - Tracks can be individually disabled (
track.enabled = false) to skip evaluation.
All sequencer operations must be called from the main thread. The SequencerManager::Update() method is called during the main game loop, after physics and before rendering. The event callback is dispatched synchronously on the main thread.
-
CreateSequence()overwrites existing sequences with the same name silently. -
PlaySequence()returnsfalseif the sequence name is not found. -
GetSequence()returnsnullptrif the name is not found. - Seeking past the end of a sequence clamps to the duration.
- Adding keyframes at negative times is undefined behavior.
- Verify the sequence was created with
CreateSequence()before callingPlaySequence(). - Check that
SequencerManager::Update(deltaTime)is called every frame. - Ensure
deltaTimeis positive and non-zero.
- Ensure
Update()is called exactly once per frame. - Check that
m_previousTimeis being correctly updated (this is handled automatically). - Verify no duplicate cues exist at the same time.
- Ensure at least 2 keyframes exist in the
CameraPathTrack. - For CatmullRom interpolation, at least 4 keyframes produce the smoothest results.
- Check for accidental Step interpolation on keyframes that should be smooth.
- Verify
startTime < endTimefor each subtitle cue. - Query
GetCurrentSubtitle()each frame and pass the text to your UI renderer. - Check that the subtitle track is enabled.
- Animation -- Animation tracks in sequences
- Audio -- Audio cue playback
- SparkEditor -- Timeline editor UI
- Event System -- Event triggers from sequences
- Scripting with AngelScript -- Script-driven cinematics
- Scene Management -- Scene-based cinematic triggers
Published from 2b03dc797148. Edit the canonical source in wiki/.
- Documentation
- Docs route
- Wiki index
- Guides
- Tutorials
- Samples
- Examples
- API Reference
- API route
- Reference
- Build Guide
- Dependencies
- FAQ
- Changelog
- Roadmap
- Contributing
- Code of Conduct
- Home
- FAQ
- Getting Started
- Quick-Start Tutorial
- Making Your First Game
- Making Your First Multiplayer Game
- Artist Workflow Guide
- Editor Walkthrough
- Migration Guide
- How SparkEngine Works
- Architecture Overview
- Engine Architecture Flowchart
- Creating a Game Module
- Game Modules (catalog)
- Entity Component System
- Rendering and Graphics
- Physics
- Cloth Simulation
- Audio
- Input System
- Camera System
- Scripting with AngelScript
- Visual Scripting
- AI and Navigation
- Animation
- 2D Systems
- Networking
- Dedicated Server
- Multiplayer Quick Start
- Area Server Architecture
- Scene Management
- Large World Support
- Collaborative Editing
- Coroutine System
- Event System
- Event Response System
- Job System
- UI System
- UI Layout Extensions
- Localization
- Dialogue System
- Destruction System
- Replay System
- Achievement System
- Loading System
- Mod System
- Content Delivery
- Tween System
- Memory Integrity
- Gameplay Systems
- Terrain and Procedural Generation
- Save System
- Persistence System
- Day Night Cycle and Weather
- Cinematic Sequencer
- Runtime Prefabs
- SparkEditor
- Editor Tutorials
- SparkConsole
- SparkDaemon
- Shader Pipeline
- Asset Pipeline
- Asset Validation
- Asset Migration
- Game Packaging
- Online Services
- DataTable System
- Loot and Crafting System
- CSG System
- Font System
- Timer Manager
- Movie Render Pipeline
- HLOD and World Partition
- Remote Debug System
- Selection Manager
- Asset Dependency Graph
- Editor Automation
- File Watcher
- Project Templates
- System Requirements
- VR Support
- Mobile Platform
- Accessibility
- Platform Input
- Platform Certification
- Cross-Compilation: Wine Testing
- RHI Abstraction Layer
- D3D11 Backend
- D3D12 Backend
- Vulkan Backend
- OpenGL Backend
- Metal Backend
- DXR Raytracing
- Hybrid Ray Tracing
- Upscaling (DLSS/FSR)
- Render Graph
- Shader Graph
- GPU Particles
- GPU-Driven Rendering
- Volumetric Fog
- Volumetric Clouds
- Global Illumination
- Virtual Texturing
- Water Rendering
- Clustered Lighting
- Material System
- Post-Processing
- Shadow System
- Particle System
- Decal System
- Sky and Atmosphere
- Foliage System
- Mesh Shaders
- Neural Rendering
- Configuration Reference
- Performance Tips
- Benchmark Framework
- Threading Model
- Fuzz Policy and Parser Security
- Memory Safety
- Memory Management Patterns
- Build System and CMake Modules
- Profiler and Debugging
- Performance Profiling Guide
- Telemetry System
- Crash Reporting
- Golden Image Testing
- Utilities
- Testing
- Fuzz Policy and Parser Security
- Codebase Statistics
- Codebase Health
- Error Handling Patterns
- Hot Reload Overview
- Troubleshooting
- Contributing
- Workflow Patterns
- Build Optimizations
- CI Reproducible Builds
- GitHub API and PR Checks
- Git Rebase Conflicts
- Clang-Format
- Code Quality Violations
- AI Bloat Pattern
- MinGW + Wine Cross-Compilation
- Live Editor Testing
- Engine & Renderer Landscape
- DuetOS Portability Catalog
- Five-Engine Analysis
- Eleven-Engine Analysis
- ThorVG / Unity Graphics Analysis
- Advanced Techniques Catalog
- Third-Party Library Evaluation
- Engine Viability Evaluation
- Engine Feature Recommendations
- Project Recommendations
- Mac Compatibility Analysis
- Codebase Observations
- Codebase Bloat Audit
- Test Suite Audit
- Documentation Coverage Audit
- ThirdParty Dependencies Audit
- Load Test Baseline
- Gameplay Systems Status
- SparkGame Module Status
- Stub and Abandoned Features
- Memory Integrity System
- Memory Safety Evaluation
- Hardware Acceleration Systems
- Jolt Physics Integration
- GPU/CPU Separation Plan
- Daemon Services Architecture
- Reflection & Polymorphism Refactoring Plan
- SparkBuild In-Tree
- Wine No-JobSystem Breakthrough
- Wine Role and Fallback Tiers