Browse Source
- Introduced SceneLoadingOverlay for improved user feedback during asset loading. - Updated SceneManager to manage loading overlay visibility and state reporting. - Refactored scene lifecycle methods to integrate loading overlay functionality. - Removed unused assets and scenes to streamline project structure. Made-with: Cursormaster
17 changed files with 2292 additions and 186 deletions
@ -0,0 +1,531 @@ |
|||
# 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`:新增 `test`、`test: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: 写一个失败的状态机迁移测试** |
|||
|
|||
```ts |
|||
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: 添加最小测试基础设施与最小实现** |
|||
|
|||
```ts |
|||
// 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"], |
|||
}, |
|||
}); |
|||
``` |
|||
|
|||
```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: 提交** |
|||
|
|||
```bash |
|||
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** |
|||
|
|||
```ts |
|||
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** |
|||
|
|||
```ts |
|||
// 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(); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
```ts |
|||
// 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: 提交** |
|||
|
|||
```bash |
|||
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 引用追踪** |
|||
|
|||
```ts |
|||
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 为门面** |
|||
|
|||
```ts |
|||
// 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 }; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
```ts |
|||
// 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: 提交** |
|||
|
|||
```bash |
|||
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: 先写失败测试,验证事件会在切换时发出** |
|||
|
|||
```ts |
|||
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 仅做委托** |
|||
|
|||
```ts |
|||
// 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); |
|||
}, |
|||
}; |
|||
} |
|||
``` |
|||
|
|||
```ts |
|||
// 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: 提交** |
|||
|
|||
```bash |
|||
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: 定义手工验收断言(固定文本,后续逐条打勾)** |
|||
|
|||
```md |
|||
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 装配** |
|||
|
|||
```ts |
|||
// 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 */ } |
|||
} |
|||
``` |
|||
|
|||
```ts |
|||
// 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(); } |
|||
} |
|||
``` |
|||
|
|||
```ts |
|||
// 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: 提交** |
|||
|
|||
```bash |
|||
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: 写迁移校验测试(至少一个场景完整生命周期)** |
|||
|
|||
```ts |
|||
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: 迁移场景到新接口并清理旧调用路径** |
|||
|
|||
```ts |
|||
// 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: 提交** |
|||
|
|||
```bash |
|||
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 手工检查 |
|||
- [ ] 无遗留占位项(无“待补充/稍后实现”字样) |
|||
|
|||
File diff suppressed because it is too large
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 317 KiB |
@ -0,0 +1,165 @@ |
|||
import Game from "@/core/Game"; |
|||
import { Container, Graphics, Text, TextStyle } from "pixi.js"; |
|||
|
|||
export interface SceneLoadingOverlayConfig { |
|||
enabled?: boolean; |
|||
minDisplayMs?: number; |
|||
maskAlpha?: number; |
|||
safeTop?: number; |
|||
safeBottom?: number; |
|||
safeHorizontal?: number; |
|||
panelWidth?: number; |
|||
panelHeight?: number; |
|||
title?: string; |
|||
loadingText?: string; |
|||
readyText?: string; |
|||
} |
|||
|
|||
export interface SceneLoadingOverlayState { |
|||
progress?: number; |
|||
text?: string; |
|||
} |
|||
|
|||
const DEFAULT_CONFIG: Required<SceneLoadingOverlayConfig> = { |
|||
enabled: true, |
|||
minDisplayMs: 350, |
|||
maskAlpha: 0.55, |
|||
safeTop: 24, |
|||
safeBottom: 34, |
|||
safeHorizontal: 16, |
|||
panelWidth: 460, |
|||
panelHeight: 138, |
|||
title: "资源加载中", |
|||
loadingText: "加载中", |
|||
readyText: "即将就绪", |
|||
}; |
|||
|
|||
export class SceneLoadingOverlay { |
|||
private readonly game = Game.getInstance(); |
|||
private readonly root = new Container(); |
|||
private readonly mask = new Graphics(); |
|||
private readonly panel = new Graphics(); |
|||
private readonly progressBg = new Graphics(); |
|||
private readonly progressFill = new Graphics(); |
|||
private readonly titleText = new Text(); |
|||
private readonly statusText = new Text(); |
|||
|
|||
private config: Required<SceneLoadingOverlayConfig> = DEFAULT_CONFIG; |
|||
private mounted = false; |
|||
private startedAt = 0; |
|||
private progress = 0; |
|||
|
|||
constructor() { |
|||
this.root.zIndex = 999999; |
|||
this.root.label = "scene-loading-overlay"; |
|||
|
|||
this.titleText.style = new TextStyle({ |
|||
fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", |
|||
fontSize: 20, |
|||
fontWeight: "700", |
|||
fill: 0xffffff, |
|||
align: "center", |
|||
letterSpacing: 1, |
|||
}); |
|||
this.titleText.anchor.set(0.5); |
|||
|
|||
this.statusText.style = new TextStyle({ |
|||
fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", |
|||
fontSize: 18, |
|||
fill: 0xd6ecff, |
|||
align: "center", |
|||
letterSpacing: 1, |
|||
}); |
|||
this.statusText.anchor.set(0.5); |
|||
|
|||
this.root.addChild( |
|||
this.mask, |
|||
this.panel, |
|||
this.progressBg, |
|||
this.progressFill, |
|||
this.titleText, |
|||
this.statusText |
|||
); |
|||
} |
|||
|
|||
show(config?: SceneLoadingOverlayConfig): void { |
|||
this.config = { ...DEFAULT_CONFIG, ...(config ?? {}) }; |
|||
if (!this.config.enabled) { |
|||
return; |
|||
} |
|||
this.startedAt = performance.now(); |
|||
this.progress = 0; |
|||
this.titleText.text = this.config.title; |
|||
this.update({ progress: 0 }); |
|||
|
|||
if (!this.mounted) { |
|||
this.game.stage.addChild(this.root); |
|||
this.mounted = true; |
|||
} |
|||
} |
|||
|
|||
update(state?: SceneLoadingOverlayState): void { |
|||
if (!this.config.enabled || !this.mounted) { |
|||
return; |
|||
} |
|||
if (typeof state?.progress === "number") { |
|||
this.progress = Math.max(0, Math.min(1, state.progress)); |
|||
} |
|||
|
|||
const { width: W, height: H } = this.game.getInfo(); |
|||
const safeTop = Math.max(0, this.config.safeTop); |
|||
const safeBottom = Math.max(0, this.config.safeBottom); |
|||
const safeHorizontal = Math.max(0, this.config.safeHorizontal); |
|||
const usableW = Math.max(240, W - safeHorizontal * 2); |
|||
const usableH = Math.max(120, H - safeTop - safeBottom); |
|||
const panelW = Math.min(this.config.panelWidth, Math.max(280, usableW * 0.76)); |
|||
const panelH = this.config.panelHeight; |
|||
const panelX = (W - panelW) / 2; |
|||
const idealPanelY = safeTop + usableH * 0.5 - panelH / 2; |
|||
const panelY = Math.max(safeTop, Math.min(idealPanelY, H - safeBottom - panelH)); |
|||
const barW = panelW - 56; |
|||
const barH = 18; |
|||
const barX = panelX + 28; |
|||
const barY = panelY + 62; |
|||
const fillW = Math.max(10, barW * this.progress); |
|||
|
|||
this.mask.clear(); |
|||
this.mask.rect(0, 0, W, H); |
|||
this.mask.fill({ color: 0x04070d, alpha: this.config.maskAlpha }); |
|||
|
|||
this.panel.clear(); |
|||
this.panel.roundRect(panelX, panelY, panelW, panelH, 18); |
|||
this.panel.fill({ color: 0x0f1f35, alpha: 0.9 }); |
|||
this.panel.stroke({ color: 0x7fc0ff, alpha: 0.28, width: 2 }); |
|||
|
|||
this.progressBg.clear(); |
|||
this.progressBg.roundRect(barX, barY, barW, barH, 9); |
|||
this.progressBg.fill({ color: 0x28425f, alpha: 0.96 }); |
|||
|
|||
this.progressFill.clear(); |
|||
this.progressFill.roundRect(barX, barY, fillW, barH, 9); |
|||
this.progressFill.fill({ color: 0x66d9ff, alpha: 0.98 }); |
|||
|
|||
const percent = Math.round(this.progress * 100); |
|||
this.titleText.position.set(W / 2, panelY + 35); |
|||
this.statusText.position.set(W / 2, panelY + 102); |
|||
this.statusText.text = |
|||
state?.text ?? (percent >= 100 ? this.config.readyText : `${this.config.loadingText} ${percent}%`); |
|||
} |
|||
|
|||
async hide(): Promise<void> { |
|||
if (!this.config.enabled || !this.mounted) { |
|||
return; |
|||
} |
|||
const elapsed = performance.now() - this.startedAt; |
|||
if (elapsed < this.config.minDisplayMs) { |
|||
await new Promise((resolve) => setTimeout(resolve, this.config.minDisplayMs - elapsed)); |
|||
} |
|||
this.progress = 1; |
|||
this.update({ progress: 1, text: this.config.readyText }); |
|||
this.game.stage.removeChild(this.root); |
|||
this.mounted = false; |
|||
} |
|||
} |
|||
|
|||
export default SceneLoadingOverlay; |
|||
@ -1,15 +0,0 @@ |
|||
/** |
|||
* Init 首屏在逻辑画布(Game.getInfo,与 designWidth 缩放一致)下的布局参数。 |
|||
* 分区:上区标题 → 中区角色 → 底区主按钮;改间距只改此处。 |
|||
*/ |
|||
export const initSceneLayout = { |
|||
titleYRatio: 0.12, |
|||
subtitleYRatio: 0.2, |
|||
heroYRatio: 0.46, |
|||
/** 主按钮相对底边的上移(逻辑像素,与 position.get 的 options.y 一致) */ |
|||
startCtaOffsetY: -110, |
|||
/** 首屏进入欢迎页时的切场景策略(对齐 scene transaction 参数) */ |
|||
nextSceneTransition: { |
|||
isHolderLast: false, |
|||
}, |
|||
} as const; |
|||
@ -1,15 +0,0 @@ |
|||
import { Graphics} from "pixi.js" |
|||
|
|||
export default new (class Circle{ |
|||
render(){ |
|||
const circle = new Graphics(); |
|||
circle.label = "circle"; |
|||
circle.circle(0, 0, 32); |
|||
circle.fill(0xfb6a8f); |
|||
circle.x = 130; |
|||
circle.y = 130; |
|||
circle.interactive = true; |
|||
// circle.buttonMode = true;
|
|||
return circle |
|||
} |
|||
}) |
|||
@ -1,34 +0,0 @@ |
|||
import Button from "@/components/Button"; |
|||
import { Container } from "pixi.js"; |
|||
import { BaseScene } from "@/scene/BaseScene"; |
|||
import { SceneType } from "@/enums/SceneType"; |
|||
import position from "@/utils/Position"; |
|||
|
|||
export default class WelcomeScene extends BaseScene { |
|||
stage: Container = new Container(); |
|||
private startBtn?: Button; |
|||
|
|||
constructor() { |
|||
super("welcome", SceneType.Normal); |
|||
} |
|||
|
|||
protected onSceneLayout(): void { |
|||
this.startBtn = new Button({ |
|||
text: "进入游戏", |
|||
onClick: () => { |
|||
this.changeScene("welcome2"); |
|||
}, |
|||
position: () => position.center(), |
|||
}); |
|||
|
|||
this.stage.addChild(this.startBtn.getView()); |
|||
} |
|||
|
|||
protected onSceneEnter(): void { |
|||
console.log("welcome onLoad"); |
|||
} |
|||
|
|||
protected onSceneExit(): void { |
|||
console.log("welcome onUnLoad"); |
|||
} |
|||
} |
|||
@ -1,46 +0,0 @@ |
|||
import { BaseScene } from "@/scene/BaseScene"; |
|||
import { SceneType } from "@/enums/SceneType"; |
|||
import { Container, FederatedPointerEvent, Graphics } from "pixi.js"; |
|||
|
|||
export default class Welcome2Scene extends BaseScene { |
|||
stage = new Container(); |
|||
private circle?: Graphics; |
|||
private goWelcome = (_e: FederatedPointerEvent): void => { |
|||
if (!this.circle) { |
|||
return; |
|||
} |
|||
this.circle.x += 10; |
|||
this.changeScene("welcome"); |
|||
}; |
|||
|
|||
constructor() { |
|||
super("welcome2", SceneType.Normal); |
|||
} |
|||
|
|||
protected onSceneLayout(): void { |
|||
this.circle = new Graphics(); |
|||
this.circle.label = "circle"; |
|||
this.circle.circle(0, 0, 32); |
|||
this.circle.fill(0xfb6a8f); |
|||
this.circle.x = 130; |
|||
this.circle.y = 300; |
|||
this.circle.eventMode = "static"; |
|||
this.circle.cursor = "pointer"; |
|||
|
|||
this.circle.on("touchend", this.goWelcome); |
|||
this.circle.on("mousedown", this.goWelcome); |
|||
this.stage.addChild(this.circle); |
|||
} |
|||
|
|||
protected onSceneEnter(): void { |
|||
console.log("onLoad 2"); |
|||
} |
|||
|
|||
protected onSceneExit(): void { |
|||
if (this.circle) { |
|||
this.circle.off("touchend", this.goWelcome); |
|||
this.circle.off("mousedown", this.goWelcome); |
|||
} |
|||
console.log("onUnLoad 2"); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue