From 610232e6b0c37fad123423e0c819018ea41247f2 Mon Sep 17 00:00:00 2001
From: YHH <359807859@qq.com>
Date: Wed, 8 Oct 2025 20:52:31 +0800
Subject: [PATCH] =?UTF-8?q?core=E5=BA=93demo=E6=9B=B4=E6=96=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
examples/core-demos/index.html | 483 ++++++++++
.../package-lock.json | 16 +-
.../package.json | 6 +-
examples/core-demos/src/demos/DemoBase.ts | 99 +++
.../core-demos/src/demos/SerializationDemo.ts | 387 ++++++++
.../core-demos/src/demos/WorkerSystemDemo.ts | 835 ++++++++++++++++++
examples/core-demos/src/demos/index.ts | 12 +
examples/core-demos/src/main.ts | 171 ++++
.../src/platform/BrowserAdapter.ts | 0
.../tsconfig.json | 14 +-
examples/core-demos/vite.config.ts | 15 +
examples/worker-system-demo/index.html | 177 ----
examples/worker-system-demo/src/GameScene.ts | 187 ----
.../src/components/index.ts | 89 --
examples/worker-system-demo/src/main.ts | 376 --------
.../src/systems/LifetimeSystem.ts | 30 -
.../src/systems/PhysicsWorkerSystem.ts | 513 -----------
.../src/systems/RenderSystem.ts | 107 ---
.../worker-system-demo/src/systems/index.ts | 3 -
.../worker-system-demo/tsconfig.node.json | 10 -
examples/worker-system-demo/vite.config.ts | 26 -
21 files changed, 2020 insertions(+), 1536 deletions(-)
create mode 100644 examples/core-demos/index.html
rename examples/{worker-system-demo => core-demos}/package-lock.json (98%)
rename examples/{worker-system-demo => core-demos}/package.json (73%)
create mode 100644 examples/core-demos/src/demos/DemoBase.ts
create mode 100644 examples/core-demos/src/demos/SerializationDemo.ts
create mode 100644 examples/core-demos/src/demos/WorkerSystemDemo.ts
create mode 100644 examples/core-demos/src/demos/index.ts
create mode 100644 examples/core-demos/src/main.ts
rename examples/{worker-system-demo => core-demos}/src/platform/BrowserAdapter.ts (100%)
rename examples/{worker-system-demo => core-demos}/tsconfig.json (71%)
create mode 100644 examples/core-demos/vite.config.ts
delete mode 100644 examples/worker-system-demo/index.html
delete mode 100644 examples/worker-system-demo/src/GameScene.ts
delete mode 100644 examples/worker-system-demo/src/components/index.ts
delete mode 100644 examples/worker-system-demo/src/main.ts
delete mode 100644 examples/worker-system-demo/src/systems/LifetimeSystem.ts
delete mode 100644 examples/worker-system-demo/src/systems/PhysicsWorkerSystem.ts
delete mode 100644 examples/worker-system-demo/src/systems/RenderSystem.ts
delete mode 100644 examples/worker-system-demo/src/systems/index.ts
delete mode 100644 examples/worker-system-demo/tsconfig.node.json
delete mode 100644 examples/worker-system-demo/vite.config.ts
diff --git a/examples/core-demos/index.html b/examples/core-demos/index.html
new file mode 100644
index 00000000..d055aacb
--- /dev/null
+++ b/examples/core-demos/index.html
@@ -0,0 +1,483 @@
+
+
+
+
+
+ ECS Framework Core Demos
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/worker-system-demo/package-lock.json b/examples/core-demos/package-lock.json
similarity index 98%
rename from examples/worker-system-demo/package-lock.json
rename to examples/core-demos/package-lock.json
index 5d0d3d01..ad7d0316 100644
--- a/examples/worker-system-demo/package-lock.json
+++ b/examples/core-demos/package-lock.json
@@ -1,11 +1,11 @@
{
- "name": "ecs-worker-system-demo",
+ "name": "ecs-core-demos",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "ecs-worker-system-demo",
+ "name": "ecs-core-demos",
"version": "1.0.0",
"dependencies": {
"@esengine/ecs-framework": "file:../../packages/core"
@@ -17,8 +17,11 @@
},
"../../packages/core": {
"name": "@esengine/ecs-framework",
- "version": "2.1.49",
+ "version": "2.1.51",
"license": "MIT",
+ "dependencies": {
+ "msgpack-lite": "^0.1.26"
+ },
"devDependencies": {
"@babel/core": "^7.28.3",
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
@@ -29,6 +32,7 @@
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-terser": "^0.4.4",
"@types/jest": "^29.5.14",
+ "@types/msgpack-lite": "^0.1.11",
"@types/node": "^20.19.17",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
@@ -553,9 +557,9 @@
}
},
"node_modules/typescript": {
- "version": "5.9.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
- "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
diff --git a/examples/worker-system-demo/package.json b/examples/core-demos/package.json
similarity index 73%
rename from examples/worker-system-demo/package.json
rename to examples/core-demos/package.json
index 143af1cf..35ecf10d 100644
--- a/examples/worker-system-demo/package.json
+++ b/examples/core-demos/package.json
@@ -1,7 +1,7 @@
{
- "name": "ecs-worker-system-demo",
+ "name": "ecs-core-demos",
"version": "1.0.0",
- "description": "ECS Framework Worker System Demo",
+ "description": "ECS Framework Core Demos - Multiple Interactive Examples",
"main": "src/main.ts",
"scripts": {
"dev": "vite",
@@ -15,4 +15,4 @@
"dependencies": {
"@esengine/ecs-framework": "file:../../packages/core"
}
-}
\ No newline at end of file
+}
diff --git a/examples/core-demos/src/demos/DemoBase.ts b/examples/core-demos/src/demos/DemoBase.ts
new file mode 100644
index 00000000..349bdd9a
--- /dev/null
+++ b/examples/core-demos/src/demos/DemoBase.ts
@@ -0,0 +1,99 @@
+import { Scene, Core } from '@esengine/ecs-framework';
+
+export interface DemoInfo {
+ id: string;
+ name: string;
+ description: string;
+ category: string;
+ icon: string;
+}
+
+export abstract class DemoBase {
+ protected scene: Scene;
+ protected canvas: HTMLCanvasElement;
+ protected ctx: CanvasRenderingContext2D;
+ protected controlPanel: HTMLElement;
+ protected isRunning: boolean = false;
+ protected animationFrameId: number | null = null;
+ protected lastTime: number = 0;
+
+ constructor(canvas: HTMLCanvasElement, controlPanel: HTMLElement) {
+ this.canvas = canvas;
+ this.ctx = canvas.getContext('2d')!;
+ this.controlPanel = controlPanel;
+ this.scene = new Scene({ name: this.getInfo().name });
+
+ // 设置canvas大小
+ this.resizeCanvas();
+ window.addEventListener('resize', () => this.resizeCanvas());
+ }
+
+ abstract getInfo(): DemoInfo;
+ abstract setup(): void;
+ abstract createControls(): void;
+
+ protected resizeCanvas() {
+ const rect = this.canvas.getBoundingClientRect();
+ this.canvas.width = rect.width;
+ this.canvas.height = rect.height;
+ }
+
+ public start() {
+ if (this.isRunning) return;
+ this.isRunning = true;
+ this.lastTime = performance.now();
+
+ // 设置当前场景到Core
+ Core.setScene(this.scene);
+
+ this.scene.begin();
+ this.loop();
+ }
+
+ public stop() {
+ this.isRunning = false;
+ if (this.animationFrameId !== null) {
+ cancelAnimationFrame(this.animationFrameId);
+ this.animationFrameId = null;
+ }
+ }
+
+ public destroy() {
+ this.stop();
+ this.scene.end();
+ }
+
+ protected loop = () => {
+ if (!this.isRunning) return;
+
+ // 计算deltaTime
+ const currentTime = performance.now();
+ const deltaTime = (currentTime - this.lastTime) / 1000; // 转换为秒
+ this.lastTime = currentTime;
+
+ // 更新ECS框架
+ Core.update(deltaTime);
+
+ // 渲染
+ this.render();
+
+ // 继续循环
+ this.animationFrameId = requestAnimationFrame(this.loop);
+ }
+
+ protected abstract render(): void;
+
+ protected showToast(message: string, icon: string = '✅') {
+ const toast = document.getElementById('toast')!;
+ const toastMessage = document.getElementById('toastMessage')!;
+ const toastIcon = toast.querySelector('.toast-icon')!;
+
+ toastIcon.textContent = icon;
+ toastMessage.textContent = message;
+
+ toast.classList.add('show');
+ setTimeout(() => {
+ toast.classList.remove('show');
+ }, 3000);
+ }
+}
diff --git a/examples/core-demos/src/demos/SerializationDemo.ts b/examples/core-demos/src/demos/SerializationDemo.ts
new file mode 100644
index 00000000..d5ff884e
--- /dev/null
+++ b/examples/core-demos/src/demos/SerializationDemo.ts
@@ -0,0 +1,387 @@
+import { DemoBase, DemoInfo } from './DemoBase';
+import {
+ Component,
+ ECSComponent,
+ Entity,
+ EntitySystem,
+ Serializable,
+ Serialize,
+ SerializeAsMap
+} from '@esengine/ecs-framework';
+
+// ===== 组件定义 =====
+@ECSComponent('SerDemo_Position')
+@Serializable({ version: 1, typeId: 'SerDemo_Position' })
+class PositionComponent extends Component {
+ @Serialize() x: number = 0;
+ @Serialize() y: number = 0;
+ constructor(x: number = 0, y: number = 0) {
+ super();
+ this.x = x;
+ this.y = y;
+ }
+}
+
+@ECSComponent('SerDemo_Velocity')
+@Serializable({ version: 1, typeId: 'SerDemo_Velocity' })
+class VelocityComponent extends Component {
+ @Serialize() vx: number = 0;
+ @Serialize() vy: number = 0;
+ constructor(vx: number = 0, vy: number = 0) {
+ super();
+ this.vx = vx;
+ this.vy = vy;
+ }
+}
+
+@ECSComponent('SerDemo_Renderable')
+@Serializable({ version: 1, typeId: 'SerDemo_Renderable' })
+class RenderableComponent extends Component {
+ @Serialize() color: string = '#ffffff';
+ @Serialize() radius: number = 10;
+ constructor(color: string = '#ffffff', radius: number = 10) {
+ super();
+ this.color = color;
+ this.radius = radius;
+ }
+}
+
+@ECSComponent('SerDemo_Player')
+@Serializable({ version: 1, typeId: 'SerDemo_Player' })
+class PlayerComponent extends Component {
+ @Serialize() name: string = 'Player';
+ @Serialize() level: number = 1;
+ @Serialize() health: number = 100;
+ @SerializeAsMap() inventory: Map = new Map();
+ constructor(name: string = 'Player') {
+ super();
+ this.name = name;
+ }
+}
+
+// ===== 系统定义 =====
+class MovementSystem extends EntitySystem {
+ update() {
+ const entities = this.scene.entities.buffer;
+ for (const entity of entities) {
+ const pos = entity.getComponent(PositionComponent);
+ const vel = entity.getComponent(VelocityComponent);
+ if (pos && vel) {
+ pos.x += vel.vx;
+ pos.y += vel.vy;
+
+ // 边界反弹
+ if (pos.x < 0 || pos.x > 1200) vel.vx *= -1;
+ if (pos.y < 0 || pos.y > 600) vel.vy *= -1;
+ }
+ }
+ }
+}
+
+class RenderSystem extends EntitySystem {
+ private canvas: HTMLCanvasElement;
+ private ctx: CanvasRenderingContext2D;
+
+ constructor(canvas: HTMLCanvasElement) {
+ super();
+ this.canvas = canvas;
+ this.ctx = canvas.getContext('2d')!;
+ }
+
+ update() {
+ // 清空画布
+ this.ctx.fillStyle = '#0a0a15';
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
+
+ // 渲染所有实体
+ const entities = this.scene.entities.buffer;
+ for (const entity of entities) {
+ const pos = entity.getComponent(PositionComponent);
+ const render = entity.getComponent(RenderableComponent);
+ if (pos && render) {
+ this.ctx.fillStyle = render.color;
+ this.ctx.beginPath();
+ this.ctx.arc(pos.x, pos.y, render.radius, 0, Math.PI * 2);
+ this.ctx.fill();
+
+ // 如果是玩家,显示名字
+ const player = entity.getComponent(PlayerComponent);
+ if (player) {
+ this.ctx.fillStyle = 'white';
+ this.ctx.font = '12px Arial';
+ this.ctx.textAlign = 'center';
+ this.ctx.fillText(player.name, pos.x, pos.y - render.radius - 5);
+ }
+ }
+ }
+ }
+}
+
+export class SerializationDemo extends DemoBase {
+ private renderSystem!: RenderSystem;
+ private jsonData: string = '';
+ private binaryData: Buffer | null = null;
+
+ getInfo(): DemoInfo {
+ return {
+ id: 'serialization',
+ name: '场景序列化',
+ description: '演示场景的序列化和反序列化功能,支持JSON和二进制格式',
+ category: '核心功能',
+ icon: '💾'
+ };
+ }
+
+ setup() {
+ // @ECSComponent装饰器会自动注册组件到ComponentRegistry
+ // ComponentRegistry会被序列化系统自动使用,无需手动注册
+
+ // 添加系统
+ this.renderSystem = new RenderSystem(this.canvas);
+ this.scene.addEntityProcessor(new MovementSystem());
+ this.scene.addEntityProcessor(this.renderSystem);
+
+ // 创建初始实体
+ this.createInitialEntities();
+
+ // 创建控制面板
+ this.createControls();
+ }
+
+ private createInitialEntities() {
+ // 创建玩家
+ const player = this.scene.createEntity('Player');
+ player.addComponent(new PositionComponent(600, 300));
+ player.addComponent(new VelocityComponent(2, 1.5));
+ player.addComponent(new RenderableComponent('#4a9eff', 15));
+ const playerComp = new PlayerComponent('Hero');
+ playerComp.level = 5;
+ playerComp.health = 100;
+ playerComp.inventory.set('sword', 1);
+ playerComp.inventory.set('potion', 5);
+ player.addComponent(playerComp);
+
+ // 创建一些随机实体
+ for (let i = 0; i < 5; i++) {
+ this.createRandomEntity();
+ }
+
+ // 设置场景数据
+ this.scene.sceneData.set('weather', 'sunny');
+ this.scene.sceneData.set('gameTime', 12.5);
+ this.scene.sceneData.set('difficulty', 'normal');
+ }
+
+ private createRandomEntity() {
+ const entity = this.scene.createEntity(`Entity_${Date.now()}`);
+ entity.addComponent(new PositionComponent(
+ Math.random() * this.canvas.width,
+ Math.random() * this.canvas.height
+ ));
+ entity.addComponent(new VelocityComponent(
+ (Math.random() - 0.5) * 3,
+ (Math.random() - 0.5) * 3
+ ));
+ const colors = ['#ff6b6b', '#4ecdc4', '#ffe66d', '#a8dadc', '#f1faee'];
+ entity.addComponent(new RenderableComponent(
+ colors[Math.floor(Math.random() * colors.length)],
+ 5 + Math.random() * 10
+ ));
+ }
+
+ createControls() {
+ this.controlPanel.innerHTML = `
+
+
实体控制
+
+
+
+
+
+
+
+
序列化操作
+
+
+
+
+
+
+
+
+
本地存储
+
+
+
+
+
+
+
+
场景数据
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
序列化数据预览
+
+ 点击序列化按钮查看数据...
+
+
+ `;
+
+ // 绑定事件
+ this.bindEvents();
+ }
+
+ private bindEvents() {
+ document.getElementById('addEntity')!.addEventListener('click', () => {
+ this.createRandomEntity();
+ this.updateStats();
+ this.showToast('添加了一个随机实体');
+ });
+
+ document.getElementById('clearEntities')!.addEventListener('click', () => {
+ this.scene.destroyAllEntities();
+ this.createInitialEntities();
+ this.updateStats();
+ this.showToast('场景已重置');
+ });
+
+ document.getElementById('serializeJSON')!.addEventListener('click', () => {
+ this.jsonData = this.scene.serialize({ format: 'json', pretty: true }) as string;
+ this.updateDataPreview(this.jsonData, 'json');
+ this.updateStats();
+ this.showToast('已序列化为JSON格式');
+ });
+
+ document.getElementById('serializeBinary')!.addEventListener('click', () => {
+ this.binaryData = this.scene.serialize({ format: 'binary' }) as Buffer;
+ const base64 = this.binaryData.toString('base64');
+ this.updateDataPreview(`Binary Data (Base64):\n${base64.substring(0, 500)}...`, 'binary');
+ this.updateStats();
+ this.showToast('已序列化为二进制格式', '🔐');
+ });
+
+ document.getElementById('deserialize')!.addEventListener('click', () => {
+ const data = this.binaryData || this.jsonData;
+ if (!data) {
+ this.showToast('请先执行序列化操作', '⚠️');
+ return;
+ }
+
+ this.scene.deserialize(data, {
+ strategy: 'replace'
+ // componentRegistry会自动从ComponentRegistry获取,无需手动传入
+ });
+
+ this.updateStats();
+ this.showToast('场景已恢复');
+ });
+
+ document.getElementById('saveLocal')!.addEventListener('click', () => {
+ const jsonData = this.scene.serialize({ format: 'json' }) as string;
+ localStorage.setItem('ecs_demo_scene', jsonData);
+ this.showToast('已保存到LocalStorage', '💾');
+ });
+
+ document.getElementById('loadLocal')!.addEventListener('click', () => {
+ const data = localStorage.getItem('ecs_demo_scene');
+ if (!data) {
+ this.showToast('LocalStorage中没有保存的场景', '⚠️');
+ return;
+ }
+
+ this.scene.deserialize(data, {
+ strategy: 'replace'
+ // componentRegistry会自动从ComponentRegistry获取,无需手动传入
+ });
+
+ this.updateStats();
+ this.showToast('已从LocalStorage加载', '📂');
+ });
+
+ document.getElementById('updateSceneData')!.addEventListener('click', () => {
+ const weather = (document.getElementById('weather') as HTMLInputElement).value;
+ const gameTime = parseFloat((document.getElementById('gameTime') as HTMLInputElement).value);
+
+ this.scene.sceneData.set('weather', weather);
+ this.scene.sceneData.set('gameTime', gameTime);
+
+ this.showToast('场景数据已更新');
+ });
+
+ // 初始更新统计
+ this.updateStats();
+ }
+
+ private updateDataPreview(data: string, format: string) {
+ const preview = document.getElementById('dataPreview')!;
+ if (format === 'json') {
+ const truncated = data.length > 1000 ? data.substring(0, 1000) + '\n...(truncated)' : data;
+ preview.textContent = truncated;
+ } else {
+ preview.textContent = data;
+ }
+ }
+
+ private updateStats() {
+ const entityCount = this.scene.entities.count;
+ document.getElementById('entityCount')!.textContent = entityCount.toString();
+
+ // 计算JSON大小
+ if (this.jsonData) {
+ const jsonSize = new Blob([this.jsonData]).size;
+ document.getElementById('jsonSize')!.textContent = this.formatBytes(jsonSize);
+ }
+
+ // 计算二进制大小
+ if (this.binaryData) {
+ const binarySize = this.binaryData.length;
+ document.getElementById('binarySize')!.textContent = this.formatBytes(binarySize);
+
+ // 计算压缩率
+ if (this.jsonData) {
+ const jsonSize = new Blob([this.jsonData]).size;
+ const ratio = ((1 - binarySize / jsonSize) * 100).toFixed(1);
+ document.getElementById('compressionRatio')!.textContent = `${ratio}%`;
+ }
+ }
+ }
+
+ private formatBytes(bytes: number): string {
+ if (bytes < 1024) return `${bytes}B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
+ }
+
+ protected render() {
+ // RenderSystem会处理渲染
+ }
+}
diff --git a/examples/core-demos/src/demos/WorkerSystemDemo.ts b/examples/core-demos/src/demos/WorkerSystemDemo.ts
new file mode 100644
index 00000000..8d3349ab
--- /dev/null
+++ b/examples/core-demos/src/demos/WorkerSystemDemo.ts
@@ -0,0 +1,835 @@
+import { DemoBase, DemoInfo } from './DemoBase';
+import { Component, ECSComponent, WorkerEntitySystem, EntitySystem, Matcher, Entity, ECSSystem, PlatformManager, Time } from '@esengine/ecs-framework';
+import { BrowserAdapter } from '../platform/BrowserAdapter';
+
+// ============ 组件定义 ============
+
+@ECSComponent('WorkerDemo_Position')
+class Position extends Component {
+ x: number = 0;
+ y: number = 0;
+
+ constructor(x: number = 0, y: number = 0) {
+ super();
+ this.x = x;
+ this.y = y;
+ }
+
+ set(x: number, y: number): void {
+ this.x = x;
+ this.y = y;
+ }
+}
+
+@ECSComponent('WorkerDemo_Velocity')
+class Velocity extends Component {
+ dx: number = 0;
+ dy: number = 0;
+
+ constructor(dx: number = 0, dy: number = 0) {
+ super();
+ this.dx = dx;
+ this.dy = dy;
+ }
+
+ set(dx: number, dy: number): void {
+ this.dx = dx;
+ this.dy = dy;
+ }
+}
+
+@ECSComponent('WorkerDemo_Physics')
+class Physics extends Component {
+ mass: number = 1;
+ bounce: number = 0.8;
+ friction: number = 0.95;
+
+ constructor(mass: number = 1, bounce: number = 0.8, friction: number = 0.95) {
+ super();
+ this.mass = mass;
+ this.bounce = bounce;
+ this.friction = friction;
+ }
+}
+
+@ECSComponent('WorkerDemo_Renderable')
+class Renderable extends Component {
+ color: string = '#ffffff';
+ size: number = 5;
+ shape: 'circle' | 'square' = 'circle';
+
+ constructor(color: string = '#ffffff', size: number = 5, shape: 'circle' | 'square' = 'circle') {
+ super();
+ this.color = color;
+ this.size = size;
+ this.shape = shape;
+ }
+}
+
+@ECSComponent('WorkerDemo_Lifetime')
+class Lifetime extends Component {
+ maxAge: number = 5;
+ currentAge: number = 0;
+
+ constructor(maxAge: number = 5) {
+ super();
+ this.maxAge = maxAge;
+ this.currentAge = 0;
+ }
+
+ isDead(): boolean {
+ return this.currentAge >= this.maxAge;
+ }
+}
+
+// ============ 系统定义 ============
+
+interface PhysicsEntityData {
+ id: number;
+ x: number;
+ y: number;
+ dx: number;
+ dy: number;
+ mass: number;
+ bounce: number;
+ friction: number;
+ radius: number;
+}
+
+interface PhysicsConfig {
+ gravity: number;
+ canvasWidth: number;
+ canvasHeight: number;
+ groundFriction: number;
+}
+
+@ECSSystem('PhysicsWorkerSystem')
+class PhysicsWorkerSystem extends WorkerEntitySystem {
+ private physicsConfig: PhysicsConfig;
+
+ constructor(enableWorker: boolean, canvasWidth: number, canvasHeight: number) {
+ const defaultConfig = {
+ gravity: 100,
+ canvasWidth,
+ canvasHeight,
+ groundFriction: 0.98
+ };
+
+ const isSharedArrayBufferAvailable = typeof SharedArrayBuffer !== 'undefined' && self.crossOriginIsolated;
+
+ super(
+ Matcher.empty().all(Position, Velocity, Physics),
+ {
+ enableWorker,
+ workerCount: isSharedArrayBufferAvailable ? (navigator.hardwareConcurrency || 2) : 1,
+ systemConfig: defaultConfig,
+ useSharedArrayBuffer: true
+ }
+ );
+
+ this.physicsConfig = defaultConfig;
+ }
+
+ protected extractEntityData(entity: Entity): PhysicsEntityData {
+ const position = entity.getComponent(Position)!;
+ const velocity = entity.getComponent(Velocity)!;
+ const physics = entity.getComponent(Physics)!;
+ const renderable = entity.getComponent(Renderable)!;
+
+ return {
+ id: entity.id,
+ x: position.x,
+ y: position.y,
+ dx: velocity.dx,
+ dy: velocity.dy,
+ mass: physics.mass,
+ bounce: physics.bounce,
+ friction: physics.friction,
+ radius: renderable.size
+ };
+ }
+
+ protected workerProcess(
+ entities: PhysicsEntityData[],
+ deltaTime: number,
+ systemConfig?: PhysicsConfig
+ ): PhysicsEntityData[] {
+ const config = systemConfig || this.physicsConfig;
+ const result = entities.map(e => ({ ...e }));
+
+ for (let i = 0; i < result.length; i++) {
+ const entity = result[i];
+
+ entity.dy += config.gravity * deltaTime;
+ entity.x += entity.dx * deltaTime;
+ entity.y += entity.dy * deltaTime;
+
+ if (entity.x <= entity.radius) {
+ entity.x = entity.radius;
+ entity.dx = -entity.dx * entity.bounce;
+ } else if (entity.x >= config.canvasWidth - entity.radius) {
+ entity.x = config.canvasWidth - entity.radius;
+ entity.dx = -entity.dx * entity.bounce;
+ }
+
+ if (entity.y <= entity.radius) {
+ entity.y = entity.radius;
+ entity.dy = -entity.dy * entity.bounce;
+ } else if (entity.y >= config.canvasHeight - entity.radius) {
+ entity.y = config.canvasHeight - entity.radius;
+ entity.dy = -entity.dy * entity.bounce;
+ entity.dx *= config.groundFriction;
+ }
+
+ entity.dx *= entity.friction;
+ entity.dy *= entity.friction;
+ }
+
+ for (let i = 0; i < result.length; i++) {
+ for (let j = i + 1; j < result.length; j++) {
+ const ball1 = result[i];
+ const ball2 = result[j];
+
+ const dx = ball2.x - ball1.x;
+ const dy = ball2.y - ball1.y;
+ const distance = Math.sqrt(dx * dx + dy * dy);
+ const minDistance = ball1.radius + ball2.radius;
+
+ if (distance < minDistance && distance > 0) {
+ const nx = dx / distance;
+ const ny = dy / distance;
+
+ const overlap = minDistance - distance;
+ const separationX = nx * overlap * 0.5;
+ const separationY = ny * overlap * 0.5;
+
+ ball1.x -= separationX;
+ ball1.y -= separationY;
+ ball2.x += separationX;
+ ball2.y += separationY;
+
+ const relativeVelocityX = ball2.dx - ball1.dx;
+ const relativeVelocityY = ball2.dy - ball1.dy;
+ const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
+
+ if (velocityAlongNormal > 0) continue;
+
+ const restitution = (ball1.bounce + ball2.bounce) * 0.5;
+ const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/ball1.mass + 1/ball2.mass);
+
+ const impulseX = impulseScalar * nx;
+ const impulseY = impulseScalar * ny;
+
+ ball1.dx -= impulseX / ball1.mass;
+ ball1.dy -= impulseY / ball1.mass;
+ ball2.dx += impulseX / ball2.mass;
+ ball2.dy += impulseY / ball2.mass;
+
+ const energyLoss = 0.98;
+ ball1.dx *= energyLoss;
+ ball1.dy *= energyLoss;
+ ball2.dx *= energyLoss;
+ ball2.dy *= energyLoss;
+ }
+ }
+ }
+
+ return result;
+ }
+
+ protected applyResult(entity: Entity, result: PhysicsEntityData): void {
+ if (!entity || !entity.enabled) return;
+
+ const position = entity.getComponent(Position);
+ const velocity = entity.getComponent(Velocity);
+
+ if (!position || !velocity) return;
+
+ position.set(result.x, result.y);
+ velocity.set(result.dx, result.dy);
+ }
+
+ public updatePhysicsConfig(newConfig: Partial): void {
+ Object.assign(this.physicsConfig, newConfig);
+ this.updateConfig({ systemConfig: this.physicsConfig });
+ }
+
+ public getPhysicsConfig(): PhysicsConfig {
+ return { ...this.physicsConfig };
+ }
+
+ protected getDefaultEntityDataSize(): number {
+ return 9;
+ }
+
+ protected writeEntityToBuffer(entityData: PhysicsEntityData, offset: number): void {
+ const sharedArray = (this as any).sharedFloatArray as Float32Array;
+ if (!sharedArray) return;
+
+ // 在第一个位置存储当前实体数量
+ const currentEntityCount = Math.floor(offset / 9) + 1;
+ sharedArray[0] = currentEntityCount;
+
+ // 数据从索引9开始存储(第一个9个位置用作元数据区域)
+ const dataOffset = offset + 9;
+ sharedArray[dataOffset + 0] = entityData.id;
+ sharedArray[dataOffset + 1] = entityData.x;
+ sharedArray[dataOffset + 2] = entityData.y;
+ sharedArray[dataOffset + 3] = entityData.dx;
+ sharedArray[dataOffset + 4] = entityData.dy;
+ sharedArray[dataOffset + 5] = entityData.mass;
+ sharedArray[dataOffset + 6] = entityData.bounce;
+ sharedArray[dataOffset + 7] = entityData.friction;
+ sharedArray[dataOffset + 8] = entityData.radius;
+ }
+
+ protected readEntityFromBuffer(offset: number): PhysicsEntityData | null {
+ const sharedArray = (this as any).sharedFloatArray as Float32Array;
+ if (!sharedArray) return null;
+
+ // 数据从索引9开始存储
+ const dataOffset = offset + 9;
+ return {
+ id: sharedArray[dataOffset + 0],
+ x: sharedArray[dataOffset + 1],
+ y: sharedArray[dataOffset + 2],
+ dx: sharedArray[dataOffset + 3],
+ dy: sharedArray[dataOffset + 4],
+ mass: sharedArray[dataOffset + 5],
+ bounce: sharedArray[dataOffset + 6],
+ friction: sharedArray[dataOffset + 7],
+ radius: sharedArray[dataOffset + 8]
+ };
+ }
+
+ protected getSharedArrayBufferProcessFunction(): any {
+ return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, systemConfig?: any) {
+ const config = systemConfig || {
+ gravity: 100,
+ canvasWidth: 800,
+ canvasHeight: 600,
+ groundFriction: 0.98
+ };
+
+ const actualEntityCount = sharedFloatArray[0];
+
+ // 基础物理更新
+ for (let i = startIndex; i < endIndex && i < actualEntityCount; i++) {
+ const offset = i * 9 + 9;
+
+ const id = sharedFloatArray[offset + 0];
+ if (id === 0) continue;
+
+ let x = sharedFloatArray[offset + 1];
+ let y = sharedFloatArray[offset + 2];
+ let dx = sharedFloatArray[offset + 3];
+ let dy = sharedFloatArray[offset + 4];
+ const bounce = sharedFloatArray[offset + 6];
+ const friction = sharedFloatArray[offset + 7];
+ const radius = sharedFloatArray[offset + 8];
+
+ // 应用重力
+ dy += config.gravity * deltaTime;
+
+ // 更新位置
+ x += dx * deltaTime;
+ y += dy * deltaTime;
+
+ // 边界碰撞
+ if (x <= radius) {
+ x = radius;
+ dx = -dx * bounce;
+ } else if (x >= config.canvasWidth - radius) {
+ x = config.canvasWidth - radius;
+ dx = -dx * bounce;
+ }
+
+ if (y <= radius) {
+ y = radius;
+ dy = -dy * bounce;
+ } else if (y >= config.canvasHeight - radius) {
+ y = config.canvasHeight - radius;
+ dy = -dy * bounce;
+ dx *= config.groundFriction;
+ }
+
+ // 空气阻力
+ dx *= friction;
+ dy *= friction;
+
+ // 写回数据
+ sharedFloatArray[offset + 1] = x;
+ sharedFloatArray[offset + 2] = y;
+ sharedFloatArray[offset + 3] = dx;
+ sharedFloatArray[offset + 4] = dy;
+ }
+
+ // 碰撞检测
+ for (let i = startIndex; i < endIndex && i < actualEntityCount; i++) {
+ const offset1 = i * 9 + 9;
+ const id1 = sharedFloatArray[offset1 + 0];
+ if (id1 === 0) continue;
+
+ let x1 = sharedFloatArray[offset1 + 1];
+ let y1 = sharedFloatArray[offset1 + 2];
+ let dx1 = sharedFloatArray[offset1 + 3];
+ let dy1 = sharedFloatArray[offset1 + 4];
+ const mass1 = sharedFloatArray[offset1 + 5];
+ const bounce1 = sharedFloatArray[offset1 + 6];
+ const radius1 = sharedFloatArray[offset1 + 8];
+
+ for (let j = 0; j < actualEntityCount; j++) {
+ if (i === j) continue;
+
+ const offset2 = j * 9 + 9;
+ const id2 = sharedFloatArray[offset2 + 0];
+ if (id2 === 0) continue;
+
+ const x2 = sharedFloatArray[offset2 + 1];
+ const y2 = sharedFloatArray[offset2 + 2];
+ const dx2 = sharedFloatArray[offset2 + 3];
+ const dy2 = sharedFloatArray[offset2 + 4];
+ const mass2 = sharedFloatArray[offset2 + 5];
+ const bounce2 = sharedFloatArray[offset2 + 6];
+ const radius2 = sharedFloatArray[offset2 + 8];
+
+ if (isNaN(x2) || isNaN(y2) || isNaN(radius2) || radius2 <= 0) continue;
+
+ const deltaX = x2 - x1;
+ const deltaY = y2 - y1;
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
+ const minDistance = radius1 + radius2;
+
+ if (distance < minDistance && distance > 0) {
+ const nx = deltaX / distance;
+ const ny = deltaY / distance;
+
+ const overlap = minDistance - distance;
+ const separationX = nx * overlap * 0.5;
+ const separationY = ny * overlap * 0.5;
+
+ x1 -= separationX;
+ y1 -= separationY;
+
+ const relativeVelocityX = dx2 - dx1;
+ const relativeVelocityY = dy2 - dy1;
+ const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
+
+ if (velocityAlongNormal > 0) continue;
+
+ const restitution = (bounce1 + bounce2) * 0.5;
+ const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/mass1 + 1/mass2);
+
+ const impulseX = impulseScalar * nx;
+ const impulseY = impulseScalar * ny;
+
+ dx1 -= impulseX / mass1;
+ dy1 -= impulseY / mass1;
+
+ const energyLoss = 0.98;
+ dx1 *= energyLoss;
+ dy1 *= energyLoss;
+ }
+ }
+
+ sharedFloatArray[offset1 + 1] = x1;
+ sharedFloatArray[offset1 + 2] = y1;
+ sharedFloatArray[offset1 + 3] = dx1;
+ sharedFloatArray[offset1 + 4] = dy1;
+ }
+ };
+ }
+}
+
+@ECSSystem('RenderSystem')
+class RenderSystem extends EntitySystem {
+ private canvas: HTMLCanvasElement;
+ private ctx: CanvasRenderingContext2D;
+
+ constructor(canvas: HTMLCanvasElement) {
+ super(Matcher.empty().all(Position, Renderable));
+ this.canvas = canvas;
+ this.ctx = canvas.getContext('2d')!;
+ }
+
+ protected override process(entities: Entity[]): void {
+ this.ctx.fillStyle = '#000';
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
+
+ for (const entity of entities) {
+ const position = entity.getComponent(Position);
+ const renderable = entity.getComponent(Renderable);
+
+ if (!position || !renderable) continue;
+
+ this.ctx.fillStyle = renderable.color;
+ this.ctx.beginPath();
+ this.ctx.arc(position.x, position.y, renderable.size, 0, Math.PI * 2);
+ this.ctx.fill();
+ }
+ }
+}
+
+@ECSSystem('LifetimeSystem')
+class LifetimeSystem extends EntitySystem {
+ constructor() {
+ super(Matcher.empty().all(Lifetime));
+ }
+
+ protected override process(entities: Entity[]): void {
+ const deltaTime = Time.deltaTime;
+
+ for (const entity of entities) {
+ const lifetime = entity.getComponent(Lifetime);
+ if (!lifetime) continue;
+
+ lifetime.currentAge += deltaTime;
+ if (lifetime.isDead()) {
+ entity.destroy();
+ }
+ }
+ }
+}
+
+// ============ Demo类 ============
+
+export class WorkerSystemDemo extends DemoBase {
+ private physicsSystem!: PhysicsWorkerSystem;
+ private renderSystem!: RenderSystem;
+ private lifetimeSystem!: LifetimeSystem;
+ private currentFPS = 0;
+ private frameCount = 0;
+ private fpsUpdateTime = 0;
+ private elements: { [key: string]: HTMLElement } = {};
+
+ getInfo(): DemoInfo {
+ return {
+ id: 'worker-system',
+ name: 'Worker System',
+ description: '演示 ECS 框架中的多线程物理计算能力',
+ category: '核心功能',
+ icon: '⚙️'
+ };
+ }
+
+ setup(): void {
+ // 注册浏览器平台适配器
+ const browserAdapter = new BrowserAdapter();
+ PlatformManager.getInstance().registerAdapter(browserAdapter);
+
+ // 初始化系统
+ this.physicsSystem = new PhysicsWorkerSystem(true, this.canvas.width, this.canvas.height);
+ this.renderSystem = new RenderSystem(this.canvas);
+ this.lifetimeSystem = new LifetimeSystem();
+
+ this.physicsSystem.updateOrder = 1;
+ this.lifetimeSystem.updateOrder = 2;
+ this.renderSystem.updateOrder = 3;
+
+ this.scene.addSystem(this.physicsSystem);
+ this.scene.addSystem(this.lifetimeSystem);
+ this.scene.addSystem(this.renderSystem);
+
+ // 创建控制面板
+ this.createControls();
+
+ // 初始化UI元素引用
+ this.initializeUIElements();
+ this.bindEvents();
+
+ // 生成初始实体
+ this.spawnInitialEntities(1000);
+ }
+
+ createControls(): void {
+ this.controlPanel.innerHTML = `
+
+
+
+
+ 1000
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
性能统计
+
FPS: 0
+
实体数量: 0
+
Worker状态: 未启用
+
Worker负载: N/A
+
+
+ `;
+ }
+
+ protected render(): void {
+ this.frameCount++;
+ const currentTime = performance.now();
+
+ if (currentTime - this.fpsUpdateTime >= 1000) {
+ this.currentFPS = this.frameCount;
+ this.frameCount = 0;
+ this.fpsUpdateTime = currentTime;
+ }
+
+ this.updateUI();
+ }
+
+ private initializeUIElements(): void {
+ const elementIds = [
+ 'entityCount', 'entityCountValue', 'toggleWorker',
+ 'gravity', 'gravityValue', 'friction', 'frictionValue', 'spawnParticles',
+ 'clearEntities', 'resetDemo', 'fps', 'entityCountStat', 'workerStatus', 'workerLoad'
+ ];
+
+ for (const id of elementIds) {
+ const element = document.getElementById(id);
+ if (element) {
+ this.elements[id] = element;
+ }
+ }
+ }
+
+ private bindEvents(): void {
+ if (this.elements.entityCount && this.elements.entityCountValue) {
+ const slider = this.elements.entityCount as HTMLInputElement;
+ slider.addEventListener('input', () => {
+ this.elements.entityCountValue.textContent = slider.value;
+ });
+
+ slider.addEventListener('change', () => {
+ const count = parseInt(slider.value);
+ this.spawnInitialEntities(count);
+ });
+ }
+
+ if (this.elements.toggleWorker) {
+ this.elements.toggleWorker.addEventListener('click', () => {
+ const workerEnabled = this.toggleWorker();
+ this.elements.toggleWorker.textContent = workerEnabled ? '禁用 Worker' : '启用 Worker';
+ });
+ }
+
+ if (this.elements.gravity && this.elements.gravityValue) {
+ const slider = this.elements.gravity as HTMLInputElement;
+ slider.addEventListener('input', () => {
+ this.elements.gravityValue.textContent = slider.value;
+ });
+
+ slider.addEventListener('change', () => {
+ const gravity = parseInt(slider.value);
+ this.updateWorkerConfig({ gravity });
+ });
+ }
+
+ if (this.elements.friction && this.elements.frictionValue) {
+ const slider = this.elements.friction as HTMLInputElement;
+ slider.addEventListener('input', () => {
+ const value = parseInt(slider.value);
+ this.elements.frictionValue.textContent = `${value}%`;
+ });
+
+ slider.addEventListener('change', () => {
+ const friction = parseInt(slider.value) / 100;
+ this.updateWorkerConfig({ friction });
+ });
+ }
+
+ if (this.elements.spawnParticles) {
+ this.elements.spawnParticles.addEventListener('click', () => {
+ const centerX = this.canvas.width / 2;
+ const centerY = this.canvas.height / 2;
+ this.spawnParticleExplosion(centerX, centerY, 100);
+ });
+ }
+
+ if (this.elements.clearEntities) {
+ this.elements.clearEntities.addEventListener('click', () => {
+ this.clearAllEntities();
+ });
+ }
+
+ if (this.elements.resetDemo) {
+ this.elements.resetDemo.addEventListener('click', () => {
+ (this.elements.entityCount as HTMLInputElement).value = '1000';
+ this.elements.entityCountValue.textContent = '1000';
+ (this.elements.gravity as HTMLInputElement).value = '100';
+ this.elements.gravityValue.textContent = '100';
+ (this.elements.friction as HTMLInputElement).value = '95';
+ this.elements.frictionValue.textContent = '95%';
+
+ this.spawnInitialEntities(1000);
+ this.updateWorkerConfig({ gravity: 100, friction: 0.95 });
+ });
+ }
+
+ this.canvas.addEventListener('click', (event) => {
+ const rect = this.canvas.getBoundingClientRect();
+ const x = event.clientX - rect.left;
+ const y = event.clientY - rect.top;
+ this.spawnParticleExplosion(x, y, 30);
+ });
+ }
+
+ private updateUI(): void {
+ const workerInfo = this.physicsSystem.getWorkerInfo();
+
+ if (this.elements.fps) {
+ this.elements.fps.textContent = this.currentFPS.toString();
+ }
+
+ if (this.elements.entityCountStat) {
+ this.elements.entityCountStat.textContent = this.scene.entities.count.toString();
+ }
+
+ if (this.elements.workerStatus) {
+ if (workerInfo.enabled) {
+ this.elements.workerStatus.textContent = `启用 (${workerInfo.workerCount} Workers)`;
+ this.elements.workerStatus.style.color = '#4eff4a';
+ } else {
+ this.elements.workerStatus.textContent = '禁用';
+ this.elements.workerStatus.style.color = '#ff4a4a';
+ }
+ }
+
+ if (this.elements.workerLoad) {
+ const entityCount = this.scene.entities.count;
+ if (workerInfo.enabled && entityCount > 0) {
+ const entitiesPerWorker = Math.ceil(entityCount / workerInfo.workerCount);
+ this.elements.workerLoad.textContent = `${entitiesPerWorker}/Worker (共${workerInfo.workerCount}个)`;
+ } else {
+ this.elements.workerLoad.textContent = 'N/A';
+ }
+ }
+ }
+
+ private spawnInitialEntities(count: number = 1000): void {
+ this.clearAllEntities();
+
+ for (let i = 0; i < count; i++) {
+ this.createParticle();
+ }
+ }
+
+ private createParticle(): void {
+ const entity = this.scene.createEntity(`Particle_${Date.now()}_${Math.random()}`);
+
+ const x = Math.random() * (this.canvas.width - 20) + 10;
+ const y = Math.random() * (this.canvas.height - 20) + 10;
+ const dx = (Math.random() - 0.5) * 200;
+ const dy = (Math.random() - 0.5) * 200;
+ const mass = Math.random() * 3 + 2;
+ const bounce = 0.85 + Math.random() * 0.15;
+ const friction = 0.998 + Math.random() * 0.002;
+
+ const colors = [
+ '#ff4444', '#44ff44', '#4444ff', '#ffff44', '#ff44ff', '#44ffff', '#ffffff',
+ '#ff8844', '#88ff44', '#4488ff', '#ff4488', '#88ff88', '#8888ff', '#ffaa44'
+ ];
+ const color = colors[Math.floor(Math.random() * colors.length)];
+ const size = Math.random() * 6 + 3;
+
+ entity.addComponent(new Position(x, y));
+ entity.addComponent(new Velocity(dx, dy));
+ entity.addComponent(new Physics(mass, bounce, friction));
+ entity.addComponent(new Renderable(color, size, 'circle'));
+ entity.addComponent(new Lifetime(5 + Math.random() * 10));
+ }
+
+ private spawnParticleExplosion(centerX: number, centerY: number, count: number = 50): void {
+ for (let i = 0; i < count; i++) {
+ const entity = this.scene.createEntity(`Explosion_${Date.now()}_${i}`);
+
+ const angle = (Math.PI * 2 * i) / count + (Math.random() - 0.5) * 0.5;
+ const distance = Math.random() * 30;
+ const x = centerX + Math.cos(angle) * distance;
+ const y = centerY + Math.sin(angle) * distance;
+
+ const speed = 100 + Math.random() * 150;
+ const dx = Math.cos(angle) * speed;
+ const dy = Math.sin(angle) * speed;
+ const mass = 0.5 + Math.random() * 1;
+ const bounce = 0.8 + Math.random() * 0.2;
+
+ const colors = ['#ffaa00', '#ff6600', '#ff0066', '#ff3300', '#ffff00'];
+ const color = colors[Math.floor(Math.random() * colors.length)];
+ const size = Math.random() * 4 + 2;
+
+ entity.addComponent(new Position(x, y));
+ entity.addComponent(new Velocity(dx, dy));
+ entity.addComponent(new Physics(mass, bounce, 0.999));
+ entity.addComponent(new Renderable(color, size, 'circle'));
+ entity.addComponent(new Lifetime(2 + Math.random() * 3));
+ }
+ }
+
+ private clearAllEntities(): void {
+ const entities = [...this.scene.entities.buffer];
+ for (const entity of entities) {
+ entity.destroy();
+ }
+ }
+
+ private toggleWorker(): boolean {
+ const workerInfo = this.physicsSystem.getWorkerInfo();
+ const newWorkerEnabled = !workerInfo.enabled;
+
+ // 保存当前物理配置
+ const currentConfig = this.physicsSystem.getPhysicsConfig();
+
+ this.scene.removeSystem(this.physicsSystem);
+ this.physicsSystem = new PhysicsWorkerSystem(newWorkerEnabled, this.canvas.width, this.canvas.height);
+ this.physicsSystem.updateOrder = 1;
+
+ // 恢复物理配置
+ this.physicsSystem.updatePhysicsConfig(currentConfig);
+
+ this.scene.addSystem(this.physicsSystem);
+
+ return newWorkerEnabled;
+ }
+
+ private updateWorkerConfig(config: { gravity?: number; friction?: number }): void {
+ if (config.gravity !== undefined || config.friction !== undefined) {
+ const physicsConfig = this.physicsSystem.getPhysicsConfig();
+ this.physicsSystem.updatePhysicsConfig({
+ gravity: config.gravity ?? physicsConfig.gravity,
+ groundFriction: config.friction ?? physicsConfig.groundFriction
+ });
+ }
+ }
+}
diff --git a/examples/core-demos/src/demos/index.ts b/examples/core-demos/src/demos/index.ts
new file mode 100644
index 00000000..004fc191
--- /dev/null
+++ b/examples/core-demos/src/demos/index.ts
@@ -0,0 +1,12 @@
+import { DemoBase } from './DemoBase';
+import { SerializationDemo } from './SerializationDemo';
+import { WorkerSystemDemo } from './WorkerSystemDemo';
+
+export { DemoBase, SerializationDemo, WorkerSystemDemo };
+
+// Demo注册表
+export const DEMO_REGISTRY: typeof DemoBase[] = [
+ SerializationDemo,
+ WorkerSystemDemo,
+ // 更多demos可以在这里添加
+];
diff --git a/examples/core-demos/src/main.ts b/examples/core-demos/src/main.ts
new file mode 100644
index 00000000..dfac37ea
--- /dev/null
+++ b/examples/core-demos/src/main.ts
@@ -0,0 +1,171 @@
+import { DEMO_REGISTRY, DemoBase } from './demos';
+import { Core } from '@esengine/ecs-framework';
+
+class DemoManager {
+ private demos: Map = new Map();
+ private currentDemo: DemoBase | null = null;
+ private canvas: HTMLCanvasElement;
+ private controlPanel: HTMLElement;
+
+ constructor() {
+ // 初始化ECS Core
+ Core.create({
+ debug: true,
+ enableEntitySystems: true
+ });
+
+ this.canvas = document.getElementById('demoCanvas') as HTMLCanvasElement;
+ this.controlPanel = document.getElementById('controlPanel') as HTMLElement;
+
+ // 注册所有demos
+ for (const DemoClass of DEMO_REGISTRY) {
+ const tempInstance = new DemoClass(this.canvas, this.controlPanel);
+ const info = tempInstance.getInfo();
+ this.demos.set(info.id, DemoClass);
+ tempInstance.destroy();
+ }
+
+ // 渲染demo列表
+ this.renderDemoList();
+
+ // 自动加载第一个demo
+ const firstDemo = DEMO_REGISTRY[0];
+ if (firstDemo) {
+ const tempInstance = new firstDemo(this.canvas, this.controlPanel);
+ const info = tempInstance.getInfo();
+ tempInstance.destroy();
+ this.loadDemo(info.id);
+ }
+ }
+
+ private renderDemoList() {
+ const demoList = document.getElementById('demoList')!;
+
+ // 按分类组织demos
+ const categories = new Map();
+
+ for (const DemoClass of DEMO_REGISTRY) {
+ const tempInstance = new DemoClass(this.canvas, this.controlPanel);
+ const info = tempInstance.getInfo();
+ tempInstance.destroy();
+
+ if (!categories.has(info.category)) {
+ categories.set(info.category, []);
+ }
+ categories.get(info.category)!.push(DemoClass);
+ }
+
+ // 渲染分类和demos
+ let html = '';
+ for (const [category, demoClasses] of categories) {
+ html += ``;
+ html += `
${category}
`;
+
+ for (const DemoClass of demoClasses) {
+ const tempInstance = new DemoClass(this.canvas, this.controlPanel);
+ const info = tempInstance.getInfo();
+ tempInstance.destroy();
+
+ html += `
+
+
${info.icon}
+
+
${info.name}
+
${info.description}
+
+
+ `;
+ }
+
+ html += `
`;
+ }
+
+ demoList.innerHTML = html;
+
+ // 绑定点击事件
+ demoList.querySelectorAll('.demo-item').forEach(item => {
+ item.addEventListener('click', () => {
+ const demoId = item.getAttribute('data-demo-id')!;
+ this.loadDemo(demoId);
+ });
+ });
+ }
+
+ private loadDemo(demoId: string) {
+ // 停止并销毁当前demo
+ if (this.currentDemo) {
+ this.currentDemo.destroy();
+ this.currentDemo = null;
+ }
+
+ // 显示加载动画
+ const loading = document.getElementById('loading')!;
+ loading.style.display = 'block';
+
+ // 延迟加载,给用户反馈
+ setTimeout(() => {
+ const DemoClass = this.demos.get(demoId);
+ if (!DemoClass) {
+ console.error(`Demo ${demoId} not found`);
+ loading.style.display = 'none';
+ return;
+ }
+
+ try {
+ // 创建新demo
+ this.currentDemo = new DemoClass(this.canvas, this.controlPanel);
+ const info = this.currentDemo.getInfo();
+
+ // 更新页面标题和描述
+ document.getElementById('demoTitle')!.textContent = info.name;
+ document.getElementById('demoDescription')!.textContent = info.description;
+
+ // 设置demo
+ this.currentDemo.setup();
+
+ // 显示控制面板
+ this.controlPanel.style.display = 'block';
+
+ // 启动demo
+ this.currentDemo.start();
+
+ // 更新菜单选中状态
+ document.querySelectorAll('.demo-item').forEach(item => {
+ item.classList.remove('active');
+ if (item.getAttribute('data-demo-id') === demoId) {
+ item.classList.add('active');
+ }
+ });
+
+ loading.style.display = 'none';
+
+ console.log(`✅ Demo "${info.name}" loaded successfully`);
+ } catch (error) {
+ console.error(`Failed to load demo ${demoId}:`, error);
+ loading.style.display = 'none';
+ this.showError('加载演示失败:' + (error as Error).message);
+ }
+ }, 300);
+ }
+
+ private showError(message: string) {
+ const toast = document.getElementById('toast')!;
+ const toastMessage = document.getElementById('toastMessage')!;
+ const toastIcon = toast.querySelector('.toast-icon')!;
+
+ toastIcon.textContent = '❌';
+ toastMessage.textContent = message;
+ toast.style.borderColor = '#f5576c';
+
+ toast.classList.add('show');
+ setTimeout(() => {
+ toast.classList.remove('show');
+ toast.style.borderColor = '#667eea';
+ }, 3000);
+ }
+}
+
+// 初始化
+window.addEventListener('DOMContentLoaded', () => {
+ new DemoManager();
+});
diff --git a/examples/worker-system-demo/src/platform/BrowserAdapter.ts b/examples/core-demos/src/platform/BrowserAdapter.ts
similarity index 100%
rename from examples/worker-system-demo/src/platform/BrowserAdapter.ts
rename to examples/core-demos/src/platform/BrowserAdapter.ts
diff --git a/examples/worker-system-demo/tsconfig.json b/examples/core-demos/tsconfig.json
similarity index 71%
rename from examples/worker-system-demo/tsconfig.json
rename to examples/core-demos/tsconfig.json
index 23255742..79486441 100644
--- a/examples/worker-system-demo/tsconfig.json
+++ b/examples/core-demos/tsconfig.json
@@ -1,8 +1,9 @@
{
"compilerOptions": {
"target": "ES2020",
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "useDefineForClassFields": true,
"module": "ESNext",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
@@ -14,12 +15,7 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"experimentalDecorators": true,
- "emitDecoratorMetadata": true,
- "baseUrl": ".",
- "paths": {
- "@/*": ["src/*"]
- }
+ "emitDecoratorMetadata": true
},
- "include": ["src/**/*"],
- "references": [{ "path": "./tsconfig.node.json" }]
-}
\ No newline at end of file
+ "include": ["src"]
+}
diff --git a/examples/core-demos/vite.config.ts b/examples/core-demos/vite.config.ts
new file mode 100644
index 00000000..4a5c4ff0
--- /dev/null
+++ b/examples/core-demos/vite.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ server: {
+ port: 3003,
+ headers: {
+ 'Cross-Origin-Opener-Policy': 'same-origin',
+ 'Cross-Origin-Embedder-Policy': 'require-corp'
+ }
+ },
+ build: {
+ target: 'es2020',
+ outDir: 'dist'
+ }
+});
diff --git a/examples/worker-system-demo/index.html b/examples/worker-system-demo/index.html
deleted file mode 100644
index de3b9004..00000000
--- a/examples/worker-system-demo/index.html
+++ /dev/null
@@ -1,177 +0,0 @@
-
-
-
-
-
- ECS Framework Worker System Demo
-
-
-
-
-
ECS Framework Worker System 演示
-
-
-
-
-
-
-
-
-
-
- 1000
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
性能统计
-
FPS: 0
-
实体数量: 0
-
Worker状态: 未启用
-
Worker负载: N/A
-
运行模式: 同步模式
-
物理系统耗时: 0ms
-
渲染系统耗时: 0ms
-
总帧时间: 0ms
-
内存使用: 0MB
-
-
-
-
-
-
\ No newline at end of file
diff --git a/examples/worker-system-demo/src/GameScene.ts b/examples/worker-system-demo/src/GameScene.ts
deleted file mode 100644
index 12bdcbf9..00000000
--- a/examples/worker-system-demo/src/GameScene.ts
+++ /dev/null
@@ -1,187 +0,0 @@
-import { Scene } from '@esengine/ecs-framework';
-import { PhysicsWorkerSystem, RenderSystem, LifetimeSystem } from './systems';
-import { Position, Velocity, Physics, Renderable, Lifetime } from './components';
-
-export class GameScene extends Scene {
- private canvas: HTMLCanvasElement;
- private physicsSystem!: PhysicsWorkerSystem;
- private renderSystem!: RenderSystem;
- private lifetimeSystem!: LifetimeSystem;
-
- constructor(canvas: HTMLCanvasElement) {
- super();
- this.canvas = canvas;
- }
-
- override initialize(): void {
- this.name = "WorkerDemoScene";
-
- // 创建系统
- this.physicsSystem = new PhysicsWorkerSystem(true); // 默认启用Worker
- this.renderSystem = new RenderSystem(this.canvas);
- this.lifetimeSystem = new LifetimeSystem();
-
- // 设置系统执行顺序
- this.physicsSystem.updateOrder = 1;
- this.lifetimeSystem.updateOrder = 2;
- this.renderSystem.updateOrder = 3;
-
- // 添加系统到场景
- this.addSystem(this.physicsSystem);
- this.addSystem(this.lifetimeSystem);
- this.addSystem(this.renderSystem);
- }
-
- override onStart(): void {
- console.log("Worker演示场景已启动");
- this.spawnInitialEntities();
- }
-
- override unload(): void {
- console.log("Worker演示场景已卸载");
- }
-
- /**
- * 生成初始实体
- */
- public spawnInitialEntities(count: number = 1000): void {
- this.clearAllEntities();
-
- for (let i = 0; i < count; i++) {
- this.createParticle();
- }
- }
-
- /**
- * 创建一个粒子实体
- */
- public createParticle(): void {
- const entity = this.createEntity(`Particle_${Date.now()}_${Math.random()}`);
-
- // 随机位置
- const x = Math.random() * (this.canvas.width - 20) + 10;
- const y = Math.random() * (this.canvas.height - 20) + 10;
-
- // 随机速度
- const dx = (Math.random() - 0.5) * 200;
- const dy = (Math.random() - 0.5) * 200;
-
- const mass = Math.random() * 3 + 2;
- const bounce = 0.85 + Math.random() * 0.15;
- const friction = 0.998 + Math.random() * 0.002;
-
- // 随机颜色和大小 - 增加更多颜色提高多样性
- const colors = [
- '#ff4444', '#44ff44', '#4444ff', '#ffff44', '#ff44ff', '#44ffff', '#ffffff',
- '#ff8844', '#88ff44', '#4488ff', '#ff4488', '#88ff88', '#8888ff', '#ffaa44',
- '#aaff44', '#44aaff', '#ff44aa', '#aa44ff', '#44ffaa', '#cccccc'
- ];
- const color = colors[Math.floor(Math.random() * colors.length)];
- const size = Math.random() * 6 + 3;
-
- // 添加组件
- entity.addComponent(new Position(x, y));
- entity.addComponent(new Velocity(dx, dy));
- entity.addComponent(new Physics(mass, bounce, friction));
- entity.addComponent(new Renderable(color, size, 'circle'));
- entity.addComponent(new Lifetime(5 + Math.random() * 10)); // 5-15秒生命周期
- }
-
- /**
- * 生成粒子爆发效果
- */
- public spawnParticleExplosion(centerX: number, centerY: number, count: number = 50): void {
- for (let i = 0; i < count; i++) {
- const entity = this.createEntity(`Explosion_${Date.now()}_${i}`);
-
- // 在中心点周围随机分布
- const angle = (Math.PI * 2 * i) / count + (Math.random() - 0.5) * 0.5;
- const distance = Math.random() * 30;
- const x = centerX + Math.cos(angle) * distance;
- const y = centerY + Math.sin(angle) * distance;
-
- // 爆炸速度
- const speed = 100 + Math.random() * 150;
- const dx = Math.cos(angle) * speed;
- const dy = Math.sin(angle) * speed;
-
- const mass = 0.5 + Math.random() * 1;
- const bounce = 0.8 + Math.random() * 0.2;
-
- // 亮色
- const colors = ['#ffaa00', '#ff6600', '#ff0066', '#ff3300', '#ffff00'];
- const color = colors[Math.floor(Math.random() * colors.length)];
- const size = Math.random() * 4 + 2;
-
- entity.addComponent(new Position(x, y));
- entity.addComponent(new Velocity(dx, dy));
- entity.addComponent(new Physics(mass, bounce, 0.999));
- entity.addComponent(new Renderable(color, size, 'circle'));
- entity.addComponent(new Lifetime(2 + Math.random() * 3)); // 短生命周期
- }
- }
-
- /**
- * 清空所有实体
- */
- public clearAllEntities(): void {
- const entities = [...this.entities.buffer]; // 复制数组避免修改原数组
- for (const entity of entities) {
- entity.destroy();
- }
- }
-
- /**
- * 切换Worker启用状态
- */
- public toggleWorker(): boolean {
- const workerInfo = this.physicsSystem.getWorkerInfo();
- const newWorkerEnabled = !workerInfo.enabled;
-
- // 重新创建物理系统
- this.removeSystem(this.physicsSystem);
- this.physicsSystem = new PhysicsWorkerSystem(newWorkerEnabled);
- this.physicsSystem.updateOrder = 1;
- this.addSystem(this.physicsSystem);
-
- return newWorkerEnabled;
- }
-
- /**
- * 更新Worker配置
- */
- public updateWorkerConfig(config: { gravity?: number; friction?: number }): void {
- if (config.gravity !== undefined || config.friction !== undefined) {
- const physicsConfig = this.physicsSystem.getPhysicsConfig();
- this.physicsSystem.updatePhysicsConfig({
- gravity: config.gravity ?? physicsConfig.gravity,
- groundFriction: config.friction ?? physicsConfig.groundFriction
- });
- }
- }
-
- /**
- * 切换 SharedArrayBuffer 状态
- */
- public toggleSharedArrayBuffer(): void {
- this.physicsSystem.disableSharedArrayBuffer();
- }
-
- /**
- * 获取物理系统状态
- */
- public getPhysicsSystemStatus() {
- return this.physicsSystem.getCurrentStatus();
- }
-
- /**
- * 获取系统信息
- */
- public getSystemInfo() {
- return {
- physics: this.physicsSystem.getWorkerInfo(),
- entityCount: this.entities.count,
- physicsConfig: this.physicsSystem.getPhysicsConfig()
- };
- }
-}
\ No newline at end of file
diff --git a/examples/worker-system-demo/src/components/index.ts b/examples/worker-system-demo/src/components/index.ts
deleted file mode 100644
index b128bc23..00000000
--- a/examples/worker-system-demo/src/components/index.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import { Component, ECSComponent } from '@esengine/ecs-framework';
-
-// 位置组件
-@ECSComponent('Position')
-export class Position extends Component {
- x: number = 0;
- y: number = 0;
-
- constructor(x: number = 0, y: number = 0) {
- super();
- this.x = x;
- this.y = y;
- }
-
- set(x: number, y: number): void {
- this.x = x;
- this.y = y;
- }
-}
-
-// 速度组件
-@ECSComponent('Velocity')
-export class Velocity extends Component {
- dx: number = 0;
- dy: number = 0;
-
- constructor(dx: number = 0, dy: number = 0) {
- super();
- this.dx = dx;
- this.dy = dy;
- }
-
- set(dx: number, dy: number): void {
- this.dx = dx;
- this.dy = dy;
- }
-
- scale(factor: number): void {
- this.dx *= factor;
- this.dy *= factor;
- }
-}
-
-// 物理组件
-@ECSComponent('Physics')
-export class Physics extends Component {
- mass: number = 1;
- bounce: number = 0.8;
- friction: number = 0.95;
-
- constructor(mass: number = 1, bounce: number = 0.8, friction: number = 0.95) {
- super();
- this.mass = mass;
- this.bounce = bounce;
- this.friction = friction;
- }
-}
-
-// 渲染组件
-@ECSComponent('Renderable')
-export class Renderable extends Component {
- color: string = '#ffffff';
- size: number = 5;
- shape: 'circle' | 'square' = 'circle';
-
- constructor(color: string = '#ffffff', size: number = 5, shape: 'circle' | 'square' = 'circle') {
- super();
- this.color = color;
- this.size = size;
- this.shape = shape;
- }
-}
-
-// 生命周期组件
-@ECSComponent('Lifetime')
-export class Lifetime extends Component {
- maxAge: number = 5;
- currentAge: number = 0;
-
- constructor(maxAge: number = 5) {
- super();
- this.maxAge = maxAge;
- this.currentAge = 0;
- }
-
- isDead(): boolean {
- return this.currentAge >= this.maxAge;
- }
-}
\ No newline at end of file
diff --git a/examples/worker-system-demo/src/main.ts b/examples/worker-system-demo/src/main.ts
deleted file mode 100644
index 60dcff14..00000000
--- a/examples/worker-system-demo/src/main.ts
+++ /dev/null
@@ -1,376 +0,0 @@
-import { Core, PlatformManager } from '@esengine/ecs-framework';
-import { GameScene } from './GameScene';
-import { BrowserAdapter } from './platform/BrowserAdapter';
-
-// 性能监控
-interface PerformanceStats {
- fps: number;
- frameTime: number;
- physicsTime: number;
- renderTime: number;
- memoryUsage: number;
-}
-
-class WorkerDemo {
- private gameScene: GameScene;
- private canvas: HTMLCanvasElement;
- private isRunning = false;
- private lastTime = 0;
- private frameCount = 0;
- private fpsUpdateTime = 0;
- private currentFPS = 0;
- private lastWorkerStatusUpdate = 0;
-
- // UI元素
- private elements: { [key: string]: HTMLElement } = {};
-
- constructor() {
- // 注册浏览器适配器
- const browserAdapter = new BrowserAdapter();
- PlatformManager.getInstance().registerAdapter(browserAdapter);
-
- // 获取canvas
- this.canvas = document.getElementById('gameCanvas') as HTMLCanvasElement;
- if (!this.canvas) {
- throw new Error('Canvas element not found');
- }
-
- // 初始化UI元素引用
- this.initializeUIElements();
-
- // 初始化ECS Core
- Core.create({
- debug: true,
- enableEntitySystems: true
- });
-
- // 创建游戏场景
- this.gameScene = new GameScene(this.canvas);
-
- // 设置场景
- Core.setScene(this.gameScene);
-
- // 绑定事件
- this.bindEvents();
-
- // 启动演示
- this.start();
- }
-
- private initializeUIElements(): void {
- const elementIds = [
- 'entityCount', 'entityCountValue', 'toggleWorker', 'toggleSAB',
- 'gravity', 'gravityValue', 'friction', 'frictionValue', 'spawnParticles',
- 'clearEntities', 'resetDemo', 'fps', 'entityCountStat', 'workerStatus', 'workerLoad',
- 'physicsTime', 'renderTime', 'frameTime', 'memoryUsage', 'sabStatus'
- ];
-
- for (const id of elementIds) {
- const element = document.getElementById(id);
- if (element) {
- this.elements[id] = element;
- } else {
- console.warn(`Element with id '${id}' not found`);
- }
- }
- }
-
- private bindEvents(): void {
- // 实体数量滑块
- if (this.elements.entityCount && this.elements.entityCountValue) {
- const slider = this.elements.entityCount as HTMLInputElement;
- slider.addEventListener('input', () => {
- this.elements.entityCountValue.textContent = slider.value;
- });
-
- slider.addEventListener('change', () => {
- const count = parseInt(slider.value);
- this.gameScene.spawnInitialEntities(count);
- });
- }
-
- // Worker切换按钮
- if (this.elements.toggleWorker) {
- this.elements.toggleWorker.addEventListener('click', () => {
- const workerEnabled = this.gameScene.toggleWorker();
- this.elements.toggleWorker.textContent = workerEnabled ? '禁用 Worker' : '启用 Worker';
- this.updateWorkerStatus();
- });
- }
-
- // SharedArrayBuffer切换按钮
- if (this.elements.toggleSAB) {
- this.elements.toggleSAB.addEventListener('click', () => {
- this.gameScene.toggleSharedArrayBuffer();
- this.updateWorkerStatus();
- });
- }
-
-
- // 重力滑块
- if (this.elements.gravity && this.elements.gravityValue) {
- const slider = this.elements.gravity as HTMLInputElement;
- slider.addEventListener('input', () => {
- this.elements.gravityValue.textContent = slider.value;
- });
-
- slider.addEventListener('change', () => {
- const gravity = parseInt(slider.value);
- this.gameScene.updateWorkerConfig({ gravity });
- });
- }
-
- // 摩擦力滑块
- if (this.elements.friction && this.elements.frictionValue) {
- const slider = this.elements.friction as HTMLInputElement;
- slider.addEventListener('input', () => {
- const value = parseInt(slider.value);
- this.elements.frictionValue.textContent = `${value}%`;
- });
-
- slider.addEventListener('change', () => {
- const friction = parseInt(slider.value) / 100;
- this.gameScene.updateWorkerConfig({ friction });
- });
- }
-
- // 生成粒子按钮
- if (this.elements.spawnParticles) {
- this.elements.spawnParticles.addEventListener('click', () => {
- const centerX = this.canvas.width / 2;
- const centerY = this.canvas.height / 2;
- this.gameScene.spawnParticleExplosion(centerX, centerY, 100);
- });
- }
-
- // 清空实体按钮
- if (this.elements.clearEntities) {
- this.elements.clearEntities.addEventListener('click', () => {
- this.gameScene.clearAllEntities();
- });
- }
-
- // 重置演示按钮
- if (this.elements.resetDemo) {
- this.elements.resetDemo.addEventListener('click', () => {
- this.resetDemo();
- });
- }
-
- // Canvas点击事件 - 在点击位置生成粒子爆发
- this.canvas.addEventListener('click', (event) => {
- const rect = this.canvas.getBoundingClientRect();
- const x = event.clientX - rect.left;
- const y = event.clientY - rect.top;
- this.gameScene.spawnParticleExplosion(x, y, 30);
- });
- }
-
- private start(): void {
- this.isRunning = true;
- this.lastTime = performance.now();
- this.gameLoop();
- console.log('Worker演示已启动');
- }
-
- private gameLoop = (): void => {
- if (!this.isRunning) return;
-
- const currentTime = performance.now();
- const deltaTime = (currentTime - this.lastTime) / 1000; // 转换为秒
- this.lastTime = currentTime;
-
- // 更新ECS框架
- const frameStartTime = performance.now();
- Core.update(deltaTime);
- const frameEndTime = performance.now();
-
- // 更新性能统计
- this.updatePerformanceStats({
- fps: this.currentFPS,
- frameTime: frameEndTime - frameStartTime,
- physicsTime: (window as any).physicsExecutionTime || 0,
- renderTime: (window as any).renderExecutionTime || 0,
- memoryUsage: this.getMemoryUsage()
- });
-
- // 更新FPS计算
- this.frameCount++;
- if (currentTime - this.fpsUpdateTime >= 1000) {
- this.currentFPS = this.frameCount;
- this.frameCount = 0;
- this.fpsUpdateTime = currentTime;
- }
-
- // 更新UI
- this.updateUI();
-
- // 继续循环
- requestAnimationFrame(this.gameLoop);
- };
-
- private updatePerformanceStats(stats: PerformanceStats): void {
- if (this.elements.fps) {
- this.elements.fps.textContent = stats.fps.toString();
- this.elements.fps.className = stats.fps >= 55 ? 'performance-high' :
- stats.fps >= 30 ? 'performance-medium' : 'performance-low';
- }
-
- if (this.elements.frameTime) {
- this.elements.frameTime.textContent = stats.frameTime.toFixed(2);
- this.elements.frameTime.className = stats.frameTime <= 16 ? 'performance-high' :
- stats.frameTime <= 33 ? 'performance-medium' : 'performance-low';
- }
-
- if (this.elements.physicsTime) {
- this.elements.physicsTime.textContent = stats.physicsTime.toFixed(2);
- this.elements.physicsTime.className = stats.physicsTime <= 8 ? 'performance-high' :
- stats.physicsTime <= 16 ? 'performance-medium' : 'performance-low';
- }
-
- if (this.elements.renderTime) {
- this.elements.renderTime.textContent = stats.renderTime.toFixed(2);
- this.elements.renderTime.className = stats.renderTime <= 8 ? 'performance-high' :
- stats.renderTime <= 16 ? 'performance-medium' : 'performance-low';
- }
-
- if (this.elements.memoryUsage) {
- this.elements.memoryUsage.textContent = stats.memoryUsage.toFixed(1);
- }
- }
-
- private updateUI(): void {
- const currentTime = performance.now();
- const systemInfo = this.gameScene.getSystemInfo();
-
- // 更新实体数量(每帧更新)
- if (this.elements.entityCountStat) {
- this.elements.entityCountStat.textContent = systemInfo.entityCount.toString();
- }
-
- // 更新Worker状态(每500ms更新一次即可)
- if (currentTime - this.lastWorkerStatusUpdate >= 500) {
- this.updateWorkerStatus();
- this.lastWorkerStatusUpdate = currentTime;
- }
-
- // 更新全局Worker信息供其他系统使用
- (window as any).workerInfo = systemInfo.physics;
- }
-
- private updateWorkerStatus(): void {
- const systemInfo = this.gameScene.getSystemInfo();
- const workerInfo = systemInfo.physics;
- const entityCount = systemInfo.entityCount;
- const status = this.gameScene.getPhysicsSystemStatus();
-
- if (this.elements.workerStatus) {
- if (workerInfo.enabled) {
- this.elements.workerStatus.textContent = `启用 (${workerInfo.workerCount} Workers)`;
- this.elements.workerStatus.className = 'worker-enabled';
- } else {
- this.elements.workerStatus.textContent = '禁用';
- this.elements.workerStatus.className = 'worker-disabled';
- }
- }
-
- if (this.elements.workerLoad) {
- if (workerInfo.enabled && entityCount > 0) {
- const entitiesPerWorker = Math.ceil(entityCount / workerInfo.workerCount);
- this.elements.workerLoad.textContent = `${entitiesPerWorker}/Worker (共${workerInfo.workerCount}个)`;
- } else {
- this.elements.workerLoad.textContent = 'N/A';
- }
- }
-
- // 更新 SharedArrayBuffer 状态
- if (this.elements.sabStatus) {
- const modeNames = {
- 'shared-buffer': 'SharedArrayBuffer模式',
- 'single-worker': '单Worker模式',
- 'multi-worker': '多Worker模式',
- 'sync': '同步模式'
- };
-
- this.elements.sabStatus.textContent = modeNames[status.mode] || status.mode;
- this.elements.sabStatus.className = status.mode === 'shared-buffer' ? 'worker-enabled' : 'worker-disabled';
- }
-
- // 更新 SharedArrayBuffer 按钮文本
- if (this.elements.toggleSAB) {
- if (status.sharedArrayBufferEnabled) {
- this.elements.toggleSAB.textContent = '禁用 SharedArrayBuffer';
- } else {
- this.elements.toggleSAB.textContent = '启用 SharedArrayBuffer';
- this.elements.toggleSAB.setAttribute('disabled', 'true'); // SAB 一旦禁用就无法重新启用
- }
- }
- }
-
- private getMemoryUsage(): number {
- if ('memory' in performance) {
- const memory = (performance as any).memory;
- return memory.usedJSHeapSize / (1024 * 1024); // MB
- }
- return 0;
- }
-
- private resetDemo(): void {
- // 重置所有控件到默认值
- if (this.elements.entityCount) {
- (this.elements.entityCount as HTMLInputElement).value = '1000';
- this.elements.entityCountValue.textContent = '1000';
- }
-
-
- if (this.elements.gravity) {
- (this.elements.gravity as HTMLInputElement).value = '100';
- this.elements.gravityValue.textContent = '100';
- }
-
- if (this.elements.friction) {
- (this.elements.friction as HTMLInputElement).value = '95';
- this.elements.frictionValue.textContent = '95%';
- }
-
- // 确保Worker被启用
- const workerInfo = this.gameScene.getSystemInfo().physics;
- if (!workerInfo.enabled) {
- this.gameScene.toggleWorker(); // 只有在禁用时才切换
- }
- if (this.elements.toggleWorker) {
- this.elements.toggleWorker.textContent = '禁用 Worker';
- }
-
- // 重新生成实体
- this.gameScene.spawnInitialEntities(1000);
-
- // 重置配置
- this.gameScene.updateWorkerConfig({
- gravity: 100,
- friction: 0.95
- });
-
- console.log('演示已重置');
- }
-
- public stop(): void {
- this.isRunning = false;
- }
-}
-
-// 启动演示
-document.addEventListener('DOMContentLoaded', () => {
- try {
- new WorkerDemo();
- } catch (error) {
- console.error('启动演示失败:', error);
- document.body.innerHTML = `
-
-
启动失败
-
错误: ${error}
-
请确保浏览器支持Web Workers和Canvas API
-
- `;
- }
-});
\ No newline at end of file
diff --git a/examples/worker-system-demo/src/systems/LifetimeSystem.ts b/examples/worker-system-demo/src/systems/LifetimeSystem.ts
deleted file mode 100644
index 399387b2..00000000
--- a/examples/worker-system-demo/src/systems/LifetimeSystem.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { EntitySystem, Matcher, Entity, ECSSystem, Time } from '@esengine/ecs-framework';
-import { Lifetime } from '../components';
-
-@ECSSystem('LifetimeSystem')
-export class LifetimeSystem extends EntitySystem {
- constructor() {
- super(Matcher.empty().all(Lifetime));
- }
-
- protected override process(entities: readonly Entity[]): void {
- const entitiesToRemove: Entity[] = [];
-
- for (const entity of entities) {
- const lifetime = entity.getComponent(Lifetime)!;
-
- // 更新年龄
- lifetime.currentAge += Time.deltaTime;
-
- // 检查是否需要销毁
- if (lifetime.isDead()) {
- entitiesToRemove.push(entity);
- }
- }
-
- // 销毁过期的实体
- for (const entity of entitiesToRemove) {
- entity.destroy();
- }
- }
-}
\ No newline at end of file
diff --git a/examples/worker-system-demo/src/systems/PhysicsWorkerSystem.ts b/examples/worker-system-demo/src/systems/PhysicsWorkerSystem.ts
deleted file mode 100644
index 3070f750..00000000
--- a/examples/worker-system-demo/src/systems/PhysicsWorkerSystem.ts
+++ /dev/null
@@ -1,513 +0,0 @@
-import { WorkerEntitySystem, Matcher, Entity, ECSSystem, SharedArrayBufferProcessFunction } from '@esengine/ecs-framework';
-import { Position, Velocity, Physics, Renderable } from '../components';
-
-interface PhysicsEntityData {
- id: number;
- x: number;
- y: number;
- dx: number;
- dy: number;
- mass: number;
- bounce: number;
- friction: number;
- radius: number;
-}
-
-interface PhysicsConfig {
- gravity: number;
- canvasWidth: number;
- canvasHeight: number;
- groundFriction: number;
-}
-
-@ECSSystem('PhysicsWorkerSystem')
-export class PhysicsWorkerSystem extends WorkerEntitySystem {
- private physicsConfig: PhysicsConfig = {
- gravity: 100,
- canvasWidth: 800,
- canvasHeight: 600,
- groundFriction: 0.98 // 减少地面摩擦
- };
-
- constructor(enableWorker: boolean = true) {
- const defaultConfig = {
- gravity: 100,
- canvasWidth: 800,
- canvasHeight: 600,
- groundFriction: 0.98
- };
-
- // 检查 SharedArrayBuffer 是否可用
- const isSharedArrayBufferAvailable = typeof SharedArrayBuffer !== 'undefined' && self.crossOriginIsolated;
-
- super(
- Matcher.empty().all(Position, Velocity, Physics),
- {
- enableWorker,
- // 当 SharedArrayBuffer 可用时使用多 Worker,否则使用单 Worker 保证碰撞检测完整性
- workerCount: isSharedArrayBufferAvailable ? (navigator.hardwareConcurrency || 2) : 1,
- systemConfig: defaultConfig,
- useSharedArrayBuffer: true // 优先使用 SharedArrayBuffer
- }
- );
- }
-
- protected extractEntityData(entity: Entity): PhysicsEntityData {
- const position = entity.getComponent(Position)!;
- const velocity = entity.getComponent(Velocity)!;
- const physics = entity.getComponent(Physics)!;
- const renderable = entity.getComponent(Renderable)!;
-
- return {
- id: entity.id,
- x: position.x,
- y: position.y,
- dx: velocity.dx,
- dy: velocity.dy,
- mass: physics.mass,
- bounce: physics.bounce,
- friction: physics.friction,
- radius: renderable.size
- };
- }
-
- /**
- * Worker处理函数 - 纯函数,会被序列化到Worker中执行
- * 注意:这个函数内部不能访问外部变量,必须是纯函数
- * 非SharedArrayBuffer模式:每个Worker只能看到分配给它的实体批次
- * 这会导致跨批次的碰撞检测缺失,但单批次内的碰撞是正确的
- */
- protected workerProcess(
- entities: PhysicsEntityData[],
- deltaTime: number,
- systemConfig?: PhysicsConfig
- ): PhysicsEntityData[] {
- const config = systemConfig || {
- gravity: 100,
- canvasWidth: 800,
- canvasHeight: 600,
- groundFriction: 0.98
- };
-
- // 创建实体副本以避免修改原始数据
- const result = entities.map(e => ({ ...e }));
-
- // 应用重力和基础物理
- for (let i = 0; i < result.length; i++) {
- const entity = result[i];
-
- // 应用重力
- entity.dy += config.gravity * deltaTime;
-
- // 更新位置
- entity.x += entity.dx * deltaTime;
- entity.y += entity.dy * deltaTime;
-
- // 边界碰撞检测和处理
- if (entity.x <= entity.radius) {
- entity.x = entity.radius;
- entity.dx = -entity.dx * entity.bounce;
- } else if (entity.x >= config.canvasWidth - entity.radius) {
- entity.x = config.canvasWidth - entity.radius;
- entity.dx = -entity.dx * entity.bounce;
- }
-
- if (entity.y <= entity.radius) {
- entity.y = entity.radius;
- entity.dy = -entity.dy * entity.bounce;
- } else if (entity.y >= config.canvasHeight - entity.radius) {
- entity.y = config.canvasHeight - entity.radius;
- entity.dy = -entity.dy * entity.bounce;
-
- // 地面摩擦力
- entity.dx *= config.groundFriction;
- }
-
- // 空气阻力
- entity.dx *= entity.friction;
- entity.dy *= entity.friction;
- }
-
- // 小球间碰撞检测
- for (let i = 0; i < result.length; i++) {
- for (let j = i + 1; j < result.length; j++) {
- const ball1 = result[i];
- const ball2 = result[j];
-
- // 计算距离
- const dx = ball2.x - ball1.x;
- const dy = ball2.y - ball1.y;
- const distance = Math.sqrt(dx * dx + dy * dy);
- const minDistance = ball1.radius + ball2.radius;
-
- // 检测碰撞
- if (distance < minDistance && distance > 0) {
- // 碰撞法线
- const nx = dx / distance;
- const ny = dy / distance;
-
- // 分离小球以避免重叠
- const overlap = minDistance - distance;
- const separationX = nx * overlap * 0.5;
- const separationY = ny * overlap * 0.5;
-
- ball1.x -= separationX;
- ball1.y -= separationY;
- ball2.x += separationX;
- ball2.y += separationY;
-
- // 相对速度
- const relativeVelocityX = ball2.dx - ball1.dx;
- const relativeVelocityY = ball2.dy - ball1.dy;
-
- // 沿碰撞法线的速度分量
- const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
-
- // 如果速度分量为正,小球正在分离,不需要处理
- if (velocityAlongNormal > 0) continue;
-
- // 计算弹性系数(两球弹性的平均值)
- const restitution = (ball1.bounce + ball2.bounce) * 0.5;
-
- // 计算冲量大小
- const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/ball1.mass + 1/ball2.mass);
-
- // 应用冲量
- const impulseX = impulseScalar * nx;
- const impulseY = impulseScalar * ny;
-
- ball1.dx -= impulseX / ball1.mass;
- ball1.dy -= impulseY / ball1.mass;
- ball2.dx += impulseX / ball2.mass;
- ball2.dy += impulseY / ball2.mass;
-
- // 轻微的能量损失,保持活力
- const energyLoss = 0.98;
- ball1.dx *= energyLoss;
- ball1.dy *= energyLoss;
- ball2.dx *= energyLoss;
- ball2.dy *= energyLoss;
- }
- }
- }
-
- return result;
- }
-
- /**
- * 应用处理结果
- */
- protected applyResult(entity: Entity, result: PhysicsEntityData): void {
- // 检查实体是否仍然存在且有效
- if (!entity || !entity.enabled) {
- return;
- }
-
- const position = entity.getComponent(Position);
- const velocity = entity.getComponent(Velocity);
-
- // 检查组件是否仍然存在(实体可能在Worker处理期间被修改)
- if (!position || !velocity) {
- return;
- }
-
- position.set(result.x, result.y);
- velocity.set(result.dx, result.dy);
- }
-
- /**
- * 更新物理配置
- */
- public updatePhysicsConfig(newConfig: Partial): void {
- Object.assign(this.physicsConfig, newConfig);
- this.updateConfig({ systemConfig: this.physicsConfig });
- }
-
- /**
- * 获取物理配置
- */
- public getPhysicsConfig(): PhysicsConfig {
- return { ...this.physicsConfig };
- }
-
- /**
- * 禁用 SharedArrayBuffer(用于测试降级行为)
- */
- public disableSharedArrayBuffer(): void {
- console.log(`[${this.systemName}] Disabling SharedArrayBuffer for testing - falling back to single Worker mode`);
-
- // 使用正式的配置更新 API
- this.updateConfig({
- useSharedArrayBuffer: false
- });
- }
-
- /**
- * 获取当前运行状态
- */
- public getCurrentStatus(): {
- mode: 'shared-buffer' | 'single-worker' | 'multi-worker' | 'sync';
- sharedArrayBufferEnabled: boolean;
- workerCount: number;
- workerEnabled: boolean;
- } {
- const workerInfo = this.getWorkerInfo();
-
- let mode: 'shared-buffer' | 'single-worker' | 'multi-worker' | 'sync' = 'sync';
-
- if (workerInfo.enabled) {
- if (workerInfo.sharedArrayBufferEnabled && workerInfo.sharedArrayBufferSupported) {
- mode = 'shared-buffer';
- } else if (workerInfo.workerCount === 1) {
- mode = 'single-worker';
- } else {
- mode = 'multi-worker';
- }
- }
-
- return {
- mode,
- sharedArrayBufferEnabled: workerInfo.sharedArrayBufferEnabled,
- workerCount: workerInfo.workerCount,
- workerEnabled: workerInfo.enabled
- };
- }
-
- private startTime: number = 0;
-
-
- /**
- * 性能监控
- */
- protected override onEnd(): void {
- super.onEnd();
- const endTime = performance.now();
- const executionTime = endTime - this.startTime;
-
- // 发送性能数据到UI
- (window as any).physicsExecutionTime = executionTime;
- }
-
- /**
- * 获取实体数据大小
- */
- protected getDefaultEntityDataSize(): number {
- return 9; // id, x, y, dx, dy, mass, bounce, friction, radius
- }
-
- /**
- * 将实体数据写入SharedArrayBuffer
- */
- protected writeEntityToBuffer(entityData: PhysicsEntityData, offset: number): void {
- const sharedArray = (this as any).sharedFloatArray as Float32Array;
- if (!sharedArray) return;
-
- // 在第一个位置存储当前实体数量,用于Worker函数判断实际有效数据范围
- const currentEntityCount = Math.floor(offset / 9) + 1;
- sharedArray[0] = currentEntityCount; // 元数据:实际实体数量
-
- // 数据从索引9开始存储(第一个9个位置用作元数据区域)
- const dataOffset = offset + 9;
- sharedArray[dataOffset + 0] = entityData.id;
- sharedArray[dataOffset + 1] = entityData.x;
- sharedArray[dataOffset + 2] = entityData.y;
- sharedArray[dataOffset + 3] = entityData.dx;
- sharedArray[dataOffset + 4] = entityData.dy;
- sharedArray[dataOffset + 5] = entityData.mass;
- sharedArray[dataOffset + 6] = entityData.bounce;
- sharedArray[dataOffset + 7] = entityData.friction;
- sharedArray[dataOffset + 8] = entityData.radius;
- }
-
- /**
- * 性能监控开始
- */
- protected override onBegin(): void {
- super.onBegin();
- this.startTime = performance.now();
- }
-
- /**
- * 从SharedArrayBuffer读取实体数据
- */
- protected readEntityFromBuffer(offset: number): PhysicsEntityData | null {
- const sharedArray = (this as any).sharedFloatArray as Float32Array;
- if (!sharedArray) return null;
-
- // 数据从索引9开始存储(第一个9个位置用作元数据区域)
- const dataOffset = offset + 9;
- return {
- id: sharedArray[dataOffset + 0],
- x: sharedArray[dataOffset + 1],
- y: sharedArray[dataOffset + 2],
- dx: sharedArray[dataOffset + 3],
- dy: sharedArray[dataOffset + 4],
- mass: sharedArray[dataOffset + 5],
- bounce: sharedArray[dataOffset + 6],
- friction: sharedArray[dataOffset + 7],
- radius: sharedArray[dataOffset + 8]
- };
- }
-
- /**
- * SharedArrayBuffer处理函数
- */
- protected getSharedArrayBufferProcessFunction(): SharedArrayBufferProcessFunction {
- return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, systemConfig?: any) {
- const config = systemConfig || {
- gravity: 100,
- canvasWidth: 800,
- canvasHeight: 600,
- groundFriction: 0.98
- };
-
- // 读取实际实体数量(存储在第一个位置)
- const actualEntityCount = sharedFloatArray[0];
-
- // 基础物理更新
- for (let i = startIndex; i < endIndex && i < actualEntityCount; i++) {
- const offset = i * 9 + 9; // 数据从索引9开始,加上元数据偏移
-
- // 读取实体数据
- const id = sharedFloatArray[offset + 0];
- if (id === 0) continue; // 跳过无效实体
-
- let x = sharedFloatArray[offset + 1];
- let y = sharedFloatArray[offset + 2];
- let dx = sharedFloatArray[offset + 3];
- let dy = sharedFloatArray[offset + 4];
- // const mass = sharedFloatArray[offset + 5]; // 未使用
- const bounce = sharedFloatArray[offset + 6];
- const friction = sharedFloatArray[offset + 7];
- const radius = sharedFloatArray[offset + 8];
-
- // 应用重力
- dy += config.gravity * deltaTime;
-
- // 更新位置
- x += dx * deltaTime;
- y += dy * deltaTime;
-
- // 边界碰撞检测和处理
- if (x <= radius) {
- x = radius;
- dx = -dx * bounce;
- } else if (x >= config.canvasWidth - radius) {
- x = config.canvasWidth - radius;
- dx = -dx * bounce;
- }
-
- if (y <= radius) {
- y = radius;
- dy = -dy * bounce;
- } else if (y >= config.canvasHeight - radius) {
- y = config.canvasHeight - radius;
- dy = -dy * bounce;
-
- // 地面摩擦力
- dx *= config.groundFriction;
- }
-
- // 空气阻力
- dx *= friction;
- dy *= friction;
-
- // 写回数据
- sharedFloatArray[offset + 1] = x;
- sharedFloatArray[offset + 2] = y;
- sharedFloatArray[offset + 3] = dx;
- sharedFloatArray[offset + 4] = dy;
- }
-
- // 小球间碰撞检测
- for (let i = startIndex; i < endIndex && i < actualEntityCount; i++) {
- const offset1 = i * 9 + 9; // 数据从索引9开始,加上元数据偏移
- const id1 = sharedFloatArray[offset1 + 0];
- if (id1 === 0) continue;
-
- let x1 = sharedFloatArray[offset1 + 1];
- let y1 = sharedFloatArray[offset1 + 2];
- let dx1 = sharedFloatArray[offset1 + 3];
- let dy1 = sharedFloatArray[offset1 + 4];
- const mass1 = sharedFloatArray[offset1 + 5];
- const bounce1 = sharedFloatArray[offset1 + 6];
- const radius1 = sharedFloatArray[offset1 + 8];
-
- // 检测与所有其他小球的碰撞(能看到所有实体,实现完整碰撞检测)
- for (let j = 0; j < actualEntityCount; j++) {
- if (i === j) continue;
-
- const offset2 = j * 9 + 9; // 数据从索引9开始,加上元数据偏移
- const id2 = sharedFloatArray[offset2 + 0];
- if (id2 === 0) continue;
-
- const x2 = sharedFloatArray[offset2 + 1];
- const y2 = sharedFloatArray[offset2 + 2];
- const dx2 = sharedFloatArray[offset2 + 3];
- const dy2 = sharedFloatArray[offset2 + 4];
- const mass2 = sharedFloatArray[offset2 + 5];
- const bounce2 = sharedFloatArray[offset2 + 6];
- const radius2 = sharedFloatArray[offset2 + 8];
-
- // 额外检查:确保位置和半径都是有效值
- if (isNaN(x2) || isNaN(y2) || isNaN(radius2) || radius2 <= 0) continue;
-
- // 计算距离
- const deltaX = x2 - x1;
- const deltaY = y2 - y1;
- const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
- const minDistance = radius1 + radius2;
-
- // 检测碰撞
- if (distance < minDistance && distance > 0) {
- // 碰撞法线
- const nx = deltaX / distance;
- const ny = deltaY / distance;
-
- // 分离小球 - 只调整当前Worker负责的球
- const overlap = minDistance - distance;
- const separationX = nx * overlap * 0.5;
- const separationY = ny * overlap * 0.5;
-
- x1 -= separationX;
- y1 -= separationY;
-
- // 相对速度
- const relativeVelocityX = dx2 - dx1;
- const relativeVelocityY = dy2 - dy1;
-
- // 沿碰撞法线的速度分量
- const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
-
- // 如果速度分量为正,小球正在分离
- if (velocityAlongNormal > 0) continue;
-
- // 弹性系数
- const restitution = (bounce1 + bounce2) * 0.5;
-
- // 冲量计算
- const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/mass1 + 1/mass2);
-
- // 应用冲量到当前小球(只更新当前Worker负责的球)
- const impulseX = impulseScalar * nx;
- const impulseY = impulseScalar * ny;
-
- dx1 -= impulseX / mass1;
- dy1 -= impulseY / mass1;
-
- // 能量损失
- const energyLoss = 0.98;
- dx1 *= energyLoss;
- dy1 *= energyLoss;
- }
- }
-
- // 只更新当前Worker负责的实体
- sharedFloatArray[offset1 + 1] = x1;
- sharedFloatArray[offset1 + 2] = y1;
- sharedFloatArray[offset1 + 3] = dx1;
- sharedFloatArray[offset1 + 4] = dy1;
- }
- };
- }
-}
\ No newline at end of file
diff --git a/examples/worker-system-demo/src/systems/RenderSystem.ts b/examples/worker-system-demo/src/systems/RenderSystem.ts
deleted file mode 100644
index 9088d825..00000000
--- a/examples/worker-system-demo/src/systems/RenderSystem.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-import { EntitySystem, Matcher, Entity, ECSSystem } from '@esengine/ecs-framework';
-import { Position, Renderable } from '../components';
-
-@ECSSystem('RenderSystem')
-export class RenderSystem extends EntitySystem {
- private canvas: HTMLCanvasElement;
- private ctx: CanvasRenderingContext2D;
- private startTime: number = 0;
- private batchCount: number = 0;
- private drawCallCount: number = 0;
-
- constructor(canvas: HTMLCanvasElement) {
- super(Matcher.empty().all(Position, Renderable));
- this.canvas = canvas;
- this.ctx = canvas.getContext('2d')!;
- }
-
- protected override onBegin(): void {
- super.onBegin();
- this.startTime = performance.now();
-
- // 清空画布
- this.ctx.fillStyle = '#000000';
- this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
- }
-
- protected override process(entities: readonly Entity[]): void {
- // 保持原始绘制顺序,但优化连续相同颜色的绘制
- let lastColor = '';
- this.drawCallCount = 0;
-
- for (const entity of entities) {
- const position = entity.getComponent(Position)!;
- const renderable = entity.getComponent(Renderable)!;
-
- // 只在颜色变化时设置fillStyle,减少状态切换
- if (renderable.color !== lastColor) {
- this.ctx.fillStyle = renderable.color;
- lastColor = renderable.color;
- }
-
- if (renderable.shape === 'circle') {
- this.ctx.beginPath();
- this.ctx.arc(position.x, position.y, renderable.size, 0, Math.PI * 2);
- this.ctx.fill();
- this.drawCallCount++;
- } else if (renderable.shape === 'square') {
- this.ctx.fillRect(
- position.x - renderable.size / 2,
- position.y - renderable.size / 2,
- renderable.size,
- renderable.size
- );
- this.drawCallCount++;
- }
- }
-
- // 计算颜色多样性用于显示
- const uniqueColors = new Set(entities.map(e => e.getComponent(Renderable)!.color));
- this.batchCount = uniqueColors.size;
- }
-
- protected override onEnd(): void {
- super.onEnd();
- const endTime = performance.now();
- const executionTime = endTime - this.startTime;
-
- // 发送性能数据到UI
- (window as any).renderExecutionTime = executionTime;
-
- // 绘制调试信息
- this.drawDebugInfo();
- }
-
- private drawDebugInfo(): void {
- const entities = this.entities;
-
- this.ctx.fillStyle = '#00ff00';
- this.ctx.font = '14px Arial';
- this.ctx.fillText(`实体数量: ${entities.length}`, 10, 20);
- this.ctx.fillText(`渲染批次: ${this.batchCount}`, 10, 140);
- this.ctx.fillText(`绘制调用: ${this.drawCallCount}`, 10, 160);
-
- const workerInfo = (window as any).workerInfo;
- if (workerInfo) {
- this.ctx.fillStyle = workerInfo.enabled ? '#00ff00' : '#ff0000';
- this.ctx.fillText(`Worker: ${workerInfo.enabled ? '启用' : '禁用'}`, 10, 40);
-
- if (workerInfo.enabled) {
- this.ctx.fillStyle = '#ffff00';
- const entitiesPerWorker = Math.ceil(entities.length / workerInfo.workerCount);
- this.ctx.fillText(`每个Worker实体: ${entitiesPerWorker}`, 10, 60);
- this.ctx.fillText(`Worker数量: ${workerInfo.workerCount}`, 10, 80);
- }
- }
-
- // 显示性能信息
- const physicsTime = (window as any).physicsExecutionTime || 0;
- const renderTime = (window as any).renderExecutionTime || 0;
-
- this.ctx.fillStyle = physicsTime > 16 ? '#ff0000' : physicsTime > 8 ? '#ffff00' : '#00ff00';
- this.ctx.fillText(`物理: ${physicsTime.toFixed(2)}ms`, 10, 100);
-
- this.ctx.fillStyle = renderTime > 16 ? '#ff0000' : renderTime > 8 ? '#ffff00' : '#00ff00';
- this.ctx.fillText(`渲染: ${renderTime.toFixed(2)}ms`, 10, 120);
- }
-}
\ No newline at end of file
diff --git a/examples/worker-system-demo/src/systems/index.ts b/examples/worker-system-demo/src/systems/index.ts
deleted file mode 100644
index 3279b8f3..00000000
--- a/examples/worker-system-demo/src/systems/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export { PhysicsWorkerSystem } from './PhysicsWorkerSystem';
-export { RenderSystem } from './RenderSystem';
-export { LifetimeSystem } from './LifetimeSystem';
\ No newline at end of file
diff --git a/examples/worker-system-demo/tsconfig.node.json b/examples/worker-system-demo/tsconfig.node.json
deleted file mode 100644
index 099658cf..00000000
--- a/examples/worker-system-demo/tsconfig.node.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "compilerOptions": {
- "composite": true,
- "skipLibCheck": true,
- "module": "ESNext",
- "moduleResolution": "bundler",
- "allowSyntheticDefaultImports": true
- },
- "include": ["vite.config.ts"]
-}
\ No newline at end of file
diff --git a/examples/worker-system-demo/vite.config.ts b/examples/worker-system-demo/vite.config.ts
deleted file mode 100644
index ca96ff59..00000000
--- a/examples/worker-system-demo/vite.config.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { defineConfig } from 'vite';
-
-export default defineConfig({
- server: {
- port: 3000,
- open: true,
- headers: {
- 'Cross-Origin-Embedder-Policy': 'require-corp',
- 'Cross-Origin-Opener-Policy': 'same-origin'
- }
- },
- build: {
- target: 'es2020',
- outDir: 'dist',
- minify: false,
- rollupOptions: {
- output: {
- format: 'es',
- manualChunks: undefined
- }
- }
- },
- esbuild: {
- target: 'es2020'
- }
-});
\ No newline at end of file