cutscene system

This commit is contained in:
Jan Racek
2026-03-24 15:51:04 +01:00
parent e51c06b012
commit 60a7063a17
12 changed files with 651 additions and 11 deletions

115
src/cutscene.hh Normal file
View File

@@ -0,0 +1,115 @@
#pragma once
#include <stdint.h>
#include <psyqo/fixed-point.hh>
#include <psyqo/trigonometry.hh>
#include <psyqo/soft-math.hh>
#include "camera.hh"
#include "gameobject.hh"
#include "audiomanager.hh"
namespace psxsplash {
static constexpr int MAX_CUTSCENES = 16;
static constexpr int MAX_TRACKS = 8;
static constexpr int MAX_KEYFRAMES = 64;
static constexpr int MAX_AUDIO_EVENTS = 64;
enum class TrackType : uint8_t {
CameraPosition = 0,
CameraRotation = 1,
ObjectPosition = 2,
ObjectRotationY = 3,
ObjectActive = 4,
};
/// Per-keyframe interpolation mode.
/// Packed into upper 3 bits of the frame field in CutsceneKeyframe.
enum class InterpMode : uint8_t {
Linear = 0, // Default linear interpolation
Step = 1, // Instant jump (no interpolation)
EaseIn = 2, // Slow start, fast end (quadratic)
EaseOut = 3, // Fast start, slow end (quadratic)
EaseInOut = 4, // Smooth start and end (smoothstep)
};
struct CutsceneKeyframe {
// Upper 3 bits = InterpMode (0-7), lower 13 bits = frame number (0-8191).
// At 30fps, max frame 8191 ≈ 4.5 minutes.
uint16_t frameAndInterp;
int16_t values[3];
uint16_t getFrame() const { return frameAndInterp & 0x1FFF; }
InterpMode getInterp() const { return static_cast<InterpMode>(frameAndInterp >> 13); }
};
static_assert(sizeof(CutsceneKeyframe) == 8, "CutsceneKeyframe must be 8 bytes");
struct CutsceneAudioEvent {
uint16_t frame;
uint8_t clipIndex;
uint8_t volume;
uint8_t pan;
uint8_t pad[3];
};
static_assert(sizeof(CutsceneAudioEvent) == 8, "CutsceneAudioEvent must be 8 bytes");
struct CutsceneTrack {
TrackType trackType;
uint8_t keyframeCount;
uint8_t pad[2];
CutsceneKeyframe* keyframes; // Points into splashpack data (resolved at load time)
GameObject* target; // nullptr = camera track
/// Initial values captured at play() time for pre-first-keyframe blending.
/// For position tracks: fp12 x,y,z. For rotation tracks: raw angle values.
/// For ObjectActive: values[0] = 1 (active) or 0 (inactive).
int16_t initialValues[3];
int16_t _initPad;
};
struct Cutscene {
const char* name; // Points into splashpack data
uint16_t totalFrames;
uint8_t trackCount;
uint8_t audioEventCount;
CutsceneTrack tracks[MAX_TRACKS];
CutsceneAudioEvent* audioEvents; // Points into splashpack data
};
/// Zero-allocation cutscene player. Call init() once after splashpack is loaded,
/// then tick() once per frame from the scene loop.
class CutscenePlayer {
public:
/// Initialize with loaded cutscene data. Safe to pass nullptr/0 if no cutscenes.
void init(Cutscene* cutscenes, int count, Camera* camera, AudioManager* audio);
/// Play cutscene by name. Returns false if not found.
bool play(const char* name);
/// Stop the current cutscene immediately.
void stop();
/// True if a cutscene is currently active.
bool isPlaying() const { return m_active != nullptr; }
/// Advance one frame. Call once per frame. Does nothing when idle.
void tick();
private:
Cutscene* m_cutscenes = nullptr;
int m_count = 0;
Cutscene* m_active = nullptr;
uint16_t m_frame = 0;
uint8_t m_nextAudio = 0;
Camera* m_camera = nullptr;
AudioManager* m_audio = nullptr;
psyqo::Trig<> m_trig;
void applyTrack(CutsceneTrack& track);
void lerpKeyframes(CutsceneKeyframe* kf, uint8_t count, const int16_t initial[3], int16_t out[3]);
void lerpAngles(CutsceneKeyframe* kf, uint8_t count, const int16_t initial[3], int16_t out[3]);
};
} // namespace psxsplash