Camera API, improved cutscene API
This commit is contained in:
@@ -15,13 +15,16 @@ void CutscenePlayer::init(Cutscene* cutscenes, int count, Camera* camera, AudioM
|
|||||||
m_active = nullptr;
|
m_active = nullptr;
|
||||||
m_frame = 0;
|
m_frame = 0;
|
||||||
m_nextAudio = 0;
|
m_nextAudio = 0;
|
||||||
|
m_loop = false;
|
||||||
m_camera = camera;
|
m_camera = camera;
|
||||||
m_audio = audio;
|
m_audio = audio;
|
||||||
m_uiSystem = uiSystem;
|
m_uiSystem = uiSystem;
|
||||||
|
m_onCompleteRef = LUA_NOREF;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CutscenePlayer::play(const char* name) {
|
bool CutscenePlayer::play(const char* name, bool loop) {
|
||||||
if (!name || !m_cutscenes) return false;
|
if (!name || !m_cutscenes) return false;
|
||||||
|
m_loop = loop;
|
||||||
|
|
||||||
for (int i = 0; i < m_count; i++) {
|
for (int i = 0; i < m_count; i++) {
|
||||||
if (m_cutscenes[i].name && streq(m_cutscenes[i].name, name)) {
|
if (m_cutscenes[i].name && streq(m_cutscenes[i].name, name)) {
|
||||||
@@ -106,7 +109,9 @@ bool CutscenePlayer::play(const char* name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CutscenePlayer::stop() {
|
void CutscenePlayer::stop() {
|
||||||
|
if (!m_active) return;
|
||||||
m_active = nullptr;
|
m_active = nullptr;
|
||||||
|
fireOnComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CutscenePlayer::tick() {
|
void CutscenePlayer::tick() {
|
||||||
@@ -130,8 +135,32 @@ void CutscenePlayer::tick() {
|
|||||||
|
|
||||||
m_frame++;
|
m_frame++;
|
||||||
if (m_frame > m_active->totalFrames) {
|
if (m_frame > m_active->totalFrames) {
|
||||||
|
if (m_loop) {
|
||||||
|
// Restart from the beginning
|
||||||
|
m_frame = 0;
|
||||||
|
m_nextAudio = 0;
|
||||||
|
} else {
|
||||||
m_active = nullptr; // Cutscene finished
|
m_active = nullptr; // Cutscene finished
|
||||||
|
fireOnComplete();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CutscenePlayer::fireOnComplete() {
|
||||||
|
if (m_onCompleteRef == LUA_NOREF || !m_luaState) return;
|
||||||
|
psyqo::Lua L(m_luaState);
|
||||||
|
L.rawGetI(LUA_REGISTRYINDEX, m_onCompleteRef);
|
||||||
|
if (L.isFunction(-1)) {
|
||||||
|
if (L.pcall(0, 0) != LUA_OK) {
|
||||||
|
printf("Cutscene onComplete error: %s\n", L.optString(-1, "unknown"));
|
||||||
|
L.pop();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
L.pop();
|
||||||
|
}
|
||||||
|
// Unreference the callback (one-shot)
|
||||||
|
luaL_unref(m_luaState, LUA_REGISTRYINDEX, m_onCompleteRef);
|
||||||
|
m_onCompleteRef = LUA_NOREF;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int32_t applyCurve(int32_t t, InterpMode mode) {
|
static int32_t applyCurve(int32_t t, InterpMode mode) {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
#include "gameobject.hh"
|
#include "gameobject.hh"
|
||||||
#include "audiomanager.hh"
|
#include "audiomanager.hh"
|
||||||
|
|
||||||
|
#include <psyqo-lua/lua.hh>
|
||||||
|
|
||||||
namespace psxsplash {
|
namespace psxsplash {
|
||||||
|
|
||||||
class UISystem; // Forward declaration
|
class UISystem; // Forward declaration
|
||||||
@@ -86,7 +88,8 @@ public:
|
|||||||
UISystem* uiSystem = nullptr);
|
UISystem* uiSystem = nullptr);
|
||||||
|
|
||||||
/// Play cutscene by name. Returns false if not found.
|
/// Play cutscene by name. Returns false if not found.
|
||||||
bool play(const char* name);
|
/// If loop is true, the cutscene replays from the start when it ends.
|
||||||
|
bool play(const char* name, bool loop = false);
|
||||||
|
|
||||||
/// Stop the current cutscene immediately.
|
/// Stop the current cutscene immediately.
|
||||||
void stop();
|
void stop();
|
||||||
@@ -94,6 +97,15 @@ public:
|
|||||||
/// True if a cutscene is currently active.
|
/// True if a cutscene is currently active.
|
||||||
bool isPlaying() const { return m_active != nullptr; }
|
bool isPlaying() const { return m_active != nullptr; }
|
||||||
|
|
||||||
|
/// Set a Lua registry reference to call when the cutscene finishes.
|
||||||
|
/// Pass LUA_NOREF to clear. The callback is called ONCE when the
|
||||||
|
/// cutscene ends (not on each loop iteration - only when it truly stops).
|
||||||
|
void setOnCompleteRef(int ref) { m_onCompleteRef = ref; }
|
||||||
|
int getOnCompleteRef() const { return m_onCompleteRef; }
|
||||||
|
|
||||||
|
/// Set the lua_State for callbacks. Must be called before play().
|
||||||
|
void setLuaState(lua_State* L) { m_luaState = L; }
|
||||||
|
|
||||||
/// Advance one frame. Call once per frame. Does nothing when idle.
|
/// Advance one frame. Call once per frame. Does nothing when idle.
|
||||||
void tick();
|
void tick();
|
||||||
|
|
||||||
@@ -103,15 +115,19 @@ private:
|
|||||||
Cutscene* m_active = nullptr;
|
Cutscene* m_active = nullptr;
|
||||||
uint16_t m_frame = 0;
|
uint16_t m_frame = 0;
|
||||||
uint8_t m_nextAudio = 0;
|
uint8_t m_nextAudio = 0;
|
||||||
|
bool m_loop = false;
|
||||||
Camera* m_camera = nullptr;
|
Camera* m_camera = nullptr;
|
||||||
AudioManager* m_audio = nullptr;
|
AudioManager* m_audio = nullptr;
|
||||||
UISystem* m_uiSystem = nullptr;
|
UISystem* m_uiSystem = nullptr;
|
||||||
|
lua_State* m_luaState = nullptr;
|
||||||
|
int m_onCompleteRef = LUA_NOREF;
|
||||||
|
|
||||||
psyqo::Trig<> m_trig;
|
psyqo::Trig<> m_trig;
|
||||||
|
|
||||||
void applyTrack(CutsceneTrack& track);
|
void applyTrack(CutsceneTrack& track);
|
||||||
void lerpKeyframes(CutsceneKeyframe* kf, uint8_t count, const int16_t initial[3], int16_t out[3]);
|
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]);
|
void lerpAngles(CutsceneKeyframe* kf, uint8_t count, const int16_t initial[3], int16_t out[3]);
|
||||||
|
void fireOnComplete();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace psxsplash
|
} // namespace psxsplash
|
||||||
|
|||||||
@@ -285,6 +285,19 @@ void LuaAPI::RegisterAll(psyqo::Lua& L, SceneManager* scene, CutscenePlayer* cut
|
|||||||
|
|
||||||
L.setGlobal("Cutscene");
|
L.setGlobal("Cutscene");
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// CONTROLS API
|
||||||
|
// ========================================================================
|
||||||
|
L.newTable();
|
||||||
|
|
||||||
|
L.push(Controls_SetEnabled);
|
||||||
|
L.setField(-2, "SetEnabled");
|
||||||
|
|
||||||
|
L.push(Controls_IsEnabled);
|
||||||
|
L.setField(-2, "IsEnabled");
|
||||||
|
|
||||||
|
L.setGlobal("Controls");
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// UI API
|
// UI API
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
@@ -1479,7 +1492,32 @@ int LuaAPI::Cutscene_Play(lua_State* L) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const char* name = lua.toString(1);
|
const char* name = lua.toString(1);
|
||||||
s_cutscenePlayer->play(name);
|
bool loop = false;
|
||||||
|
int onCompleteRef = LUA_NOREF;
|
||||||
|
|
||||||
|
// Optional second argument: options table {loop=bool, onComplete=function}
|
||||||
|
if (lua.isTable(2)) {
|
||||||
|
lua.getField(2, "loop");
|
||||||
|
if (lua.isBoolean(-1)) loop = lua.toBoolean(-1);
|
||||||
|
lua.pop();
|
||||||
|
|
||||||
|
lua.getField(2, "onComplete");
|
||||||
|
if (lua.isFunction(-1)) {
|
||||||
|
onCompleteRef = lua.ref(); // pops and stores in registry
|
||||||
|
} else {
|
||||||
|
lua.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any previous callback before starting a new cutscene
|
||||||
|
int oldRef = s_cutscenePlayer->getOnCompleteRef();
|
||||||
|
if (oldRef != LUA_NOREF) {
|
||||||
|
luaL_unref(L, LUA_REGISTRYINDEX, oldRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
s_cutscenePlayer->setLuaState(L);
|
||||||
|
s_cutscenePlayer->setOnCompleteRef(onCompleteRef);
|
||||||
|
s_cutscenePlayer->play(name, loop);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1503,6 +1541,28 @@ int LuaAPI::Cutscene_IsPlaying(lua_State* L) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// CONTROLS API IMPLEMENTATION
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
int LuaAPI::Controls_SetEnabled(lua_State* L) {
|
||||||
|
psyqo::Lua lua(L);
|
||||||
|
if (s_sceneManager && lua.isBoolean(1)) {
|
||||||
|
s_sceneManager->setControlsEnabled(lua.toBoolean(1));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int LuaAPI::Controls_IsEnabled(lua_State* L) {
|
||||||
|
psyqo::Lua lua(L);
|
||||||
|
if (s_sceneManager) {
|
||||||
|
lua.push(s_sceneManager->isControlsEnabled());
|
||||||
|
} else {
|
||||||
|
lua.push(false);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// UI API IMPLEMENTATION
|
// UI API IMPLEMENTATION
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ private:
|
|||||||
// CUTSCENE API - Cutscene playback control
|
// CUTSCENE API - Cutscene playback control
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
// Cutscene.Play(name) -> nil
|
// Cutscene.Play(name) or Cutscene.Play(name, {loop=bool, onComplete=fn})
|
||||||
static int Cutscene_Play(lua_State* L);
|
static int Cutscene_Play(lua_State* L);
|
||||||
|
|
||||||
// Cutscene.Stop() -> nil
|
// Cutscene.Stop() -> nil
|
||||||
@@ -271,6 +271,12 @@ private:
|
|||||||
// Cutscene.IsPlaying() -> boolean
|
// Cutscene.IsPlaying() -> boolean
|
||||||
static int Cutscene_IsPlaying(lua_State* L);
|
static int Cutscene_IsPlaying(lua_State* L);
|
||||||
|
|
||||||
|
// Controls.SetEnabled(bool) - enable/disable all player input
|
||||||
|
static int Controls_SetEnabled(lua_State* L);
|
||||||
|
|
||||||
|
// Controls.IsEnabled() -> boolean
|
||||||
|
static int Controls_IsEnabled(lua_State* L);
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// UI API - Canvas and element control
|
// UI API - Canvas and element control
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData, LoadingSc
|
|||||||
m_navRegions = sceneSetup.navRegions; // Nav region system (v7+)
|
m_navRegions = sceneSetup.navRegions; // Nav region system (v7+)
|
||||||
m_playerNavRegion = m_navRegions.isLoaded() ? m_navRegions.getStartRegion() : NAV_NO_REGION;
|
m_playerNavRegion = m_navRegions.isLoaded() ? m_navRegions.getStartRegion() : NAV_NO_REGION;
|
||||||
|
|
||||||
|
// If nav regions are loaded, camera follows the player. Otherwise the
|
||||||
|
// scene is in "free camera" mode where cutscenes and Lua drive the camera.
|
||||||
|
m_cameraFollowsPlayer = m_navRegions.isLoaded();
|
||||||
|
m_controlsEnabled = true;
|
||||||
|
|
||||||
// Scene type and render path
|
// Scene type and render path
|
||||||
m_sceneType = sceneSetup.sceneType;
|
m_sceneType = sceneSetup.sceneType;
|
||||||
|
|
||||||
@@ -397,6 +402,7 @@ void psxsplash::SceneManager::GameTick(psyqo::GPU &gpu) {
|
|||||||
// Save position BEFORE movement for collision detection
|
// Save position BEFORE movement for collision detection
|
||||||
psyqo::Vec3 oldPlayerPosition = m_playerPosition;
|
psyqo::Vec3 oldPlayerPosition = m_playerPosition;
|
||||||
|
|
||||||
|
if (m_controlsEnabled) {
|
||||||
m_controls.HandleControls(m_playerPosition, playerRotationX, playerRotationY, playerRotationZ, freecam, m_deltaFrames);
|
m_controls.HandleControls(m_playerPosition, playerRotationX, playerRotationY, playerRotationZ, freecam, m_deltaFrames);
|
||||||
|
|
||||||
// Jump input: Cross button triggers jump when grounded
|
// Jump input: Cross button triggers jump when grounded
|
||||||
@@ -404,6 +410,7 @@ void psxsplash::SceneManager::GameTick(psyqo::GPU &gpu) {
|
|||||||
m_velocityY = -m_jumpVelocityRaw; // Negative = upward (PSX Y-down)
|
m_velocityY = -m_jumpVelocityRaw; // Negative = upward (PSX Y-down)
|
||||||
m_isGrounded = false;
|
m_isGrounded = false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
gpu.pumpCallbacks();
|
gpu.pumpCallbacks();
|
||||||
uint32_t controlsEnd = gpu.now();
|
uint32_t controlsEnd = gpu.now();
|
||||||
@@ -460,10 +467,17 @@ void psxsplash::SceneManager::GameTick(psyqo::GPU &gpu) {
|
|||||||
psxsplash::debug::Profiler::getInstance().setSectionTime(psxsplash::debug::PROFILER_NAVMESH, navmeshTime);
|
psxsplash::debug::Profiler::getInstance().setSectionTime(psxsplash::debug::PROFILER_NAVMESH, navmeshTime);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Only snap camera to player when in player-follow mode and no
|
||||||
|
// cutscene is actively controlling the camera. In free camera mode
|
||||||
|
// (no nav regions / no PSXPlayer), the camera is driven entirely
|
||||||
|
// by cutscenes and Lua. After a cutscene ends in free mode, the
|
||||||
|
// camera stays at the last cutscene position.
|
||||||
|
if (m_cameraFollowsPlayer && !m_cutscenePlayer.isPlaying()) {
|
||||||
m_currentCamera.SetPosition(static_cast<psyqo::FixedPoint<12>>(m_playerPosition.x),
|
m_currentCamera.SetPosition(static_cast<psyqo::FixedPoint<12>>(m_playerPosition.x),
|
||||||
static_cast<psyqo::FixedPoint<12>>(m_playerPosition.y),
|
static_cast<psyqo::FixedPoint<12>>(m_playerPosition.y),
|
||||||
static_cast<psyqo::FixedPoint<12>>(m_playerPosition.z));
|
static_cast<psyqo::FixedPoint<12>>(m_playerPosition.z));
|
||||||
m_currentCamera.SetRotation(playerRotationX, playerRotationY, playerRotationZ);
|
m_currentCamera.SetRotation(playerRotationX, playerRotationY, playerRotationZ);
|
||||||
|
}
|
||||||
|
|
||||||
// Process pending scene transitions (at end of frame)
|
// Process pending scene transitions (at end of frame)
|
||||||
processPendingSceneLoad();
|
processPendingSceneLoad();
|
||||||
|
|||||||
@@ -82,6 +82,10 @@ class SceneManager {
|
|||||||
Lua& getLua() { return L; }
|
Lua& getLua() { return L; }
|
||||||
AudioManager& getAudio() { return m_audio; }
|
AudioManager& getAudio() { return m_audio; }
|
||||||
|
|
||||||
|
// Controls enable/disable (Lua-driven)
|
||||||
|
void setControlsEnabled(bool enabled) { m_controlsEnabled = enabled; }
|
||||||
|
bool isControlsEnabled() const { return m_controlsEnabled; }
|
||||||
|
|
||||||
// Scene loading (for multi-scene support)
|
// Scene loading (for multi-scene support)
|
||||||
void requestSceneLoad(int sceneIndex);
|
void requestSceneLoad(int sceneIndex);
|
||||||
int getCurrentSceneIndex() const { return m_currentSceneIndex; }
|
int getCurrentSceneIndex() const { return m_currentSceneIndex; }
|
||||||
@@ -162,6 +166,8 @@ class SceneManager {
|
|||||||
int m_deltaFrames; // Elapsed frame count (1 normally, 2+ if dropped)
|
int m_deltaFrames; // Elapsed frame count (1 normally, 2+ if dropped)
|
||||||
|
|
||||||
bool freecam = false;
|
bool freecam = false;
|
||||||
|
bool m_controlsEnabled = true; // Lua can disable all player input
|
||||||
|
bool m_cameraFollowsPlayer = true; // False when scene has no nav regions (freecam/cutscene mode)
|
||||||
|
|
||||||
// Static font pointer (set from main.cpp)
|
// Static font pointer (set from main.cpp)
|
||||||
static psyqo::Font<>* s_font;
|
static psyqo::Font<>* s_font;
|
||||||
|
|||||||
Reference in New Issue
Block a user