新增worker-system文档及源码示例

This commit is contained in:
YHH
2025-09-28 20:03:29 +08:00
parent cf2dc91af6
commit 1dfcd008aa
20 changed files with 2697 additions and 19 deletions

View File

@@ -11,6 +11,7 @@
## 特性 ## 特性
- **高性能** - 针对大规模实体优化支持SoA存储和批量处理 - **高性能** - 针对大规模实体优化支持SoA存储和批量处理
- **多线程计算** - Worker系统支持真正的并行处理充分利用多核CPU性能
- **类型安全** - 完整的TypeScript支持编译时类型检查 - **类型安全** - 完整的TypeScript支持编译时类型检查
- **现代架构** - 支持多World、多Scene的分层架构设计 - **现代架构** - 支持多World、多Scene的分层架构设计
- **开发友好** - 内置调试工具和性能监控 - **开发友好** - 内置调试工具和性能监控
@@ -86,6 +87,7 @@ function gameLoop(deltaTime: number) {
- **实体查询** - 使用 Matcher API 进行高效的实体过滤 - **实体查询** - 使用 Matcher API 进行高效的实体过滤
- **事件系统** - 类型安全的事件发布/订阅机制 - **事件系统** - 类型安全的事件发布/订阅机制
- **性能优化** - SoA 存储优化,支持大规模实体处理 - **性能优化** - SoA 存储优化,支持大规模实体处理
- **多线程支持** - Worker系统实现真正的并行计算充分利用多核CPU
- **多场景** - 支持 World/Scene 分层架构 - **多场景** - 支持 World/Scene 分层架构
- **时间管理** - 内置定时器和时间控制系统 - **时间管理** - 内置定时器和时间控制系统
@@ -101,6 +103,7 @@ function gameLoop(deltaTime: number) {
## 示例项目 ## 示例项目
- [Worker系统演示](https://esengine.github.io/ecs-framework/demos/worker-system/) - 多线程物理系统演示,展示高性能并行计算
- [割草机演示](https://github.com/esengine/lawn-mower-demo) - 完整的游戏示例 - [割草机演示](https://github.com/esengine/lawn-mower-demo) - 完整的游戏示例
## 文档 ## 文档

View File

@@ -66,7 +66,13 @@ export default defineConfig({
items: [ items: [
{ text: '实体类 (Entity)', link: '/guide/entity' }, { text: '实体类 (Entity)', link: '/guide/entity' },
{ text: '组件系统 (Component)', link: '/guide/component' }, { text: '组件系统 (Component)', link: '/guide/component' },
{ text: '系统架构 (System)', link: '/guide/system' }, {
text: '系统架构 (System)',
link: '/guide/system',
items: [
{ text: 'Worker系统 (多线程)', link: '/guide/worker-system' }
]
},
{ text: '场景管理 (Scene)', link: '/guide/scene' }, { text: '场景管理 (Scene)', link: '/guide/scene' },
{ text: '事件系统 (Event)', link: '/guide/event-system' }, { text: '事件系统 (Event)', link: '/guide/event-system' },
{ text: '时间和定时器 (Time)', link: '/guide/time-and-timers' }, { text: '时间和定时器 (Time)', link: '/guide/time-and-timers' },

View File

@@ -105,6 +105,14 @@ class AutoSaveSystem extends IntervalSystem {
} }
``` ```
### WorkerEntitySystem - 多线程系统
基于Web Worker的多线程处理系统适用于计算密集型任务能够充分利用多核CPU性能。
Worker系统提供了真正的并行计算能力支持SharedArrayBuffer优化并具有自动降级支持。特别适合物理模拟、粒子系统、AI计算等场景。
**详细内容请参考:[Worker系统](/guide/worker-system)**
## 实体匹配器 (Matcher) ## 实体匹配器 (Matcher)
Matcher 用于定义系统需要处理哪些实体。它提供了灵活的条件组合: Matcher 用于定义系统需要处理哪些实体。它提供了灵活的条件组合:

579
docs/guide/worker-system.md Normal file
View File

@@ -0,0 +1,579 @@
# Worker系统
Worker系统WorkerEntitySystem是ECS框架中基于Web Worker的多线程处理系统专为计算密集型任务设计能够充分利用多核CPU性能实现真正的并行计算。
## 核心特性
- **真正的并行计算**利用Web Worker在后台线程执行计算密集型任务
- **自动负载均衡**根据CPU核心数自动分配工作负载
- **SharedArrayBuffer优化**:零拷贝数据共享,提升大规模计算性能
- **降级支持**不支持Worker时自动回退到主线程处理
- **类型安全**完整的TypeScript支持和类型检查
## 基本用法
### 简单的物理系统示例
```typescript
interface PhysicsData {
id: number;
x: number;
y: number;
vx: number;
vy: number;
mass: number;
radius: number;
}
@ECSSystem('Physics')
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
constructor() {
super(Matcher.all(Position, Velocity, Physics), {
enableWorker: true, // 启用Worker并行处理
workerCount: 4, // Worker数量
useSharedArrayBuffer: true, // 启用SharedArrayBuffer优化
entityDataSize: 7, // 每个实体数据大小
maxEntities: 10000, // 最大实体数量
systemConfig: { // 传递给Worker的配置
gravity: 100,
friction: 0.95
}
});
}
// 数据提取将Entity转换为可序列化的数据
protected extractEntityData(entity: Entity): PhysicsData {
const position = entity.getComponent(Position);
const velocity = entity.getComponent(Velocity);
const physics = entity.getComponent(Physics);
return {
id: entity.id,
x: position.x,
y: position.y,
vx: velocity.x,
vy: velocity.y,
mass: physics.mass,
radius: physics.radius
};
}
// Worker处理函数纯函数在Worker中执行
protected workerProcess(
entities: PhysicsData[],
deltaTime: number,
config: any
): PhysicsData[] {
return entities.map(entity => {
// 应用重力
entity.vy += config.gravity * deltaTime;
// 更新位置
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
// 应用摩擦力
entity.vx *= config.friction;
entity.vy *= config.friction;
return entity;
});
}
// 结果应用将Worker处理结果应用回Entity
protected applyResult(entity: Entity, result: PhysicsData): void {
const position = entity.getComponent(Position);
const velocity = entity.getComponent(Velocity);
position.x = result.x;
position.y = result.y;
velocity.x = result.vx;
velocity.y = result.vy;
}
// SharedArrayBuffer优化支持
protected getDefaultEntityDataSize(): number {
return 7; // id, x, y, vx, vy, mass, radius
}
protected writeEntityToBuffer(entityData: PhysicsData, offset: number): void {
if (!this.sharedFloatArray) return;
this.sharedFloatArray[offset + 0] = entityData.id;
this.sharedFloatArray[offset + 1] = entityData.x;
this.sharedFloatArray[offset + 2] = entityData.y;
this.sharedFloatArray[offset + 3] = entityData.vx;
this.sharedFloatArray[offset + 4] = entityData.vy;
this.sharedFloatArray[offset + 5] = entityData.mass;
this.sharedFloatArray[offset + 6] = entityData.radius;
}
protected readEntityFromBuffer(offset: number): PhysicsData | null {
if (!this.sharedFloatArray) return null;
return {
id: this.sharedFloatArray[offset + 0],
x: this.sharedFloatArray[offset + 1],
y: this.sharedFloatArray[offset + 2],
vx: this.sharedFloatArray[offset + 3],
vy: this.sharedFloatArray[offset + 4],
mass: this.sharedFloatArray[offset + 5],
radius: this.sharedFloatArray[offset + 6]
};
}
}
```
## 配置选项
Worker系统支持丰富的配置选项
```typescript
interface WorkerSystemConfig {
/** 是否启用Worker并行处理 */
enableWorker?: boolean;
/** Worker数量默认为CPU核心数 */
workerCount?: number;
/** 系统配置数据会传递给Worker */
systemConfig?: any;
/** 是否使用SharedArrayBuffer优化 */
useSharedArrayBuffer?: boolean;
/** 每个实体在SharedArrayBuffer中占用的Float32数量 */
entityDataSize?: number;
/** 最大实体数量用于预分配SharedArrayBuffer */
maxEntities?: number;
}
```
### 配置建议
```typescript
constructor() {
super(matcher, {
// 根据任务复杂度决定是否启用
enableWorker: this.shouldUseWorker(),
// 限制Worker数量避免创建过多线程
workerCount: Math.min(navigator.hardwareConcurrency || 2, 4),
// 大量简单计算时启用SharedArrayBuffer
useSharedArrayBuffer: this.entityCount > 1000,
// 根据实际数据结构设置
entityDataSize: 8, // 确保与数据结构匹配
// 预估最大实体数量
maxEntities: 10000,
// 传递给Worker的全局配置
systemConfig: {
gravity: 9.8,
friction: 0.95,
worldBounds: { width: 1920, height: 1080 }
}
});
}
private shouldUseWorker(): boolean {
// 根据实体数量和计算复杂度决定
return this.expectedEntityCount > 100;
}
```
## 处理模式
Worker系统支持两种处理模式
### 1. 传统Worker模式
数据通过序列化在主线程和Worker间传递
```typescript
// 适用于:复杂计算逻辑,实体数量适中
constructor() {
super(matcher, {
enableWorker: true,
useSharedArrayBuffer: false, // 使用传统模式
workerCount: 2
});
}
protected workerProcess(entities: EntityData[], deltaTime: number): EntityData[] {
// 复杂的算法逻辑
return entities.map(entity => {
// AI决策、路径规划等复杂计算
return this.complexAILogic(entity, deltaTime);
});
}
```
### 2. SharedArrayBuffer模式
零拷贝数据共享,适合大量简单计算:
```typescript
// 适用于:大量实体的简单计算
constructor() {
super(matcher, {
enableWorker: true,
useSharedArrayBuffer: true, // 启用共享内存
entityDataSize: 6,
maxEntities: 10000
});
}
protected getSharedArrayBufferProcessFunction(): SharedArrayBufferProcessFunction {
return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, config: any) {
const entitySize = 6;
for (let i = startIndex; i < endIndex; i++) {
const offset = i * entitySize;
// 读取数据
let x = sharedFloatArray[offset];
let y = sharedFloatArray[offset + 1];
let vx = sharedFloatArray[offset + 2];
let vy = sharedFloatArray[offset + 3];
// 物理计算
vy += config.gravity * deltaTime;
x += vx * deltaTime;
y += vy * deltaTime;
// 写回数据
sharedFloatArray[offset] = x;
sharedFloatArray[offset + 1] = y;
sharedFloatArray[offset + 2] = vx;
sharedFloatArray[offset + 3] = vy;
}
};
}
```
## 完整示例:粒子物理系统
一个包含碰撞检测的完整粒子物理系统:
```typescript
interface ParticleData {
id: number;
x: number;
y: number;
dx: number;
dy: number;
mass: number;
radius: number;
bounce: number;
friction: number;
}
@ECSSystem('ParticlePhysics')
class ParticlePhysicsWorkerSystem extends WorkerEntitySystem<ParticleData> {
constructor() {
super(Matcher.all(Position, Velocity, Physics, Renderable), {
enableWorker: true,
workerCount: navigator.hardwareConcurrency || 2,
useSharedArrayBuffer: true,
entityDataSize: 9,
maxEntities: 5000,
systemConfig: {
gravity: 100,
canvasWidth: 800,
canvasHeight: 600,
groundFriction: 0.98
}
});
}
protected extractEntityData(entity: Entity): ParticleData {
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,
radius: renderable.size,
bounce: physics.bounce,
friction: physics.friction
};
}
protected workerProcess(
entities: ParticleData[],
deltaTime: number,
config: any
): ParticleData[] {
const result = entities.map(e => ({ ...e }));
// 基础物理更新
for (const particle of result) {
// 应用重力
particle.dy += config.gravity * deltaTime;
// 更新位置
particle.x += particle.dx * deltaTime;
particle.y += particle.dy * deltaTime;
// 边界碰撞
if (particle.x <= particle.radius) {
particle.x = particle.radius;
particle.dx = -particle.dx * particle.bounce;
} else if (particle.x >= config.canvasWidth - particle.radius) {
particle.x = config.canvasWidth - particle.radius;
particle.dx = -particle.dx * particle.bounce;
}
if (particle.y <= particle.radius) {
particle.y = particle.radius;
particle.dy = -particle.dy * particle.bounce;
} else if (particle.y >= config.canvasHeight - particle.radius) {
particle.y = config.canvasHeight - particle.radius;
particle.dy = -particle.dy * particle.bounce;
particle.dx *= config.groundFriction;
}
// 空气阻力
particle.dx *= particle.friction;
particle.dy *= particle.friction;
}
// 粒子间碰撞检测O(n²)算法)
for (let i = 0; i < result.length; i++) {
for (let j = i + 1; j < result.length; j++) {
const p1 = result[i];
const p2 = result[j];
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const minDistance = p1.radius + p2.radius;
if (distance < minDistance && distance > 0) {
// 分离粒子
const nx = dx / distance;
const ny = dy / distance;
const overlap = minDistance - distance;
p1.x -= nx * overlap * 0.5;
p1.y -= ny * overlap * 0.5;
p2.x += nx * overlap * 0.5;
p2.y += ny * overlap * 0.5;
// 弹性碰撞
const relativeVelocityX = p2.dx - p1.dx;
const relativeVelocityY = p2.dy - p1.dy;
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
if (velocityAlongNormal > 0) continue;
const restitution = (p1.bounce + p2.bounce) * 0.5;
const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/p1.mass + 1/p2.mass);
p1.dx -= impulseScalar * nx / p1.mass;
p1.dy -= impulseScalar * ny / p1.mass;
p2.dx += impulseScalar * nx / p2.mass;
p2.dy += impulseScalar * ny / p2.mass;
}
}
}
return result;
}
protected applyResult(entity: Entity, result: ParticleData): void {
if (!entity?.enabled) return;
const position = entity.getComponent(Position);
const velocity = entity.getComponent(Velocity);
if (position && velocity) {
position.set(result.x, result.y);
velocity.set(result.dx, result.dy);
}
}
protected getDefaultEntityDataSize(): number {
return 9;
}
protected writeEntityToBuffer(data: ParticleData, offset: number): void {
if (!this.sharedFloatArray) return;
this.sharedFloatArray[offset + 0] = data.id;
this.sharedFloatArray[offset + 1] = data.x;
this.sharedFloatArray[offset + 2] = data.y;
this.sharedFloatArray[offset + 3] = data.dx;
this.sharedFloatArray[offset + 4] = data.dy;
this.sharedFloatArray[offset + 5] = data.mass;
this.sharedFloatArray[offset + 6] = data.radius;
this.sharedFloatArray[offset + 7] = data.bounce;
this.sharedFloatArray[offset + 8] = data.friction;
}
protected readEntityFromBuffer(offset: number): ParticleData | null {
if (!this.sharedFloatArray) return null;
return {
id: this.sharedFloatArray[offset + 0],
x: this.sharedFloatArray[offset + 1],
y: this.sharedFloatArray[offset + 2],
dx: this.sharedFloatArray[offset + 3],
dy: this.sharedFloatArray[offset + 4],
mass: this.sharedFloatArray[offset + 5],
radius: this.sharedFloatArray[offset + 6],
bounce: this.sharedFloatArray[offset + 7],
friction: this.sharedFloatArray[offset + 8]
};
}
// 性能监控
public getPerformanceInfo(): {
enabled: boolean;
workerCount: number;
entityCount: number;
isProcessing: boolean;
} {
const workerInfo = this.getWorkerInfo();
return {
...workerInfo,
entityCount: this.entities.length
};
}
}
```
## 适用场景
Worker系统特别适合以下场景
### 1. 物理模拟
- **重力系统**:大量实体的重力计算
- **碰撞检测**:复杂的碰撞算法
- **流体模拟**:粒子流体系统
- **布料模拟**:顶点物理计算
### 2. AI计算
- **路径寻找**A*、Dijkstra等算法
- **行为树**复杂的AI决策逻辑
- **群体智能**:鸟群、鱼群算法
- **神经网络**简单的AI推理
### 3. 数据处理
- **大量实体更新**:状态机、生命周期管理
- **统计计算**:游戏数据分析
- **图像处理**:纹理生成、效果计算
- **音频处理**:音效合成、频谱分析
## 最佳实践
### 1. Worker函数要求
```typescript
// ✅ 推荐Worker处理函数是纯函数
protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] {
// 只使用参数和标准JavaScript API
return entities.map(entity => {
// 纯计算逻辑,不依赖外部状态
entity.y += entity.velocity * deltaTime;
return entity;
});
}
// ❌ 避免在Worker函数中使用外部引用
protected workerProcess(entities: PhysicsData[], deltaTime: number): PhysicsData[] {
// this 和外部变量在Worker中不可用
return entities.map(entity => {
entity.y += this.someProperty; // ❌ 错误
return entity;
});
}
```
### 2. 数据设计
```typescript
// ✅ 推荐:合理的数据设计
interface SimplePhysicsData {
x: number;
y: number;
vx: number;
vy: number;
// 保持数据结构简单,便于序列化
}
// ❌ 避免:复杂的嵌套对象
interface ComplexData {
transform: {
position: { x: number; y: number };
rotation: { angle: number };
};
// 复杂嵌套结构增加序列化开销
}
```
### 3. Worker数量控制
```typescript
// ✅ 推荐适当的Worker数量
constructor() {
super(matcher, {
workerCount: Math.min(navigator.hardwareConcurrency || 2, 4), // 限制最大数量
enableWorker: this.shouldUseWorker(), // 条件启用
});
}
private shouldUseWorker(): boolean {
// 根据实体数量和复杂度决定是否使用Worker
return this.expectedEntityCount > 100;
}
```
### 4. 性能监控
```typescript
// ✅ 推荐:性能监控
public getPerformanceMetrics(): WorkerPerformanceMetrics {
return {
...this.getWorkerInfo(),
entityCount: this.entities.length,
averageProcessTime: this.getAverageProcessTime(),
workerUtilization: this.getWorkerUtilization()
};
}
```
## 性能优化建议
### 1. 计算密集度评估
只对计算密集型任务使用Worker避免在简单计算上增加线程开销。
### 2. 数据传输优化
- 使用SharedArrayBuffer减少序列化开销
- 保持数据结构简单和扁平
- 避免频繁的大数据传输
### 3. 批处理大小
根据实体数量和Worker数量调整批处理大小平衡负载和开销。
### 4. 降级策略
始终提供主线程回退方案确保在不支持Worker的环境中正常运行。
### 5. 内存管理
及时清理Worker池和共享缓冲区避免内存泄漏。
## 在线演示
查看完整的Worker系统演示[Worker系统演示](https://esengine.github.io/ecs-framework/demos/worker-system/)
该演示展示了:
- 多线程物理计算
- 实时性能对比
- SharedArrayBuffer优化
- 大量实体的并行处理
Worker系统为ECS框架提供了强大的并行计算能力让你能够充分利用现代多核处理器的性能为复杂的游戏逻辑和计算密集型任务提供了高效的解决方案。

File diff suppressed because one or more lines are too long

View File

@@ -11914,9 +11914,10 @@ let PhysicsWorkerSystem = class extends WorkerEntitySystem {
{ {
enableWorker, enableWorker,
workerCount: navigator.hardwareConcurrency || 2, workerCount: navigator.hardwareConcurrency || 2,
// 恢复多Worker
systemConfig: defaultConfig, systemConfig: defaultConfig,
useSharedArrayBuffer: true useSharedArrayBuffer: true
// 用SharedArrayBuffer优化 // 使用SharedArrayBuffer进行全局碰撞检测
} }
); );
this.physicsConfig = { this.physicsConfig = {
@@ -11948,7 +11949,8 @@ let PhysicsWorkerSystem = class extends WorkerEntitySystem {
/** /**
* Worker处理函数 - 纯函数会被序列化到Worker中执行 * Worker处理函数 - 纯函数会被序列化到Worker中执行
* 注意这个函数内部不能访问外部变量必须是纯函数 * 注意这个函数内部不能访问外部变量必须是纯函数
* 添加了小球间碰撞检测大大增加计算复杂度 * 非SharedArrayBuffer模式每个Worker只能看到分配给它的实体批次
* 这会导致跨批次的碰撞检测缺失但单批次内的碰撞是正确的
*/ */
workerProcess(entities, deltaTime, systemConfig) { workerProcess(entities, deltaTime, systemConfig) {
const config = systemConfig || { const config = systemConfig || {
@@ -12023,7 +12025,7 @@ let PhysicsWorkerSystem = class extends WorkerEntitySystem {
return result; return result;
} }
/** /**
* 应用处理结果 - 将Worker计算结果应用回组件 * 应用处理结果
*/ */
applyResult(entity, result) { applyResult(entity, result) {
if (!entity || !entity.enabled) { if (!entity || !entity.enabled) {
@@ -12051,7 +12053,7 @@ let PhysicsWorkerSystem = class extends WorkerEntitySystem {
return { ...this.physicsConfig }; return { ...this.physicsConfig };
} }
/** /**
* 性能监控 - 重写onEnd来计算执行时间 * 性能监控
*/ */
onEnd() { onEnd() {
super.onEnd(); super.onEnd();
@@ -12060,7 +12062,7 @@ let PhysicsWorkerSystem = class extends WorkerEntitySystem {
window.physicsExecutionTime = executionTime; window.physicsExecutionTime = executionTime;
} }
/** /**
* 获取实体数据大小 - 物理系统使用9个Float32值 * 获取实体数据大小
*/ */
getDefaultEntityDataSize() { getDefaultEntityDataSize() {
return 9; return 9;
@@ -12086,7 +12088,7 @@ let PhysicsWorkerSystem = class extends WorkerEntitySystem {
sharedArray[dataOffset + 8] = entityData.radius; sharedArray[dataOffset + 8] = entityData.radius;
} }
/** /**
* 性能监控 - 重写onBegin来记录开始时间 * 性能监控开始
*/ */
onBegin() { onBegin() {
super.onBegin(); super.onBegin();
@@ -12113,8 +12115,7 @@ let PhysicsWorkerSystem = class extends WorkerEntitySystem {
}; };
} }
/** /**
* 提供SharedArrayBuffer处理函数 - 物理系统的具体实现 * SharedArrayBuffer处理函数
* 包含小球间碰撞检测的复杂计算
*/ */
getSharedArrayBufferProcessFunction() { getSharedArrayBufferProcessFunction() {
return function(sharedFloatArray, startIndex, endIndex, deltaTime, systemConfig) { return function(sharedFloatArray, startIndex, endIndex, deltaTime, systemConfig) {
@@ -12214,11 +12215,9 @@ let PhysicsWorkerSystem = class extends WorkerEntitySystem {
const impulseY = impulseScalar * ny; const impulseY = impulseScalar * ny;
dx1 -= impulseX / mass1; dx1 -= impulseX / mass1;
dy1 -= impulseY / mass1; dy1 -= impulseY / mass1;
if (i < j) { const energyLoss = 0.98;
const energyLoss = 0.98; dx1 *= energyLoss;
dx1 *= energyLoss; dy1 *= energyLoss;
dy1 *= energyLoss;
}
} }
} }
sharedFloatArray[offset1 + 1] = x1; sharedFloatArray[offset1 + 1] = x1;

View File

@@ -118,7 +118,7 @@
color: #ff4a4a; color: #ff4a4a;
} }
</style> </style>
<script type="module" crossorigin src="/ecs-framework/demos/worker-system/assets/index-a4a166b2.js"></script> <script type="module" crossorigin src="/ecs-framework/demos/worker-system/assets/index-83126548.js"></script>
</head> </head>
<body> <body>
<div class="container"> <div class="container">

View File

@@ -0,0 +1,175 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ECS Framework Worker System Demo</title>
<style>
body {
margin: 0;
padding: 20px;
font-family: Arial, sans-serif;
background: #1a1a1a;
color: white;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
text-align: center;
color: #4a9eff;
}
.demo-area {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.canvas-container {
flex: 1;
}
#gameCanvas {
border: 2px solid #4a9eff;
background: #000;
display: block;
}
.controls {
width: 300px;
background: #2a2a2a;
padding: 20px;
border-radius: 8px;
}
.control-group {
margin-bottom: 15px;
}
.control-group label {
display: block;
margin-bottom: 5px;
color: #ccc;
}
.control-group input, .control-group button {
width: 100%;
padding: 8px;
margin-bottom: 5px;
border: 1px solid #555;
background: #3a3a3a;
color: white;
border-radius: 4px;
}
.control-group button {
background: #4a9eff;
cursor: pointer;
transition: background 0.3s;
}
.control-group button:hover {
background: #3a8eef;
}
.control-group button:disabled {
background: #555;
cursor: not-allowed;
}
.stats {
background: #2a2a2a;
padding: 15px;
border-radius: 8px;
font-family: monospace;
font-size: 12px;
}
.stats h3 {
margin-top: 0;
color: #4a9eff;
}
.stat-line {
margin: 5px 0;
}
.worker-enabled {
color: #4eff4a;
}
.worker-disabled {
color: #ff4a4a;
}
.performance-high {
color: #4eff4a;
}
.performance-medium {
color: #ffff4a;
}
.performance-low {
color: #ff4a4a;
}
</style>
</head>
<body>
<div class="container">
<h1>ECS Framework Worker System 演示</h1>
<div class="demo-area">
<div class="canvas-container">
<canvas id="gameCanvas" width="800" height="600"></canvas>
</div>
<div class="controls">
<div class="control-group">
<label>实体数量:</label>
<input type="range" id="entityCount" min="100" max="10000" value="1000" step="100">
<span id="entityCountValue">1000</span>
</div>
<div class="control-group">
<label>Worker 设置:</label>
<button id="toggleWorker">禁用 Worker</button>
</div>
<div class="control-group">
<button id="spawnParticles">生成粒子系统</button>
<button id="clearEntities">清空所有实体</button>
<button id="resetDemo">重置演示</button>
</div>
<div class="control-group">
<label>物理参数:</label>
<input type="range" id="gravity" min="0" max="500" value="100" step="10">
<label>重力: <span id="gravityValue">100</span></label>
<input type="range" id="friction" min="0" max="100" value="95" step="5">
<label>摩擦力: <span id="frictionValue">95%</span></label>
</div>
</div>
</div>
<div class="stats">
<h3>性能统计</h3>
<div class="stat-line">FPS: <span id="fps">0</span></div>
<div class="stat-line">实体数量: <span id="entityCountStat">0</span></div>
<div class="stat-line">Worker状态: <span id="workerStatus" class="worker-disabled">未启用</span></div>
<div class="stat-line">Worker负载: <span id="workerLoad">N/A</span></div>
<div class="stat-line">物理系统耗时: <span id="physicsTime">0</span>ms</div>
<div class="stat-line">渲染系统耗时: <span id="renderTime">0</span>ms</div>
<div class="stat-line">总帧时间: <span id="frameTime">0</span>ms</div>
<div class="stat-line">内存使用: <span id="memoryUsage">0</span>MB</div>
</div>
</div>
<script type="module" src="src/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,626 @@
{
"name": "ecs-worker-system-demo",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ecs-worker-system-demo",
"version": "1.0.0",
"dependencies": {
"@esengine/ecs-framework": "file:../../packages/core"
},
"devDependencies": {
"typescript": "^5.0.0",
"vite": "^4.0.0"
}
},
"../../packages/core": {
"name": "@esengine/ecs-framework",
"version": "2.1.49",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.28.3",
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
"@babel/plugin-transform-optional-chaining": "^7.27.1",
"@babel/preset-env": "^7.28.3",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-commonjs": "^28.0.3",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-terser": "^0.4.4",
"@types/jest": "^29.5.14",
"@types/node": "^20.19.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"rimraf": "^5.0.0",
"rollup": "^4.42.0",
"rollup-plugin-dts": "^6.2.1",
"ts-jest": "^29.4.0",
"typescript": "^5.8.3"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esengine/ecs-framework": {
"resolved": "../../packages/core",
"link": true
},
"node_modules/esbuild": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/android-arm": "0.18.20",
"@esbuild/android-arm64": "0.18.20",
"@esbuild/android-x64": "0.18.20",
"@esbuild/darwin-arm64": "0.18.20",
"@esbuild/darwin-x64": "0.18.20",
"@esbuild/freebsd-arm64": "0.18.20",
"@esbuild/freebsd-x64": "0.18.20",
"@esbuild/linux-arm": "0.18.20",
"@esbuild/linux-arm64": "0.18.20",
"@esbuild/linux-ia32": "0.18.20",
"@esbuild/linux-loong64": "0.18.20",
"@esbuild/linux-mips64el": "0.18.20",
"@esbuild/linux-ppc64": "0.18.20",
"@esbuild/linux-riscv64": "0.18.20",
"@esbuild/linux-s390x": "0.18.20",
"@esbuild/linux-x64": "0.18.20",
"@esbuild/netbsd-x64": "0.18.20",
"@esbuild/openbsd-x64": "0.18.20",
"@esbuild/sunos-x64": "0.18.20",
"@esbuild/win32-arm64": "0.18.20",
"@esbuild/win32-ia32": "0.18.20",
"@esbuild/win32-x64": "0.18.20"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/rollup": {
"version": "3.29.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz",
"integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==",
"dev": true,
"license": "MIT",
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=14.18.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/typescript": {
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/vite": {
"version": "4.5.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz",
"integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.18.10",
"postcss": "^8.4.27",
"rollup": "^3.27.1"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
},
"peerDependencies": {
"@types/node": ">= 14",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
}
}
}
}
}

View File

@@ -0,0 +1,18 @@
{
"name": "ecs-worker-system-demo",
"version": "1.0.0",
"description": "ECS Framework Worker System Demo",
"main": "src/main.ts",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"typescript": "^5.0.0",
"vite": "^4.0.0"
},
"dependencies": {
"@esengine/ecs-framework": "file:../../packages/core"
}
}

View File

@@ -0,0 +1,173 @@
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
});
}
}
/**
* 获取系统信息
*/
public getSystemInfo() {
return {
physics: this.physicsSystem.getWorkerInfo(),
entityCount: this.entities.count,
physicsConfig: this.physicsSystem.getPhysicsConfig()
};
}
}

View File

@@ -0,0 +1,89 @@
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;
}
}

View File

@@ -0,0 +1,339 @@
import { Core } from '@esengine/ecs-framework';
import { GameScene } from './GameScene';
// 性能监控
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() {
// 获取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',
'gravity', 'gravityValue', 'friction', 'frictionValue', 'spawnParticles',
'clearEntities', 'resetDemo', 'fps', 'entityCountStat', 'workerStatus', 'workerLoad',
'physicsTime', 'renderTime', 'frameTime', 'memoryUsage'
];
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();
});
}
// 重力滑块
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;
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';
}
}
}
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 = `
<div style="padding: 20px; color: red;">
<h1>启动失败</h1>
<p>错误: ${error}</p>
<p>请确保浏览器支持Web Workers和Canvas API</p>
</div>
`;
}
});

View File

@@ -0,0 +1,30 @@
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();
}
}
}

View File

@@ -0,0 +1,466 @@
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<PhysicsEntityData> {
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
};
super(
Matcher.empty().all(Position, Velocity, Physics),
{
enableWorker,
workerCount: navigator.hardwareConcurrency || 2, // 恢复多Worker
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<PhysicsConfig>): void {
Object.assign(this.physicsConfig, newConfig);
this.updateConfig({ systemConfig: this.physicsConfig });
}
/**
* 获取物理配置
*/
public getPhysicsConfig(): PhysicsConfig {
return { ...this.physicsConfig };
}
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;
}
};
}
}

View File

@@ -0,0 +1,107 @@
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);
}
}

View File

@@ -0,0 +1,3 @@
export { PhysicsWorkerSystem } from './PhysicsWorkerSystem';
export { RenderSystem } from './RenderSystem';
export { LifetimeSystem } from './LifetimeSystem';

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,26 @@
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'
}
});