@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
JavaScript
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;
}
}