mirror of
https://github.com/Xevion/Pac-Man.git
synced 2026-01-31 02:25:04 -06:00
feat(web): implement game lifecycle management for SPA navigation
Add stop_game and restart_game FFI functions to properly pause/resume the game loop during page transitions, preventing resource leaks and audio issues when navigating between pages
This commit is contained in:
@@ -46,6 +46,51 @@ pub extern "C" fn start_game() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Called from JavaScript when navigating away from the game page.
|
||||||
|
/// Stops the Emscripten main loop and halts all audio.
|
||||||
|
#[cfg(target_os = "emscripten")]
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn stop_game() {
|
||||||
|
tracing::info!("Stopping game loop and halting audio");
|
||||||
|
unsafe {
|
||||||
|
platform::emscripten_cancel_main_loop();
|
||||||
|
sdl2::mixer::Channel::all().halt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called from JavaScript to restart the game after navigating back.
|
||||||
|
/// Creates a fresh App instance with the new canvas and starts the main loop.
|
||||||
|
#[cfg(target_os = "emscripten")]
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn restart_game() {
|
||||||
|
use std::ptr;
|
||||||
|
|
||||||
|
tracing::info!("Restarting game with fresh App instance");
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
// Drop old App to clean up resources
|
||||||
|
APP = None;
|
||||||
|
|
||||||
|
// Reinitialize audio subsystem for fresh state
|
||||||
|
sdl2::mixer::close_audio();
|
||||||
|
|
||||||
|
// Create fresh App with new canvas
|
||||||
|
match App::new() {
|
||||||
|
Ok(app) => {
|
||||||
|
APP = Some(app);
|
||||||
|
tracing::info!("Game restarted successfully");
|
||||||
|
|
||||||
|
// Signal ready and start the main loop
|
||||||
|
platform::run_script("if (window.pacmanReady) window.pacmanReady()");
|
||||||
|
platform::emscripten_set_main_loop_arg(main_loop_callback, ptr::null_mut(), 0, 1);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to restart game: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Emscripten main loop callback - runs once per frame
|
/// Emscripten main loop callback - runs once per frame
|
||||||
#[cfg(target_os = "emscripten")]
|
#[cfg(target_os = "emscripten")]
|
||||||
unsafe extern "C" fn main_loop_callback(_arg: *mut std::ffi::c_void) {
|
unsafe extern "C" fn main_loop_callback(_arg: *mut std::ffi::c_void) {
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ extern "C" {
|
|||||||
/// - `simulate_infinite_loop`: if 1, never returns (standard for games)
|
/// - `simulate_infinite_loop`: if 1, never returns (standard for games)
|
||||||
pub fn emscripten_set_main_loop_arg(func: EmMainLoopCallback, arg: *mut c_void, fps: c_int, simulate_infinite_loop: c_int);
|
pub fn emscripten_set_main_loop_arg(func: EmMainLoopCallback, arg: *mut c_void, fps: c_int, simulate_infinite_loop: c_int);
|
||||||
|
|
||||||
|
/// Cancel the currently running main loop.
|
||||||
|
/// After calling this, the loop callback will no longer be invoked.
|
||||||
|
pub fn emscripten_cancel_main_loop();
|
||||||
|
|
||||||
/// Execute JavaScript code from Rust
|
/// Execute JavaScript code from Rust
|
||||||
fn emscripten_run_script(script: *const i8);
|
fn emscripten_run_script(script: *const i8);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export interface PacmanModule {
|
||||||
|
canvas: HTMLCanvasElement;
|
||||||
|
_start_game?: () => void;
|
||||||
|
_stop_game?: () => void;
|
||||||
|
_restart_game?: () => void;
|
||||||
|
locateFile: (path: string) => string;
|
||||||
|
preRun: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PacmanWindow extends Window {
|
||||||
|
Module?: PacmanModule;
|
||||||
|
pacmanReady?: () => void;
|
||||||
|
SDL_CANVAS_ID?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPacmanWindow = (): PacmanWindow => window as unknown as PacmanWindow;
|
||||||
@@ -1,6 +1,35 @@
|
|||||||
import type { OnPageTransitionEndAsync } from "vike/types";
|
import type { OnPageTransitionEndAsync } from "vike/types";
|
||||||
|
import { getPacmanWindow } from "@/lib/pacman";
|
||||||
|
|
||||||
export const onPageTransitionEnd: OnPageTransitionEndAsync = async () => {
|
export const onPageTransitionEnd: OnPageTransitionEndAsync = async (
|
||||||
|
pageContext
|
||||||
|
) => {
|
||||||
console.log("Page transition end");
|
console.log("Page transition end");
|
||||||
document.querySelector("body")?.classList.remove("page-is-transitioning");
|
document.querySelector("body")?.classList.remove("page-is-transitioning");
|
||||||
|
|
||||||
|
// Restart the game loop when returning to the game page
|
||||||
|
if (pageContext.urlPathname === "/") {
|
||||||
|
const win = getPacmanWindow();
|
||||||
|
const module = win.Module;
|
||||||
|
|
||||||
|
if (module?._restart_game) {
|
||||||
|
const canvas = document.getElementById("canvas") as HTMLCanvasElement | null;
|
||||||
|
if (!canvas) {
|
||||||
|
console.error("Canvas element not found during game restart");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update canvas reference BEFORE restart - App::new() will read from Module.canvas
|
||||||
|
module.canvas = canvas;
|
||||||
|
// SDL2's Emscripten backend reads this for canvas lookup
|
||||||
|
win.SDL_CANVAS_ID = "#canvas";
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("Restarting game with fresh App instance");
|
||||||
|
module._restart_game();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to restart game:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import type { OnPageTransitionStartAsync } from "vike/types";
|
import type { OnPageTransitionStartAsync } from "vike/types";
|
||||||
|
import { getPacmanWindow } from "@/lib/pacman";
|
||||||
|
|
||||||
export const onPageTransitionStart: OnPageTransitionStartAsync = async () => {
|
export const onPageTransitionStart: OnPageTransitionStartAsync = async () => {
|
||||||
console.log("Page transition start");
|
console.log("Page transition start");
|
||||||
document.querySelector("body")?.classList.add("page-is-transitioning");
|
document.querySelector("body")?.classList.add("page-is-transitioning");
|
||||||
|
|
||||||
|
// Stop the game loop when navigating away from the game page
|
||||||
|
const win = getPacmanWindow();
|
||||||
|
if (win.Module?._stop_game) {
|
||||||
|
console.log("Stopping game loop for page transition");
|
||||||
|
win.Module._stop_game();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+39
-25
@@ -1,44 +1,58 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { getPacmanWindow } from "@/lib/pacman";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [gameReady, setGameReady] = useState(false);
|
const [gameReady, setGameReady] = useState(false);
|
||||||
const [gameStarted, setGameStarted] = useState(false);
|
const [gameStarted, setGameStarted] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Set up callback for when WASM signals it's ready
|
const win = getPacmanWindow();
|
||||||
(window as any).pacmanReady = () => {
|
|
||||||
|
// Always set up the ready callback (restart_game will call it too)
|
||||||
|
win.pacmanReady = () => {
|
||||||
setGameReady(true);
|
setGameReady(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!(window as any).Module) {
|
const module = win.Module;
|
||||||
const canvas = document.getElementById("canvas");
|
|
||||||
|
|
||||||
(window as any).Module = {
|
// If Module already exists (returning after navigation),
|
||||||
canvas: canvas,
|
// the onPageTransitionEnd hook handles calling restart_game
|
||||||
locateFile: (path: string) => {
|
if (module?._restart_game) {
|
||||||
return path.startsWith("/") ? path : `/${path}`;
|
setGameStarted(false);
|
||||||
},
|
// Don't delete pacmanReady here - restart_game needs it
|
||||||
preRun: [],
|
return;
|
||||||
};
|
|
||||||
|
|
||||||
const script = document.createElement("script");
|
|
||||||
script.src = "/pacman.js";
|
|
||||||
script.async = false;
|
|
||||||
document.body.appendChild(script);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
script.remove();
|
|
||||||
delete (window as any).pacmanReady;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// First time initialization
|
||||||
|
const canvas = document.getElementById("canvas") as HTMLCanvasElement | null;
|
||||||
|
if (!canvas) {
|
||||||
|
console.error("Canvas element not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
win.Module = {
|
||||||
|
canvas,
|
||||||
|
locateFile: (path: string) => {
|
||||||
|
return path.startsWith("/") ? path : `/${path}`;
|
||||||
|
},
|
||||||
|
preRun: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = "/pacman.js";
|
||||||
|
script.async = false;
|
||||||
|
document.body.appendChild(script);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
delete win.pacmanReady;
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleInteraction = useCallback(() => {
|
const handleInteraction = useCallback(() => {
|
||||||
if (gameReady && !gameStarted) {
|
if (gameReady && !gameStarted) {
|
||||||
// Call the exported Rust function to start the game
|
const win = getPacmanWindow();
|
||||||
const module = (window as any).Module;
|
if (win.Module?._start_game) {
|
||||||
if (module && module._start_game) {
|
win.Module._start_game();
|
||||||
module._start_game();
|
|
||||||
setGameStarted(true);
|
setGameStarted(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,15 @@ import tailwindcss from "@tailwindcss/vite";
|
|||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import vike from "vike/plugin";
|
import vike from "vike/plugin";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vike(), react(), tailwindcss()],
|
plugins: [vike(), react(), tailwindcss()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "."),
|
||||||
|
},
|
||||||
|
},
|
||||||
build: {
|
build: {
|
||||||
target: "es2022",
|
target: "es2022",
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user