You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

16 KiB

Pixi DX-First Runtime & Assets Redesign Implementation Plan

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: 重构运行时与资源系统,在 Web H5 下建立可观测、可回滚、可维护的 DX 优先框架底座,并交付最小内置调试 UI。

Architecture: 采用 kernel / scene / assets / devtools / ui-core 五层结构。场景切换改为事务模型(prepare/commit/rollback),资源改为 AssetGraph + AssetSession 持有关系追踪。开发态通过 overlay 与命令面板直接消费运行时事件流,优先保证定位效率,再逐步做性能优化。

Tech Stack: TypeScript, PixiJS 8, Vite 5, Vitest(新增), @pixi/devtools(已有)


Scope Check

本计划覆盖同一重构主题下的两个强耦合子系统(运行时内核与资源系统),并包含最小 devtools 交付。三部分共享同一事件流与状态模型,适合在单计划中分任务推进。

Planned File Structure

Create

  • src/kernel/AppRuntime.ts:封装 renderer/ticker/stage 初始化、resize、render 入口
  • src/kernel/RuntimeEvents.ts:定义运行时事件类型与发布接口
  • src/kernel/RuntimePlugin.ts:插件接口与插件注册器
  • src/scene/SceneStateMachine.ts:场景状态机与状态迁移校验
  • src/scene/TransitionTransaction.ts:切换事务(prepare/commit/rollback)
  • src/scene/SceneContext.ts:场景可用上下文(assets/events/runtime)
  • src/assets/AssetGraph.ts:bundle 依赖图与解析
  • src/assets/AssetSession.ts:会话申请/释放/owner 追踪
  • src/assets/AssetInspectorSnapshot.ts:供 overlay 使用的只读快照
  • src/devtools/overlay/DebugOverlay.ts:调试面板容器与刷新调度
  • src/devtools/overlay/widgets/SceneWidget.ts:场景状态展示
  • src/devtools/overlay/widgets/AssetWidget.ts:资源关系展示
  • src/devtools/overlay/widgets/EventWidget.ts:事件时间线展示
  • src/ui-core/UiPanel.ts:面板基础组件
  • src/ui-core/UiText.ts:文本基础组件
  • src/ui-core/UiButton.ts:按钮基础组件
  • src/ui-core/UiList.ts:列表基础组件
  • src/devtools/CommandPalette.ts:运行时命令入口
  • tests/kernel/scene-state-machine.test.ts:状态机单测
  • tests/kernel/transition-transaction.test.ts:事务单测
  • tests/kernel/runtime-events.test.ts:运行时事件流单测
  • tests/assets/asset-session.test.ts:资源会话单测
  • vitest.config.ts:测试配置

Modify

  • package.json:新增 testtest:watch 脚本和 vitest 依赖
  • src/core/Game.ts:退役为兼容壳,委托给 AppRuntime(过渡期)
  • src/core/AssetManager.ts:拆分职责,迁移至 assets 新实现
  • src/scene/SceneManager.ts:接入状态机和事务
  • src/scene/BaseScene.ts:场景生命周期改造(setup/enter/leave/dispose)
  • src/init.ts:运行时/场景/devtools 统一装配
  • src/main.ts:开发态开关和命令面板入口

Task 1: 建立测试基线与重构骨架

Files:

  • Create: vitest.config.ts

  • Create: tests/kernel/scene-state-machine.test.ts

  • Modify: package.json

  • Test: tests/kernel/scene-state-machine.test.ts

  • Step 1: 写一个失败的状态机迁移测试

import { describe, expect, it } from "vitest";
import { SceneStateMachine } from "@/scene/SceneStateMachine";

describe("SceneStateMachine", () => {
  it("rejects invalid transition from created to ready", () => {
    const sm = new SceneStateMachine("welcome");
    expect(() => sm.transition("ready")).toThrowError(
      /invalid transition: created -> ready/
    );
  });
});
  • Step 2: 运行测试并确认失败(因为目标文件尚不存在)

Run: npm run test -- tests/kernel/scene-state-machine.test.ts
Expected: FAIL with module resolution error for @/scene/SceneStateMachine

  • Step 3: 添加最小测试基础设施与最小实现
// vitest.config.ts
import { defineConfig } from "vitest/config";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  plugins: [tsconfigPaths()],
  test: {
    environment: "node",
    include: ["tests/**/*.test.ts"],
  },
});
// src/scene/SceneStateMachine.ts
export type SceneState =
  | "created"
  | "setup"
  | "assetsLoading"
  | "entering"
  | "ready"
  | "leaving"
  | "disposed";

const ALLOWED: Record<SceneState, SceneState[]> = {
  created: ["setup"],
  setup: ["assetsLoading"],
  assetsLoading: ["entering"],
  entering: ["ready"],
  ready: ["leaving"],
  leaving: ["disposed", "ready"],
  disposed: [],
};

export class SceneStateMachine {
  public state: SceneState = "created";
  constructor(public readonly sceneId: string) {}
  transition(next: SceneState): void {
    if (!ALLOWED[this.state].includes(next)) {
      throw new Error(`invalid transition: ${this.state} -> ${next}`);
    }
    this.state = next;
  }
}
  • Step 4: 再次运行测试并确认通过

Run: npm run test -- tests/kernel/scene-state-machine.test.ts
Expected: PASS (1 passed)

  • Step 5: 提交
git add package.json vitest.config.ts src/scene/SceneStateMachine.ts tests/kernel/scene-state-machine.test.ts
git commit -m "test: add vitest baseline and scene state machine skeleton"

Task 2: 实现场景事务切换与回滚

Files:

  • Create: src/scene/TransitionTransaction.ts

  • Create: tests/kernel/transition-transaction.test.ts

  • Modify: src/scene/SceneManager.ts

  • Test: tests/kernel/transition-transaction.test.ts

  • Step 1: 写失败测试,验证 prepare 失败会 rollback

import { describe, expect, it, vi } from "vitest";
import { TransitionTransaction } from "@/scene/TransitionTransaction";

it("rolls back when prepare fails", async () => {
  const tx = new TransitionTransaction({
    prepare: vi.fn().mockRejectedValue(new Error("load failed")),
    commit: vi.fn(),
    rollback: vi.fn(),
    finalize: vi.fn(),
  });
  await expect(tx.run()).rejects.toThrow(/load failed/);
  expect(tx.hooks.rollback).toHaveBeenCalledTimes(1);
  expect(tx.hooks.commit).not.toHaveBeenCalled();
});
  • Step 2: 运行测试确认失败

Run: npm run test -- tests/kernel/transition-transaction.test.ts
Expected: FAIL with missing module @/scene/TransitionTransaction

  • Step 3: 写最小事务实现并接入 SceneManager
// src/scene/TransitionTransaction.ts
export interface TransitionHooks {
  prepare: () => Promise<void>;
  commit: () => Promise<void>;
  rollback: () => Promise<void>;
  finalize: () => Promise<void>;
}

export class TransitionTransaction {
  constructor(public readonly hooks: TransitionHooks) {}
  async run(): Promise<void> {
    try {
      await this.hooks.prepare();
      await this.hooks.commit();
    } catch (error) {
      await this.hooks.rollback();
      throw error;
    } finally {
      await this.hooks.finalize();
    }
  }
}
// src/scene/SceneManager.ts (new usage snippet)
const tx = new TransitionTransaction({
  prepare: async () => { /* load target scene assets + setup */ },
  commit: async () => { /* mount target and enter */ },
  rollback: async () => { /* restore previous ready scene */ },
  finalize: async () => { /* emit report */ },
});
await tx.run();
  • Step 4: 运行测试确认通过

Run: npm run test -- tests/kernel/transition-transaction.test.ts
Expected: PASS

  • Step 5: 提交
git add src/scene/TransitionTransaction.ts src/scene/SceneManager.ts tests/kernel/transition-transaction.test.ts
git commit -m "feat(scene): add transition transaction with rollback flow"

Task 3: 实现 AssetGraph 与 AssetSession

Files:

  • Create: src/assets/AssetGraph.ts

  • Create: src/assets/AssetSession.ts

  • Create: src/assets/AssetInspectorSnapshot.ts

  • Create: tests/assets/asset-session.test.ts

  • Modify: src/core/AssetManager.ts

  • Test: tests/assets/asset-session.test.ts

  • Step 1: 写失败测试,验证 owner/session 引用追踪

import { describe, expect, it } from "vitest";
import { AssetSession } from "@/assets/AssetSession";

it("tracks owner and releases resources by session", async () => {
  const session = new AssetSession("scene:welcome", "tx:1");
  await session.require("bundle.home");
  const snap = session.snapshot();
  expect(snap.owners["bundle.home"]).toContain("scene:welcome");
  await session.releaseAll();
  expect(session.snapshot().activeBundles).toHaveLength(0);
});
  • Step 2: 运行测试确认失败

Run: npm run test -- tests/assets/asset-session.test.ts
Expected: FAIL with missing module @/assets/AssetSession

  • Step 3: 写最小实现并改造旧 AssetManager 为门面
// src/assets/AssetSession.ts
type Snapshot = { activeBundles: string[]; owners: Record<string, string[]> };

export class AssetSession {
  private bundles = new Set<string>();
  private ownerMap = new Map<string, Set<string>>();
  constructor(
    private readonly ownerId: string,
    private readonly transitionId: string
  ) {}
  async require(bundleName: string): Promise<void> {
    this.bundles.add(bundleName);
    if (!this.ownerMap.has(bundleName)) this.ownerMap.set(bundleName, new Set());
    this.ownerMap.get(bundleName)!.add(this.ownerId);
  }
  async releaseAll(): Promise<void> {
    this.bundles.clear();
    this.ownerMap.clear();
  }
  snapshot(): Snapshot {
    const owners: Record<string, string[]> = {};
    for (const [k, v] of this.ownerMap) owners[k] = [...v];
    return { activeBundles: [...this.bundles], owners };
  }
}
// src/core/AssetManager.ts (facade idea)
import { AssetSession } from "@/assets/AssetSession";
export class AssetManagerFacade {
  createSession(ownerId: string, transitionId: string): AssetSession {
    return new AssetSession(ownerId, transitionId);
  }
}
  • Step 4: 运行测试确认通过

Run: npm run test -- tests/assets/asset-session.test.ts
Expected: PASS

  • Step 5: 提交
git add src/assets/AssetGraph.ts src/assets/AssetSession.ts src/assets/AssetInspectorSnapshot.ts src/core/AssetManager.ts tests/assets/asset-session.test.ts
git commit -m "feat(assets): add asset graph/session with owner tracking"

Task 4: 落地 AppRuntime 与事件总线插件化

Files:

  • Create: src/kernel/AppRuntime.ts

  • Create: src/kernel/RuntimeEvents.ts

  • Create: src/kernel/RuntimePlugin.ts

  • Create: tests/kernel/runtime-events.test.ts

  • Modify: src/core/Game.ts

  • Modify: src/init.ts

  • Test: tests/kernel/runtime-events.test.ts

  • Step 1: 先写失败测试,验证事件会在切换时发出

import { describe, expect, it } from "vitest";
import { createRuntimeEvents } from "@/kernel/RuntimeEvents";

it("emits transition events in order", () => {
  const events = createRuntimeEvents();
  const names: string[] = [];
  events.onAny((e) => names.push(e.type));
  events.emit({ type: "transition:started", transitionId: "tx:1" });
  events.emit({ type: "transition:finished", transitionId: "tx:1" });
  expect(names).toEqual(["transition:started", "transition:finished"]);
});
  • Step 2: 运行测试确认失败

Run: npm run test -- tests/kernel/runtime-events.test.ts
Expected: FAIL with missing module @/kernel/RuntimeEvents

  • Step 3: 添加运行时与事件实现,并让旧 Game 仅做委托
// src/kernel/RuntimeEvents.ts
type RuntimeEvent = { type: string; [k: string]: unknown };
type Handler = (event: RuntimeEvent) => void;

export function createRuntimeEvents() {
  const handlers = new Set<Handler>();
  return {
    emit(event: RuntimeEvent) { handlers.forEach((h) => h(event)); },
    onAny(handler: Handler) {
      handlers.add(handler);
      return () => handlers.delete(handler);
    },
  };
}
// src/core/Game.ts (delegation snippet)
import { AppRuntime } from "@/kernel/AppRuntime";
const runtime = new AppRuntime();
export default runtime;
  • Step 4: 运行核心测试集合确认通过

Run: npm run test -- tests/kernel/runtime-events.test.ts tests/kernel/scene-state-machine.test.ts tests/kernel/transition-transaction.test.ts
Expected: PASS

  • Step 5: 提交
git add src/kernel/AppRuntime.ts src/kernel/RuntimeEvents.ts src/kernel/RuntimePlugin.ts src/core/Game.ts src/init.ts tests/kernel/runtime-events.test.ts
git commit -m "refactor(kernel): introduce app runtime and event bus plugin system"

Task 5: 交付最小 Debug Overlay + Command Palette

Files:

  • Create: src/devtools/overlay/DebugOverlay.ts

  • Create: src/devtools/overlay/widgets/SceneWidget.ts

  • Create: src/devtools/overlay/widgets/AssetWidget.ts

  • Create: src/devtools/overlay/widgets/EventWidget.ts

  • Create: src/devtools/CommandPalette.ts

  • Create: src/ui-core/UiPanel.ts

  • Create: src/ui-core/UiText.ts

  • Create: src/ui-core/UiButton.ts

  • Create: src/ui-core/UiList.ts

  • Modify: src/main.ts

  • Test: 手工验证(见步骤)

  • Step 1: 定义手工验收断言(固定文本,后续逐条打勾)

1.`~` 能切换 overlay 显隐,且不影响场景点击事件。
2. overlay 中可看到当前 sceneId、sceneState、最近 20 条 runtime 事件。
3. overlay 中可看到 active bundle 数和 owner/session 计数。
4. 命令面板执行 `scene.goto(welcome)` 后,sceneState 在 2 秒内回到 ready。
  • Step 2: 运行开发环境并确认当前不满足断言

Run: npm run dev
Expected: 无 overlay / 无 palette(作为失败基线)

  • Step 3: 实现最小 UI 与 devtools 装配
// src/devtools/overlay/DebugOverlay.ts
export class DebugOverlay {
  private visible = true;
  toggle(): void { this.visible = !this.visible; }
  isVisible(): boolean { return this.visible; }
  update(): void { /* render widgets from runtime snapshots */ }
}
// src/devtools/CommandPalette.ts
type Command = { id: string; run: () => Promise<void> | void };
export class CommandPalette {
  private cmds = new Map<string, Command>();
  register(c: Command): void { this.cmds.set(c.id, c); }
  execute(id: string): Promise<void> | void { return this.cmds.get(id)?.run(); }
}
// src/main.ts (hook snippet)
window.addEventListener("keydown", (e) => {
  if (e.key === "`") overlay.toggle();
});
  • Step 4: 手工验证通过

Run: npm run dev
Expected:

  • 按快捷键可开关 overlay

  • overlay 显示场景状态/事件列表/资源摘要

  • 命令 scene.goto(welcome) 可执行并反映到状态

  • Step 5: 提交

git add src/devtools src/ui-core src/main.ts
git commit -m "feat(devtools): add debug overlay and runtime command palette"

Task 6: 收口迁移与回归验证

Files:

  • Modify: src/scene/BaseScene.ts

  • Modify: src/stages/page_init.ts

  • Modify: src/stages/initSceneLayout.ts

  • Modify: src/stages/welcome/page_welcome.ts

  • Modify: src/stages/welcome2/page_welcome2.ts

  • Test: 全量测试 + 构建验证

  • Step 1: 写迁移校验测试(至少一个场景完整生命周期)

import { describe, expect, it } from "vitest";
import { SceneStateMachine } from "@/scene/SceneStateMachine";

it("completes full lifecycle for migrated scene", () => {
  const sm = new SceneStateMachine("welcome");
  sm.transition("setup");
  sm.transition("assetsLoading");
  sm.transition("entering");
  sm.transition("ready");
  expect(sm.state).toBe("ready");
});
  • Step 2: 运行该测试确认失败(迁移前不满足)

Run: npm run test -- tests/kernel/scene-state-machine.test.ts
Expected: FAIL(若场景未按新生命周期接线)

  • Step 3: 迁移场景到新接口并清理旧调用路径
// BaseScene new interface sketch
export interface RuntimeScene {
  setup(ctx: SceneContext): Promise<void> | void;
  enter(ctx: SceneContext): Promise<void> | void;
  leave(ctx: SceneContext): Promise<void> | void;
  dispose(ctx: SceneContext): Promise<void> | void;
}
  • Step 4: 运行全量验证

Run: npm run test
Expected: PASS (all tests)

Run: npm run build
Expected: build success with generated dist assets

  • Step 5: 提交
git add src/scene/BaseScene.ts src/stages tests package.json
git commit -m "refactor(scene): migrate scenes to runtime lifecycle and finalize rollout"

Final Verification Checklist

  • 所有测试通过:npm run test
  • 构建通过:npm run build
  • 开发态工具可用:npm run dev + overlay/palette 手工检查
  • 无遗留占位项(无“待补充/稍后实现”字样)