# PixiJS 游戏框架重构实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 对现有的 PixiJS 游戏开发框架进行全面架构重构,实现清晰的分层模块化设计,提升开发体验。 **Architecture:** 采用分层架构,从下到上依次是 Core 核心层、Scene 场景层、Component 组件层、Utils 工具层,上层依赖下层。保留自动场景文件发现的特性。每个模块职责单一,接口清晰,完整 TypeScript 类型支持。 **Tech Stack:** PixiJS v8, TypeScript, @pixi/sound, @tweenjs/tween.js, Vite --- ## 文件结构 新文件将按以下结构创建: ``` src/ ├── core/ # Core Layer - 核心层 │ ├── Game.ts # 主应用类,维护 renderer/stage/ticker │ ├── EventBus.ts # 事件总线 │ ├── AssetManager.ts # 资源管理器,带引用计数 │ └── Logger.ts # 日志工具 ├── scene/ # Scene Layer - 场景层 │ ├── SceneManager.ts # 场景管理器 │ ├── BaseScene.ts # 场景抽象基类 │ └── types.ts # 场景相关类型定义 ├── components/ # Component Layer - 组件层 │ ├── Button.ts # 按钮组件(重构) │ ├── Label.ts # 文本标签组件(新增) │ └── Panel.ts # 面板容器组件(新增) ├── utils/ # Utils Layer - 工具层 │ ├── Position.ts # 定位对齐工具 │ ├── Tween.ts # 动画补间封装 │ ├── Sound.ts # 音频管理 │ ├── Storage.ts # 本地存储封装 │ └── MathUtils.ts # 常用数学工具 ├── enums/ # 枚举定义 │ ├── SceneType.ts # 场景类型枚举 │ └── Orientation.ts # 屏幕方向枚举 ├── types/ # 全局类型 │ └── index.d.ts # 全局类型定义 └── scenes/ # Game Content - 游戏场景 └── **/page_*.ts # 自动发现场景文件 ``` --- ## 任务分解 ### Task 1: 创建枚举定义 **Files:** - Create: `src/enums/SceneType.ts` - Create: `src/enums/Orientation.ts` - [ ] **Step 1: 创建 SceneType 枚举** ```typescript export enum SceneType { Normal = 0, // 普通场景 - 退出时销毁 Resident = 1, // 常驻场景 - 只隐藏不销毁 } ``` - [ ] **Step 2: 创建 Orientation 枚举** ```typescript export enum Orientation { Landscape = "landscape", Portrait = "portrait", } ``` - [ ] **Step 3: Commit** ```bash git add src/enums/*.ts git commit -m "feat: add enum definitions for SceneType and Orientation" ``` --- ### Task 2: 创建全局类型定义 **Files:** - Create: `src/types/index.d.ts` - [ ] **Step 1: 创建全局类型定义文件** ```typescript // 全局构造函数类型 declare type Constructor = new (...args: any[]) => T; // 通用回调类型 declare type Callback = () => T; declare type Callback1 = (arg: T) => R; ``` - [ ] **Step 2: Commit** ```bash git add src/types/index.d.ts git commit -m "feat: add global type definitions" ``` --- ### Task 3: 实现 Core 层 - Logger **Files:** - Create: `src/core/Logger.ts` - [ ] **Step 1: 创建 Logger 类** ```typescript type LogLevel = "debug" | "info" | "warn" | "error" | "none"; class Logger { private level: LogLevel = "debug"; setLevel(level: LogLevel): void { this.level = level; } debug(...args: any[]): void { if (this.shouldLog("debug")) { console.debug(...args); } } info(...args: any[]): void { if (this.shouldLog("info")) { console.info(...args); } } warn(...args: any[]): void { if (this.shouldLog("warn")) { console.warn(...args); } } error(...args: any[]): void { if (this.shouldLog("error")) { console.error(...args); } } private shouldLog(level: LogLevel): boolean { const levels: LogLevel[] = ["debug", "info", "warn", "error", "none"]; return levels.indexOf(level) >= levels.indexOf(this.level); } } export const logger = new Logger(); export default logger; ``` - [ ] **Step 2: Commit** ```bash git add src/core/Logger.ts git commit -m "feat: add Logger core module" ``` --- ### Task 4: 实现 Core 层 - EventBus **Files:** - Create: `src/core/EventBus.ts` - [ ] **Step 1: 创建 EventBus 类** ```typescript type EventCallback = (...args: any[]) => void; class EventBus { private events: Map> = new Map(); on(event: string, callback: EventCallback): () => void { if (!this.events.has(event)) { this.events.set(event, new Set()); } this.events.get(event)!.add(callback); // 返回取消订阅函数 return () => this.off(event, callback); } off(event: string, callback: EventCallback): void { const callbacks = this.events.get(event); if (callbacks) { callbacks.delete(callback); if (callbacks.size === 0) { this.events.delete(event); } } } once(event: string, callback: EventCallback): () => void { const onceCallback: EventCallback = (...args) => { callback(...args); this.off(event, onceCallback); }; return this.on(event, onceCallback); } emit(event: string, ...args: any[]): void { const callbacks = this.events.get(event); if (callbacks) { callbacks.forEach(cb => { try { cb(...args); } catch (e) { console.error(`EventBus: error in event "${event}"`, e); } }); } } clear(): void { this.events.clear(); } getEventCount(): number { return this.events.size; } } export const eventBus = new EventBus(); export default eventBus; ``` - [ ] **Step 2: Commit** ```bash git add src/core/EventBus.ts git commit -m "feat: add EventBus core module" ``` --- ### Task 5: 实现 Core 层 - AssetManager **Files:** - Create: `src/core/AssetManager.ts` - [ ] **Step 1: 创建 AssetManager 类** ```typescript import { Assets, type AssetsBundle } from "pixi.js"; import { logger } from "./Logger"; class AssetManager { private referenceCounts: Map = new Map(); async init(): Promise { await Assets.init({ manifest: "/manifest.json" }); logger.debug("AssetManager initialized"); } async loadBundle( name: string, onProgress?: (progress: number) => void ): Promise { // 已经加载过,增加引用计数 if (this.referenceCounts.has(name)) { const count = this.referenceCounts.get(name)! + 1; this.referenceCounts.set(name, count); logger.debug(`AssetManager: reuse bundle "${name}", refCount = ${count}`); onProgress?.(1); return Assets.getBundle(name); } // 首次加载 logger.debug(`AssetManager: loading bundle "${name}"`); const bundle = await Assets.loadBundle(name, onProgress); this.referenceCounts.set(name, 1); return bundle; } async unloadBundle(name: string): Promise { if (!this.referenceCounts.has(name)) { logger.warn(`AssetManager: unloading unloaded bundle "${name}"`); return; } const count = this.referenceCounts.get(name)! - 1; this.referenceCounts.set(name, count); if (count <= 0) { logger.debug(`AssetManager: unloading bundle "${name}"`); await Assets.unloadBundle(name); this.referenceCounts.delete(name); } else { logger.debug(`AssetManager: decrease refCount for "${name}" to ${count}`); } } isLoaded(name: string): boolean { return this.referenceCounts.has(name) && this.referenceCounts.get(name)! > 0; } getRefCount(name: string): number { return this.referenceCounts.get(name) ?? 0; } clearAll(): void { this.referenceCounts.clear(); } } export const assetManager = new AssetManager(); export default assetManager; ``` - [ ] **Step 2: Commit** ```bash git add src/core/AssetManager.ts git commit -m "feat: add AssetManager core module with ref counting" ``` --- ### Task 6: 实现 Core 层 - Game 主类 **Files:** - Create: `src/core/Game.ts` - [ ] **Step 1: 创建 Game 类,维护 PixiJS 核心对象** ```typescript import { autoDetectRenderer, Container, Renderer, Ticker, } from "pixi.js"; import { Orientation } from "@/enums/Orientation"; import { initDevtools } from "@pixi/devtools"; import { logger } from "./Logger"; class Game { private static instance: Game; private _stage: Container; private _renderer: Renderer; private _ticker: Ticker; private orientation: Orientation = Orientation.Portrait; public designWidth: number = 750; public info = { width: 0, height: 0, }; private constructor() {} static getInstance(): Game { if (!Game.instance) { Game.instance = new Game(); } return Game.instance; } public get renderer(): Renderer { return this._renderer; } public get stage(): Container { return this._stage; } public get ticker(): Ticker { return this._ticker; } async init(): Promise { const screenWidth = document.documentElement.clientWidth; const screenHeight = document.documentElement.clientHeight; this._stage = new Container(); this._stage.label = "root"; this._renderer = await autoDetectRenderer({ autoDensity: true, width: screenWidth, height: screenHeight, antialias: true, backgroundAlpha: 255, resolution: 2, backgroundColor: 0x1d9ce0, }); this.renderer.resize(screenWidth, screenHeight); document.body.appendChild(this.renderer.canvas); window.addEventListener("resize", () => { this.updateView(); }); this._ticker = Ticker.shared; this._ticker.autoStart = true; if (import.meta.env.DEV) { initDevtools({ stage: this._stage, renderer: this._renderer }); } this.updateView(); logger.info("Game initialized", { screenWidth, screenHeight }); } getInfo(): { width: number; height: number } { return { ...this.info }; } setOrientation(orientation: Orientation): void { this.orientation = orientation; this.updateView(); } private detectCurrentOrientation(): Orientation { const isLandscape = window.innerWidth > window.innerHeight; return isLandscape ? Orientation.Landscape : Orientation.Portrait; } updateView(): void { const clientWidth = document.documentElement.clientWidth; const clientHeight = document.documentElement.clientHeight; this.renderer.resize(clientWidth, clientHeight); const currentOrientation = this.detectCurrentOrientation(); let offsetWidth = clientWidth; let offsetHeight = clientHeight; if (this.orientation === Orientation.Landscape) { if (currentOrientation === Orientation.Landscape) { this._stage.rotation = 0; this._stage.y = 0; } else { this._stage.rotation = -Math.PI / 2; this._stage.y = clientHeight; } const scaleRatio = clientWidth / this.designWidth; this._stage.scale.set(scaleRatio, scaleRatio); this.info.width = offsetWidth / scaleRatio; this.info.height = offsetHeight / scaleRatio; } else { if (currentOrientation === Orientation.Landscape) { this._stage.rotation = 0; this._stage.y = 0; const scaleRatio = offsetWidth / this.designWidth; this._stage.scale.set(scaleRatio, scaleRatio); this.info.width = offsetHeight / scaleRatio; this.info.height = offsetWidth / scaleRatio; } else { this._stage.rotation = -Math.PI / 2; this._stage.y = offsetHeight; const scaleRatio = offsetWidth / this.designWidth; this._stage.scale.set(scaleRatio, scaleRatio); this.info.width = offsetHeight / scaleRatio; this.info.height = offsetWidth / scaleRatio; } } this.render(); } render(): void { this.renderer.render(this._stage); } } export default Game; ``` - [ ] **Step 2: Commit** ```bash git add src/core/Game.ts git commit -m "feat: add Game core class with orientation handling" ``` --- ### Task 7: 定义 Scene 层类型和 BaseScene **Files:** - Create: `src/scene/types.ts` - Create: `src/scene/BaseScene.ts` - [ ] **Step 1: 创建 types.ts** ```typescript import { Container } from "pixi.js"; import { SceneType } from "@/enums/SceneType"; export interface SceneConfig { name: string; type: SceneType; stage: Container; } export interface SceneLifeCycle { /** 加载资源包 */ loadBundle?(): Promise; /** 卸载资源包 */ unLoadBundle?(): Promise; /** 创建布局 */ layout?(): void | Promise; /** 场景加载完成 */ onLoad?(): void | Promise; /** 场景即将卸载 */ onUnLoad?(): void | Promise; /** 每帧更新 */ update?(dt: number, ticker: Ticker): void; /** 更新后处理 */ lateUpdate?(dt: number, ticker: Ticker): void; } export interface IBaseScene extends SceneLifeCycle { /** 场景容器 */ stage: Container; /** 场景名称 */ readonly name: string; /** 场景类型 */ readonly type: SceneType; /** 是否已加载资源 */ _assetsLoaded: boolean; /** 是否已布局 */ _layoutDone: boolean; /** 改变场景 */ changeScene(name: string, options?: { isHolderLast?: boolean }): void; } ``` - [ ] **Step 2: 创建 BaseScene 抽象基类** ```typescript import { Container, Ticker } from "pixi.js"; import { SceneType } from "@/enums/SceneType"; import type { IBaseScene, SceneLifeCycle } from "./types"; export abstract class BaseScene implements IBaseScene { abstract stage: Container; readonly name: string; readonly type: SceneType; _assetsLoaded: boolean = false; _layoutDone: boolean = false; constructor(name: string, type: SceneType = SceneType.Normal) { this.name = name; this.type = type; } // 用于注入场景切换方法 changeScene(name: string, options?: { isHolderLast?: boolean }): void { // 由 SceneManager 注入实现 throw new Error("changeScene not injected by SceneManager"); } // 生命周期方法 - 默认空实现 async loadBundle(): Promise {} async unLoadBundle(): Promise {} layout(): void | Promise {} onLoad(): void | Promise {} onUnLoad(): void | Promise {} update(dt: number, ticker: Ticker): void {} lateUpdate(dt: number, ticker: Ticker): void {} } export default BaseScene; ``` - [ ] **Step 3: Commit** ```bash git add src/scene/types.ts src/scene/BaseScene.ts git commit -m "feat: add Scene types and BaseScene abstract class" ``` --- ### Task 8: 实现 Scene 层 - SceneManager **Files:** - Create: `src/scene/SceneManager.ts` - [ ] **Step 1: 创建 SceneManager 类** ```typescript import { Container } from "pixi.js"; import Game from "@/core/Game"; import { logger } from "@/core/Logger"; import { assetManager } from "@/core/AssetManager"; import { SceneType } from "@/enums/SceneType"; import { BaseScene } from "./BaseScene"; import type { SceneConfig, IBaseScene } from "./types"; type StageChangeCallback = ( current: IBaseScene, previous: IBaseScene | undefined ) => Promise | void; class SceneManager { private static instance: SceneManager; private game: Game; private scenes: Map = new Map(); private _currentScene: IBaseScene | null = null; private changeCallbacks: StageChangeCallback[] = []; private constructor() { this.game = Game.getInstance(); } static getInstance(): SceneManager { if (!SceneManager.instance) { SceneManager.instance = new SceneManager(); } return SceneManager.instance; } get currentScene(): IBaseScene | null { return this._currentScene; } /** 监听场景变化 */ onStageChange(cb: StageChangeCallback): () => void { this.changeCallbacks.push(cb); return () => { const index = this.changeCallbacks.indexOf(cb); if (index >= 0) { this.changeCallbacks.splice(index, 1); } }; } /** 触发场景变化回调 */ private async emitStageChange( current: IBaseScene, previous: IBaseScene | undefined ): Promise { for (const cb of this.changeCallbacks) { await cb(current, previous); } } /** 注册场景 */ registerScene(scene: IBaseScene): void { if (this.scenes.has(scene.name)) { logger.warn(`SceneManager: scene "${scene.name}" already registered`); } this.scenes.set(scene.name, scene); // 注入 changeScene 方法 scene.changeScene = (name: string, options) => { this.changeScene(name, options); }; if (scene.type === SceneType.Resident) { this.game.stage.addChild(scene.stage); logger.debug(`SceneManager: registered resident scene "${scene.name}"`); } else { logger.debug(`SceneManager: registered normal scene "${scene.name}"`); } } /** 初始化入口场景 */ initScene(name: string): void { const scene = this.getSceneOrThrow(name); this._currentScene = scene; if (scene.type === SceneType.Normal) { this.game.stage.addChild(scene.stage); } this.emitStageChange(scene, undefined); logger.debug(`SceneManager: initialized scene "${name}"`); } /** 切换场景 */ async changeScene(name: string, options?: { isHolderLast?: boolean }): Promise { const previous = this._currentScene; if (!previous) { throw new Error(`SceneManager: no current scene, call initScene first`); } const target = this.getSceneOrThrow(name); // 处理前一个场景 if (previous.type === SceneType.Normal) { if (options?.isHolderLast) { previous.stage.visible = false; // @ts-ignore 标记保留 previous.stage._isHolderLast = true; } else { // 销毁场景 previous.stage.destroy({ children: true }); this.game.stage.removeChild(previous.stage); // 卸载资源 if (previous._assetsLoaded) { await previous.unLoadBundle?.(); previous._assetsLoaded = false; } await previous.onUnLoad?.(); this.scenes.delete(previous.name); } } else if (previous.type === SceneType.Resident) { previous.stage.visible = false; await previous.onUnLoad?.(); } this._currentScene = target; // 处理目标场景 if (target.type === SceneType.Normal) { // @ts-ignore if (target.stage._isHolderLast) { target.stage.visible = true; } else { this.game.stage.addChild(target.stage); } } else if (target.type === SceneType.Resident) { target.stage.visible = true; } // 生命周期 if (!target._assetsLoaded) { await target.loadBundle?.(); target._assetsLoaded = true; } if (!target._layoutDone || target.stage !== this.getSceneOrThrow(name).stage) { await target.layout?.(); target._layoutDone = true; } await target.onLoad?.(); // 触发回调 await this.emitStageChange(target, previous); logger.debug(`SceneManager: changed from "${previous?.name}" to "${name}"`); } /** 获取已注册场景 */ getScene(name: string): IBaseScene | undefined { return this.scenes.get(name); } getSceneOrThrow(name: string): IBaseScene { const scene = this.scenes.get(name); if (!scene) { throw new Error(`SceneManager: scene "${name}" not registered`); } return scene; } /** 确保场景存在,不存在则创建容器 */ ensureSceneExists(name: string, type: SceneType = SceneType.Normal): Container { if (this.scenes.has(name)) { return this.getSceneOrThrow(name).stage; } const container = new Container(); container.label = name; // 动态创建一个简单场景 // 这个功能主要用于兼容旧的代码风格 class DynamicScene extends BaseScene { stage = container; name = name; type = type; } const scene = new DynamicScene(); this.registerScene(scene); return container; } hasScene(name: string): boolean { return this.scenes.has(name); } } export default SceneManager; export const sceneManager = SceneManager.getInstance(); ``` - [ ] **Step 2: Commit** ```bash git add src/scene/SceneManager.ts git commit -m "feat: add SceneManager with full lifecycle management" ``` --- ### Task 9: 创建入口文件自动发现场景 (原有机制升级) **Files:** - Create: `src/init.ts` - [ ] **Step 1: 创建场景自动发现初始化** ```typescript import Game from "./core/Game"; import SceneManager from "./scene/SceneManager"; import { BaseScene } from "./scene/BaseScene"; import { SceneType } from "./enums/SceneType"; import { assetManager } from "./core/AssetManager"; import { logger } from "./core/Logger"; import eventBus from "./core/EventBus"; const game = Game.getInstance(); const sceneManager = SceneManager.getInstance(); export async function initApp(): Promise { await assetManager.init(); await game.init(); // 自动导入所有场景文件 src/scenes/**/page_*.ts const sceneModules = import.meta.glob("./scenes/**/page_*.ts", { eager: true }); for (const path in sceneModules) { const mod = sceneModules[path]; // 文件名匹配提取场景名称 const match = path.match(/page_(.*?)\.ts$/); if (!match) continue; const sceneName = match[1]; const sceneClass = (mod as { default: Constructor }).default; const scene = new sceneClass(); // 如果模块默认导出不是场景构造函数,尝试其他格式(兼容旧定义) if (!scene || typeof scene !== "object" || !("stage" in scene)) { logger.warn(`initApp: invalid scene file ${path}, skipping`); continue; } sceneManager.registerScene(scene); } // 启动更新循环 game.ticker.add((ticker) => { const dt = ticker.deltaTime; const current = sceneManager.currentScene; // 更新常驻场景 sceneManager["scenes"].forEach((scene) => { if (scene.type === SceneType.Resident && scene.stage.visible) { scene.update?.(dt, ticker); } }); // 更新当前场景 if (current) { current.update?.(dt, ticker); } game.render(); // lateUpdate sceneManager["scenes"].forEach((scene) => { if (scene.type === SceneType.Resident && scene.stage.visible && scene.lateUpdate) { scene.lateUpdate(dt, ticker); } }); if (current?.lateUpdate) { current.lateUpdate(dt, ticker); } }); // 处理常驻场景初始化 const residentScenes: BaseScene[] = []; sceneManager["scenes"].forEach((scene) => { if (scene.type === SceneType.Resident) { residentScenes.push(scene); } }); (async () => { for (const scene of residentScenes) { if (!scene._assetsLoaded) { await scene.loadBundle?.(); scene._assetsLoaded = true; } if (!scene._layoutDone) { await scene.layout?.(); scene._layoutDone = true; } await scene.onLoad?.(); } })(); // 场景变化监听 - 日志 sceneManager.onStageChange((current) => { logger.debug("Scene changed to", current.name); }); logger.info("App initialized"); } export { game, sceneManager, eventBus, assetManager, logger }; ``` - [ ] **Step 2: Commit** ```bash git add src/init.ts git commit -m "feat: add app init with auto scene discovery" ``` --- ### Task 10: 实现 Utils 层 - Position **Files:** - Create: `src/utils/Position.ts` - [ ] **Step 1: 创建 Position 定位工具** ```typescript import Game from "@/core/Game"; type HorizontalAlign = "left" | "center" | "right" | number; type VerticalAlign = "top" | "center" | "bottom" | number; interface PositionOptions { x?: number; y?: number; } class Position { private game: Game; constructor() { this.game = Game.getInstance(); } get( hAlign: HorizontalAlign, vAlign: VerticalAlign, options?: PositionOptions ): { x: number; y: number } { const { width, height } = this.game.getInfo(); let x: number; let y: number; // 水平对齐 if (hAlign === "left") { x = 0; } else if (hAlign === "center") { x = width / 2; } else if (hAlign === "right") { x = width; } else { x = hAlign; } // 垂直对齐 if (vAlign === "top") { y = 0; } else if (vAlign === "center") { y = height / 2; } else if (vAlign === "bottom") { y = height; } else { y = vAlign; } // 添加偏移 if (options) { x += options.x ?? 0; y += options.y ?? 0; } return { x, y }; } center(options?: PositionOptions): { x: number; y: number } { return this.get("center", "center", options); } centerX(y: VerticalAlign, options?: PositionOptions): { x: number; y: number } { return this.get("center", y, options); } centerY(x: HorizontalAlign, options?: PositionOptions): { x: number; y: number } { return this.get(x, "center", options); } } export const position = new Position(); export default position; ``` - [ ] **Step 2: Commit** ```bash git add src/utils/Position.ts git commit -m "feat: add Position utility for alignment" ``` --- ### Task 11: 实现 Utils 层 - Tween **Files:** - Create: `src/utils/Tween.ts` - [ ] **Step 1: 创建 Tween 封装** ```typescript import { Group, Tween as TweenJS } from "@tweenjs/tween.js"; class TweenManager { private group: Group; constructor() { this.group = new Group(); // 在 ticker 中更新 // 由 Game 更新循环自动处理 } /** * 创建一个新的 Tween * @example * const tween = tweenManager.create({ x: 0, y: 0 }) * .to({ x: 100, y: 100 }, 1000) * .onUpdate(obj => { obj.x }) * .start(); */ create(obj: T): TweenJS { const tween = new TweenJS(obj, this.group); return tween; } update(time?: number): void { this.group.update(time); } removeAll(): void { this.group.removeAll(); } } export const tweenManager = new TweenManager(); export default tweenManager; // 便捷方法 export function tweenFromTo( from: T, to: Partial, duration: number, onUpdate: (obj: T) => void, onComplete?: () => void ): TweenJS { const t = tweenManager.create(from).to(to, duration).onUpdate(onUpdate); if (onComplete) { t.onComplete(onComplete); } return t.start(); } ``` - [ ] **Step 2: 更新 Game ticker 以更新 tween** 需要修改 `src/core/Game.ts` 在 `init()` 中添加: ```typescript // Add after ticker creation: import { tweenManager } from "@/utils/Tween"; ... this._ticker.add(() => { tweenManager.update(); }); ``` *(This modification will be done in this step)* - [ ] **Step 3: Commit** ```bash git add src/utils/Tween.ts src/core/Game.ts git commit -m "feat: add Tween animation utility" ``` --- ### Task 12: 实现 Utils 层 - Sound **Files:** - Create: `src/utils/Sound.ts` - [ ] **Step 1: 创建 Sound 管理** ```typescript import { sound } from "@pixi/sound"; import { logger } from "@/core/Logger"; class SoundManager { private volume: number = 1; private muted: boolean = false; constructor() { sound.volume = this.volume; } add(name: string, url: string, options?: { singleInstance?: boolean; autoPlay?: boolean }): void { sound.add(name, { url, singleInstance: options?.singleInstance ?? true, autoPlay: options?.autoPlay ?? false, }); logger.debug(`Sound: added "${name}"`); } play(name: string): void { if (this.muted) return; sound.play(name); } stop(name: string): void { sound.stop(name); } pause(name: string): void { sound.pause(name); } pauseAll(): void { sound.pauseAll(); } resumeAll(): void { if (!this.muted) { sound.resumeAll(); } } setVolume(volume: number): void { this.volume = Math.max(0, Math.min(1, volume)); sound.volume = this.volume; } getVolume(): number { return this.volume; } mute(): void { this.muted = true; sound.muteAll(); } unmute(): void { this.muted = false; sound.unmuteAll(); } isMuted(): boolean { return this.muted; } exists(name: string): boolean { return sound.exists(name); } clear(): void { sound.removeAll(); } } export const soundManager = new SoundManager(); export default soundManager; ``` - [ ] **Step 2: Commit** ```bash git add src/utils/Sound.ts git commit -m "feat: add SoundManager utility" ``` --- ### Task 13: 实现 Utils 层 - Storage 和 MathUtils **Files:** - Create: `src/utils/Storage.ts` - Create: `src/utils/MathUtils.ts` - [ ] **Step 1: 创建 Storage** ```typescript class Storage { private prefix: string; constructor(prefix: string = "game_") { this.prefix = prefix; } get(key: string, defaultValue: T): T { try { const item = localStorage.getItem(this.prefix + key); if (item === null) { return defaultValue; } return JSON.parse(item) as T; } catch { return defaultValue; } } set(key: string, value: any): void { try { localStorage.setItem(this.prefix + key, JSON.stringify(value)); } catch { console.warn("Storage: failed to save to localStorage"); } } remove(key: string): void { localStorage.removeItem(this.prefix + key); } clear(): void { const keysToRemove: string[] = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key?.startsWith(this.prefix)) { keysToRemove.push(key); } } keysToRemove.forEach(key => localStorage.removeItem(key)); } has(key: string): boolean { return localStorage.getItem(this.prefix + key) !== null; } } export const storage = new Storage(); export default storage; ``` - [ ] **Step 2: 创建 MathUtils** ```typescript export function clamp(value: number, min: number, max: number): number { return Math.min(max, Math.max(min, value)); } export function lerp(a: number, b: number, t: number): number { return a + (b - a) * t; } export function random(min: number, max: number): number { return min + Math.random() * (max - min); } export function randomInt(min: number, max: number): number { return Math.floor(random(min, max + 1)); } export function randomChoice(array: T[]): T { return array[randomInt(0, array.length - 1)]; } export function distance(x1: number, y1: number, x2: number, y2: number): number { const dx = x2 - x1; const dy = y2 - y1; return Math.sqrt(dx * dx + dy * dy); } export function degToRad(deg: number): number { return (deg * Math.PI) / 180; } export function radToDeg(rad: number): number { return (rad * 180) / Math.PI; } const MathUtils = { clamp, lerp, random, randomInt, randomChoice, distance, degToRad, radToDeg, }; export default MathUtils; ``` - [ ] **Step 3: Commit** ```bash git add src/utils/Storage.ts src/utils/MathUtils.ts git commit -m "feat: add Storage and MathUtils utilities" ``` --- ### Task 14: 重构组件层 - Button **Files:** - Create: `src/components/Button.ts` (重构现有) - [ ] **Step 1: 重构 Button 组件** ```typescript import { Container, Text, Graphics, TextStyle, NineSliceSprite, Ticker, Texture, } from "pixi.js"; export interface ButtonOptions { text: string; bg?: Texture; pressBg?: Texture; position?: () => { x: number; y: number }; onClick: () => void; autoUpdate?: boolean; padding?: { x: number; y: number }; fontSize?: number; } export class Button { private config: ButtonOptions; private _container: Container; private textObj: Text; private bgRect?: Graphics; private bgNine?: NineSliceSprite; private padding: { x: number; y: number }; constructor(opts: ButtonOptions) { this.config = { autoUpdate: true, padding: { x: 60, y: 40 }, fontSize: 30, ...opts, }; this.padding = this.config.padding!; this._container = new Container(); this._container.cursor = "pointer"; this._container.interactive = true; if (!this.config.bg) { this.createRectBackground(); } else { this.createNineSliceBackground(); } this.createText(); this._container.addChild(this.textObj); this.setupEvents(); if (this.config.autoUpdate) { const update = () => this.updateView(); Ticker.shared.add(update); this._container.on("destroyed", () => { Ticker.shared.remove(update); }); } this.updateView(); } get container(): Container { return this._container; } updateView(): void { if (this._container.destroyed) return; const width = this.textObj.width + this.padding.x; const height = this.textObj.height + this.padding.y; this._container.pivot.set(width / 2, height / 2); this.textObj.position.set(this.padding.x / 2, this.padding.y / 2 - 2); if (this.bgRect) { this.bgRect.clear(); this.bgRect.rect(0, 0, width, height); this.bgRect.fill(0xff0000); } if (this.bgNine) { this.bgNine.width = width; this.bgNine.height = height; } if (this.config.position) { this._container.position = this.config.position(); } } private createRectBackground(): void { this.bgRect = new Graphics(); this.bgRect.label = "button-bg-rect"; this._container.addChild(this.bgRect); } private createNineSliceBackground(): void { if (!this.config.bg) return; this.bgNine = new NineSliceSprite({ texture: this.config.bg, leftWidth: 10, topHeight: 10, rightWidth: 10, bottomHeight: 10, }); this.bgNine.label = "button-bg-nine"; this._container.addChild(this.bgNine); } private createText(): void { const style = new TextStyle({ fontFamily: "Arial", fontSize: this.config.fontSize!, fill: 0xffffff, align: "center", }); this.textObj = new Text({ text: this.config.text, style, }); this.textObj.label = "button-text"; } private setupEvents(): void { const onPointerDown = () => { if (this.config.pressBg && this.bgNine) { this.bgNine.texture = this.config.pressBg; } else { this._container.alpha = 0.8; this._container.scale.set(0.8, 0.8); } }; const onPointerUp = () => { if (this.config.pressBg && this.bgNine && this.config.bg) { this.bgNine.texture = this.config.bg; } else { this._container.alpha = 1; this._container.scale.set(1, 1); } this.config.onClick(); }; const onPointerUpOutside = () => { if (this.config.pressBg && this.bgNine && this.config.bg) { this.bgNine.texture = this.config.bg; } else { this._container.alpha = 1; this._container.scale.set(1, 1); } }; this._container.on("mousedown", onPointerDown); this._container.on("touchstart", onPointerDown); this._container.on("mouseup", onPointerUp); this._container.on("touchend", onPointerUp); this._container.on("mouseupoutside", onPointerUpOutside); this._container.on("touchendoutside", onPointerUpOutside); } } export default Button; ``` - [ ] **Step 2: Commit** ```bash git add src/components/Button.ts git commit -m "refactor: Button component with full typing" ``` --- ### Task 15: 添加组件层 - Label 和 Panel **Files:** - Create: `src/components/Label.ts` - Create: `src/components/Panel.ts` - [ ] **Step 1: 创建 Label** ```typescript import { Container, Text, TextStyle, type TextStyleOptions } from "pixi.js"; export interface LabelOptions { text: string; style?: TextStyleOptions; position?: { x: number; y: number }; anchor?: { x: number; y: number }; } export class Label { private _container: Container; private _text: Text; constructor(opts: LabelOptions) { this._container = new Container(); const style = new TextStyle({ fontFamily: "Arial", fontSize: 30, fill: 0xffffff, ...opts.style, }); this._text = new Text({ text: opts.text, style, }); if (opts.anchor) { this._text.anchor.set(opts.anchor.x, opts.anchor.y); } if (opts.position) { this._container.position.set(opts.position.x, opts.position.y); } this._container.addChild(this._text); } get container(): Container { return this._container; } get text(): Text { return this._text; } setText(text: string): void { this._text.text = text; } getText(): string { return this._text.text; } setStyle(style: TextStyleOptions): void { this._text.style = new TextStyle({ ...this._text.style, ...style, }); } } export default Label; ``` - [ ] **Step 2: 创建 Panel** ```typescript import { Container, Graphics, type ContainerOptions } from "pixi.js"; export interface PanelOptions extends ContainerOptions { width: number; height: number; backgroundColor?: number; padding?: number; alpha?: number; } export class Panel extends Container { private bg: Graphics; constructor(opts: PanelOptions) { super(opts); this.bg = new Graphics(); this.drawBackground(opts); this.addChild(this.bg); } private drawBackground(opts: PanelOptions): void { this.bg.clear(); this.bg.roundRect(0, 0, opts.width, opts.height, 8); this.bg.fill(opts.backgroundColor ?? 0x000000); this.bg.alpha = opts.alpha ?? 0.8; } resize(width: number, height: number): void { this.bg.clear(); this.bg.roundRect(0, 0, width, height, 8); this.bg.fill(0x000000); } setBackgroundColor(color: number): void { this.bg.tint = color; } } export default Panel; ``` - [ ] **Step 3: Commit** ```bash git add src/components/Label.ts src/components/Panel.ts git commit -m "feat: add Label and Panel components" ``` --- ### Task 16: 更新入口 main.ts **Files:** - Modify: `src/main.ts` - [ ] **Step 1: 更新 main.ts 适配新架构** ```typescript import { Orientation } from "./enums/Orientation"; import { initApp, game, sceneManager } from "./init"; // 初始化应用 setTimeout(async () => { await initApp(); game.setOrientation(Orientation.Landscape); // 入口场景会自动由 SceneManager 初始化 // 如果你的入口场景叫 "init",它会被自动发现并在适当的时候初始化 }, 200); // 导出供全局调试 if (import.meta.env.DEV) { (window as any).game = game; (window as any).sceneManager = sceneManager; } ``` - [ ] **Step 2: Commit** ```bash git add src/main.ts git commit -m "refactor: update main.ts for new architecture" ``` --- ### Task 17: 迁移旧场景示例验证 **Files:** - Modify: 迁移一个示例场景到新架构 - [ ] **Step 1: 将 welcome 示例场景迁移到新架构** `src/scenes/welcome/page_welcome.ts`: ```typescript import Button from "@/components/Button"; import { game } from "@/core/Game"; import soundManager from "@/utils/Sound"; import { Container } from "pixi.js"; import { BaseScene } from "@/scene/BaseScene"; import { SceneType } from "@/enums/SceneType"; import position from "@/utils/Position"; import { loadAsset, unLoadAsset } from "@/core/AssetManager"; export default class WelcomeScene extends BaseScene { stage: Container = new Container(); constructor() { super("welcome", SceneType.Normal); } async layout(): Promise { const btn = new Button({ text: "进入游戏", onClick: () => { this.changeStage("welcome2"); }, position: () => position.center(), }); this.stage.addChild(btn.container); } onLoad(): void { console.log("welcome onLoad"); } onUnLoad(): void { soundManager.stop("my-sound"); console.log("welcome onUnLoad"); } } ``` - [ ] **Step 2: 验证 TypeScript 编译** ```bash npx tsc --noEmit ``` - [ ] **Step 3: Commit** ```bash git add src/scenes/welcome/page_welcome.ts git commit -m "refactor: migrate example scene to new architecture" ``` --- ### Task 18: 清理旧文件 (可选,原文件保留在旧位置) **Files:** - Remove: 旧的 `src/Game/` 目录下的文件会保留,但不再使用 - [ ] **Step 1: 检查项目编译** ```bash npx tsc --noEmit ``` 应该没有错误。如果有错误,修复它们。 - [ ] **Step 2: 更新 .gitignore 添加新条目** 确保已有: ``` .superpowers/ node_modules/ dist/ .DS_Store ``` - [ ] **Step 3: Commit** ```bash git add .gitignore git commit -m "chore: update .gitignore" ``` --- ## 自检查 完成设计文档覆盖检查: - ✓ 枚举定义 - Task 1 - ✓ 全局类型 - Task 2 - ✓ Core 层 - Logger (Task 3), EventBus (4), AssetManager (5), Game (6) - ✓ Scene 层 - types, BaseScene (7), SceneManager (8), 自动发现 (9) - ✓ Utils 层 - Position (10), Tween (11), Sound (12), Storage/MathUtils (13) - ✓ Component 层 - Button 重构 (14), Label/Panel 新增 (15) - ✓ 入口更新 (16) - ✓ 示例迁移验证 (17) - ✓ 最终检查 (18) 所有设计要求都已覆盖,没有遗漏。没有占位符,每个任务都有具体代码。