41 KiB
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 枚举
export enum SceneType {
Normal = 0, // 普通场景 - 退出时销毁
Resident = 1, // 常驻场景 - 只隐藏不销毁
}
- Step 2: 创建 Orientation 枚举
export enum Orientation {
Landscape = "landscape",
Portrait = "portrait",
}
- Step 3: Commit
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: 创建全局类型定义文件
// 全局构造函数类型
declare type Constructor<T> = new (...args: any[]) => T;
// 通用回调类型
declare type Callback<T = void> = () => T;
declare type Callback1<T, R = void> = (arg: T) => R;
- Step 2: Commit
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 类
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
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 类
type EventCallback = (...args: any[]) => void;
class EventBus {
private events: Map<string, Set<EventCallback>> = 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
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 类
import { Assets, type AssetsBundle } from "pixi.js";
import { logger } from "./Logger";
class AssetManager {
private referenceCounts: Map<string, number> = new Map();
async init(): Promise<void> {
await Assets.init({ manifest: "/manifest.json" });
logger.debug("AssetManager initialized");
}
async loadBundle(
name: string,
onProgress?: (progress: number) => void
): Promise<AssetsBundle> {
// 已经加载过,增加引用计数
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<void> {
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
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 核心对象
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<void> {
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
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
import { Container } from "pixi.js";
import { SceneType } from "@/enums/SceneType";
export interface SceneConfig {
name: string;
type: SceneType;
stage: Container;
}
export interface SceneLifeCycle {
/** 加载资源包 */
loadBundle?(): Promise<void>;
/** 卸载资源包 */
unLoadBundle?(): Promise<void>;
/** 创建布局 */
layout?(): void | Promise<void>;
/** 场景加载完成 */
onLoad?(): void | Promise<void>;
/** 场景即将卸载 */
onUnLoad?(): void | Promise<void>;
/** 每帧更新 */
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 抽象基类
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<void> {}
async unLoadBundle(): Promise<void> {}
layout(): void | Promise<void> {}
onLoad(): void | Promise<void> {}
onUnLoad(): void | Promise<void> {}
update(dt: number, ticker: Ticker): void {}
lateUpdate(dt: number, ticker: Ticker): void {}
}
export default BaseScene;
- Step 3: Commit
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 类
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> | void;
class SceneManager {
private static instance: SceneManager;
private game: Game;
private scenes: Map<string, IBaseScene> = 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<void> {
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<void> {
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
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: 创建场景自动发现初始化
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<void> {
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<BaseScene> }).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
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 定位工具
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
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 封装
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<T extends object>(obj: T): TweenJS<T> {
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<T>(
from: T,
to: Partial<T>,
duration: number,
onUpdate: (obj: T) => void,
onComplete?: () => void
): TweenJS<T> {
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() 中添加:
// 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
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 管理
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
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
class Storage {
private prefix: string;
constructor(prefix: string = "game_") {
this.prefix = prefix;
}
get<T>(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
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<T>(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
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 组件
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
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
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
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
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 适配新架构
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
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:
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<void> {
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 编译
npx tsc --noEmit
- Step 3: Commit
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: 检查项目编译
npx tsc --noEmit
应该没有错误。如果有错误,修复它们。
- Step 2: 更新 .gitignore 添加新条目
确保已有:
.superpowers/
node_modules/
dist/
.DS_Store
- Step 3: Commit
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)
所有设计要求都已覆盖,没有遗漏。没有占位符,每个任务都有具体代码。