3.3 KiB
3.3 KiB
Skill — Singleton Managers
Pattern
Every manager follows the exact same singleton structure:
export class SomeManager {
private static _instance: SomeManager | null = null;
static getInstance(): SomeManager {
if (!SomeManager._instance) {
SomeManager._instance = new SomeManager();
}
return SomeManager._instance;
}
private constructor() {
// init logic
}
destroy(): void {
// cleanup logic
SomeManager._instance = null;
}
}
Managers in this project
| Manager | File | Role |
|---|---|---|
AudioManager |
src/managers/AudioManager.ts |
Music and SFX playback. |
InteractionManager |
src/managers/InteractionManager.ts |
Focus, nearby, trigger, grab, and hand-grab interaction state. |
GameManager |
target-state only | Future single source of truth for phase, zone, mission, input lock, dialogue. |
CinematicManager |
target-state only | Future GSAP timeline orchestrator. |
ZoneManager |
target-state only | Future zone entry/exit detection and LOD triggers. |
Target-State GameManager
GameManager does not exist in the current implementation. The following pattern is target-state guidance only and should not be applied until the manager exists in code.
export class GameManager {
cinematic!: CinematicManager;
audio!: AudioManager;
zone!: ZoneManager;
private constructor() {
this.cinematic = CinematicManager.getInstance();
this.audio = AudioManager.getInstance();
this.zone = ZoneManager.getInstance();
}
}
When a GameManager exists, components and hooks should access other managers through it:
// Correct
GameManager.getInstance().cinematic.play("intro");
// Wrong — never import sub-managers directly in components
CinematicManager.getInstance().play("intro");
Target-State Subscribe Pattern
private listeners = new Set<() => void>()
subscribe(listener: () => void): () => void {
this.listeners.add(listener)
return () => this.listeners.delete(listener)
}
private emit(): void {
this.listeners.forEach((cb) => cb())
}
In that target-state manager, every set*() method calls this.emit() to notify subscribers.
Target-State React Bridge Hook
// hooks/useGameState.ts
export function useGameState() {
const game = GameManager.getInstance();
const [state, setState] = useState(game.getState());
useEffect(() => {
return game.subscribe(() => setState({ ...game.getState() }));
}, [game]);
return state;
}
Rules
- Do not add a
GameManagerunless the feature requires a real shared gameplay state owner. - Current managers may be imported directly until the target-state orchestrator exists.
- Keep singleton managers limited to side-effect services or shared interaction state.
- Always call
destroy()on cleanup when a manager owns external resources. - Never create manager instances with
new— always use.getInstance().