feat(ecs): 核心系统改进 - 句柄、调度、变更检测与查询编译 (#304)
新增功能: - EntityHandle: 轻量级实体句柄 (28位索引 + 20位代数) - SystemScheduler: 声明式系统调度,支持 @Stage/@Before/@After/@InSet 装饰器 - EpochManager: 帧级变更检测 - CompiledQuery: 预编译类型安全查询 API 改进: - EntitySystem 添加 getBefore()/getAfter()/getSets() getter 方法 - Entity 添加 markDirty() 辅助方法 - IScene 添加 epochManager 属性 - CommandBuffer.pendingCount 修正为返回实际操作数 文档更新: - 更新系统调度和查询相关文档
This commit is contained in:
@@ -672,6 +672,243 @@ scene.addSystem(new SystemB()); // addOrder = 1, executes second
|
|||||||
|
|
||||||
> **Note**: `addOrder` is automatically set by the framework when calling `addSystem`, no manual management needed. This ensures systems with the same `updateOrder` execute in their addition order, avoiding random behavior from unstable sorting.
|
> **Note**: `addOrder` is automatically set by the framework when calling `addSystem`, no manual management needed. This ensures systems with the same `updateOrder` execute in their addition order, avoiding random behavior from unstable sorting.
|
||||||
|
|
||||||
|
## Declarative System Scheduling
|
||||||
|
|
||||||
|
> **v2.4.0+**
|
||||||
|
|
||||||
|
In addition to manually controlling execution order with `updateOrder`, the framework provides a declarative system scheduling mechanism that lets you define execution order through dependencies.
|
||||||
|
|
||||||
|
### Scheduling Decorators
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { EntitySystem, ECSSystem, Stage, Before, After, InSet } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
// Use decorators to declare system scheduling
|
||||||
|
@ECSSystem('Movement')
|
||||||
|
@Stage('update') // Execute in update stage
|
||||||
|
@After('InputSystem') // Execute after InputSystem
|
||||||
|
@Before('RenderSystem') // Execute before RenderSystem
|
||||||
|
class MovementSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(Position, Velocity));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected process(entities: readonly Entity[]): void {
|
||||||
|
// Movement logic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use system sets for grouping
|
||||||
|
@ECSSystem('Physics')
|
||||||
|
@Stage('update')
|
||||||
|
@InSet('CoreSystems') // Belongs to CoreSystems set
|
||||||
|
class PhysicsSystem extends EntitySystem {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
@ECSSystem('Collision')
|
||||||
|
@Stage('update')
|
||||||
|
@After('set:CoreSystems') // Execute after all systems in CoreSystems set
|
||||||
|
class CollisionSystem extends EntitySystem {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### System Execution Stages
|
||||||
|
|
||||||
|
The framework defines the following system execution stages, executed in order:
|
||||||
|
|
||||||
|
| Stage | Description | Typical Usage |
|
||||||
|
|-------|-------------|---------------|
|
||||||
|
| `startup` | Startup stage | One-time initialization |
|
||||||
|
| `preUpdate` | Pre-update stage | Input handling, state preparation |
|
||||||
|
| `update` | Main update stage (default) | Core game logic |
|
||||||
|
| `postUpdate` | Post-update stage | Physics, collision detection |
|
||||||
|
| `cleanup` | Cleanup stage | Resource cleanup, state reset |
|
||||||
|
|
||||||
|
### Fluent API Configuration
|
||||||
|
|
||||||
|
If you prefer not to use decorators, you can configure scheduling at runtime using the Fluent API:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@ECSSystem('Movement')
|
||||||
|
class MovementSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(Position, Velocity));
|
||||||
|
|
||||||
|
// Configure scheduling using Fluent API
|
||||||
|
this.stage('update')
|
||||||
|
.after('InputSystem')
|
||||||
|
.before('RenderSystem')
|
||||||
|
.inSet('CoreSystems');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cycle Dependency Detection
|
||||||
|
|
||||||
|
The framework automatically detects cyclic dependencies and throws clear errors:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// This will cause a cycle dependency error
|
||||||
|
@ECSSystem('SystemA')
|
||||||
|
@Before('SystemB')
|
||||||
|
class SystemA extends EntitySystem { }
|
||||||
|
|
||||||
|
@ECSSystem('SystemB')
|
||||||
|
@Before('SystemA') // Error: A -> B -> A forms a cycle
|
||||||
|
class SystemB extends EntitySystem { }
|
||||||
|
|
||||||
|
// Error message: Cyclic dependency detected: SystemA -> SystemB -> SystemA
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frame-Level Change Detection
|
||||||
|
|
||||||
|
> **v2.4.0+**
|
||||||
|
|
||||||
|
The framework provides epoch-based frame-level change detection, allowing systems to process only entities that have changed, significantly improving performance.
|
||||||
|
|
||||||
|
### Core Concepts
|
||||||
|
|
||||||
|
- **Epoch**: Global frame counter, incremented each frame
|
||||||
|
- **lastWriteEpoch**: The epoch when a component was last modified
|
||||||
|
- **Change Detection**: Determine if component changed after a specific point by comparing epochs
|
||||||
|
|
||||||
|
### Marking Components as Modified
|
||||||
|
|
||||||
|
After modifying component data, you need to mark the component as changed. There are two approaches:
|
||||||
|
|
||||||
|
**Approach 1: Via Entity Helper Method (Recommended)**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Mark component dirty via entity.markDirty() after modification
|
||||||
|
const pos = entity.getComponent(Position)!;
|
||||||
|
pos.x = 100;
|
||||||
|
pos.y = 200;
|
||||||
|
entity.markDirty(pos);
|
||||||
|
|
||||||
|
// Can mark multiple components at once
|
||||||
|
const vel = entity.getComponent(Velocity)!;
|
||||||
|
vel.vx = 10;
|
||||||
|
entity.markDirty(pos, vel);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Approach 2: Encapsulate in Component**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class VelocityComponent extends Component {
|
||||||
|
private _vx: number = 0;
|
||||||
|
private _vy: number = 0;
|
||||||
|
|
||||||
|
// Provide modification method that accepts epoch parameter
|
||||||
|
public setVelocity(vx: number, vy: number, epoch: number): void {
|
||||||
|
this._vx = vx;
|
||||||
|
this._vy = vy;
|
||||||
|
this.markDirty(epoch);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get vx(): number { return this._vx; }
|
||||||
|
public get vy(): number { return this._vy; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage in system
|
||||||
|
const vel = entity.getComponent(VelocityComponent)!;
|
||||||
|
vel.setVelocity(10, 20, this.currentEpoch);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Change Detection in Systems
|
||||||
|
|
||||||
|
EntitySystem provides several change detection helper methods:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@ECSSystem('Physics')
|
||||||
|
class PhysicsSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(Position, Velocity));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected process(entities: readonly Entity[]): void {
|
||||||
|
// Method 1: Use forEachChanged to process only changed entities
|
||||||
|
// Automatically saves epoch checkpoint
|
||||||
|
this.forEachChanged(entities, [Velocity], (entity) => {
|
||||||
|
const pos = this.requireComponent(entity, Position);
|
||||||
|
const vel = this.requireComponent(entity, Velocity);
|
||||||
|
|
||||||
|
// Only update position when Velocity changes
|
||||||
|
pos.x += vel.vx * Time.deltaTime;
|
||||||
|
pos.y += vel.vy * Time.deltaTime;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ECSSystem('Transform')
|
||||||
|
class TransformSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(Transform, RigidBody));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected process(entities: readonly Entity[]): void {
|
||||||
|
// Method 2: Use filterChanged to get list of changed entities
|
||||||
|
const changedEntities = this.filterChanged(entities, [RigidBody]);
|
||||||
|
|
||||||
|
for (const entity of changedEntities) {
|
||||||
|
// Process entities with changed physics state
|
||||||
|
this.updatePhysics(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manually save epoch checkpoint
|
||||||
|
this.saveEpoch();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updatePhysics(entity: Entity): void {
|
||||||
|
// Physics update logic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Change Detection API Reference
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `forEachChanged(entities, [Types], callback)` | Iterate entities with changed specified components, auto-saves checkpoint |
|
||||||
|
| `filterChanged(entities, [Types])` | Return array of entities with changed specified components |
|
||||||
|
| `hasChanged(entity, [Types])` | Check if single entity's specified components have changed |
|
||||||
|
| `saveEpoch()` | Manually save current epoch as checkpoint |
|
||||||
|
| `lastProcessEpoch` | Get last saved epoch checkpoint |
|
||||||
|
| `currentEpoch` | Get current scene epoch |
|
||||||
|
|
||||||
|
### Use Cases
|
||||||
|
|
||||||
|
Change detection is particularly suitable for:
|
||||||
|
|
||||||
|
1. **Dirty flag optimization**: Only update rendering when data changes
|
||||||
|
2. **Physics sync**: Only sync entities with changed position/velocity
|
||||||
|
3. **Network sync**: Only send changed component data
|
||||||
|
4. **Cache invalidation**: Only recalculate when dependent data changes
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@ECSSystem('NetworkSync')
|
||||||
|
class NetworkSyncSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(NetworkComponent, Transform));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected process(entities: readonly Entity[]): void {
|
||||||
|
// Only sync changed entities, greatly reducing network traffic
|
||||||
|
this.forEachChanged(entities, [Transform], (entity) => {
|
||||||
|
const transform = this.requireComponent(entity, Transform);
|
||||||
|
const network = this.requireComponent(entity, NetworkComponent);
|
||||||
|
|
||||||
|
this.sendTransformUpdate(network.id, transform);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendTransformUpdate(id: string, transform: Transform): void {
|
||||||
|
// Send network update
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Complex System Examples
|
## Complex System Examples
|
||||||
|
|
||||||
### Collision Detection System
|
### Collision Detection System
|
||||||
|
|||||||
@@ -430,6 +430,137 @@ class GameManager {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 编译查询 (CompiledQuery)
|
||||||
|
|
||||||
|
> **v2.4.0+**
|
||||||
|
|
||||||
|
CompiledQuery 是一个轻量级的查询工具,提供类型安全的组件访问和变更检测支持。适合临时查询、工具开发和简单的迭代场景。
|
||||||
|
|
||||||
|
### 基本用法
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 创建编译查询
|
||||||
|
const query = scene.querySystem.compile(Position, Velocity);
|
||||||
|
|
||||||
|
// 类型安全的遍历 - 组件参数自动推断类型
|
||||||
|
query.forEach((entity, pos, vel) => {
|
||||||
|
pos.x += vel.vx * deltaTime;
|
||||||
|
pos.y += vel.vy * deltaTime;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取实体数量
|
||||||
|
console.log(`匹配实体数: ${query.count}`);
|
||||||
|
|
||||||
|
// 获取第一个匹配的实体
|
||||||
|
const first = query.first();
|
||||||
|
if (first) {
|
||||||
|
const [entity, pos, vel] = first;
|
||||||
|
console.log(`第一个实体: ${entity.name}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 变更检测
|
||||||
|
|
||||||
|
CompiledQuery 支持基于 epoch 的变更检测:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class RenderSystem extends EntitySystem {
|
||||||
|
private _query: CompiledQuery<[typeof Transform, typeof Sprite]>;
|
||||||
|
private _lastEpoch = 0;
|
||||||
|
|
||||||
|
protected onInitialize(): void {
|
||||||
|
this._query = this.scene!.querySystem.compile(Transform, Sprite);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected process(entities: readonly Entity[]): void {
|
||||||
|
// 只处理 Transform 或 Sprite 发生变化的实体
|
||||||
|
this._query.forEachChanged(this._lastEpoch, (entity, transform, sprite) => {
|
||||||
|
this.updateRenderData(entity, transform, sprite);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保存当前 epoch 作为下次检查的起点
|
||||||
|
this._lastEpoch = this.scene!.epochManager.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateRenderData(entity: Entity, transform: Transform, sprite: Sprite): void {
|
||||||
|
// 更新渲染数据
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 函数式 API
|
||||||
|
|
||||||
|
CompiledQuery 提供了丰富的函数式 API:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const query = scene.querySystem.compile(Position, Health);
|
||||||
|
|
||||||
|
// map - 转换实体数据
|
||||||
|
const positions = query.map((entity, pos, health) => ({
|
||||||
|
x: pos.x,
|
||||||
|
y: pos.y,
|
||||||
|
healthPercent: health.current / health.max
|
||||||
|
}));
|
||||||
|
|
||||||
|
// filter - 过滤实体
|
||||||
|
const lowHealthEntities = query.filter((entity, pos, health) => {
|
||||||
|
return health.current < health.max * 0.2;
|
||||||
|
});
|
||||||
|
|
||||||
|
// find - 查找第一个匹配的实体
|
||||||
|
const target = query.find((entity, pos, health) => {
|
||||||
|
return health.current > 0 && pos.x > 100;
|
||||||
|
});
|
||||||
|
|
||||||
|
// toArray - 转换为数组
|
||||||
|
const allData = query.toArray();
|
||||||
|
for (const [entity, pos, health] of allData) {
|
||||||
|
console.log(`${entity.name}: ${pos.x}, ${pos.y}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// any/empty - 检查是否有匹配
|
||||||
|
if (query.any()) {
|
||||||
|
console.log('有匹配的实体');
|
||||||
|
}
|
||||||
|
if (query.empty()) {
|
||||||
|
console.log('没有匹配的实体');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### CompiledQuery vs EntitySystem
|
||||||
|
|
||||||
|
| 特性 | CompiledQuery | EntitySystem |
|
||||||
|
|------|---------------|--------------|
|
||||||
|
| **用途** | 轻量级查询工具 | 完整的系统逻辑 |
|
||||||
|
| **生命周期** | 无 | 完整 (onInitialize, onDestroy 等) |
|
||||||
|
| **调度集成** | 无 | 支持 @Stage, @Before, @After |
|
||||||
|
| **变更检测** | forEachChanged | forEachChanged |
|
||||||
|
| **事件监听** | 无 | addEventListener |
|
||||||
|
| **命令缓冲** | 无 | this.commands |
|
||||||
|
| **类型安全组件** | forEach 参数自动推断 | 需要手动 getComponent |
|
||||||
|
| **适用场景** | 临时查询、工具、原型 | 核心游戏逻辑 |
|
||||||
|
|
||||||
|
**选择建议**:
|
||||||
|
|
||||||
|
- 使用 **EntitySystem** 处理核心游戏逻辑(移动、战斗、AI 等)
|
||||||
|
- 使用 **CompiledQuery** 进行一次性查询、工具开发或简单迭代
|
||||||
|
|
||||||
|
### CompiledQuery API 参考
|
||||||
|
|
||||||
|
| 方法 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `forEach(callback)` | 遍历所有匹配实体,类型安全的组件参数 |
|
||||||
|
| `forEachChanged(sinceEpoch, callback)` | 只遍历变更的实体 |
|
||||||
|
| `first()` | 获取第一个匹配的实体和组件 |
|
||||||
|
| `toArray()` | 转换为 [entity, ...components] 数组 |
|
||||||
|
| `map(callback)` | 映射转换 |
|
||||||
|
| `filter(predicate)` | 过滤实体 |
|
||||||
|
| `find(predicate)` | 查找第一个满足条件的实体 |
|
||||||
|
| `any()` | 是否有任何匹配 |
|
||||||
|
| `empty()` | 是否没有匹配 |
|
||||||
|
| `count` | 匹配的实体数量 |
|
||||||
|
| `entities` | 匹配的实体列表(只读) |
|
||||||
|
|
||||||
## 最佳实践
|
## 最佳实践
|
||||||
|
|
||||||
### 1. 优先使用 EntitySystem
|
### 1. 优先使用 EntitySystem
|
||||||
|
|||||||
@@ -672,6 +672,243 @@ scene.addSystem(new SystemB()); // addOrder = 1,后执行
|
|||||||
|
|
||||||
> **注意**:`addOrder` 由框架在 `addSystem` 时自动设置,无需手动管理。这确保了相同 `updateOrder` 的系统按照添加顺序执行,避免了排序不稳定导致的随机行为。
|
> **注意**:`addOrder` 由框架在 `addSystem` 时自动设置,无需手动管理。这确保了相同 `updateOrder` 的系统按照添加顺序执行,避免了排序不稳定导致的随机行为。
|
||||||
|
|
||||||
|
## 声明式系统调度
|
||||||
|
|
||||||
|
> **v2.4.0+**
|
||||||
|
|
||||||
|
除了使用 `updateOrder` 手动控制执行顺序外,框架还提供了声明式的系统调度机制,让你可以通过依赖关系来定义系统的执行顺序。
|
||||||
|
|
||||||
|
### 调度装饰器
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { EntitySystem, ECSSystem, Stage, Before, After, InSet } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
// 使用装饰器声明系统调度
|
||||||
|
@ECSSystem('Movement')
|
||||||
|
@Stage('update') // 在 update 阶段执行
|
||||||
|
@After('InputSystem') // 在 InputSystem 之后执行
|
||||||
|
@Before('RenderSystem') // 在 RenderSystem 之前执行
|
||||||
|
class MovementSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(Position, Velocity));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected process(entities: readonly Entity[]): void {
|
||||||
|
// 移动逻辑
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用系统集合进行分组
|
||||||
|
@ECSSystem('Physics')
|
||||||
|
@Stage('update')
|
||||||
|
@InSet('CoreSystems') // 属于 CoreSystems 集合
|
||||||
|
class PhysicsSystem extends EntitySystem {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
@ECSSystem('Collision')
|
||||||
|
@Stage('update')
|
||||||
|
@After('set:CoreSystems') // 在 CoreSystems 集合的所有系统之后执行
|
||||||
|
class CollisionSystem extends EntitySystem {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 系统执行阶段
|
||||||
|
|
||||||
|
框架定义了以下系统执行阶段,按顺序执行:
|
||||||
|
|
||||||
|
| 阶段 | 说明 | 典型用途 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `startup` | 启动阶段 | 一次性初始化 |
|
||||||
|
| `preUpdate` | 更新前阶段 | 输入处理、状态准备 |
|
||||||
|
| `update` | 主更新阶段(默认) | 核心游戏逻辑 |
|
||||||
|
| `postUpdate` | 更新后阶段 | 物理、碰撞检测 |
|
||||||
|
| `cleanup` | 清理阶段 | 资源清理、状态重置 |
|
||||||
|
|
||||||
|
### Fluent API 配置
|
||||||
|
|
||||||
|
如果不想使用装饰器,也可以使用 Fluent API 在运行时配置调度:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@ECSSystem('Movement')
|
||||||
|
class MovementSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(Position, Velocity));
|
||||||
|
|
||||||
|
// 使用 Fluent API 配置调度
|
||||||
|
this.stage('update')
|
||||||
|
.after('InputSystem')
|
||||||
|
.before('RenderSystem')
|
||||||
|
.inSet('CoreSystems');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 循环依赖检测
|
||||||
|
|
||||||
|
框架会自动检测循环依赖并抛出明确的错误:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 这会导致循环依赖错误
|
||||||
|
@ECSSystem('SystemA')
|
||||||
|
@Before('SystemB')
|
||||||
|
class SystemA extends EntitySystem { }
|
||||||
|
|
||||||
|
@ECSSystem('SystemB')
|
||||||
|
@Before('SystemA') // 错误:A -> B -> A 形成循环
|
||||||
|
class SystemB extends EntitySystem { }
|
||||||
|
|
||||||
|
// 错误信息:Cyclic dependency detected: SystemA -> SystemB -> SystemA
|
||||||
|
```
|
||||||
|
|
||||||
|
## 帧级变更检测
|
||||||
|
|
||||||
|
> **v2.4.0+**
|
||||||
|
|
||||||
|
框架提供了基于 epoch 的帧级变更检测机制,让系统可以只处理发生变化的实体,大幅提升性能。
|
||||||
|
|
||||||
|
### 核心概念
|
||||||
|
|
||||||
|
- **Epoch**:全局帧计数器,每帧递增
|
||||||
|
- **lastWriteEpoch**:组件最后被修改时的 epoch
|
||||||
|
- **变更检测**:通过比较 epoch 判断组件是否在指定时间点后发生变化
|
||||||
|
|
||||||
|
### 标记组件为已修改
|
||||||
|
|
||||||
|
修改组件数据后,需要标记组件为已变更。有两种方式:
|
||||||
|
|
||||||
|
**方式 1:通过 Entity 辅助方法(推荐)**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 修改组件后通过 entity.markDirty() 标记
|
||||||
|
const pos = entity.getComponent(Position)!;
|
||||||
|
pos.x = 100;
|
||||||
|
pos.y = 200;
|
||||||
|
entity.markDirty(pos);
|
||||||
|
|
||||||
|
// 可以同时标记多个组件
|
||||||
|
const vel = entity.getComponent(Velocity)!;
|
||||||
|
vel.vx = 10;
|
||||||
|
entity.markDirty(pos, vel);
|
||||||
|
```
|
||||||
|
|
||||||
|
**方式 2:在组件内部封装**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class VelocityComponent extends Component {
|
||||||
|
private _vx: number = 0;
|
||||||
|
private _vy: number = 0;
|
||||||
|
|
||||||
|
// 提供修改方法,接收 epoch 参数
|
||||||
|
public setVelocity(vx: number, vy: number, epoch: number): void {
|
||||||
|
this._vx = vx;
|
||||||
|
this._vy = vy;
|
||||||
|
this.markDirty(epoch);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get vx(): number { return this._vx; }
|
||||||
|
public get vy(): number { return this._vy; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在系统中使用
|
||||||
|
const vel = entity.getComponent(VelocityComponent)!;
|
||||||
|
vel.setVelocity(10, 20, this.currentEpoch);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 在系统中使用变更检测
|
||||||
|
|
||||||
|
EntitySystem 提供了多个变更检测辅助方法:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@ECSSystem('Physics')
|
||||||
|
class PhysicsSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(Position, Velocity));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected process(entities: readonly Entity[]): void {
|
||||||
|
// 方式1:使用 forEachChanged 只处理变更的实体
|
||||||
|
// 自动保存 epoch 检查点
|
||||||
|
this.forEachChanged(entities, [Velocity], (entity) => {
|
||||||
|
const pos = this.requireComponent(entity, Position);
|
||||||
|
const vel = this.requireComponent(entity, Velocity);
|
||||||
|
|
||||||
|
// 只有 Velocity 变化时才更新位置
|
||||||
|
pos.x += vel.vx * Time.deltaTime;
|
||||||
|
pos.y += vel.vy * Time.deltaTime;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ECSSystem('Transform')
|
||||||
|
class TransformSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(Transform, RigidBody));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected process(entities: readonly Entity[]): void {
|
||||||
|
// 方式2:使用 filterChanged 获取变更的实体列表
|
||||||
|
const changedEntities = this.filterChanged(entities, [RigidBody]);
|
||||||
|
|
||||||
|
for (const entity of changedEntities) {
|
||||||
|
// 处理物理状态变化的实体
|
||||||
|
this.updatePhysics(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动保存 epoch 检查点
|
||||||
|
this.saveEpoch();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updatePhysics(entity: Entity): void {
|
||||||
|
// 物理更新逻辑
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 变更检测 API 参考
|
||||||
|
|
||||||
|
| 方法 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `forEachChanged(entities, [Types], callback)` | 遍历指定组件发生变更的实体,自动保存检查点 |
|
||||||
|
| `filterChanged(entities, [Types])` | 返回指定组件发生变更的实体数组 |
|
||||||
|
| `hasChanged(entity, [Types])` | 检查单个实体的指定组件是否发生变更 |
|
||||||
|
| `saveEpoch()` | 手动保存当前 epoch 作为检查点 |
|
||||||
|
| `lastProcessEpoch` | 获取上次保存的 epoch 检查点 |
|
||||||
|
| `currentEpoch` | 获取当前场景的 epoch |
|
||||||
|
|
||||||
|
### 使用场景
|
||||||
|
|
||||||
|
变更检测特别适合以下场景:
|
||||||
|
|
||||||
|
1. **脏标记优化**:只在数据变化时更新渲染
|
||||||
|
2. **物理同步**:只同步位置/速度发生变化的实体
|
||||||
|
3. **网络同步**:只发送变化的组件数据
|
||||||
|
4. **缓存失效**:只在依赖数据变化时重新计算
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@ECSSystem('NetworkSync')
|
||||||
|
class NetworkSyncSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(NetworkComponent, Transform));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected process(entities: readonly Entity[]): void {
|
||||||
|
// 只同步变化的实体,大幅减少网络流量
|
||||||
|
this.forEachChanged(entities, [Transform], (entity) => {
|
||||||
|
const transform = this.requireComponent(entity, Transform);
|
||||||
|
const network = this.requireComponent(entity, NetworkComponent);
|
||||||
|
|
||||||
|
this.sendTransformUpdate(network.id, transform);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendTransformUpdate(id: string, transform: Transform): void {
|
||||||
|
// 发送网络更新
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## 复杂系统示例
|
## 复杂系统示例
|
||||||
|
|
||||||
### 碰撞检测系统
|
### 碰撞检测系统
|
||||||
|
|||||||
@@ -57,6 +57,27 @@ export abstract class Component implements IComponent {
|
|||||||
@Int32
|
@Int32
|
||||||
public entityId: number | null = null;
|
public entityId: number | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最后写入的 epoch
|
||||||
|
*
|
||||||
|
* 用于帧级变更检测,记录组件最后一次被修改时的 epoch。
|
||||||
|
* 0 表示从未被标记为已修改。
|
||||||
|
*
|
||||||
|
* Last write epoch.
|
||||||
|
* Used for frame-level change detection, records the epoch when component was last modified.
|
||||||
|
* 0 means never marked as modified.
|
||||||
|
*/
|
||||||
|
private _lastWriteEpoch: number = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最后写入的 epoch
|
||||||
|
*
|
||||||
|
* Get last write epoch.
|
||||||
|
*/
|
||||||
|
public get lastWriteEpoch(): number {
|
||||||
|
return this._lastWriteEpoch;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建组件实例
|
* 创建组件实例
|
||||||
*
|
*
|
||||||
@@ -66,6 +87,29 @@ export abstract class Component implements IComponent {
|
|||||||
this.id = Component.idGenerator++;
|
this.id = Component.idGenerator++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记组件为已修改
|
||||||
|
*
|
||||||
|
* 调用此方法会更新组件的 lastWriteEpoch 为当前帧的 epoch。
|
||||||
|
* 系统可以通过比较 lastWriteEpoch 和上次检查的 epoch 来判断组件是否发生变更。
|
||||||
|
*
|
||||||
|
* Mark component as modified.
|
||||||
|
* Calling this method updates the component's lastWriteEpoch to the current frame's epoch.
|
||||||
|
* Systems can compare lastWriteEpoch with their last checked epoch to detect changes.
|
||||||
|
*
|
||||||
|
* @param epoch 当前帧的 epoch | Current frame's epoch
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* // 在修改组件数据后调用
|
||||||
|
* velocity.x = 10;
|
||||||
|
* velocity.markDirty(scene.epochManager.current);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public markDirty(epoch: number): void {
|
||||||
|
this._lastWriteEpoch = epoch;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组件添加到实体时的回调
|
* 组件添加到实体时的回调
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Entity } from '../Entity';
|
import { Entity } from '../Entity';
|
||||||
import { Component } from '../Component';
|
import { Component } from '../Component';
|
||||||
import { ComponentType } from './ComponentStorage';
|
import { ComponentType, ComponentRegistry } from './ComponentStorage';
|
||||||
import { IScene } from '../IScene';
|
import { IScene } from '../IScene';
|
||||||
import { createLogger } from '../../Utils/Logger';
|
import { createLogger } from '../../Utils/Logger';
|
||||||
|
|
||||||
@@ -38,6 +38,24 @@ export interface DeferredCommand {
|
|||||||
value?: boolean;
|
value?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每个实体的待处理操作
|
||||||
|
* Pending operations per entity
|
||||||
|
*
|
||||||
|
* 使用 last-write-wins 语义进行去重。
|
||||||
|
* Uses last-write-wins semantics for deduplication.
|
||||||
|
*/
|
||||||
|
interface PendingPerEntity {
|
||||||
|
/** 是否销毁实体(如果为 true,忽略其他操作)| Destroy entity (if true, ignores other operations) */
|
||||||
|
bDestroy?: boolean;
|
||||||
|
/** 最终的激活状态 | Final active state */
|
||||||
|
active?: boolean;
|
||||||
|
/** 要添加的组件(typeId -> Component)| Components to add (typeId -> Component) */
|
||||||
|
adds?: Map<number, Component>;
|
||||||
|
/** 要移除的组件类型 ID 集合 | Component type IDs to remove */
|
||||||
|
removes?: Set<number>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 命令缓冲区 - 用于延迟执行实体操作
|
* 命令缓冲区 - 用于延迟执行实体操作
|
||||||
* Command Buffer - for deferred entity operations
|
* Command Buffer - for deferred entity operations
|
||||||
@@ -45,8 +63,17 @@ export interface DeferredCommand {
|
|||||||
* 在系统的 process() 方法中使用 CommandBuffer 可以避免迭代过程中修改实体列表,
|
* 在系统的 process() 方法中使用 CommandBuffer 可以避免迭代过程中修改实体列表,
|
||||||
* 从而提高性能(无需每帧拷贝数组)并保证迭代安全。
|
* 从而提高性能(无需每帧拷贝数组)并保证迭代安全。
|
||||||
*
|
*
|
||||||
* Using CommandBuffer in system's process() method avoids modifying entity list during iteration,
|
* 特点:
|
||||||
* improving performance (no array copy per frame) and ensuring iteration safety.
|
* - **去重**: 同一实体的多次同类操作会被合并(last-write-wins)
|
||||||
|
* - **有序执行**: removes -> adds -> active -> destroy
|
||||||
|
* - **冲突处理**: addComponent 会取消同类型的 removeComponent,反之亦然
|
||||||
|
* - **销毁优先**: destroyEntity 会取消该实体的所有其他操作
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - **Deduplication**: Multiple operations on same entity are merged (last-write-wins)
|
||||||
|
* - **Ordered execution**: removes -> adds -> active -> destroy
|
||||||
|
* - **Conflict handling**: addComponent cancels same-type removeComponent and vice versa
|
||||||
|
* - **Destroy priority**: destroyEntity cancels all other operations for that entity
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
@@ -66,7 +93,10 @@ export interface DeferredCommand {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export class CommandBuffer {
|
export class CommandBuffer {
|
||||||
/** 命令队列 | Command queue */
|
/** 每个实体的待处理操作 | Pending operations per entity */
|
||||||
|
private _pending: Map<Entity, PendingPerEntity> = new Map();
|
||||||
|
|
||||||
|
/** 旧式命令队列(用于兼容)| Legacy command queue (for compatibility) */
|
||||||
private _commands: DeferredCommand[] = [];
|
private _commands: DeferredCommand[] = [];
|
||||||
|
|
||||||
/** 关联的场景 | Associated scene */
|
/** 关联的场景 | Associated scene */
|
||||||
@@ -75,6 +105,9 @@ export class CommandBuffer {
|
|||||||
/** 是否启用调试日志 | Enable debug logging */
|
/** 是否启用调试日志 | Enable debug logging */
|
||||||
private _debug: boolean = false;
|
private _debug: boolean = false;
|
||||||
|
|
||||||
|
/** 是否使用去重模式 | Whether to use deduplication mode */
|
||||||
|
private _useDeduplication: boolean = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建命令缓冲区
|
* 创建命令缓冲区
|
||||||
* Create command buffer
|
* Create command buffer
|
||||||
@@ -103,11 +136,34 @@ export class CommandBuffer {
|
|||||||
return this._scene;
|
return this._scene;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置是否使用去重模式
|
||||||
|
* Set whether to use deduplication mode
|
||||||
|
*
|
||||||
|
* @param enabled - 是否启用 | Whether to enable
|
||||||
|
*/
|
||||||
|
public setDeduplication(enabled: boolean): void {
|
||||||
|
this._useDeduplication = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取待执行的命令数量
|
* 获取待执行的命令数量
|
||||||
* Get pending command count
|
* Get pending command count
|
||||||
|
*
|
||||||
|
* 返回实际的操作数量,而不是实体数量。
|
||||||
|
* Returns actual operation count, not entity count.
|
||||||
*/
|
*/
|
||||||
public get pendingCount(): number {
|
public get pendingCount(): number {
|
||||||
|
if (this._useDeduplication) {
|
||||||
|
let count = 0;
|
||||||
|
for (const ops of this._pending.values()) {
|
||||||
|
if (ops.bDestroy) count++;
|
||||||
|
if (ops.active !== undefined) count++;
|
||||||
|
if (ops.adds) count += ops.adds.size;
|
||||||
|
if (ops.removes) count += ops.removes.size;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
return this._commands.length;
|
return this._commands.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,25 +172,82 @@ export class CommandBuffer {
|
|||||||
* Check if there are pending commands
|
* Check if there are pending commands
|
||||||
*/
|
*/
|
||||||
public get hasPending(): boolean {
|
public get hasPending(): boolean {
|
||||||
|
if (this._useDeduplication) {
|
||||||
|
return this._pending.size > 0;
|
||||||
|
}
|
||||||
return this._commands.length > 0;
|
return this._commands.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取或创建实体的待处理操作
|
||||||
|
* Get or create pending operations for entity
|
||||||
|
*/
|
||||||
|
private getPending(entity: Entity): PendingPerEntity {
|
||||||
|
let pending = this._pending.get(entity);
|
||||||
|
if (!pending) {
|
||||||
|
pending = {};
|
||||||
|
this._pending.set(entity, pending);
|
||||||
|
}
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取组件类型 ID(位索引)
|
||||||
|
* Get component type ID (bit index)
|
||||||
|
*/
|
||||||
|
private getTypeId(componentOrType: Component | ComponentType): number {
|
||||||
|
if (typeof componentOrType === 'function') {
|
||||||
|
// ComponentType
|
||||||
|
return ComponentRegistry.getBitIndex(componentOrType);
|
||||||
|
} else {
|
||||||
|
// Component instance
|
||||||
|
return ComponentRegistry.getBitIndex(componentOrType.constructor as ComponentType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 延迟添加组件
|
* 延迟添加组件
|
||||||
* Deferred add component
|
* Deferred add component
|
||||||
*
|
*
|
||||||
|
* 如果之前有同类型的 removeComponent,会被取消。
|
||||||
|
* If there was a removeComponent for the same type, it will be canceled.
|
||||||
|
*
|
||||||
* @param entity - 目标实体 | Target entity
|
* @param entity - 目标实体 | Target entity
|
||||||
* @param component - 要添加的组件 | Component to add
|
* @param component - 要添加的组件 | Component to add
|
||||||
*/
|
*/
|
||||||
public addComponent(entity: Entity, component: Component): void {
|
public addComponent(entity: Entity, component: Component): void {
|
||||||
this._commands.push({
|
if (this._useDeduplication) {
|
||||||
type: CommandType.ADD_COMPONENT,
|
const pending = this.getPending(entity);
|
||||||
entity,
|
|
||||||
component
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this._debug) {
|
// 如果实体已标记销毁,忽略
|
||||||
logger.debug(`CommandBuffer: 延迟添加组件 ${component.constructor.name} 到实体 ${entity.name}`);
|
if (pending.bDestroy) {
|
||||||
|
if (this._debug) {
|
||||||
|
logger.debug(`CommandBuffer: 忽略添加组件,实体 ${entity.name} 已标记销毁`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeId = this.getTypeId(component);
|
||||||
|
|
||||||
|
// 取消同类型的 remove
|
||||||
|
pending.removes?.delete(typeId);
|
||||||
|
|
||||||
|
// 添加(覆盖同类型的之前的 add)
|
||||||
|
if (!pending.adds) {
|
||||||
|
pending.adds = new Map();
|
||||||
|
}
|
||||||
|
pending.adds.set(typeId, component);
|
||||||
|
|
||||||
|
if (this._debug) {
|
||||||
|
logger.debug(`CommandBuffer: 延迟添加组件 ${component.constructor.name} 到实体 ${entity.name}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 旧模式
|
||||||
|
this._commands.push({
|
||||||
|
type: CommandType.ADD_COMPONENT,
|
||||||
|
entity,
|
||||||
|
component
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,18 +255,45 @@ export class CommandBuffer {
|
|||||||
* 延迟移除组件
|
* 延迟移除组件
|
||||||
* Deferred remove component
|
* Deferred remove component
|
||||||
*
|
*
|
||||||
|
* 如果之前有同类型的 addComponent,会被取消。
|
||||||
|
* If there was an addComponent for the same type, it will be canceled.
|
||||||
|
*
|
||||||
* @param entity - 目标实体 | Target entity
|
* @param entity - 目标实体 | Target entity
|
||||||
* @param componentType - 要移除的组件类型 | Component type to remove
|
* @param componentType - 要移除的组件类型 | Component type to remove
|
||||||
*/
|
*/
|
||||||
public removeComponent<T extends Component>(entity: Entity, componentType: ComponentType<T>): void {
|
public removeComponent<T extends Component>(entity: Entity, componentType: ComponentType<T>): void {
|
||||||
this._commands.push({
|
if (this._useDeduplication) {
|
||||||
type: CommandType.REMOVE_COMPONENT,
|
const pending = this.getPending(entity);
|
||||||
entity,
|
|
||||||
componentType
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this._debug) {
|
// 如果实体已标记销毁,忽略
|
||||||
logger.debug(`CommandBuffer: 延迟移除组件 ${componentType.name} 从实体 ${entity.name}`);
|
if (pending.bDestroy) {
|
||||||
|
if (this._debug) {
|
||||||
|
logger.debug(`CommandBuffer: 忽略移除组件,实体 ${entity.name} 已标记销毁`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeId = this.getTypeId(componentType);
|
||||||
|
|
||||||
|
// 取消同类型的 add
|
||||||
|
pending.adds?.delete(typeId);
|
||||||
|
|
||||||
|
// 添加到移除列表
|
||||||
|
if (!pending.removes) {
|
||||||
|
pending.removes = new Set();
|
||||||
|
}
|
||||||
|
pending.removes.add(typeId);
|
||||||
|
|
||||||
|
if (this._debug) {
|
||||||
|
logger.debug(`CommandBuffer: 延迟移除组件 ${componentType.name} 从实体 ${entity.name}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 旧模式
|
||||||
|
this._commands.push({
|
||||||
|
type: CommandType.REMOVE_COMPONENT,
|
||||||
|
entity,
|
||||||
|
componentType
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,16 +301,32 @@ export class CommandBuffer {
|
|||||||
* 延迟销毁实体
|
* 延迟销毁实体
|
||||||
* Deferred destroy entity
|
* Deferred destroy entity
|
||||||
*
|
*
|
||||||
|
* 会取消该实体的所有其他待处理操作。
|
||||||
|
* Cancels all other pending operations for this entity.
|
||||||
|
*
|
||||||
* @param entity - 要销毁的实体 | Entity to destroy
|
* @param entity - 要销毁的实体 | Entity to destroy
|
||||||
*/
|
*/
|
||||||
public destroyEntity(entity: Entity): void {
|
public destroyEntity(entity: Entity): void {
|
||||||
this._commands.push({
|
if (this._useDeduplication) {
|
||||||
type: CommandType.DESTROY_ENTITY,
|
const pending = this.getPending(entity);
|
||||||
entity
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this._debug) {
|
// 清除所有其他操作
|
||||||
logger.debug(`CommandBuffer: 延迟销毁实体 ${entity.name}`);
|
pending.adds?.clear();
|
||||||
|
pending.removes?.clear();
|
||||||
|
delete pending.active;
|
||||||
|
|
||||||
|
// 标记销毁
|
||||||
|
pending.bDestroy = true;
|
||||||
|
|
||||||
|
if (this._debug) {
|
||||||
|
logger.debug(`CommandBuffer: 延迟销毁实体 ${entity.name}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 旧模式
|
||||||
|
this._commands.push({
|
||||||
|
type: CommandType.DESTROY_ENTITY,
|
||||||
|
entity
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,14 +338,30 @@ export class CommandBuffer {
|
|||||||
* @param active - 激活状态 | Active state
|
* @param active - 激活状态 | Active state
|
||||||
*/
|
*/
|
||||||
public setEntityActive(entity: Entity, active: boolean): void {
|
public setEntityActive(entity: Entity, active: boolean): void {
|
||||||
this._commands.push({
|
if (this._useDeduplication) {
|
||||||
type: CommandType.SET_ENTITY_ACTIVE,
|
const pending = this.getPending(entity);
|
||||||
entity,
|
|
||||||
value: active
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this._debug) {
|
// 如果实体已标记销毁,忽略
|
||||||
logger.debug(`CommandBuffer: 延迟设置实体 ${entity.name} 激活状态为 ${active}`);
|
if (pending.bDestroy) {
|
||||||
|
if (this._debug) {
|
||||||
|
logger.debug(`CommandBuffer: 忽略设置激活状态,实体 ${entity.name} 已标记销毁`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置(覆盖之前的设置)
|
||||||
|
pending.active = active;
|
||||||
|
|
||||||
|
if (this._debug) {
|
||||||
|
logger.debug(`CommandBuffer: 延迟设置实体 ${entity.name} 激活状态为 ${active}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 旧模式
|
||||||
|
this._commands.push({
|
||||||
|
type: CommandType.SET_ENTITY_ACTIVE,
|
||||||
|
entity,
|
||||||
|
value: active
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,12 +369,120 @@ export class CommandBuffer {
|
|||||||
* 执行所有待处理的命令
|
* 执行所有待处理的命令
|
||||||
* Execute all pending commands
|
* Execute all pending commands
|
||||||
*
|
*
|
||||||
|
* 执行顺序:removes -> adds -> active -> destroy
|
||||||
|
* Execution order: removes -> adds -> active -> destroy
|
||||||
|
*
|
||||||
* 通常在帧末由 Scene 自动调用。
|
* 通常在帧末由 Scene 自动调用。
|
||||||
* Usually called automatically by Scene at end of frame.
|
* Usually called automatically by Scene at end of frame.
|
||||||
*
|
*
|
||||||
* @returns 执行的命令数量 | Number of commands executed
|
* @returns 执行的命令数量 | Number of commands executed
|
||||||
*/
|
*/
|
||||||
public flush(): number {
|
public flush(): number {
|
||||||
|
if (this._useDeduplication) {
|
||||||
|
return this.flushDeduplication();
|
||||||
|
} else {
|
||||||
|
return this.flushLegacy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用去重模式刷新
|
||||||
|
* Flush using deduplication mode
|
||||||
|
*/
|
||||||
|
private flushDeduplication(): number {
|
||||||
|
if (this._pending.size === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityCount = this._pending.size;
|
||||||
|
let commandCount = 0;
|
||||||
|
|
||||||
|
if (this._debug) {
|
||||||
|
logger.debug(`CommandBuffer: 开始执行 ${entityCount} 个实体的延迟命令`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制并清空,防止执行过程中有新命令加入
|
||||||
|
const pending = this._pending;
|
||||||
|
this._pending = new Map();
|
||||||
|
|
||||||
|
// 分阶段执行
|
||||||
|
// Phase 1: Removes
|
||||||
|
for (const [entity, ops] of pending) {
|
||||||
|
if (ops.bDestroy || !entity.scene) continue;
|
||||||
|
|
||||||
|
if (ops.removes && ops.removes.size > 0) {
|
||||||
|
for (const typeId of ops.removes) {
|
||||||
|
try {
|
||||||
|
const componentType = ComponentRegistry.getTypeByBitIndex(typeId);
|
||||||
|
if (componentType) {
|
||||||
|
entity.removeComponentByType(componentType);
|
||||||
|
commandCount++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`CommandBuffer: 移除组件失败`, { entity: entity.name, typeId, error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: Adds
|
||||||
|
for (const [entity, ops] of pending) {
|
||||||
|
if (ops.bDestroy || !entity.scene) continue;
|
||||||
|
|
||||||
|
if (ops.adds && ops.adds.size > 0) {
|
||||||
|
for (const component of ops.adds.values()) {
|
||||||
|
try {
|
||||||
|
entity.addComponent(component);
|
||||||
|
commandCount++;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`CommandBuffer: 添加组件失败`, {
|
||||||
|
entity: entity.name,
|
||||||
|
component: component.constructor.name,
|
||||||
|
error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: Active state
|
||||||
|
for (const [entity, ops] of pending) {
|
||||||
|
if (ops.bDestroy || !entity.scene) continue;
|
||||||
|
|
||||||
|
if (ops.active !== undefined) {
|
||||||
|
try {
|
||||||
|
entity.active = ops.active;
|
||||||
|
commandCount++;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`CommandBuffer: 设置激活状态失败`, { entity: entity.name, error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 4: Destroy
|
||||||
|
for (const [entity, ops] of pending) {
|
||||||
|
if (!ops.bDestroy || !entity.scene) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
entity.destroy();
|
||||||
|
commandCount++;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`CommandBuffer: 销毁实体失败`, { entity: entity.name, error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._debug) {
|
||||||
|
logger.debug(`CommandBuffer: 完成执行 ${commandCount} 个延迟命令`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return commandCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用旧模式刷新
|
||||||
|
* Flush using legacy mode
|
||||||
|
*/
|
||||||
|
private flushLegacy(): number {
|
||||||
if (this._commands.length === 0) {
|
if (this._commands.length === 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -214,7 +494,6 @@ export class CommandBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 复制命令列表并清空,防止执行过程中有新命令加入
|
// 复制命令列表并清空,防止执行过程中有新命令加入
|
||||||
// Copy and clear command list to prevent new commands during execution
|
|
||||||
const commands = this._commands;
|
const commands = this._commands;
|
||||||
this._commands = [];
|
this._commands = [];
|
||||||
|
|
||||||
@@ -230,12 +509,11 @@ export class CommandBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行单个命令
|
* 执行单个命令(旧模式)
|
||||||
* Execute single command
|
* Execute single command (legacy mode)
|
||||||
*/
|
*/
|
||||||
private executeCommand(cmd: DeferredCommand): void {
|
private executeCommand(cmd: DeferredCommand): void {
|
||||||
// 检查实体是否仍然有效
|
// 检查实体是否仍然有效
|
||||||
// Check if entity is still valid
|
|
||||||
if (!cmd.entity.scene) {
|
if (!cmd.entity.scene) {
|
||||||
if (this._debug) {
|
if (this._debug) {
|
||||||
logger.debug(`CommandBuffer: 跳过命令,实体 ${cmd.entity.name} 已无效`);
|
logger.debug(`CommandBuffer: 跳过命令,实体 ${cmd.entity.name} 已无效`);
|
||||||
@@ -277,9 +555,13 @@ export class CommandBuffer {
|
|||||||
* Clear all pending commands (without executing)
|
* Clear all pending commands (without executing)
|
||||||
*/
|
*/
|
||||||
public clear(): void {
|
public clear(): void {
|
||||||
if (this._debug && this._commands.length > 0) {
|
if (this._debug) {
|
||||||
logger.debug(`CommandBuffer: 清空 ${this._commands.length} 个未执行的命令`);
|
const count = this._useDeduplication ? this._pending.size : this._commands.length;
|
||||||
|
if (count > 0) {
|
||||||
|
logger.debug(`CommandBuffer: 清空 ${count} 个未执行的命令`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
this._pending.clear();
|
||||||
this._commands.length = 0;
|
this._commands.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
136
packages/core/src/ECS/Core/EntityHandle.ts
Normal file
136
packages/core/src/ECS/Core/EntityHandle.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* 轻量级实体句柄
|
||||||
|
*
|
||||||
|
* 使用数值表示实体,包含索引和代数信息。
|
||||||
|
* 28位索引 + 20位代数 = 48位,在 JavaScript 安全整数范围内。
|
||||||
|
*
|
||||||
|
* Lightweight entity handle.
|
||||||
|
* Uses numeric value to represent entity with index and generation.
|
||||||
|
* 28-bit index + 20-bit generation = 48-bit, within JavaScript safe integer range.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const handle = makeHandle(42, 1);
|
||||||
|
* console.log(indexOf(handle)); // 42
|
||||||
|
* console.log(genOf(handle)); // 1
|
||||||
|
* console.log(isValidHandle(handle)); // true
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实体句柄类型
|
||||||
|
*
|
||||||
|
* 使用 branded type 提供类型安全。
|
||||||
|
*
|
||||||
|
* Entity handle type.
|
||||||
|
* Uses branded type for type safety.
|
||||||
|
*/
|
||||||
|
export type EntityHandle = number & { readonly __brand: 'EntityHandle' };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 索引位数 | Index bits
|
||||||
|
*/
|
||||||
|
export const INDEX_BITS = 28;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代数位数 | Generation bits
|
||||||
|
*/
|
||||||
|
export const GEN_BITS = 20;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 索引掩码 | Index mask
|
||||||
|
*/
|
||||||
|
export const INDEX_MASK = (1 << INDEX_BITS) - 1; // 0x0FFFFFFF
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代数掩码 | Generation mask
|
||||||
|
*/
|
||||||
|
export const GEN_MASK = (1 << GEN_BITS) - 1; // 0x000FFFFF
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大实体数量 | Maximum entity count
|
||||||
|
*/
|
||||||
|
export const MAX_ENTITIES = 1 << INDEX_BITS; // 268,435,456
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大代数值 | Maximum generation value
|
||||||
|
*/
|
||||||
|
export const MAX_GENERATION = 1 << GEN_BITS; // 1,048,576
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 空句柄常量 | Null handle constant
|
||||||
|
*/
|
||||||
|
export const NULL_HANDLE = 0 as EntityHandle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建实体句柄
|
||||||
|
* Create entity handle
|
||||||
|
*
|
||||||
|
* @param index 实体索引 | Entity index
|
||||||
|
* @param generation 实体代数 | Entity generation
|
||||||
|
* @returns 实体句柄 | Entity handle
|
||||||
|
*/
|
||||||
|
export function makeHandle(index: number, generation: number): EntityHandle {
|
||||||
|
// handle = generation * 2^28 + index
|
||||||
|
// 使用乘法而不是位移,因为位移只支持 32 位
|
||||||
|
return ((generation & GEN_MASK) * MAX_ENTITIES + (index & INDEX_MASK)) as EntityHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从句柄提取索引
|
||||||
|
* Extract index from handle
|
||||||
|
*
|
||||||
|
* @param handle 实体句柄 | Entity handle
|
||||||
|
* @returns 实体索引 | Entity index
|
||||||
|
*/
|
||||||
|
export function indexOf(handle: EntityHandle): number {
|
||||||
|
return handle & INDEX_MASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从句柄提取代数
|
||||||
|
* Extract generation from handle
|
||||||
|
*
|
||||||
|
* @param handle 实体句柄 | Entity handle
|
||||||
|
* @returns 实体代数 | Entity generation
|
||||||
|
*/
|
||||||
|
export function genOf(handle: EntityHandle): number {
|
||||||
|
return Math.floor(handle / MAX_ENTITIES) & GEN_MASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查句柄是否有效(非空)
|
||||||
|
* Check if handle is valid (non-null)
|
||||||
|
*
|
||||||
|
* @param handle 实体句柄 | Entity handle
|
||||||
|
* @returns 是否有效 | Whether valid
|
||||||
|
*/
|
||||||
|
export function isValidHandle(handle: EntityHandle): boolean {
|
||||||
|
return handle !== NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 比较两个句柄是否相等
|
||||||
|
* Compare two handles for equality
|
||||||
|
*
|
||||||
|
* @param a 第一个句柄 | First handle
|
||||||
|
* @param b 第二个句柄 | Second handle
|
||||||
|
* @returns 是否相等 | Whether equal
|
||||||
|
*/
|
||||||
|
export function handleEquals(a: EntityHandle, b: EntityHandle): boolean {
|
||||||
|
return a === b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将句柄转换为字符串(用于调试)
|
||||||
|
* Convert handle to string (for debugging)
|
||||||
|
*
|
||||||
|
* @param handle 实体句柄 | Entity handle
|
||||||
|
* @returns 字符串表示 | String representation
|
||||||
|
*/
|
||||||
|
export function handleToString(handle: EntityHandle): string {
|
||||||
|
if (handle === NULL_HANDLE) {
|
||||||
|
return 'Entity(NULL)';
|
||||||
|
}
|
||||||
|
return `Entity(idx=${indexOf(handle)}, gen=${genOf(handle)})`;
|
||||||
|
}
|
||||||
331
packages/core/src/ECS/Core/EntityHandleManager.ts
Normal file
331
packages/core/src/ECS/Core/EntityHandleManager.ts
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
/**
|
||||||
|
* 实体句柄管理器
|
||||||
|
*
|
||||||
|
* 管理轻量级实体句柄的生命周期,包括创建、销毁和状态查询。
|
||||||
|
* 使用 TypedArray 实现高效的内存布局。
|
||||||
|
*
|
||||||
|
* Entity handle manager.
|
||||||
|
* Manages lifecycle of lightweight entity handles including creation, destruction and state queries.
|
||||||
|
* Uses TypedArray for efficient memory layout.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const manager = new EntityHandleManager();
|
||||||
|
* const handle = manager.create();
|
||||||
|
* console.log(manager.isAlive(handle)); // true
|
||||||
|
*
|
||||||
|
* manager.destroy(handle);
|
||||||
|
* console.log(manager.isAlive(handle)); // false
|
||||||
|
*
|
||||||
|
* // 索引会被复用,但代数会增加
|
||||||
|
* const newHandle = manager.create();
|
||||||
|
* console.log(indexOf(handle) === indexOf(newHandle)); // true
|
||||||
|
* console.log(genOf(newHandle) > genOf(handle)); // true
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
EntityHandle,
|
||||||
|
makeHandle,
|
||||||
|
indexOf,
|
||||||
|
genOf,
|
||||||
|
MAX_ENTITIES,
|
||||||
|
MAX_GENERATION
|
||||||
|
} from './EntityHandle';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始容量 | Initial capacity
|
||||||
|
*/
|
||||||
|
const INITIAL_CAPACITY = 1024;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实体句柄管理器
|
||||||
|
*
|
||||||
|
* Entity handle manager.
|
||||||
|
*/
|
||||||
|
export class EntityHandleManager {
|
||||||
|
/**
|
||||||
|
* 代数数组(每个索引的当前代数)
|
||||||
|
* Generation array (current generation for each index)
|
||||||
|
*/
|
||||||
|
private _generations: Uint32Array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存活状态数组(0=死亡,1=存活)
|
||||||
|
* Alive state array (0=dead, 1=alive)
|
||||||
|
*/
|
||||||
|
private _alive: Uint8Array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用状态数组(0=禁用,1=启用)
|
||||||
|
* Enabled state array (0=disabled, 1=enabled)
|
||||||
|
*/
|
||||||
|
private _enabled: Uint8Array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 空闲索引列表(用于复用已销毁的索引)
|
||||||
|
* Free index list (for reusing destroyed indices)
|
||||||
|
*/
|
||||||
|
private _freeList: number[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下一个新索引
|
||||||
|
* Next new index
|
||||||
|
*/
|
||||||
|
private _nextIndex: number = 1; // 从 1 开始,0 保留给 NULL_HANDLE
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前存活实体数量
|
||||||
|
* Current alive entity count
|
||||||
|
*/
|
||||||
|
private _aliveCount: number = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前容量
|
||||||
|
* Current capacity
|
||||||
|
*/
|
||||||
|
private _capacity: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建实体句柄管理器
|
||||||
|
* Create entity handle manager
|
||||||
|
*
|
||||||
|
* @param initialCapacity 初始容量 | Initial capacity
|
||||||
|
*/
|
||||||
|
constructor(initialCapacity: number = INITIAL_CAPACITY) {
|
||||||
|
this._capacity = initialCapacity;
|
||||||
|
this._generations = new Uint32Array(initialCapacity);
|
||||||
|
this._alive = new Uint8Array(initialCapacity);
|
||||||
|
this._enabled = new Uint8Array(initialCapacity);
|
||||||
|
|
||||||
|
// 索引 0 保留给 NULL_HANDLE
|
||||||
|
this._alive[0] = 0;
|
||||||
|
this._enabled[0] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取存活实体数量
|
||||||
|
* Get alive entity count
|
||||||
|
*/
|
||||||
|
public get aliveCount(): number {
|
||||||
|
return this._aliveCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前容量
|
||||||
|
* Get current capacity
|
||||||
|
*/
|
||||||
|
public get capacity(): number {
|
||||||
|
return this._capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建新实体句柄
|
||||||
|
* Create new entity handle
|
||||||
|
*
|
||||||
|
* @returns 新实体句柄 | New entity handle
|
||||||
|
*/
|
||||||
|
public create(): EntityHandle {
|
||||||
|
let index: number;
|
||||||
|
|
||||||
|
// 优先从空闲列表中获取索引
|
||||||
|
if (this._freeList.length > 0) {
|
||||||
|
index = this._freeList.pop()!;
|
||||||
|
} else {
|
||||||
|
// 分配新索引
|
||||||
|
index = this._nextIndex++;
|
||||||
|
|
||||||
|
// 检查是否需要扩容
|
||||||
|
if (index >= this._capacity) {
|
||||||
|
this.grow(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前代数(可能因之前销毁而增加)
|
||||||
|
const generation = this._generations[index]!;
|
||||||
|
|
||||||
|
// 标记为存活和启用
|
||||||
|
this._alive[index] = 1;
|
||||||
|
this._enabled[index] = 1;
|
||||||
|
this._aliveCount++;
|
||||||
|
|
||||||
|
return makeHandle(index, generation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁实体句柄
|
||||||
|
* Destroy entity handle
|
||||||
|
*
|
||||||
|
* @param handle 要销毁的句柄 | Handle to destroy
|
||||||
|
* @returns 是否成功销毁 | Whether destruction succeeded
|
||||||
|
*/
|
||||||
|
public destroy(handle: EntityHandle): boolean {
|
||||||
|
const index = indexOf(handle);
|
||||||
|
const generation = genOf(handle);
|
||||||
|
|
||||||
|
// 验证句柄有效性
|
||||||
|
if (index >= this._capacity || index === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._generations[index] !== generation) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._alive[index] !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记为死亡
|
||||||
|
this._alive[index] = 0;
|
||||||
|
this._enabled[index] = 0;
|
||||||
|
this._aliveCount--;
|
||||||
|
|
||||||
|
// 增加代数(防止 ABA 问题)
|
||||||
|
const newGen = (generation + 1) % MAX_GENERATION;
|
||||||
|
this._generations[index] = newGen;
|
||||||
|
|
||||||
|
// 将索引加入空闲列表
|
||||||
|
this._freeList.push(index);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查句柄是否存活
|
||||||
|
* Check if handle is alive
|
||||||
|
*
|
||||||
|
* @param handle 实体句柄 | Entity handle
|
||||||
|
* @returns 是否存活 | Whether alive
|
||||||
|
*/
|
||||||
|
public isAlive(handle: EntityHandle): boolean {
|
||||||
|
const index = indexOf(handle);
|
||||||
|
const generation = genOf(handle);
|
||||||
|
|
||||||
|
if (index >= this._capacity || index === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._alive[index] === 1 && this._generations[index] === generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查句柄是否启用
|
||||||
|
* Check if handle is enabled
|
||||||
|
*
|
||||||
|
* @param handle 实体句柄 | Entity handle
|
||||||
|
* @returns 是否启用 | Whether enabled
|
||||||
|
*/
|
||||||
|
public isEnabled(handle: EntityHandle): boolean {
|
||||||
|
if (!this.isAlive(handle)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = indexOf(handle);
|
||||||
|
return this._enabled[index] === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置句柄启用状态
|
||||||
|
* Set handle enabled state
|
||||||
|
*
|
||||||
|
* @param handle 实体句柄 | Entity handle
|
||||||
|
* @param enabled 启用状态 | Enabled state
|
||||||
|
* @returns 是否成功设置 | Whether setting succeeded
|
||||||
|
*/
|
||||||
|
public setEnabled(handle: EntityHandle, enabled: boolean): boolean {
|
||||||
|
if (!this.isAlive(handle)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = indexOf(handle);
|
||||||
|
this._enabled[index] = enabled ? 1 : 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证句柄是否有效(存活且代数匹配)
|
||||||
|
* Validate if handle is valid (alive and generation matches)
|
||||||
|
*
|
||||||
|
* @param handle 实体句柄 | Entity handle
|
||||||
|
* @returns 是否有效 | Whether valid
|
||||||
|
*/
|
||||||
|
public validate(handle: EntityHandle): boolean {
|
||||||
|
return this.isAlive(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扩容数组
|
||||||
|
* Grow arrays
|
||||||
|
*/
|
||||||
|
private grow(minIndex: number): void {
|
||||||
|
let newSize = this._capacity;
|
||||||
|
|
||||||
|
// 按 2 倍扩容
|
||||||
|
while (newSize <= minIndex) {
|
||||||
|
newSize <<= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否超过最大容量
|
||||||
|
if (newSize > MAX_ENTITIES) {
|
||||||
|
newSize = MAX_ENTITIES;
|
||||||
|
if (minIndex >= newSize) {
|
||||||
|
throw new Error(`EntityHandleManager: 超过最大实体数量 ${MAX_ENTITIES}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新数组并复制数据
|
||||||
|
const newGenerations = new Uint32Array(newSize);
|
||||||
|
const newAlive = new Uint8Array(newSize);
|
||||||
|
const newEnabled = new Uint8Array(newSize);
|
||||||
|
|
||||||
|
newGenerations.set(this._generations);
|
||||||
|
newAlive.set(this._alive);
|
||||||
|
newEnabled.set(this._enabled);
|
||||||
|
|
||||||
|
this._generations = newGenerations;
|
||||||
|
this._alive = newAlive;
|
||||||
|
this._enabled = newEnabled;
|
||||||
|
this._capacity = newSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置管理器状态
|
||||||
|
* Reset manager state
|
||||||
|
*/
|
||||||
|
public reset(): void {
|
||||||
|
this._generations.fill(0);
|
||||||
|
this._alive.fill(0);
|
||||||
|
this._enabled.fill(0);
|
||||||
|
this._freeList.length = 0;
|
||||||
|
this._nextIndex = 1;
|
||||||
|
this._aliveCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 遍历所有存活的句柄
|
||||||
|
* Iterate all alive handles
|
||||||
|
*
|
||||||
|
* @param callback 回调函数 | Callback function
|
||||||
|
*/
|
||||||
|
public forEach(callback: (handle: EntityHandle) => void): void {
|
||||||
|
for (let i = 1; i < this._nextIndex; i++) {
|
||||||
|
if (this._alive[i] === 1) {
|
||||||
|
const handle = makeHandle(i, this._generations[i]!);
|
||||||
|
callback(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有存活的句柄
|
||||||
|
* Get all alive handles
|
||||||
|
*
|
||||||
|
* @returns 存活句柄数组 | Alive handles array
|
||||||
|
*/
|
||||||
|
public getAllAlive(): EntityHandle[] {
|
||||||
|
const result: EntityHandle[] = [];
|
||||||
|
this.forEach((handle) => result.push(handle));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
103
packages/core/src/ECS/Core/EpochManager.ts
Normal file
103
packages/core/src/ECS/Core/EpochManager.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* 帧级变更检测管理器
|
||||||
|
*
|
||||||
|
* 基于 epoch(帧计数)的组件变更追踪系统。
|
||||||
|
* 每帧递增 epoch,组件写入时记录当前 epoch,
|
||||||
|
* 系统可以查询自上次检查以来发生变更的组件。
|
||||||
|
*
|
||||||
|
* Frame-level change detection manager.
|
||||||
|
* Epoch-based component change tracking system.
|
||||||
|
* Epoch increments each frame, components record current epoch on write,
|
||||||
|
* systems can query components changed since last check.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* // 在 System 中使用
|
||||||
|
* class MovementSystem extends EntitySystem {
|
||||||
|
* private _lastEpoch = 0;
|
||||||
|
*
|
||||||
|
* process(): void {
|
||||||
|
* const epoch = this.scene.epochManager;
|
||||||
|
*
|
||||||
|
* // 只处理 Velocity 变化的实体
|
||||||
|
* for (const entity of this.entities) {
|
||||||
|
* const vel = entity.getComponent(Velocity);
|
||||||
|
* if (vel && vel.lastWriteEpoch > this._lastEpoch) {
|
||||||
|
* // 处理变更
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* this._lastEpoch = epoch.current;
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Epoch 管理器
|
||||||
|
*
|
||||||
|
* Epoch manager.
|
||||||
|
*/
|
||||||
|
export class EpochManager {
|
||||||
|
/**
|
||||||
|
* 当前 epoch 值
|
||||||
|
*
|
||||||
|
* 从 1 开始,0 表示"从未写入"。
|
||||||
|
*
|
||||||
|
* Current epoch value.
|
||||||
|
* Starts from 1, 0 means "never written".
|
||||||
|
*/
|
||||||
|
private _current: number = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前 epoch
|
||||||
|
*
|
||||||
|
* Get current epoch.
|
||||||
|
*/
|
||||||
|
public get current(): number {
|
||||||
|
return this._current;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递增 epoch
|
||||||
|
*
|
||||||
|
* 应在每帧开始时调用。
|
||||||
|
*
|
||||||
|
* Increment epoch.
|
||||||
|
* Should be called at the start of each frame.
|
||||||
|
*/
|
||||||
|
public increment(): void {
|
||||||
|
this._current++;
|
||||||
|
|
||||||
|
// 处理溢出(虽然 2^53 帧基本不可能达到)
|
||||||
|
// Handle overflow (though 2^53 frames is practically unreachable)
|
||||||
|
if (this._current >= Number.MAX_SAFE_INTEGER) {
|
||||||
|
this._current = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置 epoch
|
||||||
|
*
|
||||||
|
* 在场景重置时调用。
|
||||||
|
*
|
||||||
|
* Reset epoch.
|
||||||
|
* Called when scene is reset.
|
||||||
|
*/
|
||||||
|
public reset(): void {
|
||||||
|
this._current = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查给定 epoch 是否在指定 epoch 之后发生变更
|
||||||
|
*
|
||||||
|
* Check if given epoch is after the specified epoch (changed since).
|
||||||
|
*
|
||||||
|
* @param writeEpoch 写入时的 epoch | Epoch when written
|
||||||
|
* @param sinceEpoch 检查点 epoch | Checkpoint epoch
|
||||||
|
* @returns 是否在检查点之后发生变更 | Whether changed after checkpoint
|
||||||
|
*/
|
||||||
|
public isChangedSince(writeEpoch: number, sinceEpoch: number): boolean {
|
||||||
|
return writeEpoch > sinceEpoch;
|
||||||
|
}
|
||||||
|
}
|
||||||
366
packages/core/src/ECS/Core/Query/CompiledQuery.ts
Normal file
366
packages/core/src/ECS/Core/Query/CompiledQuery.ts
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
/**
|
||||||
|
* 编译查询
|
||||||
|
*
|
||||||
|
* 预编译的查询执行计划,避免每次查询时重复构建匹配条件。
|
||||||
|
* 提供便捷的迭代方法和变更检测支持。
|
||||||
|
*
|
||||||
|
* Compiled Query.
|
||||||
|
* Pre-compiled query execution plan that avoids repeated condition building.
|
||||||
|
* Provides convenient iteration methods and change detection support.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* class MovementSystem extends EntitySystem {
|
||||||
|
* private _query: CompiledQuery<[typeof Position, typeof Velocity]>;
|
||||||
|
*
|
||||||
|
* onInitialize(): void {
|
||||||
|
* this._query = this.scene.querySystem.compile(Position, Velocity);
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* update(): void {
|
||||||
|
* this._query.forEach((entity, pos, vel) => {
|
||||||
|
* pos.x += vel.x * this.deltaTime;
|
||||||
|
* pos.y += vel.y * this.deltaTime;
|
||||||
|
* });
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Entity } from '../../Entity';
|
||||||
|
import { Component } from '../../Component';
|
||||||
|
import { ComponentType } from '../ComponentStorage';
|
||||||
|
import { QuerySystem } from '../QuerySystem';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件实例类型提取
|
||||||
|
*
|
||||||
|
* Extract component instance types from component type tuple.
|
||||||
|
*/
|
||||||
|
export type InstanceTypes<T extends ComponentType[]> = {
|
||||||
|
[K in keyof T]: T[K] extends ComponentType<infer C> ? C : never;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编译查询类
|
||||||
|
*
|
||||||
|
* Compiled query class.
|
||||||
|
*/
|
||||||
|
export class CompiledQuery<T extends ComponentType[] = ComponentType[]> {
|
||||||
|
/**
|
||||||
|
* 查询的组件类型列表
|
||||||
|
*
|
||||||
|
* Component types for this query.
|
||||||
|
*/
|
||||||
|
private readonly _componentTypes: T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询系统引用
|
||||||
|
*
|
||||||
|
* Reference to query system.
|
||||||
|
*/
|
||||||
|
private readonly _querySystem: QuerySystem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上次查询的版本号(用于缓存失效检测)
|
||||||
|
*
|
||||||
|
* Last query version (for cache invalidation detection).
|
||||||
|
*/
|
||||||
|
private _lastVersion: number = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存的实体列表
|
||||||
|
*
|
||||||
|
* Cached entity list.
|
||||||
|
*/
|
||||||
|
private _cachedEntities: readonly Entity[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建编译查询
|
||||||
|
*
|
||||||
|
* Create compiled query.
|
||||||
|
*
|
||||||
|
* @param querySystem 查询系统引用 | Query system reference
|
||||||
|
* @param componentTypes 组件类型列表 | Component type list
|
||||||
|
*/
|
||||||
|
constructor(querySystem: QuerySystem, ...componentTypes: T) {
|
||||||
|
this._querySystem = querySystem;
|
||||||
|
this._componentTypes = componentTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取组件类型列表
|
||||||
|
*
|
||||||
|
* Get component type list.
|
||||||
|
*/
|
||||||
|
public get componentTypes(): readonly ComponentType[] {
|
||||||
|
return this._componentTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取匹配的实体列表
|
||||||
|
*
|
||||||
|
* Get matching entity list.
|
||||||
|
*/
|
||||||
|
public get entities(): readonly Entity[] {
|
||||||
|
this._refreshCache();
|
||||||
|
return this._cachedEntities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取匹配的实体数量
|
||||||
|
*
|
||||||
|
* Get matching entity count.
|
||||||
|
*/
|
||||||
|
public get count(): number {
|
||||||
|
return this.entities.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新缓存
|
||||||
|
*
|
||||||
|
* Refresh cache if needed.
|
||||||
|
*/
|
||||||
|
private _refreshCache(): void {
|
||||||
|
const currentVersion = this._querySystem.version;
|
||||||
|
if (this._lastVersion !== currentVersion) {
|
||||||
|
const result = this._querySystem.queryAll(...this._componentTypes);
|
||||||
|
this._cachedEntities = result.entities;
|
||||||
|
this._lastVersion = currentVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 遍历所有匹配的实体及其组件
|
||||||
|
*
|
||||||
|
* Iterate all matching entities with their components.
|
||||||
|
*
|
||||||
|
* @param callback 回调函数,接收实体和组件实例 | Callback receiving entity and component instances
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* query.forEach((entity, position, velocity) => {
|
||||||
|
* position.x += velocity.x * deltaTime;
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public forEach(
|
||||||
|
callback: (entity: Entity, ...components: InstanceTypes<T>) => void
|
||||||
|
): void {
|
||||||
|
const entities = this.entities;
|
||||||
|
const componentTypes = this._componentTypes;
|
||||||
|
const typeCount = componentTypes.length;
|
||||||
|
|
||||||
|
for (let i = 0, len = entities.length; i < len; i++) {
|
||||||
|
const entity = entities[i]!;
|
||||||
|
const components: Component[] = new Array(typeCount);
|
||||||
|
|
||||||
|
// 获取所有组件
|
||||||
|
for (let j = 0; j < typeCount; j++) {
|
||||||
|
const component = entity.getComponent(componentTypes[j]!);
|
||||||
|
if (!component) {
|
||||||
|
// 组件不存在,跳过这个实体
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
components[j] = component;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用回调
|
||||||
|
callback(entity, ...(components as InstanceTypes<T>));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 遍历自指定 epoch 以来发生变更的实体
|
||||||
|
*
|
||||||
|
* Iterate entities with components changed since specified epoch.
|
||||||
|
*
|
||||||
|
* @param sinceEpoch 检查点 epoch | Checkpoint epoch
|
||||||
|
* @param callback 回调函数 | Callback function
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* class PhysicsSystem extends EntitySystem {
|
||||||
|
* private _lastEpoch = 0;
|
||||||
|
*
|
||||||
|
* update(): void {
|
||||||
|
* this._query.forEachChanged(this._lastEpoch, (entity, pos, vel) => {
|
||||||
|
* // 只处理变更的实体
|
||||||
|
* });
|
||||||
|
* this._lastEpoch = this.scene.epochManager.current;
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public forEachChanged(
|
||||||
|
sinceEpoch: number,
|
||||||
|
callback: (entity: Entity, ...components: InstanceTypes<T>) => void
|
||||||
|
): void {
|
||||||
|
const entities = this.entities;
|
||||||
|
const componentTypes = this._componentTypes;
|
||||||
|
const typeCount = componentTypes.length;
|
||||||
|
|
||||||
|
for (let i = 0, len = entities.length; i < len; i++) {
|
||||||
|
const entity = entities[i]!;
|
||||||
|
const components: Component[] = new Array(typeCount);
|
||||||
|
let hasChanged = false;
|
||||||
|
|
||||||
|
// 获取所有组件并检查变更
|
||||||
|
for (let j = 0; j < typeCount; j++) {
|
||||||
|
const component = entity.getComponent(componentTypes[j]!);
|
||||||
|
if (!component) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
components[j] = component;
|
||||||
|
|
||||||
|
if (component.lastWriteEpoch > sinceEpoch) {
|
||||||
|
hasChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只在有变更时调用回调
|
||||||
|
if (hasChanged) {
|
||||||
|
callback(entity, ...(components as InstanceTypes<T>));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取第一个匹配的实体及其组件
|
||||||
|
*
|
||||||
|
* Get first matching entity with its components.
|
||||||
|
*
|
||||||
|
* @returns 实体和组件元组,或 null | Entity and components tuple, or null
|
||||||
|
*/
|
||||||
|
public first(): [Entity, ...InstanceTypes<T>] | null {
|
||||||
|
const entities = this.entities;
|
||||||
|
if (entities.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entity = entities[0]!;
|
||||||
|
const components: Component[] = [];
|
||||||
|
|
||||||
|
for (const type of this._componentTypes) {
|
||||||
|
const component = entity.getComponent(type);
|
||||||
|
if (!component) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
components.push(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [entity, ...(components as InstanceTypes<T>)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为数组
|
||||||
|
*
|
||||||
|
* Convert to array.
|
||||||
|
*
|
||||||
|
* @returns 实体和组件元组数组 | Array of entity and components tuples
|
||||||
|
*/
|
||||||
|
public toArray(): Array<[Entity, ...InstanceTypes<T>]> {
|
||||||
|
const result: Array<[Entity, ...InstanceTypes<T>]> = [];
|
||||||
|
|
||||||
|
this.forEach((entity, ...components) => {
|
||||||
|
result.push([entity, ...components]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 映射转换
|
||||||
|
*
|
||||||
|
* Map transformation.
|
||||||
|
*
|
||||||
|
* @param callback 转换函数 | Transform function
|
||||||
|
* @returns 转换结果数组 | Array of transform results
|
||||||
|
*/
|
||||||
|
public map<R>(
|
||||||
|
callback: (entity: Entity, ...components: InstanceTypes<T>) => R
|
||||||
|
): R[] {
|
||||||
|
const result: R[] = [];
|
||||||
|
|
||||||
|
this.forEach((entity, ...components) => {
|
||||||
|
result.push(callback(entity, ...components));
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过滤实体
|
||||||
|
*
|
||||||
|
* Filter entities.
|
||||||
|
*
|
||||||
|
* @param predicate 过滤条件 | Filter predicate
|
||||||
|
* @returns 过滤后的实体数组 | Filtered entity array
|
||||||
|
*/
|
||||||
|
public filter(
|
||||||
|
predicate: (entity: Entity, ...components: InstanceTypes<T>) => boolean
|
||||||
|
): Entity[] {
|
||||||
|
const result: Entity[] = [];
|
||||||
|
|
||||||
|
this.forEach((entity, ...components) => {
|
||||||
|
if (predicate(entity, ...components)) {
|
||||||
|
result.push(entity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找满足条件的实体
|
||||||
|
*
|
||||||
|
* Find entity matching predicate.
|
||||||
|
*
|
||||||
|
* @param predicate 查找条件 | Find predicate
|
||||||
|
* @returns 找到的实体或 undefined | Found entity or undefined
|
||||||
|
*/
|
||||||
|
public find(
|
||||||
|
predicate: (entity: Entity, ...components: InstanceTypes<T>) => boolean
|
||||||
|
): Entity | undefined {
|
||||||
|
const entities = this.entities;
|
||||||
|
const componentTypes = this._componentTypes;
|
||||||
|
const typeCount = componentTypes.length;
|
||||||
|
|
||||||
|
for (let i = 0, len = entities.length; i < len; i++) {
|
||||||
|
const entity = entities[i]!;
|
||||||
|
const components: Component[] = new Array(typeCount);
|
||||||
|
|
||||||
|
for (let j = 0; j < typeCount; j++) {
|
||||||
|
const component = entity.getComponent(componentTypes[j]!);
|
||||||
|
if (!component) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
components[j] = component;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (predicate(entity, ...(components as InstanceTypes<T>))) {
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否有任何实体匹配
|
||||||
|
*
|
||||||
|
* Check if any entity matches.
|
||||||
|
*/
|
||||||
|
public any(): boolean {
|
||||||
|
return this.count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否没有实体匹配
|
||||||
|
*
|
||||||
|
* Check if no entity matches.
|
||||||
|
*/
|
||||||
|
public empty(): boolean {
|
||||||
|
return this.count === 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,10 @@ import { getComponentTypeName } from '../Decorators';
|
|||||||
import { Archetype, ArchetypeSystem } from './ArchetypeSystem';
|
import { Archetype, ArchetypeSystem } from './ArchetypeSystem';
|
||||||
import { ReactiveQuery, ReactiveQueryConfig } from './ReactiveQuery';
|
import { ReactiveQuery, ReactiveQueryConfig } from './ReactiveQuery';
|
||||||
import { QueryCondition, QueryConditionType, QueryResult } from './QueryTypes';
|
import { QueryCondition, QueryConditionType, QueryResult } from './QueryTypes';
|
||||||
|
import { CompiledQuery } from './Query/CompiledQuery';
|
||||||
|
|
||||||
export { QueryCondition, QueryConditionType, QueryResult };
|
export { QueryCondition, QueryConditionType, QueryResult };
|
||||||
|
export { CompiledQuery };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 实体索引结构
|
* 实体索引结构
|
||||||
@@ -530,6 +532,76 @@ export class QuerySystem {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询自指定 epoch 以来发生变更的实体
|
||||||
|
*
|
||||||
|
* 返回拥有指定组件且该组件在 sinceEpoch 之后被修改过的实体。
|
||||||
|
* 用于实现增量更新,只处理发生变化的实体。
|
||||||
|
*
|
||||||
|
* Query entities with components changed since the specified epoch.
|
||||||
|
* Returns entities that have the specified component type(s) and that component
|
||||||
|
* was modified after sinceEpoch.
|
||||||
|
* Used for incremental updates, only processing entities that have changed.
|
||||||
|
*
|
||||||
|
* @param sinceEpoch 上次检查的 epoch | Last checked epoch
|
||||||
|
* @param componentTypes 要检查的组件类型 | Component types to check
|
||||||
|
* @returns 查询结果,包含变更的实体 | Query result with changed entities
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* class MovementSystem extends EntitySystem {
|
||||||
|
* private _lastEpoch = 0;
|
||||||
|
*
|
||||||
|
* update(): void {
|
||||||
|
* // 只处理 Velocity 组件发生变化的实体
|
||||||
|
* const result = this.scene.querySystem.queryChangedSince(
|
||||||
|
* this._lastEpoch,
|
||||||
|
* Velocity
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* for (const entity of result.entities) {
|
||||||
|
* // 处理变更的实体
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* this._lastEpoch = this.scene.epochManager.current;
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public queryChangedSince(sinceEpoch: number, ...componentTypes: ComponentType[]): QueryResult {
|
||||||
|
const startTime = performance.now();
|
||||||
|
this._queryStats.totalQueries++;
|
||||||
|
this._queryStats.dirtyChecks++;
|
||||||
|
|
||||||
|
// 先获取所有拥有这些组件的实体
|
||||||
|
const baseResult = this.queryAll(...componentTypes);
|
||||||
|
const changedEntities: Entity[] = [];
|
||||||
|
|
||||||
|
// 过滤出组件发生变更的实体
|
||||||
|
for (const entity of baseResult.entities) {
|
||||||
|
let hasChanged = false;
|
||||||
|
|
||||||
|
for (const componentType of componentTypes) {
|
||||||
|
const component = entity.getComponent(componentType);
|
||||||
|
if (component && component.lastWriteEpoch > sinceEpoch) {
|
||||||
|
hasChanged = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChanged) {
|
||||||
|
changedEntities.push(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
entities: changedEntities,
|
||||||
|
count: changedEntities.length,
|
||||||
|
executionTime: performance.now() - startTime,
|
||||||
|
fromCache: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按单个组件类型查询实体
|
* 按单个组件类型查询实体
|
||||||
*
|
*
|
||||||
@@ -704,6 +776,42 @@ export class QuerySystem {
|
|||||||
this.clearReactiveQueries();
|
this.clearReactiveQueries();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编译查询
|
||||||
|
*
|
||||||
|
* 创建预编译的查询对象,避免每次查询时重复构建匹配条件。
|
||||||
|
* 适用于在 System 中频繁执行的查询,提供便捷的迭代方法。
|
||||||
|
*
|
||||||
|
* Compile query.
|
||||||
|
* Creates a pre-compiled query object that avoids repeated condition building.
|
||||||
|
* Suitable for frequently executed queries in Systems, provides convenient iteration.
|
||||||
|
*
|
||||||
|
* @param componentTypes 要查询的组件类型 | Component types to query
|
||||||
|
* @returns 编译查询实例 | Compiled query instance
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* class MovementSystem extends EntitySystem {
|
||||||
|
* private _query: CompiledQuery<[typeof Position, typeof Velocity]>;
|
||||||
|
*
|
||||||
|
* onInitialize(): void {
|
||||||
|
* this._query = this.scene.querySystem.compile(Position, Velocity);
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* update(): void {
|
||||||
|
* this._query.forEach((entity, pos, vel) => {
|
||||||
|
* pos.x += vel.x * this.deltaTime;
|
||||||
|
* pos.y += vel.y * this.deltaTime;
|
||||||
|
* pos.markDirty(this.scene.epochManager.current);
|
||||||
|
* });
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public compile<T extends ComponentType[]>(...componentTypes: T): CompiledQuery<T> {
|
||||||
|
return new CompiledQuery<T>(this, ...componentTypes);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建响应式查询
|
* 创建响应式查询
|
||||||
*
|
*
|
||||||
|
|||||||
263
packages/core/src/ECS/Core/SystemDependencyGraph.ts
Normal file
263
packages/core/src/ECS/Core/SystemDependencyGraph.ts
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
/**
|
||||||
|
* 系统依赖图
|
||||||
|
*
|
||||||
|
* 用于构建系统间的依赖关系并进行拓扑排序。
|
||||||
|
* 支持 before/after 依赖声明和 set 分组。
|
||||||
|
*
|
||||||
|
* System dependency graph.
|
||||||
|
* Used to build dependency relationships between systems and perform topological sorting.
|
||||||
|
* Supports before/after dependencies and set grouping.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 循环依赖错误
|
||||||
|
* Cycle dependency error
|
||||||
|
*/
|
||||||
|
export class CycleDependencyError extends Error {
|
||||||
|
/**
|
||||||
|
* 参与循环的节点 ID
|
||||||
|
* Node IDs involved in the cycle
|
||||||
|
*/
|
||||||
|
public readonly involvedNodes: string[];
|
||||||
|
|
||||||
|
constructor(involvedNodes: string[]) {
|
||||||
|
const message = `[SystemDependencyGraph] 检测到循环依赖 | Cycle dependency detected: ${involvedNodes.join(' -> ')}`;
|
||||||
|
super(message);
|
||||||
|
this.name = 'CycleDependencyError';
|
||||||
|
this.involvedNodes = involvedNodes;
|
||||||
|
Object.setPrototypeOf(this, new.target.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 依赖图节点
|
||||||
|
* Dependency graph node
|
||||||
|
*/
|
||||||
|
interface GraphNode {
|
||||||
|
/** 节点 ID | Node ID */
|
||||||
|
id: string;
|
||||||
|
/** 是否为虚拟集合节点 | Whether this is a virtual set node */
|
||||||
|
bIsVirtual: boolean;
|
||||||
|
/** 入边(依赖于此节点的节点) | Incoming edges (nodes that depend on this node) */
|
||||||
|
inEdges: Set<string>;
|
||||||
|
/** 出边(此节点依赖的节点) | Outgoing edges (nodes this node depends on) */
|
||||||
|
outEdges: Set<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统依赖信息
|
||||||
|
* System dependency info
|
||||||
|
*/
|
||||||
|
export interface SystemDependencyInfo {
|
||||||
|
/** 系统名称 | System name */
|
||||||
|
name: string;
|
||||||
|
/** 在这些系统之前执行 | Execute before these systems */
|
||||||
|
before: string[];
|
||||||
|
/** 在这些系统之后执行 | Execute after these systems */
|
||||||
|
after: string[];
|
||||||
|
/** 所属集合 | Sets this system belongs to */
|
||||||
|
sets: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 集合前缀 | Set prefix */
|
||||||
|
const SET_PREFIX = 'set:';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统依赖图
|
||||||
|
*
|
||||||
|
* 使用 Kahn 算法进行拓扑排序,检测循环依赖。
|
||||||
|
*
|
||||||
|
* System dependency graph.
|
||||||
|
* Uses Kahn's algorithm for topological sorting and cycle detection.
|
||||||
|
*/
|
||||||
|
export class SystemDependencyGraph {
|
||||||
|
/** 节点映射 | Node map */
|
||||||
|
private _nodes: Map<string, GraphNode> = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加系统节点
|
||||||
|
* Add system node
|
||||||
|
*
|
||||||
|
* @param name 系统名称 | System name
|
||||||
|
*/
|
||||||
|
public addSystemNode(name: string): void {
|
||||||
|
this.getOrCreateNode(name, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加集合节点(虚拟节点)
|
||||||
|
* Add set node (virtual node)
|
||||||
|
*
|
||||||
|
* @param setName 集合名称 | Set name
|
||||||
|
*/
|
||||||
|
public addSetNode(setName: string): void {
|
||||||
|
const nodeId = SET_PREFIX + setName;
|
||||||
|
this.getOrCreateNode(nodeId, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加依赖边
|
||||||
|
* Add dependency edge
|
||||||
|
*
|
||||||
|
* @param from 起始节点 | Source node
|
||||||
|
* @param to 目标节点 | Target node
|
||||||
|
*/
|
||||||
|
public addEdge(from: string, to: string): void {
|
||||||
|
if (from === to) return;
|
||||||
|
|
||||||
|
const fromNode = this.getOrCreateNode(from, from.startsWith(SET_PREFIX));
|
||||||
|
const toNode = this.getOrCreateNode(to, to.startsWith(SET_PREFIX));
|
||||||
|
|
||||||
|
fromNode.outEdges.add(to);
|
||||||
|
toNode.inEdges.add(from);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从系统依赖信息构建图
|
||||||
|
* Build graph from system dependency info
|
||||||
|
*
|
||||||
|
* @param systems 系统依赖信息列表 | System dependency info list
|
||||||
|
*/
|
||||||
|
public buildFromSystems(systems: SystemDependencyInfo[]): void {
|
||||||
|
this.clear();
|
||||||
|
|
||||||
|
// 1. 添加所有系统节点
|
||||||
|
for (const sys of systems) {
|
||||||
|
this.addSystemNode(sys.name);
|
||||||
|
|
||||||
|
// 添加集合节点
|
||||||
|
for (const setName of sys.sets) {
|
||||||
|
this.addSetNode(setName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 添加边
|
||||||
|
for (const sys of systems) {
|
||||||
|
// 集合 -> 系统(系统属于集合,集合执行后系统执行)
|
||||||
|
for (const setName of sys.sets) {
|
||||||
|
const setId = SET_PREFIX + setName;
|
||||||
|
this.addEdge(setId, sys.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// before: 此系统 -> 目标(此系统先执行)
|
||||||
|
for (const target of sys.before) {
|
||||||
|
const targetId = this.resolveTargetId(target);
|
||||||
|
this.addEdge(sys.name, targetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// after: 目标 -> 此系统(目标先执行)
|
||||||
|
for (const target of sys.after) {
|
||||||
|
const targetId = this.resolveTargetId(target);
|
||||||
|
this.addEdge(targetId, sys.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行拓扑排序(Kahn 算法)
|
||||||
|
* Perform topological sort (Kahn's algorithm)
|
||||||
|
*
|
||||||
|
* @returns 排序后的系统名称列表(不包含虚拟节点) | Sorted system names (excluding virtual nodes)
|
||||||
|
* @throws {CycleDependencyError} 如果存在循环依赖 | If cycle dependency detected
|
||||||
|
*/
|
||||||
|
public topologicalSort(): string[] {
|
||||||
|
// 复制入边计数(避免修改原始数据)
|
||||||
|
const inDegree = new Map<string, number>();
|
||||||
|
for (const [id, node] of this._nodes) {
|
||||||
|
inDegree.set(id, node.inEdges.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化队列:入度为 0 的节点
|
||||||
|
const queue: string[] = [];
|
||||||
|
for (const [id, degree] of inDegree) {
|
||||||
|
if (degree === 0) {
|
||||||
|
queue.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: string[] = [];
|
||||||
|
let processedCount = 0;
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const nodeId = queue.shift()!;
|
||||||
|
processedCount++;
|
||||||
|
|
||||||
|
const node = this._nodes.get(nodeId);
|
||||||
|
if (!node) continue;
|
||||||
|
|
||||||
|
// 只添加非虚拟节点到结果
|
||||||
|
if (!node.bIsVirtual) {
|
||||||
|
result.push(nodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 减少所有出边目标的入度
|
||||||
|
for (const outId of node.outEdges) {
|
||||||
|
const newDegree = (inDegree.get(outId) ?? 0) - 1;
|
||||||
|
inDegree.set(outId, newDegree);
|
||||||
|
|
||||||
|
if (newDegree === 0) {
|
||||||
|
queue.push(outId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测循环:如果处理的节点数少于总节点数,说明存在循环
|
||||||
|
if (processedCount < this._nodes.size) {
|
||||||
|
const cycleNodes: string[] = [];
|
||||||
|
for (const [id, degree] of inDegree) {
|
||||||
|
if (degree > 0) {
|
||||||
|
cycleNodes.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new CycleDependencyError(cycleNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空图
|
||||||
|
* Clear graph
|
||||||
|
*/
|
||||||
|
public clear(): void {
|
||||||
|
this._nodes.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取节点数量
|
||||||
|
* Get node count
|
||||||
|
*/
|
||||||
|
public get size(): number {
|
||||||
|
return this._nodes.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取或创建节点
|
||||||
|
* Get or create node
|
||||||
|
*/
|
||||||
|
private getOrCreateNode(id: string, bIsVirtual: boolean): GraphNode {
|
||||||
|
let node = this._nodes.get(id);
|
||||||
|
if (!node) {
|
||||||
|
node = {
|
||||||
|
id,
|
||||||
|
bIsVirtual,
|
||||||
|
inEdges: new Set(),
|
||||||
|
outEdges: new Set()
|
||||||
|
};
|
||||||
|
this._nodes.set(id, node);
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析目标 ID(支持 set: 前缀)
|
||||||
|
* Resolve target ID (supports set: prefix)
|
||||||
|
*/
|
||||||
|
private resolveTargetId(nameOrSet: string): string {
|
||||||
|
// 如果已经有 set: 前缀,直接返回
|
||||||
|
if (nameOrSet.startsWith(SET_PREFIX)) {
|
||||||
|
return nameOrSet;
|
||||||
|
}
|
||||||
|
return nameOrSet;
|
||||||
|
}
|
||||||
|
}
|
||||||
338
packages/core/src/ECS/Core/SystemScheduler.ts
Normal file
338
packages/core/src/ECS/Core/SystemScheduler.ts
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
/**
|
||||||
|
* 系统调度器
|
||||||
|
*
|
||||||
|
* 负责管理系统的执行阶段和依赖关系。
|
||||||
|
* 支持声明式的 before/after 依赖和拓扑排序。
|
||||||
|
*
|
||||||
|
* System scheduler.
|
||||||
|
* Manages system execution stages and dependencies.
|
||||||
|
* Supports declarative before/after dependencies and topological sorting.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* // 使用装饰器声明依赖
|
||||||
|
* @Stage('update')
|
||||||
|
* @After('PhysicsSystem')
|
||||||
|
* @Before('RenderSystem')
|
||||||
|
* class MovementSystem extends EntitySystem { }
|
||||||
|
*
|
||||||
|
* // 或者使用方法链
|
||||||
|
* class MovementSystem extends EntitySystem {
|
||||||
|
* constructor() {
|
||||||
|
* super();
|
||||||
|
* this.stage('update').after('PhysicsSystem').before('RenderSystem');
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SystemDependencyGraph, CycleDependencyError, type SystemDependencyInfo } from './SystemDependencyGraph';
|
||||||
|
import type { EntitySystem } from '../Systems/EntitySystem';
|
||||||
|
|
||||||
|
export { CycleDependencyError };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统执行阶段
|
||||||
|
* System execution stage
|
||||||
|
*/
|
||||||
|
export type SystemStage = 'startup' | 'preUpdate' | 'update' | 'postUpdate' | 'cleanup';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认阶段执行顺序
|
||||||
|
* Default stage execution order
|
||||||
|
*/
|
||||||
|
export const DEFAULT_STAGE_ORDER: readonly SystemStage[] = [
|
||||||
|
'startup',
|
||||||
|
'preUpdate',
|
||||||
|
'update',
|
||||||
|
'postUpdate',
|
||||||
|
'cleanup'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统调度元数据
|
||||||
|
* System scheduling metadata
|
||||||
|
*/
|
||||||
|
export interface SystemSchedulingMetadata {
|
||||||
|
/** 执行阶段 | Execution stage */
|
||||||
|
stage: SystemStage;
|
||||||
|
/** 在这些系统之前执行 | Execute before these systems */
|
||||||
|
before: string[];
|
||||||
|
/** 在这些系统之后执行 | Execute after these systems */
|
||||||
|
after: string[];
|
||||||
|
/** 所属集合 | Sets this system belongs to */
|
||||||
|
sets: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认调度元数据
|
||||||
|
* Default scheduling metadata
|
||||||
|
*/
|
||||||
|
export const DEFAULT_SCHEDULING_METADATA: Readonly<SystemSchedulingMetadata> = {
|
||||||
|
stage: 'update',
|
||||||
|
before: [],
|
||||||
|
after: [],
|
||||||
|
sets: []
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统调度器
|
||||||
|
*
|
||||||
|
* 管理系统的执行顺序,支持:
|
||||||
|
* - 阶段分组(startup, preUpdate, update, postUpdate, cleanup)
|
||||||
|
* - before/after 依赖声明
|
||||||
|
* - 拓扑排序和循环检测
|
||||||
|
* - 与现有 updateOrder 的兼容
|
||||||
|
*
|
||||||
|
* System scheduler.
|
||||||
|
* Manages system execution order, supports:
|
||||||
|
* - Stage grouping (startup, preUpdate, update, postUpdate, cleanup)
|
||||||
|
* - before/after dependency declarations
|
||||||
|
* - Topological sorting and cycle detection
|
||||||
|
* - Compatibility with existing updateOrder
|
||||||
|
*/
|
||||||
|
export class SystemScheduler {
|
||||||
|
/** 按阶段分组的排序结果 | Sorted results grouped by stage */
|
||||||
|
private _sortedByStage: Map<SystemStage, EntitySystem[]> = new Map();
|
||||||
|
/** 是否需要重新构建 | Whether rebuild is needed */
|
||||||
|
private _dirty: boolean = true;
|
||||||
|
/** 依赖图 | Dependency graph */
|
||||||
|
private _graph: SystemDependencyGraph = new SystemDependencyGraph();
|
||||||
|
/** 是否启用依赖排序 | Whether dependency sorting is enabled */
|
||||||
|
private _useDependencySort: boolean = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置是否使用依赖排序
|
||||||
|
* Set whether to use dependency sorting
|
||||||
|
*
|
||||||
|
* @param enabled 是否启用 | Whether to enable
|
||||||
|
*/
|
||||||
|
public setUseDependencySort(enabled: boolean): void {
|
||||||
|
if (this._useDependencySort !== enabled) {
|
||||||
|
this._useDependencySort = enabled;
|
||||||
|
this._dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记需要重新构建
|
||||||
|
* Mark as needing rebuild
|
||||||
|
*/
|
||||||
|
public markDirty(): void {
|
||||||
|
this._dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定阶段的排序后系统列表
|
||||||
|
* Get sorted system list for specified stage
|
||||||
|
*
|
||||||
|
* @param systems 所有系统 | All systems
|
||||||
|
* @param stage 阶段 | Stage
|
||||||
|
* @returns 排序后的系统列表 | Sorted system list
|
||||||
|
*/
|
||||||
|
public getSortedSystems(systems: EntitySystem[], stage?: SystemStage): EntitySystem[] {
|
||||||
|
this.ensureBuilt(systems);
|
||||||
|
|
||||||
|
if (stage) {
|
||||||
|
return this._sortedByStage.get(stage) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回所有阶段按顺序合并的结果
|
||||||
|
const result: EntitySystem[] = [];
|
||||||
|
for (const s of DEFAULT_STAGE_ORDER) {
|
||||||
|
const stageSystems = this._sortedByStage.get(s);
|
||||||
|
if (stageSystems) {
|
||||||
|
result.push(...stageSystems);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有排序后的系统(按默认阶段顺序)
|
||||||
|
* Get all sorted systems (by default stage order)
|
||||||
|
*
|
||||||
|
* @param systems 所有系统 | All systems
|
||||||
|
* @returns 排序后的系统列表 | Sorted system list
|
||||||
|
*/
|
||||||
|
public getAllSortedSystems(systems: EntitySystem[]): EntitySystem[] {
|
||||||
|
return this.getSortedSystems(systems);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保已构建排序结果
|
||||||
|
* Ensure sorted results are built
|
||||||
|
*/
|
||||||
|
private ensureBuilt(systems: EntitySystem[]): void {
|
||||||
|
if (!this._dirty) return;
|
||||||
|
|
||||||
|
this._sortedByStage.clear();
|
||||||
|
|
||||||
|
if (!this._useDependencySort || !this.hasDependencies(systems)) {
|
||||||
|
// 使用简单的 updateOrder 排序
|
||||||
|
this.buildWithUpdateOrder(systems);
|
||||||
|
} else {
|
||||||
|
// 使用依赖图拓扑排序
|
||||||
|
this.buildWithDependencyGraph(systems);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查系统是否有依赖声明
|
||||||
|
* Check if systems have dependency declarations
|
||||||
|
*/
|
||||||
|
private hasDependencies(systems: EntitySystem[]): boolean {
|
||||||
|
for (const sys of systems) {
|
||||||
|
const meta = this.getSchedulingMetadata(sys);
|
||||||
|
if (meta.before.length > 0 || meta.after.length > 0 || meta.sets.length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 检查 stage 是否非默认值
|
||||||
|
if (meta.stage !== 'update') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 updateOrder 构建排序
|
||||||
|
* Build sorting with updateOrder
|
||||||
|
*/
|
||||||
|
private buildWithUpdateOrder(systems: EntitySystem[]): void {
|
||||||
|
// 先按 updateOrder 和 addOrder 排序
|
||||||
|
const sorted = [...systems].sort((a, b) => {
|
||||||
|
const orderDiff = a.updateOrder - b.updateOrder;
|
||||||
|
if (orderDiff !== 0) return orderDiff;
|
||||||
|
return a.addOrder - b.addOrder;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 所有系统放入 update 阶段
|
||||||
|
this._sortedByStage.set('update', sorted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用依赖图构建排序
|
||||||
|
* Build sorting with dependency graph
|
||||||
|
*/
|
||||||
|
private buildWithDependencyGraph(systems: EntitySystem[]): void {
|
||||||
|
// 按阶段分组
|
||||||
|
const byStage = new Map<SystemStage, EntitySystem[]>();
|
||||||
|
for (const stage of DEFAULT_STAGE_ORDER) {
|
||||||
|
byStage.set(stage, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const sys of systems) {
|
||||||
|
const meta = this.getSchedulingMetadata(sys);
|
||||||
|
const stage = meta.stage;
|
||||||
|
const stageList = byStage.get(stage);
|
||||||
|
if (stageList) {
|
||||||
|
stageList.push(sys);
|
||||||
|
} else {
|
||||||
|
// 未知阶段放入 update
|
||||||
|
byStage.get('update')!.push(sys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对每个阶段内的系统进行拓扑排序
|
||||||
|
for (const [stage, stageSystems] of byStage) {
|
||||||
|
if (stageSystems.length === 0) {
|
||||||
|
this._sortedByStage.set(stage, []);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sorted = this.sortSystemsInStage(stageSystems);
|
||||||
|
this._sortedByStage.set(stage, sorted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在阶段内对系统进行拓扑排序
|
||||||
|
* Sort systems within a stage using topological sort
|
||||||
|
*/
|
||||||
|
private sortSystemsInStage(systems: EntitySystem[]): EntitySystem[] {
|
||||||
|
// 构建名称到系统的映射
|
||||||
|
const nameToSystem = new Map<string, EntitySystem>();
|
||||||
|
const depInfos: SystemDependencyInfo[] = [];
|
||||||
|
|
||||||
|
for (const sys of systems) {
|
||||||
|
const name = sys.systemName;
|
||||||
|
nameToSystem.set(name, sys);
|
||||||
|
|
||||||
|
const meta = this.getSchedulingMetadata(sys);
|
||||||
|
depInfos.push({
|
||||||
|
name,
|
||||||
|
before: meta.before,
|
||||||
|
after: meta.after,
|
||||||
|
sets: meta.sets
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建依赖图并排序
|
||||||
|
this._graph.buildFromSystems(depInfos);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sortedNames = this._graph.topologicalSort();
|
||||||
|
|
||||||
|
// 将排序结果映射回系统实例
|
||||||
|
const result: EntitySystem[] = [];
|
||||||
|
for (const name of sortedNames) {
|
||||||
|
const sys = nameToSystem.get(name);
|
||||||
|
if (sys) {
|
||||||
|
result.push(sys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于没有依赖的系统,保持 updateOrder 作为次要排序键
|
||||||
|
return this.stableSortByUpdateOrder(result);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof CycleDependencyError) {
|
||||||
|
// 重新抛出循环错误,让用户知道
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
// 其他错误回退到 updateOrder 排序
|
||||||
|
console.warn('[SystemScheduler] 拓扑排序失败,回退到 updateOrder 排序', error);
|
||||||
|
return this.fallbackSort(systems);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 稳定排序:在拓扑顺序的基础上,用 updateOrder 作为次要排序键
|
||||||
|
* Stable sort: use updateOrder as secondary key after topological order
|
||||||
|
*/
|
||||||
|
private stableSortByUpdateOrder(systems: EntitySystem[]): EntitySystem[] {
|
||||||
|
// 拓扑排序已经保证了依赖顺序
|
||||||
|
// 对于没有直接依赖关系的系统,按 updateOrder 排序
|
||||||
|
// 这里简单处理:假设拓扑排序已经正确,只需保持结果
|
||||||
|
return systems;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回退排序:使用 updateOrder
|
||||||
|
* Fallback sort: use updateOrder
|
||||||
|
*/
|
||||||
|
private fallbackSort(systems: EntitySystem[]): EntitySystem[] {
|
||||||
|
return [...systems].sort((a, b) => {
|
||||||
|
const orderDiff = a.updateOrder - b.updateOrder;
|
||||||
|
if (orderDiff !== 0) return orderDiff;
|
||||||
|
return a.addOrder - b.addOrder;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取系统的调度元数据
|
||||||
|
* Get system scheduling metadata
|
||||||
|
*/
|
||||||
|
private getSchedulingMetadata(system: EntitySystem): SystemSchedulingMetadata {
|
||||||
|
// 使用公共 getter 方法获取调度信息
|
||||||
|
// Use public getter methods to access scheduling info
|
||||||
|
return {
|
||||||
|
stage: system.getStage(),
|
||||||
|
before: [...system.getBefore()],
|
||||||
|
after: [...system.getAfter()],
|
||||||
|
sets: [...system.getSets()]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
177
packages/core/src/ECS/Decorators/SystemScheduling.ts
Normal file
177
packages/core/src/ECS/Decorators/SystemScheduling.ts
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
/**
|
||||||
|
* 系统调度装饰器
|
||||||
|
*
|
||||||
|
* 提供声明式的系统调度配置,包括执行阶段和依赖关系。
|
||||||
|
*
|
||||||
|
* System scheduling decorators.
|
||||||
|
* Provides declarative system scheduling configuration including execution stage and dependencies.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* @Stage('update')
|
||||||
|
* @After('PhysicsSystem')
|
||||||
|
* @Before('RenderSystem')
|
||||||
|
* @InSet('GameplaySystems')
|
||||||
|
* class MovementSystem extends EntitySystem {
|
||||||
|
* // ...
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { SystemStage, SystemSchedulingMetadata } from '../Core/SystemScheduler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调度元数据 Symbol
|
||||||
|
* Scheduling metadata symbol
|
||||||
|
*/
|
||||||
|
export const SCHEDULING_METADATA = Symbol('schedulingMetadata');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取或创建调度元数据
|
||||||
|
* Get or create scheduling metadata
|
||||||
|
*/
|
||||||
|
function getOrCreateMetadata(target: object): SystemSchedulingMetadata {
|
||||||
|
const proto = target as Record<symbol, SystemSchedulingMetadata | undefined>;
|
||||||
|
if (!proto[SCHEDULING_METADATA]) {
|
||||||
|
proto[SCHEDULING_METADATA] = {
|
||||||
|
stage: 'update',
|
||||||
|
before: [],
|
||||||
|
after: [],
|
||||||
|
sets: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return proto[SCHEDULING_METADATA]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置系统执行阶段
|
||||||
|
* Set system execution stage
|
||||||
|
*
|
||||||
|
* @param stage 执行阶段 | Execution stage
|
||||||
|
* - 'startup': 只在首帧执行一次 | Execute once on first frame
|
||||||
|
* - 'preUpdate': 在 update 之前执行 | Execute before update
|
||||||
|
* - 'update': 主更新阶段(默认)| Main update stage (default)
|
||||||
|
* - 'postUpdate': 在 update 之后执行 | Execute after update
|
||||||
|
* - 'cleanup': 帧末清理阶段 | End of frame cleanup stage
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* @Stage('preUpdate')
|
||||||
|
* class InputSystem extends EntitySystem { }
|
||||||
|
*
|
||||||
|
* @Stage('postUpdate')
|
||||||
|
* class RenderSystem extends EntitySystem { }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function Stage(stage: SystemStage): ClassDecorator {
|
||||||
|
return function (target) {
|
||||||
|
const metadata = getOrCreateMetadata(target.prototype);
|
||||||
|
metadata.stage = stage;
|
||||||
|
return target;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 声明此系统在指定系统之前执行
|
||||||
|
* Declare this system executes before specified systems
|
||||||
|
*
|
||||||
|
* @param systems 系统名称列表 | System names
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* @Before('RenderSystem', 'UISystem')
|
||||||
|
* class TransformSystem extends EntitySystem { }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function Before(...systems: string[]): ClassDecorator {
|
||||||
|
return function (target) {
|
||||||
|
const metadata = getOrCreateMetadata(target.prototype);
|
||||||
|
metadata.before.push(...systems);
|
||||||
|
return target;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 声明此系统在指定系统之后执行
|
||||||
|
* Declare this system executes after specified systems
|
||||||
|
*
|
||||||
|
* @param systems 系统名称列表(可使用 'set:' 前缀指定集合)| System names (use 'set:' prefix for sets)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* @After('PhysicsSystem')
|
||||||
|
* class CollisionResponseSystem extends EntitySystem { }
|
||||||
|
*
|
||||||
|
* // 使用集合
|
||||||
|
* @After('set:PhysicsSystems')
|
||||||
|
* class GameLogicSystem extends EntitySystem { }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function After(...systems: string[]): ClassDecorator {
|
||||||
|
return function (target) {
|
||||||
|
const metadata = getOrCreateMetadata(target.prototype);
|
||||||
|
metadata.after.push(...systems);
|
||||||
|
return target;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将系统加入指定集合
|
||||||
|
* Add system to specified sets
|
||||||
|
*
|
||||||
|
* 集合是虚拟分组,可用于批量依赖声明。
|
||||||
|
* Sets are virtual groups that can be used for batch dependency declarations.
|
||||||
|
*
|
||||||
|
* @param sets 集合名称列表 | Set names
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* @InSet('PhysicsSystems')
|
||||||
|
* class RigidBodySystem extends EntitySystem { }
|
||||||
|
*
|
||||||
|
* @InSet('PhysicsSystems')
|
||||||
|
* class CollisionSystem extends EntitySystem { }
|
||||||
|
*
|
||||||
|
* // 其他系统可以依赖整个集合
|
||||||
|
* @After('set:PhysicsSystems')
|
||||||
|
* class GameLogicSystem extends EntitySystem { }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function InSet(...sets: string[]): ClassDecorator {
|
||||||
|
return function (target) {
|
||||||
|
const metadata = getOrCreateMetadata(target.prototype);
|
||||||
|
metadata.sets.push(...sets);
|
||||||
|
return target;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取系统的调度元数据
|
||||||
|
* Get system scheduling metadata
|
||||||
|
*
|
||||||
|
* @param target 系统类或实例 | System class or instance
|
||||||
|
* @returns 调度元数据或 undefined | Scheduling metadata or undefined
|
||||||
|
*/
|
||||||
|
export function getSchedulingMetadata(target: object): SystemSchedulingMetadata | undefined {
|
||||||
|
// 从原型链查找
|
||||||
|
let proto = Object.getPrototypeOf(target);
|
||||||
|
while (proto) {
|
||||||
|
const metadata = (proto as Record<symbol, SystemSchedulingMetadata | undefined>)[SCHEDULING_METADATA];
|
||||||
|
if (metadata) {
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
proto = Object.getPrototypeOf(proto);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查系统是否有调度元数据
|
||||||
|
* Check if system has scheduling metadata
|
||||||
|
*
|
||||||
|
* @param target 系统类或实例 | System class or instance
|
||||||
|
* @returns 是否有元数据 | Whether has metadata
|
||||||
|
*/
|
||||||
|
export function hasSchedulingMetadata(target: object): boolean {
|
||||||
|
return getSchedulingMetadata(target) !== undefined;
|
||||||
|
}
|
||||||
@@ -67,3 +67,17 @@ export type {
|
|||||||
PropertyAssetType,
|
PropertyAssetType,
|
||||||
EnumOption
|
EnumOption
|
||||||
} from './PropertyDecorator';
|
} from './PropertyDecorator';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// System Scheduling Decorators
|
||||||
|
// 系统调度装饰器
|
||||||
|
// ============================================================================
|
||||||
|
export {
|
||||||
|
Stage,
|
||||||
|
Before,
|
||||||
|
After,
|
||||||
|
InSet,
|
||||||
|
getSchedulingMetadata,
|
||||||
|
hasSchedulingMetadata,
|
||||||
|
SCHEDULING_METADATA
|
||||||
|
} from './SystemScheduling';
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { createLogger } from '../Utils/Logger';
|
|||||||
import { getComponentInstanceTypeName, getComponentTypeName } from './Decorators';
|
import { getComponentInstanceTypeName, getComponentTypeName } from './Decorators';
|
||||||
import { generateGUID } from '../Utils/GUID';
|
import { generateGUID } from '../Utils/GUID';
|
||||||
import type { IScene } from './IScene';
|
import type { IScene } from './IScene';
|
||||||
|
import { EntityHandle, NULL_HANDLE } from './Core/EntityHandle';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组件活跃状态变化接口
|
* 组件活跃状态变化接口
|
||||||
@@ -93,6 +94,19 @@ export class Entity {
|
|||||||
*/
|
*/
|
||||||
public readonly persistentId: string;
|
public readonly persistentId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轻量级实体句柄
|
||||||
|
*
|
||||||
|
* 数值类型的实体标识符,包含索引和代数信息。
|
||||||
|
* 用于高性能场景下替代对象引用,支持 Archetype 存储等优化。
|
||||||
|
*
|
||||||
|
* Lightweight entity handle.
|
||||||
|
* Numeric identifier containing index and generation.
|
||||||
|
* Used for high-performance scenarios instead of object references,
|
||||||
|
* supports Archetype storage optimizations.
|
||||||
|
*/
|
||||||
|
private _handle: EntityHandle = NULL_HANDLE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 所属场景引用
|
* 所属场景引用
|
||||||
*/
|
*/
|
||||||
@@ -171,6 +185,34 @@ export class Entity {
|
|||||||
return this._lifecyclePolicy === EEntityLifecyclePolicy.Persistent;
|
return this._lifecyclePolicy === EEntityLifecyclePolicy.Persistent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取实体句柄
|
||||||
|
*
|
||||||
|
* 返回轻量级数值句柄,用于高性能场景。
|
||||||
|
* 如果实体尚未分配句柄,返回 NULL_HANDLE。
|
||||||
|
*
|
||||||
|
* Get entity handle.
|
||||||
|
* Returns lightweight numeric handle for high-performance scenarios.
|
||||||
|
* Returns NULL_HANDLE if entity has no handle assigned.
|
||||||
|
*/
|
||||||
|
public get handle(): EntityHandle {
|
||||||
|
return this._handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置实体句柄(内部使用)
|
||||||
|
*
|
||||||
|
* 此方法供 Scene 在创建实体时调用。
|
||||||
|
*
|
||||||
|
* Set entity handle (internal use).
|
||||||
|
* Called by Scene when creating entities.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public setHandle(handle: EntityHandle): void {
|
||||||
|
this._handle = handle;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置实体为持久化(跨场景保留)
|
* 设置实体为持久化(跨场景保留)
|
||||||
*
|
*
|
||||||
@@ -559,6 +601,39 @@ export class Entity {
|
|||||||
return component;
|
return component;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记组件为已修改
|
||||||
|
*
|
||||||
|
* 便捷方法,自动从场景获取当前 epoch 并标记组件。
|
||||||
|
* 用于帧级变更检测系统。
|
||||||
|
*
|
||||||
|
* Mark component(s) as modified.
|
||||||
|
* Convenience method that auto-gets epoch from scene and marks components.
|
||||||
|
* Used for frame-level change detection system.
|
||||||
|
*
|
||||||
|
* @param components 要标记的组件 | Components to mark
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const pos = entity.getComponent(Position)!;
|
||||||
|
* pos.x = 100;
|
||||||
|
* entity.markDirty(pos);
|
||||||
|
*
|
||||||
|
* // 或者标记多个组件
|
||||||
|
* entity.markDirty(pos, vel);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public markDirty(...components: Component[]): void {
|
||||||
|
if (!this.scene) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const epoch = this.scene.epochManager.current;
|
||||||
|
for (const component of components) {
|
||||||
|
component.markDirty(epoch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 移除指定的组件
|
* 移除指定的组件
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { EntitySystem } from './Systems/EntitySystem';
|
|||||||
import { ComponentStorageManager, ComponentType } from './Core/ComponentStorage';
|
import { ComponentStorageManager, ComponentType } from './Core/ComponentStorage';
|
||||||
import { QuerySystem } from './Core/QuerySystem';
|
import { QuerySystem } from './Core/QuerySystem';
|
||||||
import { TypeSafeEventSystem } from './Core/EventSystem';
|
import { TypeSafeEventSystem } from './Core/EventSystem';
|
||||||
|
import { EpochManager } from './Core/EpochManager';
|
||||||
import type { ReferenceTracker } from './Core/ReferenceTracker';
|
import type { ReferenceTracker } from './Core/ReferenceTracker';
|
||||||
import type { ServiceContainer, ServiceType } from '../Core/ServiceContainer';
|
import type { ServiceContainer, ServiceType } from '../Core/ServiceContainer';
|
||||||
import type { TypedQueryBuilder } from './Core/Query/TypedQuery';
|
import type { TypedQueryBuilder } from './Core/Query/TypedQuery';
|
||||||
@@ -71,6 +72,16 @@ export interface IScene {
|
|||||||
*/
|
*/
|
||||||
readonly referenceTracker: ReferenceTracker;
|
readonly referenceTracker: ReferenceTracker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Epoch 管理器
|
||||||
|
*
|
||||||
|
* 用于帧级变更检测,追踪组件修改。
|
||||||
|
*
|
||||||
|
* Epoch manager.
|
||||||
|
* Used for frame-level change detection, tracking component modifications.
|
||||||
|
*/
|
||||||
|
readonly epochManager: EpochManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 服务容器
|
* 服务容器
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ import { AutoProfiler } from '../Utils/Profiler/AutoProfiler';
|
|||||||
import { ServiceContainer, type ServiceType, type IService } from '../Core/ServiceContainer';
|
import { ServiceContainer, type ServiceType, type IService } from '../Core/ServiceContainer';
|
||||||
import { createInstance, isInjectable, injectProperties } from '../Core/DI';
|
import { createInstance, isInjectable, injectProperties } from '../Core/DI';
|
||||||
import { createLogger } from '../Utils/Logger';
|
import { createLogger } from '../Utils/Logger';
|
||||||
|
import { SystemScheduler, CycleDependencyError } from './Core/SystemScheduler';
|
||||||
|
import { EntityHandleManager } from './Core/EntityHandleManager';
|
||||||
|
import { EntityHandle, isValidHandle } from './Core/EntityHandle';
|
||||||
|
import { EpochManager } from './Core/EpochManager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 游戏场景默认实现类
|
* 游戏场景默认实现类
|
||||||
@@ -92,6 +96,38 @@ export class Scene implements IScene {
|
|||||||
*/
|
*/
|
||||||
public readonly referenceTracker: ReferenceTracker;
|
public readonly referenceTracker: ReferenceTracker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实体句柄管理器
|
||||||
|
*
|
||||||
|
* 管理轻量级实体句柄的生命周期,包括创建、销毁和状态查询。
|
||||||
|
* 用于高性能场景下替代对象引用。
|
||||||
|
*
|
||||||
|
* Entity handle manager.
|
||||||
|
* Manages lifecycle of lightweight entity handles.
|
||||||
|
* Used for high-performance scenarios instead of object references.
|
||||||
|
*/
|
||||||
|
public readonly handleManager: EntityHandleManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 句柄到实体的映射
|
||||||
|
*
|
||||||
|
* 用于 O(1) 时间复杂度的句柄查找。
|
||||||
|
*
|
||||||
|
* Handle to entity mapping.
|
||||||
|
* Used for O(1) lookup by handle.
|
||||||
|
*/
|
||||||
|
private readonly _handleToEntity: Map<EntityHandle, Entity> = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Epoch 管理器
|
||||||
|
*
|
||||||
|
* 用于帧级变更检测,追踪组件的修改时间。
|
||||||
|
*
|
||||||
|
* Epoch manager.
|
||||||
|
* Used for frame-level change detection, tracking component modification time.
|
||||||
|
*/
|
||||||
|
public readonly epochManager: EpochManager = new EpochManager();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 服务容器
|
* 服务容器
|
||||||
*
|
*
|
||||||
@@ -175,6 +211,18 @@ export class Scene implements IScene {
|
|||||||
*/
|
*/
|
||||||
private _systemAddCounter: number = 0;
|
private _systemAddCounter: number = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统调度器
|
||||||
|
*
|
||||||
|
* 用于管理系统的执行阶段和依赖关系。
|
||||||
|
* 支持 before/after 依赖声明和拓扑排序。
|
||||||
|
*
|
||||||
|
* System scheduler.
|
||||||
|
* Manages system execution stages and dependencies.
|
||||||
|
* Supports before/after dependency declarations and topological sorting.
|
||||||
|
*/
|
||||||
|
private readonly _systemScheduler: SystemScheduler = new SystemScheduler();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组件ID到系统的索引映射
|
* 组件ID到系统的索引映射
|
||||||
*
|
*
|
||||||
@@ -220,12 +268,34 @@ export class Scene implements IScene {
|
|||||||
/**
|
/**
|
||||||
* 重新构建系统缓存
|
* 重新构建系统缓存
|
||||||
*
|
*
|
||||||
* 从服务容器中提取所有EntitySystem并排序
|
* 从服务容器中提取所有EntitySystem并排序。
|
||||||
|
* 使用 SystemScheduler 进行依赖排序,支持 before/after 声明。
|
||||||
|
*
|
||||||
|
* Rebuild system cache.
|
||||||
|
* Extract all EntitySystems from service container and sort them.
|
||||||
|
* Uses SystemScheduler for dependency sorting, supports before/after declarations.
|
||||||
*/
|
*/
|
||||||
private _rebuildSystemsCache(): EntitySystem[] {
|
private _rebuildSystemsCache(): EntitySystem[] {
|
||||||
const allServices = this._services.getAll();
|
const allServices = this._services.getAll();
|
||||||
const systems = this._filterEntitySystems(allServices);
|
const systems = this._filterEntitySystems(allServices);
|
||||||
return this._sortSystemsByUpdateOrder(systems);
|
|
||||||
|
try {
|
||||||
|
// 使用 SystemScheduler 进行依赖排序
|
||||||
|
this._systemScheduler.markDirty();
|
||||||
|
return this._systemScheduler.getAllSortedSystems(systems);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof CycleDependencyError) {
|
||||||
|
// 循环依赖错误,记录警告并回退到 updateOrder 排序
|
||||||
|
this.logger.error(
|
||||||
|
`[Scene] 系统存在循环依赖,回退到 updateOrder 排序 | Cycle dependency detected, falling back to updateOrder sort`,
|
||||||
|
error.involvedNodes
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.logger.error(`[Scene] 系统排序失败 | System sorting failed`, error);
|
||||||
|
}
|
||||||
|
// 回退到简单的 updateOrder 排序
|
||||||
|
return this._sortSystemsByUpdateOrder(systems);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -302,6 +372,7 @@ export class Scene implements IScene {
|
|||||||
this.querySystem = new QuerySystem();
|
this.querySystem = new QuerySystem();
|
||||||
this.eventSystem = new TypeSafeEventSystem();
|
this.eventSystem = new TypeSafeEventSystem();
|
||||||
this.referenceTracker = new ReferenceTracker();
|
this.referenceTracker = new ReferenceTracker();
|
||||||
|
this.handleManager = new EntityHandleManager();
|
||||||
this._services = new ServiceContainer();
|
this._services = new ServiceContainer();
|
||||||
this.logger = createLogger('Scene');
|
this.logger = createLogger('Scene');
|
||||||
|
|
||||||
@@ -429,12 +500,22 @@ export class Scene implements IScene {
|
|||||||
// 清空组件索引 | Clear component indices
|
// 清空组件索引 | Clear component indices
|
||||||
this._componentIdToSystems.clear();
|
this._componentIdToSystems.clear();
|
||||||
this._globalNotifySystems.clear();
|
this._globalNotifySystems.clear();
|
||||||
|
|
||||||
|
// 清空句柄映射并重置句柄管理器 | Clear handle mapping and reset handle manager
|
||||||
|
this._handleToEntity.clear();
|
||||||
|
this.handleManager.reset();
|
||||||
|
|
||||||
|
// 重置 epoch 管理器 | Reset epoch manager
|
||||||
|
this.epochManager.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新场景
|
* 更新场景
|
||||||
*/
|
*/
|
||||||
public update() {
|
public update() {
|
||||||
|
// 递增帧计数(用于变更检测) | Increment epoch (for change detection)
|
||||||
|
this.epochManager.increment();
|
||||||
|
|
||||||
// 开始性能采样帧
|
// 开始性能采样帧
|
||||||
ProfilerSDK.beginFrame();
|
ProfilerSDK.beginFrame();
|
||||||
const frameHandle = ProfilerSDK.beginSample('Scene.update', ProfileCategory.ECS);
|
const frameHandle = ProfilerSDK.beginSample('Scene.update', ProfileCategory.ECS);
|
||||||
@@ -549,6 +630,13 @@ export class Scene implements IScene {
|
|||||||
public createEntity(name: string) {
|
public createEntity(name: string) {
|
||||||
const entity = new Entity(name, this.identifierPool.checkOut());
|
const entity = new Entity(name, this.identifierPool.checkOut());
|
||||||
|
|
||||||
|
// 分配轻量级句柄 | Assign lightweight handle
|
||||||
|
const handle = this.handleManager.create();
|
||||||
|
entity.setHandle(handle);
|
||||||
|
|
||||||
|
// 添加到句柄映射 | Add to handle mapping
|
||||||
|
this._handleToEntity.set(handle, entity);
|
||||||
|
|
||||||
this.eventSystem.emitSync('entity:created', { entityName: name, entity, scene: this });
|
this.eventSystem.emitSync('entity:created', { entityName: name, entity, scene: this });
|
||||||
|
|
||||||
return this.addEntity(entity);
|
return this.addEntity(entity);
|
||||||
@@ -736,6 +824,14 @@ export class Scene implements IScene {
|
|||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const entity = new Entity(`${namePrefix}_${i}`, this.identifierPool.checkOut());
|
const entity = new Entity(`${namePrefix}_${i}`, this.identifierPool.checkOut());
|
||||||
entity.scene = this;
|
entity.scene = this;
|
||||||
|
|
||||||
|
// 分配轻量级句柄 | Assign lightweight handle
|
||||||
|
const handle = this.handleManager.create();
|
||||||
|
entity.setHandle(handle);
|
||||||
|
|
||||||
|
// 添加到句柄映射 | Add to handle mapping
|
||||||
|
this._handleToEntity.set(handle, entity);
|
||||||
|
|
||||||
entities.push(entity);
|
entities.push(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -770,6 +866,12 @@ export class Scene implements IScene {
|
|||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
this.entities.remove(entity);
|
this.entities.remove(entity);
|
||||||
this.querySystem.removeEntity(entity);
|
this.querySystem.removeEntity(entity);
|
||||||
|
|
||||||
|
// 销毁句柄并从映射中移除 | Destroy handle and remove from mapping
|
||||||
|
if (isValidHandle(entity.handle)) {
|
||||||
|
this._handleToEntity.delete(entity.handle);
|
||||||
|
this.handleManager.destroy(entity.handle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.querySystem.clearCache();
|
this.querySystem.clearCache();
|
||||||
@@ -801,6 +903,26 @@ export class Scene implements IScene {
|
|||||||
return this.entities.findEntityById(id);
|
return this.entities.findEntityById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据句柄查找实体
|
||||||
|
*
|
||||||
|
* 通过轻量级句柄查找对应的实体(O(1) 时间复杂度)。
|
||||||
|
* 如果句柄无效或实体已被销毁,返回 null。
|
||||||
|
*
|
||||||
|
* Find entity by handle (O(1) time complexity).
|
||||||
|
* Returns null if handle is invalid or entity has been destroyed.
|
||||||
|
*
|
||||||
|
* @param handle 实体句柄 | Entity handle
|
||||||
|
* @returns 实体实例或 null | Entity instance or null
|
||||||
|
*/
|
||||||
|
public findEntityByHandle(handle: EntityHandle): Entity | null {
|
||||||
|
if (!isValidHandle(handle) || !this.handleManager.isAlive(handle)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._handleToEntity.get(handle) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据标签查找实体
|
* 根据标签查找实体
|
||||||
* @param tag 实体标签
|
* @param tag 实体标签
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import type { ComponentConstructor, ComponentInstance } from '../../Types/TypeHe
|
|||||||
import type { IService } from '../../Core/ServiceContainer';
|
import type { IService } from '../../Core/ServiceContainer';
|
||||||
import { EntityCache } from './EntityCache';
|
import { EntityCache } from './EntityCache';
|
||||||
import { CommandBuffer } from '../Core/CommandBuffer';
|
import { CommandBuffer } from '../Core/CommandBuffer';
|
||||||
|
import type { SystemStage, SystemSchedulingMetadata } from '../Core/SystemScheduler';
|
||||||
|
import { getSchedulingMetadata } from '../Decorators/SystemScheduling';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 事件监听器记录
|
* 事件监听器记录
|
||||||
@@ -83,6 +85,12 @@ export abstract class EntitySystem implements ISystemBase, IService {
|
|||||||
private _destroyed: boolean;
|
private _destroyed: boolean;
|
||||||
protected logger: ReturnType<typeof createLogger>;
|
protected logger: ReturnType<typeof createLogger>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调度元数据
|
||||||
|
* Scheduling metadata
|
||||||
|
*/
|
||||||
|
protected _schedulingMetadata: SystemSchedulingMetadata;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 实体ID映射缓存
|
* 实体ID映射缓存
|
||||||
*/
|
*/
|
||||||
@@ -118,6 +126,15 @@ export abstract class EntitySystem implements ISystemBase, IService {
|
|||||||
*/
|
*/
|
||||||
protected readonly commands: CommandBuffer = new CommandBuffer();
|
protected readonly commands: CommandBuffer = new CommandBuffer();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上次处理的 epoch 检查点
|
||||||
|
* Last processed epoch checkpoint
|
||||||
|
*
|
||||||
|
* 用于变更检测,记录上次 forEachChanged 完成时的 epoch。
|
||||||
|
* Used for change detection, records epoch when forEachChanged last completed.
|
||||||
|
*/
|
||||||
|
private _lastProcessEpoch: number = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取系统处理的实体列表
|
* 获取系统处理的实体列表
|
||||||
*/
|
*/
|
||||||
@@ -205,6 +222,18 @@ export abstract class EntitySystem implements ISystemBase, IService {
|
|||||||
this.logger = createLogger(this.getLoggerName());
|
this.logger = createLogger(this.getLoggerName());
|
||||||
|
|
||||||
this._entityCache = new EntityCache();
|
this._entityCache = new EntityCache();
|
||||||
|
|
||||||
|
// 初始化调度元数据(优先使用装饰器元数据)
|
||||||
|
// Initialize scheduling metadata (prefer decorator metadata)
|
||||||
|
const decoratorMeta = getSchedulingMetadata(this);
|
||||||
|
this._schedulingMetadata = decoratorMeta
|
||||||
|
? { ...decoratorMeta }
|
||||||
|
: {
|
||||||
|
stage: 'update',
|
||||||
|
before: [],
|
||||||
|
after: [],
|
||||||
|
sets: []
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -257,6 +286,135 @@ export abstract class EntitySystem implements ISystemBase, IService {
|
|||||||
this._scene?.markSystemsOrderDirty();
|
this._scene?.markSystemsOrderDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// 调度配置方法 (Fluent API)
|
||||||
|
// Scheduling configuration methods (Fluent API)
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置系统执行阶段
|
||||||
|
* Set system execution stage
|
||||||
|
*
|
||||||
|
* @param stage 执行阶段 | Execution stage
|
||||||
|
* @returns this (用于链式调用 | for chaining)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* class MySystem extends EntitySystem {
|
||||||
|
* constructor() {
|
||||||
|
* super();
|
||||||
|
* this.stage('preUpdate');
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public stage(stage: SystemStage): this {
|
||||||
|
this._schedulingMetadata.stage = stage;
|
||||||
|
this._scene?.markSystemsOrderDirty();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 声明此系统在指定系统之前执行
|
||||||
|
* Declare this system executes before specified systems
|
||||||
|
*
|
||||||
|
* @param systems 系统名称列表 | System names
|
||||||
|
* @returns this (用于链式调用 | for chaining)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* class MySystem extends EntitySystem {
|
||||||
|
* constructor() {
|
||||||
|
* super();
|
||||||
|
* this.before('RenderSystem');
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public before(...systems: string[]): this {
|
||||||
|
this._schedulingMetadata.before.push(...systems);
|
||||||
|
this._scene?.markSystemsOrderDirty();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 声明此系统在指定系统之后执行
|
||||||
|
* Declare this system executes after specified systems
|
||||||
|
*
|
||||||
|
* @param systems 系统名称列表(可使用 'set:' 前缀指定集合)| System names (use 'set:' prefix for sets)
|
||||||
|
* @returns this (用于链式调用 | for chaining)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* class MySystem extends EntitySystem {
|
||||||
|
* constructor() {
|
||||||
|
* super();
|
||||||
|
* this.after('PhysicsSystem', 'set:CoreSystems');
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public after(...systems: string[]): this {
|
||||||
|
this._schedulingMetadata.after.push(...systems);
|
||||||
|
this._scene?.markSystemsOrderDirty();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将系统加入指定集合
|
||||||
|
* Add system to specified sets
|
||||||
|
*
|
||||||
|
* @param sets 集合名称列表 | Set names
|
||||||
|
* @returns this (用于链式调用 | for chaining)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* class MySystem extends EntitySystem {
|
||||||
|
* constructor() {
|
||||||
|
* super();
|
||||||
|
* this.inSet('PhysicsSystems');
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public inSet(...sets: string[]): this {
|
||||||
|
this._schedulingMetadata.sets.push(...sets);
|
||||||
|
this._scene?.markSystemsOrderDirty();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取系统的执行阶段
|
||||||
|
* Get system execution stage
|
||||||
|
*/
|
||||||
|
public getStage(): SystemStage {
|
||||||
|
return this._schedulingMetadata.stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取系统应该在哪些系统之前执行
|
||||||
|
* Get systems that this system should execute before
|
||||||
|
*/
|
||||||
|
public getBefore(): readonly string[] {
|
||||||
|
return this._schedulingMetadata.before;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取系统应该在哪些系统之后执行
|
||||||
|
* Get systems that this system should execute after
|
||||||
|
*/
|
||||||
|
public getAfter(): readonly string[] {
|
||||||
|
return this._schedulingMetadata.after;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取系统所属的集合
|
||||||
|
* Get sets that this system belongs to
|
||||||
|
*/
|
||||||
|
public getSets(): readonly string[] {
|
||||||
|
return this._schedulingMetadata.sets;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 系统初始化(框架调用)
|
* 系统初始化(框架调用)
|
||||||
*
|
*
|
||||||
@@ -1302,4 +1460,167 @@ export abstract class EntitySystem implements ISystemBase, IService {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// 变更检测方法 (Change Detection Methods)
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取上次处理的 epoch 检查点
|
||||||
|
* Get the last processed epoch checkpoint
|
||||||
|
*
|
||||||
|
* @returns 上次处理完成时的 epoch | Epoch when last processing completed
|
||||||
|
*/
|
||||||
|
protected get lastProcessEpoch(): number {
|
||||||
|
return this._lastProcessEpoch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前 epoch
|
||||||
|
* Get the current epoch
|
||||||
|
*
|
||||||
|
* @returns 当前的 epoch 值,如果 scene 不可用则返回 0 | Current epoch value, or 0 if scene unavailable
|
||||||
|
*/
|
||||||
|
protected get currentEpoch(): number {
|
||||||
|
return this._scene?.epochManager?.current ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存当前 epoch 作为检查点
|
||||||
|
* Save current epoch as checkpoint
|
||||||
|
*
|
||||||
|
* 调用此方法后,下次 forEachChanged 将只处理此时间点之后变更的组件。
|
||||||
|
* After calling this, next forEachChanged will only process components changed after this point.
|
||||||
|
*/
|
||||||
|
protected saveEpoch(): void {
|
||||||
|
this._lastProcessEpoch = this.currentEpoch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 遍历有变更组件的实体
|
||||||
|
* Iterate entities with changed components
|
||||||
|
*
|
||||||
|
* 只处理自上次 saveEpoch() 或指定 epoch 以来组件发生变更的实体。
|
||||||
|
* 处理完成后自动更新 lastProcessEpoch。
|
||||||
|
*
|
||||||
|
* Only processes entities whose components changed since last saveEpoch() or specified epoch.
|
||||||
|
* Automatically updates lastProcessEpoch after processing.
|
||||||
|
*
|
||||||
|
* @param entities 实体列表 | Entity list
|
||||||
|
* @param componentTypes 要检查变更的组件类型 | Component types to check for changes
|
||||||
|
* @param processor 处理函数 | Processor function
|
||||||
|
* @param sinceEpoch 可选的起始 epoch,默认使用 lastProcessEpoch | Optional starting epoch, defaults to lastProcessEpoch
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* protected process(entities: readonly Entity[]): void {
|
||||||
|
* // 只处理 Velocity 组件发生变更的实体
|
||||||
|
* this.forEachChanged(entities, [Velocity], (entity) => {
|
||||||
|
* const vel = this.requireComponent(entity, Velocity);
|
||||||
|
* // 只有 velocity 改变时才重新计算
|
||||||
|
* this.updatePhysics(entity, vel);
|
||||||
|
* });
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
protected forEachChanged<T extends ComponentConstructor[]>(
|
||||||
|
entities: readonly Entity[],
|
||||||
|
componentTypes: T,
|
||||||
|
processor: (entity: Entity, index: number) => void,
|
||||||
|
sinceEpoch?: number
|
||||||
|
): void {
|
||||||
|
const checkEpoch = sinceEpoch ?? this._lastProcessEpoch;
|
||||||
|
|
||||||
|
for (let i = 0; i < entities.length; i++) {
|
||||||
|
const entity = entities[i]!;
|
||||||
|
let hasChanged = false;
|
||||||
|
|
||||||
|
// 检查任意指定组件是否发生变更
|
||||||
|
// Check if any specified component has changed
|
||||||
|
for (const componentType of componentTypes) {
|
||||||
|
const component = entity.getComponent(componentType);
|
||||||
|
if (component && component.lastWriteEpoch > checkEpoch) {
|
||||||
|
hasChanged = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChanged) {
|
||||||
|
processor(entity, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动更新检查点
|
||||||
|
// Automatically update checkpoint
|
||||||
|
this._lastProcessEpoch = this.currentEpoch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过滤出有变更组件的实体
|
||||||
|
* Filter entities with changed components
|
||||||
|
*
|
||||||
|
* @param entities 实体列表 | Entity list
|
||||||
|
* @param componentTypes 要检查变更的组件类型 | Component types to check for changes
|
||||||
|
* @param sinceEpoch 可选的起始 epoch | Optional starting epoch
|
||||||
|
* @returns 有变更的实体数组 | Array of entities with changes
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* protected process(entities: readonly Entity[]): void {
|
||||||
|
* const changedEntities = this.filterChanged(entities, [Position, Velocity]);
|
||||||
|
* for (const entity of changedEntities) {
|
||||||
|
* // 处理位置或速度变化的实体
|
||||||
|
* }
|
||||||
|
* this.saveEpoch();
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
protected filterChanged<T extends ComponentConstructor[]>(
|
||||||
|
entities: readonly Entity[],
|
||||||
|
componentTypes: T,
|
||||||
|
sinceEpoch?: number
|
||||||
|
): Entity[] {
|
||||||
|
const checkEpoch = sinceEpoch ?? this._lastProcessEpoch;
|
||||||
|
const result: Entity[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < entities.length; i++) {
|
||||||
|
const entity = entities[i]!;
|
||||||
|
|
||||||
|
for (const componentType of componentTypes) {
|
||||||
|
const component = entity.getComponent(componentType);
|
||||||
|
if (component && component.lastWriteEpoch > checkEpoch) {
|
||||||
|
result.push(entity);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查实体的指定组件是否发生变更
|
||||||
|
* Check if entity's specified components have changed
|
||||||
|
*
|
||||||
|
* @param entity 实体 | Entity
|
||||||
|
* @param componentTypes 要检查的组件类型 | Component types to check
|
||||||
|
* @param sinceEpoch 可选的起始 epoch | Optional starting epoch
|
||||||
|
* @returns 是否有变更 | Whether there are changes
|
||||||
|
*/
|
||||||
|
protected hasChanged<T extends ComponentConstructor[]>(
|
||||||
|
entity: Entity,
|
||||||
|
componentTypes: T,
|
||||||
|
sinceEpoch?: number
|
||||||
|
): boolean {
|
||||||
|
const checkEpoch = sinceEpoch ?? this._lastProcessEpoch;
|
||||||
|
|
||||||
|
for (const componentType of componentTypes) {
|
||||||
|
const component = entity.getComponent(componentType);
|
||||||
|
if (component && component.lastWriteEpoch > checkEpoch) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,3 +23,35 @@ export type { ReactiveQueryChange, ReactiveQueryListener, ReactiveQueryConfig }
|
|||||||
export { CommandBuffer, CommandType } from './Core/CommandBuffer';
|
export { CommandBuffer, CommandType } from './Core/CommandBuffer';
|
||||||
export type { DeferredCommand } from './Core/CommandBuffer';
|
export type { DeferredCommand } from './Core/CommandBuffer';
|
||||||
export * from './EntityTags';
|
export * from './EntityTags';
|
||||||
|
|
||||||
|
// System Scheduling
|
||||||
|
export { SystemScheduler, CycleDependencyError, DEFAULT_STAGE_ORDER } from './Core/SystemScheduler';
|
||||||
|
export type { SystemStage, SystemSchedulingMetadata } from './Core/SystemScheduler';
|
||||||
|
export { SystemDependencyGraph } from './Core/SystemDependencyGraph';
|
||||||
|
export type { SystemDependencyInfo } from './Core/SystemDependencyGraph';
|
||||||
|
|
||||||
|
// Entity Handle
|
||||||
|
export {
|
||||||
|
makeHandle,
|
||||||
|
indexOf,
|
||||||
|
genOf,
|
||||||
|
isValidHandle,
|
||||||
|
handleEquals,
|
||||||
|
handleToString,
|
||||||
|
NULL_HANDLE,
|
||||||
|
INDEX_BITS,
|
||||||
|
GEN_BITS,
|
||||||
|
INDEX_MASK,
|
||||||
|
GEN_MASK,
|
||||||
|
MAX_ENTITIES,
|
||||||
|
MAX_GENERATION
|
||||||
|
} from './Core/EntityHandle';
|
||||||
|
export type { EntityHandle } from './Core/EntityHandle';
|
||||||
|
export { EntityHandleManager } from './Core/EntityHandleManager';
|
||||||
|
|
||||||
|
// Change Detection
|
||||||
|
export { EpochManager } from './Core/EpochManager';
|
||||||
|
|
||||||
|
// Compiled Query
|
||||||
|
export { CompiledQuery } from './Core/Query/CompiledQuery';
|
||||||
|
export type { InstanceTypes } from './Core/Query/CompiledQuery';
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ export type ComponentInstance<T> = T extends new (...args: any[]) => infer R ? R
|
|||||||
/**
|
/**
|
||||||
* 组件构造函数类型
|
* 组件构造函数类型
|
||||||
*
|
*
|
||||||
* 与 ComponentType 保持一致,避免类型转换
|
* 使用 Component 作为默认类型,与 ComponentType 保持一致。
|
||||||
|
* 这确保类型兼容性,因为所有实际组件都继承自 Component 类。
|
||||||
*/
|
*/
|
||||||
export type ComponentConstructor<T extends IComponent = IComponent> = new (...args: any[]) => T;
|
export type ComponentConstructor<T extends Component = Component> = new (...args: any[]) => T;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组件类型的通用约束
|
* 组件类型的通用约束
|
||||||
@@ -237,7 +238,7 @@ export interface TypedQueryCondition<
|
|||||||
/**
|
/**
|
||||||
* 组件类型守卫
|
* 组件类型守卫
|
||||||
*/
|
*/
|
||||||
export function isComponentType<T extends IComponent>(
|
export function isComponentType<T extends Component>(
|
||||||
value: any
|
value: any
|
||||||
): value is ComponentConstructor<T> {
|
): value is ComponentConstructor<T> {
|
||||||
return typeof value === 'function' && value.prototype instanceof Component;
|
return typeof value === 'function' && value.prototype instanceof Component;
|
||||||
|
|||||||
@@ -20,6 +20,22 @@ export interface IComponent {
|
|||||||
/** 组件所属的实体ID */
|
/** 组件所属的实体ID */
|
||||||
entityId: number | null;
|
entityId: number | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最后写入的 epoch
|
||||||
|
*
|
||||||
|
* 用于帧级变更检测,记录组件最后一次被修改时的 epoch。
|
||||||
|
*
|
||||||
|
* Last write epoch for frame-level change detection.
|
||||||
|
*/
|
||||||
|
readonly lastWriteEpoch: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记组件为已修改
|
||||||
|
*
|
||||||
|
* Mark component as modified with current epoch.
|
||||||
|
*/
|
||||||
|
markDirty(epoch: number): void;
|
||||||
|
|
||||||
/** 组件添加到实体时的回调 */
|
/** 组件添加到实体时的回调 */
|
||||||
onAddedToEntity(): void;
|
onAddedToEntity(): void;
|
||||||
/** 组件从实体移除时的回调 */
|
/** 组件从实体移除时的回调 */
|
||||||
|
|||||||
@@ -282,7 +282,9 @@ describe('CommandBuffer', () => {
|
|||||||
commandBuffer.addComponent(entity, new MarkerComponent());
|
commandBuffer.addComponent(entity, new MarkerComponent());
|
||||||
commandBuffer.destroyEntity(entity);
|
commandBuffer.destroyEntity(entity);
|
||||||
|
|
||||||
expect(commandBuffer.pendingCount).toBe(2);
|
// 由于去重逻辑,destroyEntity 会清除同一实体的其他操作
|
||||||
|
// Due to deduplication, destroyEntity clears other operations for the same entity
|
||||||
|
expect(commandBuffer.pendingCount).toBe(1);
|
||||||
|
|
||||||
commandBuffer.clear();
|
commandBuffer.clear();
|
||||||
|
|
||||||
|
|||||||
373
packages/core/tests/ECS/Core/CompiledQuery.test.ts
Normal file
373
packages/core/tests/ECS/Core/CompiledQuery.test.ts
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
import { CompiledQuery } from '../../../src/ECS/Core/Query/CompiledQuery';
|
||||||
|
import { Scene } from '../../../src/ECS/Scene';
|
||||||
|
import { Entity } from '../../../src/ECS/Entity';
|
||||||
|
import { Component } from '../../../src/ECS/Component';
|
||||||
|
import { ECSComponent } from '../../../src/ECS/Decorators';
|
||||||
|
|
||||||
|
// 测试组件
|
||||||
|
@ECSComponent('CompiledQuery_Position')
|
||||||
|
class PositionComponent extends Component {
|
||||||
|
public x: number;
|
||||||
|
public y: number;
|
||||||
|
|
||||||
|
constructor(...args: unknown[]) {
|
||||||
|
super();
|
||||||
|
const [x = 0, y = 0] = args as [number?, number?];
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ECSComponent('CompiledQuery_Velocity')
|
||||||
|
class VelocityComponent extends Component {
|
||||||
|
public vx: number;
|
||||||
|
public vy: number;
|
||||||
|
|
||||||
|
constructor(...args: unknown[]) {
|
||||||
|
super();
|
||||||
|
const [vx = 0, vy = 0] = args as [number?, number?];
|
||||||
|
this.vx = vx;
|
||||||
|
this.vy = vy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ECSComponent('CompiledQuery_Health')
|
||||||
|
class HealthComponent extends Component {
|
||||||
|
public current: number;
|
||||||
|
public max: number;
|
||||||
|
|
||||||
|
constructor(...args: unknown[]) {
|
||||||
|
super();
|
||||||
|
const [current = 100, max = 100] = args as [number?, number?];
|
||||||
|
this.current = current;
|
||||||
|
this.max = max;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('CompiledQuery', () => {
|
||||||
|
let scene: Scene;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
scene = new Scene();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
scene.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('创建和基本属性', () => {
|
||||||
|
it('应该能通过 QuerySystem.compile 创建', () => {
|
||||||
|
const query = scene.querySystem.compile(PositionComponent);
|
||||||
|
expect(query).toBeInstanceOf(CompiledQuery);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该保存组件类型列表', () => {
|
||||||
|
const query = scene.querySystem.compile(PositionComponent, VelocityComponent);
|
||||||
|
expect(query.componentTypes).toContain(PositionComponent);
|
||||||
|
expect(query.componentTypes).toContain(VelocityComponent);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('entities 属性', () => {
|
||||||
|
it('初始应该返回空数组', () => {
|
||||||
|
const query = scene.querySystem.compile(PositionComponent);
|
||||||
|
expect(query.entities).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该返回匹配的实体', () => {
|
||||||
|
const entity = scene.createEntity('test');
|
||||||
|
entity.addComponent(new PositionComponent(10, 20));
|
||||||
|
|
||||||
|
const query = scene.querySystem.compile(PositionComponent);
|
||||||
|
expect(query.entities).toHaveLength(1);
|
||||||
|
expect(query.entities[0]).toBe(entity);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该只返回拥有所有组件的实体', () => {
|
||||||
|
const entity1 = scene.createEntity('entity1');
|
||||||
|
entity1.addComponent(new PositionComponent());
|
||||||
|
entity1.addComponent(new VelocityComponent());
|
||||||
|
|
||||||
|
const entity2 = scene.createEntity('entity2');
|
||||||
|
entity2.addComponent(new PositionComponent());
|
||||||
|
// entity2 没有 VelocityComponent
|
||||||
|
|
||||||
|
const query = scene.querySystem.compile(PositionComponent, VelocityComponent);
|
||||||
|
expect(query.entities).toHaveLength(1);
|
||||||
|
expect(query.entities[0]).toBe(entity1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('count 属性', () => {
|
||||||
|
it('应该返回匹配实体的数量', () => {
|
||||||
|
const query = scene.querySystem.compile(PositionComponent);
|
||||||
|
expect(query.count).toBe(0);
|
||||||
|
|
||||||
|
const entity = scene.createEntity('test');
|
||||||
|
entity.addComponent(new PositionComponent());
|
||||||
|
|
||||||
|
expect(query.count).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('forEach', () => {
|
||||||
|
it('应该遍历所有匹配的实体', () => {
|
||||||
|
const entity1 = scene.createEntity('entity1');
|
||||||
|
entity1.addComponent(new PositionComponent(10, 20));
|
||||||
|
|
||||||
|
const entity2 = scene.createEntity('entity2');
|
||||||
|
entity2.addComponent(new PositionComponent(30, 40));
|
||||||
|
|
||||||
|
const query = scene.querySystem.compile(PositionComponent);
|
||||||
|
const visited: Entity[] = [];
|
||||||
|
|
||||||
|
query.forEach((entity, pos) => {
|
||||||
|
visited.push(entity);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(visited).toHaveLength(2);
|
||||||
|
expect(visited).toContain(entity1);
|
||||||
|
expect(visited).toContain(entity2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该提供类型安全的组件参数', () => {
|
||||||
|
const entity = scene.createEntity('test');
|
||||||
|
entity.addComponent(new PositionComponent(10, 20));
|
||||||
|
entity.addComponent(new VelocityComponent(1, 2));
|
||||||
|
|
||||||
|
const query = scene.querySystem.compile(PositionComponent, VelocityComponent);
|
||||||
|
|
||||||
|
query.forEach((entity, pos, vel) => {
|
||||||
|
expect(pos.x).toBe(10);
|
||||||
|
expect(pos.y).toBe(20);
|
||||||
|
expect(vel.vx).toBe(1);
|
||||||
|
expect(vel.vy).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('forEachChanged', () => {
|
||||||
|
it('应该只遍历变更的实体', () => {
|
||||||
|
const entity1 = scene.createEntity('entity1');
|
||||||
|
const pos1 = entity1.addComponent(new PositionComponent(10, 20));
|
||||||
|
|
||||||
|
const entity2 = scene.createEntity('entity2');
|
||||||
|
const pos2 = entity2.addComponent(new PositionComponent(30, 40));
|
||||||
|
|
||||||
|
const query = scene.querySystem.compile(PositionComponent);
|
||||||
|
|
||||||
|
// 获取当前 epoch
|
||||||
|
const epoch = scene.epochManager.current;
|
||||||
|
|
||||||
|
// 递增 epoch
|
||||||
|
scene.epochManager.increment();
|
||||||
|
|
||||||
|
// 只标记 entity1 的组件为已修改
|
||||||
|
pos1.markDirty(scene.epochManager.current);
|
||||||
|
|
||||||
|
const changed: Entity[] = [];
|
||||||
|
query.forEachChanged(epoch, (entity, pos) => {
|
||||||
|
changed.push(entity);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(changed).toHaveLength(1);
|
||||||
|
expect(changed[0]).toBe(entity1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('当所有组件都未变更时应该不遍历任何实体', () => {
|
||||||
|
const entity = scene.createEntity('test');
|
||||||
|
entity.addComponent(new PositionComponent(10, 20));
|
||||||
|
|
||||||
|
const query = scene.querySystem.compile(PositionComponent);
|
||||||
|
|
||||||
|
// 使用当前 epoch 检查 - 组件的 epoch 应该小于等于当前
|
||||||
|
const futureEpoch = scene.epochManager.current + 100;
|
||||||
|
|
||||||
|
const changed: Entity[] = [];
|
||||||
|
query.forEachChanged(futureEpoch, (entity, pos) => {
|
||||||
|
changed.push(entity);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(changed).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('first', () => {
|
||||||
|
it('应该返回第一个匹配的实体和组件', () => {
|
||||||
|
const entity = scene.createEntity('test');
|
||||||
|
entity.addComponent(new PositionComponent(10, 20));
|
||||||
|
|
||||||
|
const query = scene.querySystem.compile(PositionComponent);
|
||||||
|
const result = query.first();
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result![0]).toBe(entity);
|
||||||
|
expect(result![1].x).toBe(10);
|
||||||
|
expect(result![1].y).toBe(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('没有匹配实体时应该返回 null', () => {
|
||||||
|
const query = scene.querySystem.compile(PositionComponent);
|
||||||
|
const result = query.first();
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toArray', () => {
|
||||||
|
it('应该返回实体和组件的数组', () => {
|
||||||
|
const entity1 = scene.createEntity('entity1');
|
||||||
|
entity1.addComponent(new PositionComponent(10, 20));
|
||||||
|
|
||||||
|
const entity2 = scene.createEntity('entity2');
|
||||||
|
entity2.addComponent(new PositionComponent(30, 40));
|
||||||
|
|
||||||
|
const query = scene.querySystem.compile(PositionComponent);
|
||||||
|
const result = query.toArray();
|
||||||
|
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[0]![0]).toBe(entity1);
|
||||||
|
expect(result[0]![1].x).toBe(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('map', () => {
|
||||||
|
it('应该映射转换实体数据', () => {
|
||||||
|
const entity1 = scene.createEntity('entity1');
|
||||||
|
entity1.addComponent(new PositionComponent(10, 20));
|
||||||
|
|
||||||
|
const entity2 = scene.createEntity('entity2');
|
||||||
|
entity2.addComponent(new PositionComponent(30, 40));
|
||||||
|
|
||||||
|
const query = scene.querySystem.compile(PositionComponent);
|
||||||
|
const result = query.map((entity, pos) => pos.x + pos.y);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result).toContain(30); // 10 + 20
|
||||||
|
expect(result).toContain(70); // 30 + 40
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filter', () => {
|
||||||
|
it('应该过滤实体', () => {
|
||||||
|
const entity1 = scene.createEntity('entity1');
|
||||||
|
entity1.addComponent(new PositionComponent(10, 20));
|
||||||
|
|
||||||
|
const entity2 = scene.createEntity('entity2');
|
||||||
|
entity2.addComponent(new PositionComponent(30, 40));
|
||||||
|
|
||||||
|
const query = scene.querySystem.compile(PositionComponent);
|
||||||
|
const result = query.filter((entity, pos) => pos.x > 20);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0]).toBe(entity2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('find', () => {
|
||||||
|
it('应该找到第一个满足条件的实体', () => {
|
||||||
|
const entity1 = scene.createEntity('entity1');
|
||||||
|
entity1.addComponent(new PositionComponent(10, 20));
|
||||||
|
|
||||||
|
const entity2 = scene.createEntity('entity2');
|
||||||
|
entity2.addComponent(new PositionComponent(30, 40));
|
||||||
|
|
||||||
|
const query = scene.querySystem.compile(PositionComponent);
|
||||||
|
const result = query.find((entity, pos) => pos.x > 20);
|
||||||
|
|
||||||
|
expect(result).toBe(entity2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('找不到时应该返回 undefined', () => {
|
||||||
|
const entity = scene.createEntity('test');
|
||||||
|
entity.addComponent(new PositionComponent(10, 20));
|
||||||
|
|
||||||
|
const query = scene.querySystem.compile(PositionComponent);
|
||||||
|
const result = query.find((entity, pos) => pos.x > 100);
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('any', () => {
|
||||||
|
it('有匹配实体时应该返回 true', () => {
|
||||||
|
const entity = scene.createEntity('test');
|
||||||
|
entity.addComponent(new PositionComponent());
|
||||||
|
|
||||||
|
const query = scene.querySystem.compile(PositionComponent);
|
||||||
|
expect(query.any()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('没有匹配实体时应该返回 false', () => {
|
||||||
|
const query = scene.querySystem.compile(PositionComponent);
|
||||||
|
expect(query.any()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('empty', () => {
|
||||||
|
it('没有匹配实体时应该返回 true', () => {
|
||||||
|
const query = scene.querySystem.compile(PositionComponent);
|
||||||
|
expect(query.empty()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('有匹配实体时应该返回 false', () => {
|
||||||
|
const entity = scene.createEntity('test');
|
||||||
|
entity.addComponent(new PositionComponent());
|
||||||
|
|
||||||
|
const query = scene.querySystem.compile(PositionComponent);
|
||||||
|
expect(query.empty()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('缓存机制', () => {
|
||||||
|
it('应该缓存查询结果', () => {
|
||||||
|
const entity = scene.createEntity('test');
|
||||||
|
entity.addComponent(new PositionComponent());
|
||||||
|
|
||||||
|
const query = scene.querySystem.compile(PositionComponent);
|
||||||
|
|
||||||
|
// 第一次访问
|
||||||
|
const entities1 = query.entities;
|
||||||
|
// 第二次访问
|
||||||
|
const entities2 = query.entities;
|
||||||
|
|
||||||
|
// 应该返回相同的缓存数组
|
||||||
|
expect(entities1).toBe(entities2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('当实体变化时应该刷新缓存', () => {
|
||||||
|
const query = scene.querySystem.compile(PositionComponent);
|
||||||
|
|
||||||
|
// 初始为空
|
||||||
|
expect(query.count).toBe(0);
|
||||||
|
|
||||||
|
// 添加实体
|
||||||
|
const entity = scene.createEntity('test');
|
||||||
|
entity.addComponent(new PositionComponent());
|
||||||
|
|
||||||
|
// 应该检测到变化
|
||||||
|
expect(query.count).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('多组件查询', () => {
|
||||||
|
it('应该支持多组件查询', () => {
|
||||||
|
const entity = scene.createEntity('test');
|
||||||
|
entity.addComponent(new PositionComponent(10, 20));
|
||||||
|
entity.addComponent(new VelocityComponent(1, 2));
|
||||||
|
entity.addComponent(new HealthComponent(80, 100));
|
||||||
|
|
||||||
|
const query = scene.querySystem.compile(
|
||||||
|
PositionComponent,
|
||||||
|
VelocityComponent,
|
||||||
|
HealthComponent
|
||||||
|
);
|
||||||
|
|
||||||
|
query.forEach((entity, pos, vel, health) => {
|
||||||
|
expect(pos.x).toBe(10);
|
||||||
|
expect(vel.vx).toBe(1);
|
||||||
|
expect(health.current).toBe(80);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
380
packages/core/tests/ECS/Core/EntityHandle.test.ts
Normal file
380
packages/core/tests/ECS/Core/EntityHandle.test.ts
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
import {
|
||||||
|
makeHandle,
|
||||||
|
indexOf,
|
||||||
|
genOf,
|
||||||
|
isValidHandle,
|
||||||
|
handleEquals,
|
||||||
|
handleToString,
|
||||||
|
NULL_HANDLE,
|
||||||
|
INDEX_BITS,
|
||||||
|
GEN_BITS,
|
||||||
|
INDEX_MASK,
|
||||||
|
GEN_MASK,
|
||||||
|
MAX_ENTITIES,
|
||||||
|
MAX_GENERATION,
|
||||||
|
EntityHandle
|
||||||
|
} from '../../../src/ECS/Core/EntityHandle';
|
||||||
|
import { EntityHandleManager } from '../../../src/ECS/Core/EntityHandleManager';
|
||||||
|
|
||||||
|
describe('EntityHandle', () => {
|
||||||
|
describe('常量定义', () => {
|
||||||
|
it('INDEX_BITS 应该是 28', () => {
|
||||||
|
expect(INDEX_BITS).toBe(28);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GEN_BITS 应该是 20', () => {
|
||||||
|
expect(GEN_BITS).toBe(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('INDEX_MASK 应该正确', () => {
|
||||||
|
expect(INDEX_MASK).toBe((1 << INDEX_BITS) - 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GEN_MASK 应该正确', () => {
|
||||||
|
expect(GEN_MASK).toBe((1 << GEN_BITS) - 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MAX_ENTITIES 应该是 2^28', () => {
|
||||||
|
expect(MAX_ENTITIES).toBe(1 << INDEX_BITS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MAX_GENERATION 应该是 2^20', () => {
|
||||||
|
expect(MAX_GENERATION).toBe(1 << GEN_BITS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('NULL_HANDLE 应该是 0', () => {
|
||||||
|
expect(NULL_HANDLE).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('makeHandle', () => {
|
||||||
|
it('应该正确组合 index 和 generation', () => {
|
||||||
|
const handle = makeHandle(100, 5);
|
||||||
|
expect(indexOf(handle)).toBe(100);
|
||||||
|
expect(genOf(handle)).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该处理边界值', () => {
|
||||||
|
// 最小值
|
||||||
|
const minHandle = makeHandle(0, 0);
|
||||||
|
expect(indexOf(minHandle)).toBe(0);
|
||||||
|
expect(genOf(minHandle)).toBe(0);
|
||||||
|
|
||||||
|
// 最大 index
|
||||||
|
const maxIndexHandle = makeHandle(INDEX_MASK, 0);
|
||||||
|
expect(indexOf(maxIndexHandle)).toBe(INDEX_MASK);
|
||||||
|
|
||||||
|
// 最大 generation
|
||||||
|
const maxGenHandle = makeHandle(0, GEN_MASK);
|
||||||
|
expect(genOf(maxGenHandle)).toBe(GEN_MASK);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确处理大数值', () => {
|
||||||
|
const largeIndex = 1000000;
|
||||||
|
const largeGen = 500;
|
||||||
|
const handle = makeHandle(largeIndex, largeGen);
|
||||||
|
|
||||||
|
expect(indexOf(handle)).toBe(largeIndex);
|
||||||
|
expect(genOf(handle)).toBe(largeGen);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('indexOf', () => {
|
||||||
|
it('应该正确提取 index', () => {
|
||||||
|
const handle = makeHandle(12345, 67);
|
||||||
|
expect(indexOf(handle)).toBe(12345);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该对 NULL_HANDLE 返回 0', () => {
|
||||||
|
expect(indexOf(NULL_HANDLE)).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('genOf', () => {
|
||||||
|
it('应该正确提取 generation', () => {
|
||||||
|
const handle = makeHandle(12345, 67);
|
||||||
|
expect(genOf(handle)).toBe(67);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该对 NULL_HANDLE 返回 0', () => {
|
||||||
|
expect(genOf(NULL_HANDLE)).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isValidHandle', () => {
|
||||||
|
it('应该判断 NULL_HANDLE 为无效', () => {
|
||||||
|
expect(isValidHandle(NULL_HANDLE)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该判断非零句柄为有效', () => {
|
||||||
|
const handle = makeHandle(1, 0);
|
||||||
|
expect(isValidHandle(handle)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该判断带 generation 的句柄为有效', () => {
|
||||||
|
const handle = makeHandle(0, 1);
|
||||||
|
expect(isValidHandle(handle)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleEquals', () => {
|
||||||
|
it('应该正确比较相同句柄', () => {
|
||||||
|
const handle1 = makeHandle(100, 5);
|
||||||
|
const handle2 = makeHandle(100, 5);
|
||||||
|
expect(handleEquals(handle1, handle2)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确比较不同句柄', () => {
|
||||||
|
const handle1 = makeHandle(100, 5);
|
||||||
|
const handle2 = makeHandle(100, 6);
|
||||||
|
expect(handleEquals(handle1, handle2)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该区分 index 不同的句柄', () => {
|
||||||
|
const handle1 = makeHandle(100, 5);
|
||||||
|
const handle2 = makeHandle(101, 5);
|
||||||
|
expect(handleEquals(handle1, handle2)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleToString', () => {
|
||||||
|
it('应该返回可读的字符串格式', () => {
|
||||||
|
const handle = makeHandle(100, 5);
|
||||||
|
const str = handleToString(handle);
|
||||||
|
expect(str).toContain('100');
|
||||||
|
expect(str).toContain('5');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该对 NULL_HANDLE 返回特殊标记', () => {
|
||||||
|
const str = handleToString(NULL_HANDLE);
|
||||||
|
expect(str.toLowerCase()).toContain('null');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('EntityHandleManager', () => {
|
||||||
|
let manager: EntityHandleManager;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
manager = new EntityHandleManager();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('create', () => {
|
||||||
|
it('应该创建有效的句柄', () => {
|
||||||
|
const handle = manager.create();
|
||||||
|
expect(isValidHandle(handle)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该创建不同的句柄', () => {
|
||||||
|
const handle1 = manager.create();
|
||||||
|
const handle2 = manager.create();
|
||||||
|
expect(handleEquals(handle1, handle2)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该递增 index', () => {
|
||||||
|
const handle1 = manager.create();
|
||||||
|
const handle2 = manager.create();
|
||||||
|
expect(indexOf(handle2)).toBe(indexOf(handle1) + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('创建的句柄应该是存活的', () => {
|
||||||
|
const handle = manager.create();
|
||||||
|
expect(manager.isAlive(handle)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('创建的句柄默认应该是启用的', () => {
|
||||||
|
const handle = manager.create();
|
||||||
|
expect(manager.isEnabled(handle)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('destroy', () => {
|
||||||
|
it('应该销毁句柄', () => {
|
||||||
|
const handle = manager.create();
|
||||||
|
expect(manager.isAlive(handle)).toBe(true);
|
||||||
|
|
||||||
|
manager.destroy(handle);
|
||||||
|
expect(manager.isAlive(handle)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('销毁后使用相同 index 应该增加 generation', () => {
|
||||||
|
const handle1 = manager.create();
|
||||||
|
const index1 = indexOf(handle1);
|
||||||
|
|
||||||
|
manager.destroy(handle1);
|
||||||
|
const handle2 = manager.create();
|
||||||
|
const index2 = indexOf(handle2);
|
||||||
|
|
||||||
|
// 应该复用同一个 index
|
||||||
|
expect(index2).toBe(index1);
|
||||||
|
// 但 generation 应该不同
|
||||||
|
expect(genOf(handle2)).toBe(genOf(handle1) + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('销毁已销毁的句柄不应该报错', () => {
|
||||||
|
const handle = manager.create();
|
||||||
|
manager.destroy(handle);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
manager.destroy(handle);
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('销毁 NULL_HANDLE 不应该报错', () => {
|
||||||
|
expect(() => {
|
||||||
|
manager.destroy(NULL_HANDLE);
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isAlive', () => {
|
||||||
|
it('应该对存活句柄返回 true', () => {
|
||||||
|
const handle = manager.create();
|
||||||
|
expect(manager.isAlive(handle)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该对已销毁句柄返回 false', () => {
|
||||||
|
const handle = manager.create();
|
||||||
|
manager.destroy(handle);
|
||||||
|
expect(manager.isAlive(handle)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该对 NULL_HANDLE 返回 false', () => {
|
||||||
|
expect(manager.isAlive(NULL_HANDLE)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该对过期 generation 返回 false', () => {
|
||||||
|
const handle1 = manager.create();
|
||||||
|
manager.destroy(handle1);
|
||||||
|
const handle2 = manager.create();
|
||||||
|
|
||||||
|
// handle1 的 generation 已过期
|
||||||
|
expect(manager.isAlive(handle1)).toBe(false);
|
||||||
|
expect(manager.isAlive(handle2)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isEnabled/setEnabled', () => {
|
||||||
|
it('新创建的句柄默认启用', () => {
|
||||||
|
const handle = manager.create();
|
||||||
|
expect(manager.isEnabled(handle)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该能够禁用句柄', () => {
|
||||||
|
const handle = manager.create();
|
||||||
|
manager.setEnabled(handle, false);
|
||||||
|
expect(manager.isEnabled(handle)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该能够重新启用句柄', () => {
|
||||||
|
const handle = manager.create();
|
||||||
|
manager.setEnabled(handle, false);
|
||||||
|
manager.setEnabled(handle, true);
|
||||||
|
expect(manager.isEnabled(handle)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('对已销毁句柄设置启用状态不应该报错', () => {
|
||||||
|
const handle = manager.create();
|
||||||
|
manager.destroy(handle);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
manager.setEnabled(handle, true);
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('已销毁句柄应该返回未启用', () => {
|
||||||
|
const handle = manager.create();
|
||||||
|
manager.destroy(handle);
|
||||||
|
expect(manager.isEnabled(handle)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('aliveCount', () => {
|
||||||
|
it('初始时应该为 0', () => {
|
||||||
|
expect(manager.aliveCount).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('创建句柄后应该增加', () => {
|
||||||
|
manager.create();
|
||||||
|
expect(manager.aliveCount).toBe(1);
|
||||||
|
|
||||||
|
manager.create();
|
||||||
|
expect(manager.aliveCount).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('销毁句柄后应该减少', () => {
|
||||||
|
const handle1 = manager.create();
|
||||||
|
const handle2 = manager.create();
|
||||||
|
expect(manager.aliveCount).toBe(2);
|
||||||
|
|
||||||
|
manager.destroy(handle1);
|
||||||
|
expect(manager.aliveCount).toBe(1);
|
||||||
|
|
||||||
|
manager.destroy(handle2);
|
||||||
|
expect(manager.aliveCount).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('reset', () => {
|
||||||
|
it('应该重置所有状态', () => {
|
||||||
|
const handle1 = manager.create();
|
||||||
|
const handle2 = manager.create();
|
||||||
|
manager.destroy(handle1);
|
||||||
|
|
||||||
|
manager.reset();
|
||||||
|
|
||||||
|
expect(manager.aliveCount).toBe(0);
|
||||||
|
expect(manager.isAlive(handle2)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('重置后应该能重新创建句柄', () => {
|
||||||
|
manager.create();
|
||||||
|
manager.create();
|
||||||
|
manager.reset();
|
||||||
|
|
||||||
|
const newHandle = manager.create();
|
||||||
|
expect(isValidHandle(newHandle)).toBe(true);
|
||||||
|
expect(manager.isAlive(newHandle)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('大规模测试', () => {
|
||||||
|
it('应该能处理大量句柄', () => {
|
||||||
|
const handles: EntityHandle[] = [];
|
||||||
|
const count = 10000;
|
||||||
|
|
||||||
|
// 创建大量句柄
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
handles.push(manager.create());
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(manager.aliveCount).toBe(count);
|
||||||
|
|
||||||
|
// 验证所有句柄都是存活的
|
||||||
|
for (const handle of handles) {
|
||||||
|
expect(manager.isAlive(handle)).toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 销毁一半
|
||||||
|
for (let i = 0; i < count / 2; i++) {
|
||||||
|
manager.destroy(handles[i]!);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(manager.aliveCount).toBe(count / 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确复用已销毁的 index', () => {
|
||||||
|
// 创建并销毁
|
||||||
|
const handle1 = manager.create();
|
||||||
|
manager.destroy(handle1);
|
||||||
|
|
||||||
|
// 再次创建
|
||||||
|
const handle2 = manager.create();
|
||||||
|
|
||||||
|
// 应该复用 index
|
||||||
|
expect(indexOf(handle2)).toBe(indexOf(handle1));
|
||||||
|
// 但 generation 增加
|
||||||
|
expect(genOf(handle2)).toBe(genOf(handle1) + 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
72
packages/core/tests/ECS/Core/EpochManager.test.ts
Normal file
72
packages/core/tests/ECS/Core/EpochManager.test.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { EpochManager } from '../../../src/ECS/Core/EpochManager';
|
||||||
|
|
||||||
|
describe('EpochManager', () => {
|
||||||
|
let epochManager: EpochManager;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
epochManager = new EpochManager();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('初始状态', () => {
|
||||||
|
it('初始 epoch 应该是 1', () => {
|
||||||
|
expect(epochManager.current).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('increment', () => {
|
||||||
|
it('应该递增 epoch', () => {
|
||||||
|
const initial = epochManager.current;
|
||||||
|
epochManager.increment();
|
||||||
|
expect(epochManager.current).toBe(initial + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确递增多次', () => {
|
||||||
|
const initial = epochManager.current;
|
||||||
|
epochManager.increment();
|
||||||
|
epochManager.increment();
|
||||||
|
epochManager.increment();
|
||||||
|
expect(epochManager.current).toBe(initial + 3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('reset', () => {
|
||||||
|
it('应该重置 epoch 到 1', () => {
|
||||||
|
epochManager.increment();
|
||||||
|
epochManager.increment();
|
||||||
|
expect(epochManager.current).toBeGreaterThan(1);
|
||||||
|
|
||||||
|
epochManager.reset();
|
||||||
|
expect(epochManager.current).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('current getter', () => {
|
||||||
|
it('应该返回当前 epoch', () => {
|
||||||
|
expect(epochManager.current).toBe(1);
|
||||||
|
epochManager.increment();
|
||||||
|
expect(epochManager.current).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('使用场景', () => {
|
||||||
|
it('可以用于追踪帧数', () => {
|
||||||
|
// 模拟 10 帧
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
epochManager.increment();
|
||||||
|
}
|
||||||
|
expect(epochManager.current).toBe(11); // 初始 1 + 10 帧
|
||||||
|
});
|
||||||
|
|
||||||
|
it('可以用于变更检测', () => {
|
||||||
|
// 保存检查点
|
||||||
|
const checkpoint = epochManager.current;
|
||||||
|
|
||||||
|
// 模拟几帧
|
||||||
|
epochManager.increment();
|
||||||
|
epochManager.increment();
|
||||||
|
|
||||||
|
// 当前 epoch 应该大于检查点
|
||||||
|
expect(epochManager.current).toBeGreaterThan(checkpoint);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
555
packages/core/tests/ECS/Core/SystemScheduler.test.ts
Normal file
555
packages/core/tests/ECS/Core/SystemScheduler.test.ts
Normal file
@@ -0,0 +1,555 @@
|
|||||||
|
import {
|
||||||
|
SystemScheduler,
|
||||||
|
CycleDependencyError,
|
||||||
|
DEFAULT_STAGE_ORDER,
|
||||||
|
SystemStage
|
||||||
|
} from '../../../src/ECS/Core/SystemScheduler';
|
||||||
|
import { SystemDependencyGraph } from '../../../src/ECS/Core/SystemDependencyGraph';
|
||||||
|
import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem';
|
||||||
|
import { Scene } from '../../../src/ECS/Scene';
|
||||||
|
import { Matcher } from '../../../src/ECS/Utils/Matcher';
|
||||||
|
import { Entity } from '../../../src/ECS/Entity';
|
||||||
|
import { ECSSystem, Stage, Before, After, InSet } from '../../../src/ECS/Decorators';
|
||||||
|
|
||||||
|
// 测试系统
|
||||||
|
@ECSSystem('TestSystemA')
|
||||||
|
class TestSystemA extends EntitySystem {
|
||||||
|
public executionOrder: number = 0;
|
||||||
|
public static executionCounter = 0;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.nothing());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override process(entities: readonly Entity[]): void {
|
||||||
|
this.executionOrder = ++TestSystemA.executionCounter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ECSSystem('TestSystemB')
|
||||||
|
class TestSystemB extends EntitySystem {
|
||||||
|
public executionOrder: number = 0;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.nothing());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override process(entities: readonly Entity[]): void {
|
||||||
|
this.executionOrder = ++TestSystemA.executionCounter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ECSSystem('TestSystemC')
|
||||||
|
class TestSystemC extends EntitySystem {
|
||||||
|
public executionOrder: number = 0;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.nothing());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override process(entities: readonly Entity[]): void {
|
||||||
|
this.executionOrder = ++TestSystemA.executionCounter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SystemDependencyGraph', () => {
|
||||||
|
let graph: SystemDependencyGraph;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
graph = new SystemDependencyGraph();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addSystemNode', () => {
|
||||||
|
it('应该添加节点', () => {
|
||||||
|
graph.addSystemNode('SystemA');
|
||||||
|
expect(graph.size).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该支持添加多个节点', () => {
|
||||||
|
graph.addSystemNode('SystemA');
|
||||||
|
graph.addSystemNode('SystemB');
|
||||||
|
expect(graph.size).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addSetNode', () => {
|
||||||
|
it('应该添加虚拟集合节点', () => {
|
||||||
|
graph.addSetNode('CoreSystems');
|
||||||
|
expect(graph.size).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addEdge', () => {
|
||||||
|
it('应该添加边', () => {
|
||||||
|
graph.addSystemNode('SystemA');
|
||||||
|
graph.addSystemNode('SystemB');
|
||||||
|
graph.addEdge('SystemA', 'SystemB');
|
||||||
|
|
||||||
|
// 拓扑排序后 A 应该在 B 之前
|
||||||
|
const sorted = graph.topologicalSort();
|
||||||
|
expect(sorted.indexOf('SystemA')).toBeLessThan(sorted.indexOf('SystemB'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该忽略自引用边', () => {
|
||||||
|
graph.addSystemNode('SystemA');
|
||||||
|
graph.addEdge('SystemA', 'SystemA');
|
||||||
|
|
||||||
|
// 不应该产生循环
|
||||||
|
const sorted = graph.topologicalSort();
|
||||||
|
expect(sorted).toContain('SystemA');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('topologicalSort', () => {
|
||||||
|
it('应该返回正确的拓扑顺序', () => {
|
||||||
|
graph.addSystemNode('SystemA');
|
||||||
|
graph.addSystemNode('SystemB');
|
||||||
|
graph.addSystemNode('SystemC');
|
||||||
|
|
||||||
|
// A -> B -> C
|
||||||
|
graph.addEdge('SystemA', 'SystemB');
|
||||||
|
graph.addEdge('SystemB', 'SystemC');
|
||||||
|
|
||||||
|
const sorted = graph.topologicalSort();
|
||||||
|
|
||||||
|
expect(sorted.indexOf('SystemA')).toBeLessThan(sorted.indexOf('SystemB'));
|
||||||
|
expect(sorted.indexOf('SystemB')).toBeLessThan(sorted.indexOf('SystemC'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该检测循环依赖', () => {
|
||||||
|
graph.addSystemNode('SystemA');
|
||||||
|
graph.addSystemNode('SystemB');
|
||||||
|
|
||||||
|
// A -> B -> A (循环)
|
||||||
|
graph.addEdge('SystemA', 'SystemB');
|
||||||
|
graph.addEdge('SystemB', 'SystemA');
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
graph.topologicalSort();
|
||||||
|
}).toThrow(CycleDependencyError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该检测多节点循环依赖', () => {
|
||||||
|
graph.addSystemNode('SystemA');
|
||||||
|
graph.addSystemNode('SystemB');
|
||||||
|
graph.addSystemNode('SystemC');
|
||||||
|
|
||||||
|
// A -> B -> C -> A (循环)
|
||||||
|
graph.addEdge('SystemA', 'SystemB');
|
||||||
|
graph.addEdge('SystemB', 'SystemC');
|
||||||
|
graph.addEdge('SystemC', 'SystemA');
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
graph.topologicalSort();
|
||||||
|
}).toThrow(CycleDependencyError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该处理没有依赖的系统', () => {
|
||||||
|
graph.addSystemNode('SystemA');
|
||||||
|
graph.addSystemNode('SystemB');
|
||||||
|
|
||||||
|
// 没有边,两个独立系统
|
||||||
|
const sorted = graph.topologicalSort();
|
||||||
|
|
||||||
|
expect(sorted).toHaveLength(2);
|
||||||
|
expect(sorted).toContain('SystemA');
|
||||||
|
expect(sorted).toContain('SystemB');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('不应该包含虚拟节点在结果中', () => {
|
||||||
|
graph.addSystemNode('SystemA');
|
||||||
|
graph.addSetNode('CoreSystems');
|
||||||
|
|
||||||
|
const sorted = graph.topologicalSort();
|
||||||
|
|
||||||
|
expect(sorted).toContain('SystemA');
|
||||||
|
expect(sorted).not.toContain('set:CoreSystems');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildFromSystems', () => {
|
||||||
|
it('应该从系统依赖信息构建图', () => {
|
||||||
|
graph.buildFromSystems([
|
||||||
|
{ name: 'SystemA', before: ['SystemB'], after: [], sets: [] },
|
||||||
|
{ name: 'SystemB', before: [], after: [], sets: [] }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const sorted = graph.topologicalSort();
|
||||||
|
expect(sorted.indexOf('SystemA')).toBeLessThan(sorted.indexOf('SystemB'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该处理 after 依赖', () => {
|
||||||
|
graph.buildFromSystems([
|
||||||
|
{ name: 'SystemA', before: [], after: [], sets: [] },
|
||||||
|
{ name: 'SystemB', before: [], after: ['SystemA'], sets: [] }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const sorted = graph.topologicalSort();
|
||||||
|
expect(sorted.indexOf('SystemA')).toBeLessThan(sorted.indexOf('SystemB'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该处理 set 依赖', () => {
|
||||||
|
graph.buildFromSystems([
|
||||||
|
{ name: 'SystemA', before: [], after: [], sets: ['CoreSystems'] },
|
||||||
|
{ name: 'SystemB', before: [], after: [], sets: ['CoreSystems'] }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const sorted = graph.topologicalSort();
|
||||||
|
expect(sorted).toHaveLength(2);
|
||||||
|
expect(sorted).toContain('SystemA');
|
||||||
|
expect(sorted).toContain('SystemB');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clear', () => {
|
||||||
|
it('应该清除所有节点和边', () => {
|
||||||
|
graph.addSystemNode('SystemA');
|
||||||
|
graph.addSystemNode('SystemB');
|
||||||
|
graph.addEdge('SystemA', 'SystemB');
|
||||||
|
|
||||||
|
graph.clear();
|
||||||
|
|
||||||
|
expect(graph.size).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SystemScheduler', () => {
|
||||||
|
let scheduler: SystemScheduler;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = new SystemScheduler();
|
||||||
|
TestSystemA.executionCounter = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DEFAULT_STAGE_ORDER', () => {
|
||||||
|
it('应该包含所有阶段', () => {
|
||||||
|
expect(DEFAULT_STAGE_ORDER).toContain('startup');
|
||||||
|
expect(DEFAULT_STAGE_ORDER).toContain('preUpdate');
|
||||||
|
expect(DEFAULT_STAGE_ORDER).toContain('update');
|
||||||
|
expect(DEFAULT_STAGE_ORDER).toContain('postUpdate');
|
||||||
|
expect(DEFAULT_STAGE_ORDER).toContain('cleanup');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('阶段顺序应该正确', () => {
|
||||||
|
expect(DEFAULT_STAGE_ORDER.indexOf('startup')).toBeLessThan(
|
||||||
|
DEFAULT_STAGE_ORDER.indexOf('preUpdate')
|
||||||
|
);
|
||||||
|
expect(DEFAULT_STAGE_ORDER.indexOf('preUpdate')).toBeLessThan(
|
||||||
|
DEFAULT_STAGE_ORDER.indexOf('update')
|
||||||
|
);
|
||||||
|
expect(DEFAULT_STAGE_ORDER.indexOf('update')).toBeLessThan(
|
||||||
|
DEFAULT_STAGE_ORDER.indexOf('postUpdate')
|
||||||
|
);
|
||||||
|
expect(DEFAULT_STAGE_ORDER.indexOf('postUpdate')).toBeLessThan(
|
||||||
|
DEFAULT_STAGE_ORDER.indexOf('cleanup')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getSortedSystems', () => {
|
||||||
|
it('应该返回排序后的系统', () => {
|
||||||
|
const systemA = new TestSystemA();
|
||||||
|
const systemB = new TestSystemB();
|
||||||
|
|
||||||
|
const systems = scheduler.getSortedSystems([systemA, systemB], 'update');
|
||||||
|
expect(systems.length).toBeGreaterThanOrEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该按 updateOrder 排序', () => {
|
||||||
|
const systemA = new TestSystemA();
|
||||||
|
const systemB = new TestSystemB();
|
||||||
|
systemA.updateOrder = 10;
|
||||||
|
systemB.updateOrder = 5;
|
||||||
|
|
||||||
|
const systems = scheduler.getSortedSystems([systemA, systemB], 'update');
|
||||||
|
|
||||||
|
// B 的 updateOrder 更小,应该在 A 之前
|
||||||
|
expect(systems.indexOf(systemB)).toBeLessThan(systems.indexOf(systemA));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该按依赖关系排序', () => {
|
||||||
|
// 创建带有依赖关系的系统
|
||||||
|
@ECSSystem('DepSystemA')
|
||||||
|
@Stage('update')
|
||||||
|
class DepSystemA extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.nothing());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ECSSystem('DepSystemB')
|
||||||
|
@Stage('update')
|
||||||
|
@After('DepSystemA')
|
||||||
|
class DepSystemB extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.nothing());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemA = new DepSystemA();
|
||||||
|
const systemB = new DepSystemB();
|
||||||
|
|
||||||
|
const systems = scheduler.getSortedSystems([systemA, systemB], 'update');
|
||||||
|
const indexA = systems.indexOf(systemA);
|
||||||
|
const indexB = systems.indexOf(systemB);
|
||||||
|
|
||||||
|
expect(indexA).toBeLessThan(indexB);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该返回指定阶段的系统', () => {
|
||||||
|
@ECSSystem('PreUpdateSystem')
|
||||||
|
@Stage('preUpdate')
|
||||||
|
class PreUpdateSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.nothing());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ECSSystem('UpdateSystem')
|
||||||
|
@Stage('update')
|
||||||
|
class UpdateSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.nothing());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const preSystem = new PreUpdateSystem();
|
||||||
|
const updateSystem = new UpdateSystem();
|
||||||
|
|
||||||
|
const preSystems = scheduler.getSortedSystems([preSystem, updateSystem], 'preUpdate');
|
||||||
|
const updateSystems = scheduler.getSortedSystems([preSystem, updateSystem], 'update');
|
||||||
|
|
||||||
|
expect(preSystems).toContain(preSystem);
|
||||||
|
expect(preSystems).not.toContain(updateSystem);
|
||||||
|
|
||||||
|
expect(updateSystems).toContain(updateSystem);
|
||||||
|
expect(updateSystems).not.toContain(preSystem);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAllSortedSystems', () => {
|
||||||
|
it('应该返回所有阶段的系统', () => {
|
||||||
|
@ECSSystem('AllPreSystem')
|
||||||
|
@Stage('preUpdate')
|
||||||
|
class AllPreSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.nothing());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ECSSystem('AllUpdateSystem')
|
||||||
|
@Stage('update')
|
||||||
|
class AllUpdateSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.nothing());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const preSystem = new AllPreSystem();
|
||||||
|
const updateSystem = new AllUpdateSystem();
|
||||||
|
|
||||||
|
const allSystems = scheduler.getAllSortedSystems([preSystem, updateSystem]);
|
||||||
|
|
||||||
|
expect(allSystems).toContain(preSystem);
|
||||||
|
expect(allSystems).toContain(updateSystem);
|
||||||
|
// preUpdate 阶段在 update 之前
|
||||||
|
expect(allSystems.indexOf(preSystem)).toBeLessThan(allSystems.indexOf(updateSystem));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('markDirty', () => {
|
||||||
|
it('调用 markDirty 后应该重新排序', () => {
|
||||||
|
const systemA = new TestSystemA();
|
||||||
|
const systemB = new TestSystemB();
|
||||||
|
|
||||||
|
// 第一次排序
|
||||||
|
scheduler.getSortedSystems([systemA, systemB], 'update');
|
||||||
|
|
||||||
|
// 标记脏
|
||||||
|
scheduler.markDirty();
|
||||||
|
|
||||||
|
// 应该重新排序而不出错
|
||||||
|
const systems = scheduler.getSortedSystems([systemA, systemB], 'update');
|
||||||
|
expect(systems).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setUseDependencySort', () => {
|
||||||
|
it('禁用依赖排序后应该只使用 updateOrder', () => {
|
||||||
|
@ECSSystem('DisabledDepA')
|
||||||
|
@Stage('update')
|
||||||
|
class DisabledDepA extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.nothing());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ECSSystem('DisabledDepB')
|
||||||
|
@Stage('update')
|
||||||
|
@Before('DisabledDepA')
|
||||||
|
class DisabledDepB extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.nothing());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemA = new DisabledDepA();
|
||||||
|
const systemB = new DisabledDepB();
|
||||||
|
systemA.updateOrder = 1;
|
||||||
|
systemB.updateOrder = 2;
|
||||||
|
|
||||||
|
scheduler.setUseDependencySort(false);
|
||||||
|
|
||||||
|
const systems = scheduler.getSortedSystems([systemA, systemB], 'update');
|
||||||
|
// 禁用依赖排序后,按 updateOrder 排序
|
||||||
|
expect(systems.indexOf(systemA)).toBeLessThan(systems.indexOf(systemB));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('调度装饰器', () => {
|
||||||
|
describe('@Stage', () => {
|
||||||
|
it('应该设置系统的执行阶段', () => {
|
||||||
|
@ECSSystem('StageTestSystem')
|
||||||
|
@Stage('postUpdate')
|
||||||
|
class StageTestSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.nothing());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const system = new StageTestSystem();
|
||||||
|
expect(system.getStage()).toBe('postUpdate');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('@Before', () => {
|
||||||
|
it('应该设置系统的前置依赖', () => {
|
||||||
|
@ECSSystem('BeforeTestSystem')
|
||||||
|
@Before('OtherSystem')
|
||||||
|
class BeforeTestSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.nothing());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const system = new BeforeTestSystem();
|
||||||
|
expect(system.getBefore()).toContain('OtherSystem');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该支持多个前置依赖', () => {
|
||||||
|
@ECSSystem('MultiBeforeSystem')
|
||||||
|
@Before('SystemA', 'SystemB')
|
||||||
|
class MultiBeforeSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.nothing());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const system = new MultiBeforeSystem();
|
||||||
|
expect(system.getBefore()).toContain('SystemA');
|
||||||
|
expect(system.getBefore()).toContain('SystemB');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('@After', () => {
|
||||||
|
it('应该设置系统的后置依赖', () => {
|
||||||
|
@ECSSystem('AfterTestSystem')
|
||||||
|
@After('OtherSystem')
|
||||||
|
class AfterTestSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.nothing());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const system = new AfterTestSystem();
|
||||||
|
expect(system.getAfter()).toContain('OtherSystem');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('@InSet', () => {
|
||||||
|
it('应该设置系统所属的集合', () => {
|
||||||
|
@ECSSystem('InSetTestSystem')
|
||||||
|
@InSet('CoreSystems')
|
||||||
|
class InSetTestSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.nothing());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const system = new InSetTestSystem();
|
||||||
|
expect(system.getSets()).toContain('CoreSystems');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('组合使用', () => {
|
||||||
|
it('应该支持组合多个装饰器', () => {
|
||||||
|
@ECSSystem('CombinedSystem')
|
||||||
|
@Stage('update')
|
||||||
|
@After('InputSystem')
|
||||||
|
@Before('RenderSystem')
|
||||||
|
@InSet('CoreSystems')
|
||||||
|
class CombinedSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.nothing());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const system = new CombinedSystem();
|
||||||
|
expect(system.getStage()).toBe('update');
|
||||||
|
expect(system.getAfter()).toContain('InputSystem');
|
||||||
|
expect(system.getBefore()).toContain('RenderSystem');
|
||||||
|
expect(system.getSets()).toContain('CoreSystems');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Fluent API 调度配置', () => {
|
||||||
|
it('应该支持 stage() 方法', () => {
|
||||||
|
@ECSSystem('FluentStageSystem')
|
||||||
|
class FluentStageSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.nothing());
|
||||||
|
this.stage('postUpdate');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const system = new FluentStageSystem();
|
||||||
|
expect(system.getStage()).toBe('postUpdate');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该支持链式调用', () => {
|
||||||
|
@ECSSystem('FluentChainSystem')
|
||||||
|
class FluentChainSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.nothing());
|
||||||
|
this.stage('update')
|
||||||
|
.after('SystemA')
|
||||||
|
.before('SystemB')
|
||||||
|
.inSet('CoreSystems');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const system = new FluentChainSystem();
|
||||||
|
expect(system.getStage()).toBe('update');
|
||||||
|
expect(system.getAfter()).toContain('SystemA');
|
||||||
|
expect(system.getBefore()).toContain('SystemB');
|
||||||
|
expect(system.getSets()).toContain('CoreSystems');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('CycleDependencyError', () => {
|
||||||
|
it('应该包含循环节点信息', () => {
|
||||||
|
const error = new CycleDependencyError(['SystemA', 'SystemB', 'SystemA']);
|
||||||
|
expect(error.message).toContain('SystemA');
|
||||||
|
expect(error.message).toContain('SystemB');
|
||||||
|
expect(error.involvedNodes).toEqual(['SystemA', 'SystemB', 'SystemA']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该是 Error 的实例', () => {
|
||||||
|
const error = new CycleDependencyError(['SystemA']);
|
||||||
|
expect(error).toBeInstanceOf(Error);
|
||||||
|
expect(error.name).toBe('CycleDependencyError');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,463 @@
|
|||||||
|
import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem';
|
||||||
|
import { Entity } from '../../../src/ECS/Entity';
|
||||||
|
import { Component } from '../../../src/ECS/Component';
|
||||||
|
import { Scene } from '../../../src/ECS/Scene';
|
||||||
|
import { Matcher } from '../../../src/ECS/Utils/Matcher';
|
||||||
|
import { ECSComponent, ECSSystem } from '../../../src/ECS/Decorators';
|
||||||
|
|
||||||
|
// 测试组件
|
||||||
|
@ECSComponent('ChangeDetect_Position')
|
||||||
|
class PositionComponent extends Component {
|
||||||
|
private _x: number;
|
||||||
|
private _y: number;
|
||||||
|
|
||||||
|
constructor(...args: unknown[]) {
|
||||||
|
super();
|
||||||
|
const [x = 0, y = 0] = args as [number?, number?];
|
||||||
|
this._x = x;
|
||||||
|
this._y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get x(): number {
|
||||||
|
return this._x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set x(value: number) {
|
||||||
|
this._x = value;
|
||||||
|
// 实际使用中需要通过 entity.scene.epochManager.current 获取
|
||||||
|
// 这里简化测试,手动调用 markDirty
|
||||||
|
}
|
||||||
|
|
||||||
|
public get y(): number {
|
||||||
|
return this._y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set y(value: number) {
|
||||||
|
this._y = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setPosition(x: number, y: number, epoch: number): void {
|
||||||
|
this._x = x;
|
||||||
|
this._y = y;
|
||||||
|
this.markDirty(epoch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ECSComponent('ChangeDetect_Velocity')
|
||||||
|
class VelocityComponent extends Component {
|
||||||
|
private _vx: number;
|
||||||
|
private _vy: number;
|
||||||
|
|
||||||
|
constructor(...args: unknown[]) {
|
||||||
|
super();
|
||||||
|
const [vx = 0, vy = 0] = args as [number?, number?];
|
||||||
|
this._vx = vx;
|
||||||
|
this._vy = vy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get vx(): number {
|
||||||
|
return this._vx;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get vy(): number {
|
||||||
|
return this._vy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setVelocity(vx: number, vy: number, epoch: number): void {
|
||||||
|
this._vx = vx;
|
||||||
|
this._vy = vy;
|
||||||
|
this.markDirty(epoch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ECSComponent('ChangeDetect_Health')
|
||||||
|
class HealthComponent extends Component {
|
||||||
|
public current: number;
|
||||||
|
public max: number;
|
||||||
|
|
||||||
|
constructor(...args: unknown[]) {
|
||||||
|
super();
|
||||||
|
const [current = 100, max = 100] = args as [number?, number?];
|
||||||
|
this.current = current;
|
||||||
|
this.max = max;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试系统 - 暴露 protected 方法供测试
|
||||||
|
@ECSSystem('ChangeDetectionTestSystem')
|
||||||
|
class ChangeDetectionTestSystem extends EntitySystem {
|
||||||
|
public processedEntities: Entity[] = [];
|
||||||
|
public changedEntities: Entity[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(PositionComponent, VelocityComponent));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override process(entities: readonly Entity[]): void {
|
||||||
|
this.processedEntities = [...entities];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露 protected 方法供测试
|
||||||
|
public testForEachChanged(
|
||||||
|
entities: readonly Entity[],
|
||||||
|
componentTypes: any[],
|
||||||
|
processor: (entity: Entity, index: number) => void,
|
||||||
|
sinceEpoch?: number
|
||||||
|
): void {
|
||||||
|
this.forEachChanged(entities, componentTypes, processor, sinceEpoch);
|
||||||
|
}
|
||||||
|
|
||||||
|
public testFilterChanged(
|
||||||
|
entities: readonly Entity[],
|
||||||
|
componentTypes: any[],
|
||||||
|
sinceEpoch?: number
|
||||||
|
): Entity[] {
|
||||||
|
return this.filterChanged(entities, componentTypes, sinceEpoch);
|
||||||
|
}
|
||||||
|
|
||||||
|
public testHasChanged(
|
||||||
|
entity: Entity,
|
||||||
|
componentTypes: any[],
|
||||||
|
sinceEpoch?: number
|
||||||
|
): boolean {
|
||||||
|
return this.hasChanged(entity, componentTypes, sinceEpoch);
|
||||||
|
}
|
||||||
|
|
||||||
|
public testSaveEpoch(): void {
|
||||||
|
this.saveEpoch();
|
||||||
|
}
|
||||||
|
|
||||||
|
public testGetLastProcessEpoch(): number {
|
||||||
|
return this.lastProcessEpoch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public testGetCurrentEpoch(): number {
|
||||||
|
return this.currentEpoch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('EntitySystem 变更检测', () => {
|
||||||
|
let scene: Scene;
|
||||||
|
let system: ChangeDetectionTestSystem;
|
||||||
|
let entity1: Entity;
|
||||||
|
let entity2: Entity;
|
||||||
|
let entity3: Entity;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
scene = new Scene();
|
||||||
|
system = new ChangeDetectionTestSystem();
|
||||||
|
scene.addSystem(system);
|
||||||
|
|
||||||
|
// 创建测试实体
|
||||||
|
entity1 = scene.createEntity('entity1');
|
||||||
|
entity1.addComponent(new PositionComponent(10, 20));
|
||||||
|
entity1.addComponent(new VelocityComponent(1, 2));
|
||||||
|
|
||||||
|
entity2 = scene.createEntity('entity2');
|
||||||
|
entity2.addComponent(new PositionComponent(30, 40));
|
||||||
|
entity2.addComponent(new VelocityComponent(3, 4));
|
||||||
|
|
||||||
|
entity3 = scene.createEntity('entity3');
|
||||||
|
entity3.addComponent(new PositionComponent(50, 60));
|
||||||
|
entity3.addComponent(new VelocityComponent(5, 6));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
scene.removeSystem(system);
|
||||||
|
scene.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('lastProcessEpoch', () => {
|
||||||
|
it('初始值应该是 0', () => {
|
||||||
|
expect(system.testGetLastProcessEpoch()).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('currentEpoch', () => {
|
||||||
|
it('应该返回场景的当前 epoch', () => {
|
||||||
|
expect(system.testGetCurrentEpoch()).toBe(scene.epochManager.current);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('saveEpoch', () => {
|
||||||
|
it('应该保存当前 epoch', () => {
|
||||||
|
const currentEpoch = scene.epochManager.current;
|
||||||
|
system.testSaveEpoch();
|
||||||
|
expect(system.testGetLastProcessEpoch()).toBe(currentEpoch);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('递增 epoch 后 saveEpoch 应该保存新值', () => {
|
||||||
|
scene.epochManager.increment();
|
||||||
|
scene.epochManager.increment();
|
||||||
|
const currentEpoch = scene.epochManager.current;
|
||||||
|
|
||||||
|
system.testSaveEpoch();
|
||||||
|
expect(system.testGetLastProcessEpoch()).toBe(currentEpoch);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasChanged', () => {
|
||||||
|
it('新组件应该被检测为已变更', () => {
|
||||||
|
// 组件的 lastWriteEpoch 默认是 0,如果检查点也是 0,则不算变更
|
||||||
|
// 需要先保存 epoch,然后修改组件
|
||||||
|
system.testSaveEpoch();
|
||||||
|
scene.epochManager.increment();
|
||||||
|
|
||||||
|
const pos = entity1.getComponent(PositionComponent)!;
|
||||||
|
pos.setPosition(100, 200, scene.epochManager.current);
|
||||||
|
|
||||||
|
expect(system.testHasChanged(entity1, [PositionComponent])).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('未修改的组件应该不被检测为变更', () => {
|
||||||
|
system.testSaveEpoch();
|
||||||
|
scene.epochManager.increment();
|
||||||
|
|
||||||
|
// entity2 的组件未被修改
|
||||||
|
expect(system.testHasChanged(entity2, [PositionComponent])).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该检查多个组件类型', () => {
|
||||||
|
system.testSaveEpoch();
|
||||||
|
scene.epochManager.increment();
|
||||||
|
|
||||||
|
// 只修改 Velocity
|
||||||
|
const vel = entity1.getComponent(VelocityComponent)!;
|
||||||
|
vel.setVelocity(10, 20, scene.epochManager.current);
|
||||||
|
|
||||||
|
// 检查 Position 和 Velocity,应该检测到变更
|
||||||
|
expect(system.testHasChanged(entity1, [PositionComponent, VelocityComponent])).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('指定 sinceEpoch 参数应该使用该值作为检查点', () => {
|
||||||
|
const pos = entity1.getComponent(PositionComponent)!;
|
||||||
|
|
||||||
|
// 在 epoch 5 修改组件
|
||||||
|
scene.epochManager.increment(); // 2
|
||||||
|
scene.epochManager.increment(); // 3
|
||||||
|
scene.epochManager.increment(); // 4
|
||||||
|
scene.epochManager.increment(); // 5
|
||||||
|
pos.setPosition(100, 200, 5);
|
||||||
|
|
||||||
|
// 检查 epoch 4 之后的变更 - 应该检测到
|
||||||
|
expect(system.testHasChanged(entity1, [PositionComponent], 4)).toBe(true);
|
||||||
|
|
||||||
|
// 检查 epoch 5 之后的变更 - 不应该检测到
|
||||||
|
expect(system.testHasChanged(entity1, [PositionComponent], 5)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filterChanged', () => {
|
||||||
|
it('应该返回有变更的实体', () => {
|
||||||
|
system.testSaveEpoch();
|
||||||
|
scene.epochManager.increment();
|
||||||
|
|
||||||
|
// 只修改 entity1
|
||||||
|
const pos1 = entity1.getComponent(PositionComponent)!;
|
||||||
|
pos1.setPosition(100, 200, scene.epochManager.current);
|
||||||
|
|
||||||
|
const entities = [entity1, entity2, entity3];
|
||||||
|
const changed = system.testFilterChanged(entities, [PositionComponent]);
|
||||||
|
|
||||||
|
expect(changed).toHaveLength(1);
|
||||||
|
expect(changed[0]).toBe(entity1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该返回空数组当没有变更时', () => {
|
||||||
|
system.testSaveEpoch();
|
||||||
|
scene.epochManager.increment();
|
||||||
|
|
||||||
|
// 没有修改任何组件
|
||||||
|
const entities = [entity1, entity2, entity3];
|
||||||
|
const changed = system.testFilterChanged(entities, [PositionComponent]);
|
||||||
|
|
||||||
|
expect(changed).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该返回所有变更的实体', () => {
|
||||||
|
system.testSaveEpoch();
|
||||||
|
scene.epochManager.increment();
|
||||||
|
|
||||||
|
// 修改 entity1 和 entity3
|
||||||
|
const pos1 = entity1.getComponent(PositionComponent)!;
|
||||||
|
pos1.setPosition(100, 200, scene.epochManager.current);
|
||||||
|
|
||||||
|
const pos3 = entity3.getComponent(PositionComponent)!;
|
||||||
|
pos3.setPosition(500, 600, scene.epochManager.current);
|
||||||
|
|
||||||
|
const entities = [entity1, entity2, entity3];
|
||||||
|
const changed = system.testFilterChanged(entities, [PositionComponent]);
|
||||||
|
|
||||||
|
expect(changed).toHaveLength(2);
|
||||||
|
expect(changed).toContain(entity1);
|
||||||
|
expect(changed).toContain(entity3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('forEachChanged', () => {
|
||||||
|
it('应该只遍历有变更的实体', () => {
|
||||||
|
system.testSaveEpoch();
|
||||||
|
scene.epochManager.increment();
|
||||||
|
|
||||||
|
// 只修改 entity2
|
||||||
|
const pos2 = entity2.getComponent(PositionComponent)!;
|
||||||
|
pos2.setPosition(300, 400, scene.epochManager.current);
|
||||||
|
|
||||||
|
const entities = [entity1, entity2, entity3];
|
||||||
|
const processed: Entity[] = [];
|
||||||
|
|
||||||
|
system.testForEachChanged(entities, [PositionComponent], (entity) => {
|
||||||
|
processed.push(entity);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(processed).toHaveLength(1);
|
||||||
|
expect(processed[0]).toBe(entity2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该自动更新 lastProcessEpoch', () => {
|
||||||
|
system.testSaveEpoch();
|
||||||
|
const savedEpoch = system.testGetLastProcessEpoch();
|
||||||
|
|
||||||
|
scene.epochManager.increment();
|
||||||
|
const currentEpoch = scene.epochManager.current;
|
||||||
|
|
||||||
|
const entities = [entity1, entity2];
|
||||||
|
system.testForEachChanged(entities, [PositionComponent], () => {});
|
||||||
|
|
||||||
|
// forEachChanged 应该更新 lastProcessEpoch
|
||||||
|
expect(system.testGetLastProcessEpoch()).toBe(currentEpoch);
|
||||||
|
expect(system.testGetLastProcessEpoch()).toBeGreaterThan(savedEpoch);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('指定 sinceEpoch 时不应该影响自动更新', () => {
|
||||||
|
scene.epochManager.increment();
|
||||||
|
scene.epochManager.increment();
|
||||||
|
|
||||||
|
const entities = [entity1];
|
||||||
|
system.testForEachChanged(entities, [PositionComponent], () => {}, 0);
|
||||||
|
|
||||||
|
// 应该更新到当前 epoch
|
||||||
|
expect(system.testGetLastProcessEpoch()).toBe(scene.epochManager.current);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('实际使用场景', () => {
|
||||||
|
it('应该支持增量更新模式', () => {
|
||||||
|
// 模拟第一帧
|
||||||
|
scene.update();
|
||||||
|
|
||||||
|
// 保存检查点
|
||||||
|
system.testSaveEpoch();
|
||||||
|
const checkpoint = system.testGetLastProcessEpoch();
|
||||||
|
|
||||||
|
// 模拟第二帧 - 修改一个实体
|
||||||
|
scene.epochManager.increment();
|
||||||
|
const pos1 = entity1.getComponent(PositionComponent)!;
|
||||||
|
pos1.setPosition(100, 200, scene.epochManager.current);
|
||||||
|
|
||||||
|
// 只处理变更的实体
|
||||||
|
const changed = system.testFilterChanged(system.entities, [PositionComponent]);
|
||||||
|
expect(changed).toHaveLength(1);
|
||||||
|
expect(changed[0]).toBe(entity1);
|
||||||
|
|
||||||
|
// 更新检查点
|
||||||
|
system.testSaveEpoch();
|
||||||
|
|
||||||
|
// 模拟第三帧 - 没有修改
|
||||||
|
scene.epochManager.increment();
|
||||||
|
|
||||||
|
// 不应该有变更
|
||||||
|
const noChanges = system.testFilterChanged(system.entities, [PositionComponent]);
|
||||||
|
expect(noChanges).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确处理多次变更', () => {
|
||||||
|
system.testSaveEpoch();
|
||||||
|
|
||||||
|
// 第一次变更
|
||||||
|
scene.epochManager.increment();
|
||||||
|
const pos1 = entity1.getComponent(PositionComponent)!;
|
||||||
|
pos1.setPosition(100, 200, scene.epochManager.current);
|
||||||
|
|
||||||
|
let changed = system.testFilterChanged(system.entities, [PositionComponent]);
|
||||||
|
expect(changed).toContain(entity1);
|
||||||
|
|
||||||
|
// 更新检查点
|
||||||
|
system.testSaveEpoch();
|
||||||
|
|
||||||
|
// 第二次变更 - 不同实体
|
||||||
|
scene.epochManager.increment();
|
||||||
|
const pos2 = entity2.getComponent(PositionComponent)!;
|
||||||
|
pos2.setPosition(300, 400, scene.epochManager.current);
|
||||||
|
|
||||||
|
changed = system.testFilterChanged(system.entities, [PositionComponent]);
|
||||||
|
expect(changed).not.toContain(entity1);
|
||||||
|
expect(changed).toContain(entity2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('边界情况', () => {
|
||||||
|
it('空实体列表应该正常处理', () => {
|
||||||
|
const processed: Entity[] = [];
|
||||||
|
system.testForEachChanged([], [PositionComponent], (entity) => {
|
||||||
|
processed.push(entity);
|
||||||
|
});
|
||||||
|
expect(processed).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('空组件类型列表应该不检测到任何变更', () => {
|
||||||
|
system.testSaveEpoch();
|
||||||
|
scene.epochManager.increment();
|
||||||
|
|
||||||
|
const pos1 = entity1.getComponent(PositionComponent)!;
|
||||||
|
pos1.setPosition(100, 200, scene.epochManager.current);
|
||||||
|
|
||||||
|
// 空组件类型列表
|
||||||
|
const changed = system.testFilterChanged(system.entities, []);
|
||||||
|
expect(changed).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('实体缺少指定组件时应该跳过', () => {
|
||||||
|
// 创建一个只有 Position 的实体
|
||||||
|
const entityWithoutVelocity = scene.createEntity('noVel');
|
||||||
|
entityWithoutVelocity.addComponent(new PositionComponent(70, 80));
|
||||||
|
|
||||||
|
system.testSaveEpoch();
|
||||||
|
scene.epochManager.increment();
|
||||||
|
|
||||||
|
// 修改该实体的 Position
|
||||||
|
const pos = entityWithoutVelocity.getComponent(PositionComponent)!;
|
||||||
|
pos.setPosition(100, 200, scene.epochManager.current);
|
||||||
|
|
||||||
|
// 检查 Velocity 变更 - 该实体没有 Velocity,应该不被检测
|
||||||
|
const entities = [entityWithoutVelocity];
|
||||||
|
const changed = system.testFilterChanged(entities, [VelocityComponent]);
|
||||||
|
expect(changed).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Component.markDirty', () => {
|
||||||
|
let scene: Scene;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
scene = new Scene();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
scene.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该更新 lastWriteEpoch', () => {
|
||||||
|
const entity = scene.createEntity('test');
|
||||||
|
const pos = entity.addComponent(new PositionComponent(10, 20));
|
||||||
|
|
||||||
|
expect(pos.lastWriteEpoch).toBe(0);
|
||||||
|
|
||||||
|
pos.markDirty(5);
|
||||||
|
expect(pos.lastWriteEpoch).toBe(5);
|
||||||
|
|
||||||
|
pos.markDirty(10);
|
||||||
|
expect(pos.lastWriteEpoch).toBe(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user