Browse Source

refactor(kernel): introduce app runtime and event bus plugin system

Add AppRuntime as the central runtime orchestrator and migrate init wiring to runtime-managed game/scene/event instances.
Introduce RuntimeEvents plus plugin hooks so scene-change and render lifecycle signals are extensible without breaking existing init exports.

Made-with: Cursor
master
npmrun 2 weeks ago
parent
commit
5977d399f4
  1. 6
      src/core/Game.ts
  2. 29
      src/init.ts
  3. 60
      src/kernel/AppRuntime.ts
  4. 59
      src/kernel/RuntimeEvents.ts
  5. 14
      src/kernel/RuntimePlugin.ts
  6. 80
      tests/kernel/runtime-events.test.ts

6
src/core/Game.ts

@ -31,6 +31,7 @@ class Game {
/** 连续旋转时忽略过期的 setTimeout 回调 */ /** 连续旋转时忽略过期的 setTimeout 回调 */
private _orientationLayoutGen = 0; private _orientationLayoutGen = 0;
private _rootResizeObserver: ResizeObserver | null = null; private _rootResizeObserver: ResizeObserver | null = null;
private _afterRender: (() => void) | null = null;
private constructor() {} private constructor() {}
@ -282,6 +283,11 @@ class Game {
render(): void { render(): void {
this.renderer.render(this.stage); this.renderer.render(this.stage);
this._afterRender?.();
}
setAfterRenderHook(hook: (() => void) | null): void {
this._afterRender = hook;
} }
} }

29
src/init.ts

@ -1,19 +1,35 @@
import Game from "./core/Game";
import SceneManager from "./scene/SceneManager";
import { BaseScene } from "./scene/BaseScene"; import { BaseScene } from "./scene/BaseScene";
import type { IBaseScene } from "./scene/types"; import type { IBaseScene } from "./scene/types";
import { SceneType } from "./enums/SceneType"; import { SceneType } from "./enums/SceneType";
import { assetManager } from "./core/AssetManager"; import { assetManager } from "./core/AssetManager";
import { logger } from "./core/Logger"; import { logger } from "./core/Logger";
import eventBus from "./core/EventBus";
import { Container } from "pixi.js"; import { Container } from "pixi.js";
import { appRuntime } from "./kernel/AppRuntime";
const game = Game.getInstance(); import type { RuntimePlugin } from "./kernel/RuntimePlugin";
const sceneManager = SceneManager.getInstance();
const game = appRuntime.game;
const sceneManager = appRuntime.sceneManager;
const eventBus = appRuntime.events;
const runtimeSceneEventsPlugin: RuntimePlugin = {
name: "runtime-scene-events",
setup: ({ game: runtimeGame, sceneManager: runtimeSceneManager, events }) => {
runtimeGame.setAfterRenderHook(() => {
events.emit("game:rendered", undefined);
});
runtimeSceneManager.onStageChange((current, previous) => {
events.emit("scene:changed", {
current: current.name,
previous: previous?.name,
});
});
},
};
type Constructor<T> = new (...args: unknown[]) => T; type Constructor<T> = new (...args: unknown[]) => T;
export async function initApp(): Promise<void> { export async function initApp(): Promise<void> {
appRuntime.use(runtimeSceneEventsPlugin);
await assetManager.init(); await assetManager.init();
await game.init(); await game.init();
@ -121,6 +137,7 @@ export async function initApp(): Promise<void> {
}); });
logger.info("App initialized"); logger.info("App initialized");
appRuntime.markReady();
} }
export { game, sceneManager, eventBus, assetManager, logger }; export { game, sceneManager, eventBus, assetManager, logger };

60
src/kernel/AppRuntime.ts

@ -0,0 +1,60 @@
import Game from "@/core/Game";
import SceneManager from "@/scene/SceneManager";
import { RuntimeEvents } from "./RuntimeEvents";
import type { RuntimePlugin, RuntimePluginContext } from "./RuntimePlugin";
export class AppRuntime {
private static instance: AppRuntime;
public readonly game: Game;
public readonly sceneManager: SceneManager;
public readonly events: RuntimeEvents;
private readonly installedPlugins = new Set<string>();
private startedAt = 0;
constructor(
game: Game = Game.getInstance(),
sceneManager: SceneManager = SceneManager.getInstance(),
events: RuntimeEvents = new RuntimeEvents()
) {
this.game = game;
this.sceneManager = sceneManager;
this.events = events;
}
static getInstance(): AppRuntime {
if (!AppRuntime.instance) {
AppRuntime.instance = new AppRuntime();
}
return AppRuntime.instance;
}
get pluginContext(): RuntimePluginContext {
return {
game: this.game,
sceneManager: this.sceneManager,
events: this.events,
};
}
use(plugin: RuntimePlugin): this {
if (this.installedPlugins.has(plugin.name)) {
return this;
}
plugin.setup(this.pluginContext);
this.installedPlugins.add(plugin.name);
return this;
}
markReady(): void {
if (this.startedAt > 0) {
return;
}
this.startedAt = Date.now();
this.events.emit("runtime:ready", { startedAt: this.startedAt });
}
}
export const appRuntime = AppRuntime.getInstance();

59
src/kernel/RuntimeEvents.ts

@ -0,0 +1,59 @@
type EventCallback<T = unknown> = (payload: T) => void;
export interface RuntimeEventMap {
"runtime:ready": { startedAt: number };
"scene:changed": { current: string; previous?: string };
"game:rendered": undefined;
}
export class RuntimeEvents {
private events = new Map<string, Set<EventCallback>>();
on<T = unknown>(event: string, callback: EventCallback<T>): () => void {
if (!this.events.has(event)) {
this.events.set(event, new Set());
}
this.events.get(event)!.add(callback as EventCallback);
return () => this.off(event, callback);
}
off<T = unknown>(event: string, callback: EventCallback<T>): void {
const callbacks = this.events.get(event);
if (!callbacks) {
return;
}
callbacks.delete(callback as EventCallback);
if (callbacks.size === 0) {
this.events.delete(event);
}
}
once<T = unknown>(event: string, callback: EventCallback<T>): () => void {
const wrapped: EventCallback<T> = (payload) => {
callback(payload);
this.off(event, wrapped);
};
return this.on(event, wrapped);
}
emit<T = unknown>(event: string, payload: T): void {
const callbacks = this.events.get(event);
if (!callbacks) {
return;
}
callbacks.forEach((cb) => {
cb(payload);
});
}
clear(): void {
this.events.clear();
}
getEventCount(): number {
return this.events.size;
}
}

14
src/kernel/RuntimePlugin.ts

@ -0,0 +1,14 @@
import type Game from "@/core/Game";
import type SceneManager from "@/scene/SceneManager";
import type { RuntimeEvents } from "./RuntimeEvents";
export interface RuntimePluginContext {
game: Game;
sceneManager: SceneManager;
events: RuntimeEvents;
}
export interface RuntimePlugin {
name: string;
setup: (context: RuntimePluginContext) => void | (() => void);
}

80
tests/kernel/runtime-events.test.ts

@ -0,0 +1,80 @@
import { describe, expect, it, vi } from "vitest";
import { RuntimeEvents } from "@/kernel/RuntimeEvents";
import { AppRuntime } from "@/kernel/AppRuntime";
import type { RuntimePlugin } from "@/kernel/RuntimePlugin";
vi.mock("@/core/Game", () => {
class MockGame {
private static instance: MockGame;
static getInstance(): MockGame {
if (!MockGame.instance) {
MockGame.instance = new MockGame();
}
return MockGame.instance;
}
}
return { default: MockGame };
});
vi.mock("@/scene/SceneManager", () => {
class MockSceneManager {
private static instance: MockSceneManager;
static getInstance(): MockSceneManager {
if (!MockSceneManager.instance) {
MockSceneManager.instance = new MockSceneManager();
}
return MockSceneManager.instance;
}
}
return { default: MockSceneManager };
});
describe("RuntimeEvents", () => {
it("supports on/off/emit", () => {
const events = new RuntimeEvents();
const handler = vi.fn();
events.on("runtime:ready", handler);
events.emit("runtime:ready", { startedAt: 1 });
events.off("runtime:ready", handler);
events.emit("runtime:ready", { startedAt: 2 });
expect(handler).toHaveBeenCalledTimes(1);
expect(handler).toHaveBeenCalledWith({ startedAt: 1 });
});
it("supports once subscriptions", () => {
const events = new RuntimeEvents();
const handler = vi.fn();
events.once("scene:changed", handler);
events.emit("scene:changed", { current: "a", previous: undefined });
events.emit("scene:changed", { current: "b", previous: "a" });
expect(handler).toHaveBeenCalledTimes(1);
expect(handler).toHaveBeenCalledWith({ current: "a", previous: undefined });
});
});
describe("AppRuntime plugin lifecycle", () => {
it("installs plugin and keeps compatibility exports", () => {
const runtime = new AppRuntime();
const setup = vi.fn();
const plugin: RuntimePlugin = {
name: "test-plugin",
setup,
};
runtime.use(plugin);
runtime.use(plugin);
expect(setup).toHaveBeenCalledTimes(1);
expect(runtime.game).toBeDefined();
expect(runtime.sceneManager).toBeDefined();
expect(runtime.events).toBeDefined();
});
});
Loading…
Cancel
Save