perf(core): 优化 EntitySystem 迭代性能,添加 CommandBuffer 延迟命令 (#281)
* perf(core): 优化 EntitySystem 迭代性能,添加 CommandBuffer 延迟命令 ReactiveQuery 快照优化: - 添加快照机制,避免每帧拷贝数组 - 只在实体列表变化时创建新快照 - 静态场景下多个系统共享同一快照 CommandBuffer 延迟命令系统: - 支持延迟添加/移除组件、销毁实体、设置实体激活状态 - 每个系统拥有独立的 commands 属性 - 命令在帧末统一执行,避免迭代过程中修改实体列表 Scene 更新: - 在 lateUpdate 后自动刷新所有系统的命令缓冲区 文档: - 更新系统文档,添加 CommandBuffer 使用说明 * fix(ci): upgrade first-interaction action to v1.3.0 Fix Docker build failure in welcome workflow. * fix(ci): upgrade pnpm/action-setup to v4 and fix unused import - Upgrade pnpm/action-setup@v2 to v4 in all workflow files - Remove unused CommandType import in CommandBuffer.test.ts * fix(ci): remove duplicate pnpm version specification
This commit is contained in:
@@ -334,6 +334,110 @@ class DamageSystem extends EntitySystem {
|
||||
|
||||
The framework creates a snapshot of the entity list before each `process`/`lateProcess` call, ensuring that component changes during iteration won't cause entities to be skipped or processed multiple times.
|
||||
|
||||
## Command Buffer (CommandBuffer)
|
||||
|
||||
> **v2.2.22+**
|
||||
|
||||
CommandBuffer provides a mechanism for deferred execution of entity operations. When you need to destroy entities or perform other operations that might affect iteration during processing, CommandBuffer allows you to defer these operations to the end of the frame.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
Every EntitySystem has a built-in `commands` property:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Damage')
|
||||
class DamageSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Health, DamageReceiver));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
const damage = entity.getComponent(DamageReceiver);
|
||||
|
||||
if (health && damage) {
|
||||
health.current -= damage.amount;
|
||||
|
||||
// Use command buffer to defer component removal
|
||||
this.commands.removeComponent(entity, DamageReceiver);
|
||||
|
||||
if (health.current <= 0) {
|
||||
// Defer adding death marker
|
||||
this.commands.addComponent(entity, new Dead());
|
||||
// Defer entity destruction
|
||||
this.commands.destroyEntity(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Supported Commands
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `addComponent(entity, component)` | Defer adding component |
|
||||
| `removeComponent(entity, ComponentType)` | Defer removing component |
|
||||
| `destroyEntity(entity)` | Defer destroying entity |
|
||||
| `setEntityActive(entity, active)` | Defer setting entity active state |
|
||||
|
||||
### Execution Timing
|
||||
|
||||
Commands in the buffer are automatically executed after the `lateUpdate` phase of each frame. Execution order matches the order commands were queued.
|
||||
|
||||
```
|
||||
Scene Update Flow:
|
||||
1. onBegin()
|
||||
2. process()
|
||||
3. lateProcess()
|
||||
4. onEnd()
|
||||
5. flushCommandBuffers() <-- Commands execute here
|
||||
```
|
||||
|
||||
### Use Cases
|
||||
|
||||
CommandBuffer is suitable for:
|
||||
|
||||
1. **Destroying entities during iteration**: Avoid modifying collection being traversed
|
||||
2. **Batch deferred operations**: Merge multiple operations to execute at end of frame
|
||||
3. **Cross-system coordination**: One system marks, another system responds
|
||||
|
||||
```typescript
|
||||
// Example: Enemy death system
|
||||
@ECSSystem('EnemyDeath')
|
||||
class EnemyDeathSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Enemy, Health));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health && health.current <= 0) {
|
||||
// Play death animation, spawn loot, etc.
|
||||
this.spawnLoot(entity);
|
||||
|
||||
// Defer destruction, doesn't affect current iteration
|
||||
this.commands.destroyEntity(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private spawnLoot(entity: Entity): void {
|
||||
// Loot spawning logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- Commands skip already destroyed entities (safety check)
|
||||
- Single command failure doesn't affect other commands
|
||||
- Commands execute in queue order
|
||||
- Command queue clears after each `flush()`
|
||||
|
||||
## System Properties and Methods
|
||||
|
||||
### Important Properties
|
||||
|
||||
@@ -334,6 +334,110 @@ class DamageSystem extends EntitySystem {
|
||||
|
||||
框架会在每次 `process`/`lateProcess` 调用前创建实体列表的快照,确保迭代过程中的组件变化不会导致跳过实体或重复处理。
|
||||
|
||||
## 命令缓冲区 (CommandBuffer)
|
||||
|
||||
> **v2.2.22+**
|
||||
|
||||
CommandBuffer 提供了一种延迟执行实体操作的机制。当你需要在迭代过程中销毁实体或进行其他可能影响迭代的操作时,使用 CommandBuffer 可以将这些操作推迟到帧末统一执行。
|
||||
|
||||
### 基本用法
|
||||
|
||||
每个 EntitySystem 都内置了 `commands` 属性:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Damage')
|
||||
class DamageSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Health, DamageReceiver));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
const damage = entity.getComponent(DamageReceiver);
|
||||
|
||||
if (health && damage) {
|
||||
health.current -= damage.amount;
|
||||
|
||||
// 使用命令缓冲区延迟移除组件
|
||||
this.commands.removeComponent(entity, DamageReceiver);
|
||||
|
||||
if (health.current <= 0) {
|
||||
// 延迟添加死亡标记
|
||||
this.commands.addComponent(entity, new Dead());
|
||||
// 延迟销毁实体
|
||||
this.commands.destroyEntity(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 支持的命令
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `addComponent(entity, component)` | 延迟添加组件 |
|
||||
| `removeComponent(entity, ComponentType)` | 延迟移除组件 |
|
||||
| `destroyEntity(entity)` | 延迟销毁实体 |
|
||||
| `setEntityActive(entity, active)` | 延迟设置实体激活状态 |
|
||||
|
||||
### 执行时机
|
||||
|
||||
命令缓冲区中的命令会在每帧的 `lateUpdate` 阶段之后自动执行。执行顺序与命令入队顺序一致。
|
||||
|
||||
```
|
||||
场景更新流程:
|
||||
1. onBegin()
|
||||
2. process()
|
||||
3. lateProcess()
|
||||
4. onEnd()
|
||||
5. flushCommandBuffers() <-- 命令在这里执行
|
||||
```
|
||||
|
||||
### 使用场景
|
||||
|
||||
CommandBuffer 适用于以下场景:
|
||||
|
||||
1. **在迭代中销毁实体**:避免修改正在遍历的集合
|
||||
2. **批量延迟操作**:将多个操作合并到帧末执行
|
||||
3. **跨系统协调**:一个系统标记,另一个系统响应
|
||||
|
||||
```typescript
|
||||
// 示例:敌人死亡系统
|
||||
@ECSSystem('EnemyDeath')
|
||||
class EnemyDeathSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Enemy, Health));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health && health.current <= 0) {
|
||||
// 播放死亡动画、掉落物品等
|
||||
this.spawnLoot(entity);
|
||||
|
||||
// 延迟销毁,不影响当前迭代
|
||||
this.commands.destroyEntity(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private spawnLoot(entity: Entity): void {
|
||||
// 掉落物品逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- 命令会跳过已销毁的实体(安全检查)
|
||||
- 单个命令执行失败不会影响其他命令
|
||||
- 命令按入队顺序执行
|
||||
- 每次 `flush()` 后命令队列会清空
|
||||
|
||||
## 系统属性和方法
|
||||
|
||||
### 重要属性
|
||||
|
||||
Reference in New Issue
Block a user