UNPKG

@hiddentao/clockwork-engine

Version:

A TypeScript/PIXI.js game engine for deterministic, replayable games with built-in rendering

263 lines (262 loc) 8.42 kB
import { EventEmitter } from "./EventEmitter"; import { GameEventManager } from "./GameEventManager"; import { GameObjectGroup } from "./GameObjectGroup"; import { PRNG } from "./PRNG"; import { Timer } from "./Timer"; import { UserInputEventSource } from "./UserInputEventSource"; import { AssetLoader } from "./assets/AssetLoader"; import { CollisionGrid } from "./geometry"; import { GameState } from "./types"; export var GameEngineEventType; (function (GameEngineEventType) { GameEngineEventType["STATE_CHANGE"] = "stateChange"; })(GameEngineEventType || (GameEngineEventType = {})); export class GameEngine extends EventEmitter { constructor(options) { super(); this.gameObjectGroups = new Map(); this.totalTicks = 0; this.state = GameState.READY; this.seed = ""; this.gameConfig = {}; this.prng = new PRNG(); this.timer = new Timer(); this.recorder = undefined; this.loader = options.loader; this.platform = options.platform; // Initialize default AssetLoader if not provided this.assetLoader = options.assetLoader || new AssetLoader(options.loader, options.platform.rendering, options.platform.audio); this.eventManager = new GameEventManager(new UserInputEventSource(), this); this.collisionTree = new CollisionGrid(); } setState(newState) { const oldState = this.state; this.state = newState; this.emit(GameEngineEventType.STATE_CHANGE, newState, oldState); } /** * Reset the game engine to initial state * Clears all game objects, resets tick counter, and prepares for new game * If assetLoader is configured, preloads all registered assets before setup() * @param gameConfig Game configuration containing seed and initial state */ async reset(gameConfig) { this.gameConfig = gameConfig; if (gameConfig.prngSeed !== undefined) { this.seed = gameConfig.prngSeed; } this.setState(GameState.READY); this.prng.reset(this.seed); this.totalTicks = 0; this.gameObjectGroups.clear(); this.timer.reset(); this.eventManager.reset(); this.collisionTree.clear(); // Preload assets before setup() if assetLoader is configured if (this.assetLoader) { await this.assetLoader.preloadAssets(); } await this.setup(gameConfig); } /** * Start the game by transitioning from READY to PLAYING state * Enables game loop processing and begins gameplay * @throws Error if engine is not in READY state */ start() { if (this.state !== GameState.READY) { throw new Error(`Cannot start game: expected READY state, got ${this.state}`); } this.setState(GameState.PLAYING); } /** * Pause the game by transitioning from PLAYING to PAUSED state * Stops game loop processing while maintaining current state * @throws Error if engine is not in PLAYING state */ pause() { if (this.state !== GameState.PLAYING) { throw new Error(`Cannot pause game: expected PLAYING state, got ${this.state}`); } this.setState(GameState.PAUSED); } /** * Resume the game by transitioning from PAUSED to PLAYING state * Restarts game loop processing from paused state * @throws Error if engine is not in PAUSED state */ resume() { if (this.state !== GameState.PAUSED) { throw new Error(`Cannot resume game: expected PAUSED state, got ${this.state}`); } this.setState(GameState.PLAYING); } /** * End the game by transitioning to ENDED state * Stops all game processing and marks game as finished * @throws Error if engine is not in PLAYING or PAUSED state */ end() { if (this.state !== GameState.PLAYING && this.state !== GameState.PAUSED) { throw new Error(`Cannot end game: expected PLAYING or PAUSED state, got ${this.state}`); } this.setState(GameState.ENDED); } /** * Update the game state for the current tick * Processes inputs, timers, and game objects in deterministic order * @param deltaTicks Number of ticks to advance the simulation */ update(deltaTicks) { if (this.state !== GameState.PLAYING) { return; } // Update tick counter to maintain deterministic timing this.totalTicks += deltaTicks; // Record tick progression for replay system if (this.recorder) { this.recorder.recordFrameUpdate(deltaTicks, this.totalTicks); } // Process queued events at current tick this.eventManager.update(deltaTicks, this.totalTicks); // Execute scheduled timer callbacks this.timer.update(deltaTicks, this.totalTicks); // Update all registered game objects by type for (const [_type, group] of this.gameObjectGroups) { group.update(deltaTicks, this.totalTicks); } } /** * Register a GameObject with the engine * Automatically creates and manages groups by type * @param gameObject The game object to register * @param overrideType Optional type to use instead of gameObject.getType() */ registerGameObject(gameObject, overrideType) { const type = overrideType || gameObject.getType(); let group = this.gameObjectGroups.get(type); if (!group) { group = new GameObjectGroup(); this.gameObjectGroups.set(type, group); } group.add(gameObject); } /** * Get a GameObjectGroup by type * Returns undefined if no objects of that type have been registered */ getGameObjectGroup(type) { return this.gameObjectGroups.get(type); } /** * Get all registered object types */ getRegisteredTypes() { return Array.from(this.gameObjectGroups.keys()); } /** * Get the current game state */ getState() { return this.state; } /** * Get the seed used for this game session */ getSeed() { return this.seed; } /** * Get the game configuration */ getGameConfig() { return this.gameConfig; } /** * Get the total number of ticks processed */ getTotalTicks() { return this.totalTicks; } /** * Get the PRNG instance for deterministic random numbers */ getPRNG() { return this.prng; } /** * Clear all destroyed GameObjects from all groups * @returns Total number of destroyed objects removed */ clearDestroyedGameObjects() { let totalRemoved = 0; for (const group of this.gameObjectGroups.values()) { totalRemoved += group.clearDestroyed(); } return totalRemoved; } /** * Schedule a one-time callback to execute after the specified number of ticks */ setTimeout(callback, ticks) { return this.timer.setTimeout(callback, ticks); } /** * Schedule a repeating callback to execute every specified number of ticks */ setInterval(callback, ticks) { return this.timer.setInterval(callback, ticks); } /** * Cancel a timer */ clearTimer(id) { return this.timer.clearTimer(id); } /** * Get the timer system */ getTimer() { return this.timer; } /** * Get the event manager */ getEventManager() { return this.eventManager; } /** * Set the game recorder for recording gameplay * Also sets the recorder on the event manager */ setGameRecorder(recorder) { this.recorder = recorder; this.eventManager.setRecorder(recorder); } /** * Get the collision tree for spatial collision detection */ getCollisionTree() { return this.collisionTree; } /** * Get the loader instance for data loading */ getLoader() { return this.loader; } /** * Get the platform layer */ getPlatform() { return this.platform; } /** * Get the asset loader for managing game assets */ getAssetLoader() { return this.assetLoader; } }