新增worker-system文档及源码示例
This commit is contained in:
@@ -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) - 完整的游戏示例
|
||||||
|
|
||||||
## 文档
|
## 文档
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
@@ -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
579
docs/guide/worker-system.md
Normal 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
@@ -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;
|
||||||
@@ -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">
|
||||||
|
|||||||
175
examples/worker-system-demo/index.html
Normal file
175
examples/worker-system-demo/index.html
Normal 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>
|
||||||
626
examples/worker-system-demo/package-lock.json
generated
Normal file
626
examples/worker-system-demo/package-lock.json
generated
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
examples/worker-system-demo/package.json
Normal file
18
examples/worker-system-demo/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
173
examples/worker-system-demo/src/GameScene.ts
Normal file
173
examples/worker-system-demo/src/GameScene.ts
Normal 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()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
89
examples/worker-system-demo/src/components/index.ts
Normal file
89
examples/worker-system-demo/src/components/index.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
339
examples/worker-system-demo/src/main.ts
Normal file
339
examples/worker-system-demo/src/main.ts
Normal 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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
});
|
||||||
30
examples/worker-system-demo/src/systems/LifetimeSystem.ts
Normal file
30
examples/worker-system-demo/src/systems/LifetimeSystem.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
466
examples/worker-system-demo/src/systems/PhysicsWorkerSystem.ts
Normal file
466
examples/worker-system-demo/src/systems/PhysicsWorkerSystem.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
107
examples/worker-system-demo/src/systems/RenderSystem.ts
Normal file
107
examples/worker-system-demo/src/systems/RenderSystem.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
examples/worker-system-demo/src/systems/index.ts
Normal file
3
examples/worker-system-demo/src/systems/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { PhysicsWorkerSystem } from './PhysicsWorkerSystem';
|
||||||
|
export { RenderSystem } from './RenderSystem';
|
||||||
|
export { LifetimeSystem } from './LifetimeSystem';
|
||||||
25
examples/worker-system-demo/tsconfig.json
Normal file
25
examples/worker-system-demo/tsconfig.json
Normal 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" }]
|
||||||
|
}
|
||||||
10
examples/worker-system-demo/tsconfig.node.json
Normal file
10
examples/worker-system-demo/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
26
examples/worker-system-demo/vite.config.ts
Normal file
26
examples/worker-system-demo/vite.config.ts
Normal 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'
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user