Skip to content

Commit 6c35984

Browse files
committed
feat(system): implement system scheduler with dependency management
- Added SystemScheduler class to manage system dependencies and execution order. - Updated World class to utilize SystemScheduler for registering systems. - Enhanced README with system scheduling features and examples. - Added tests for SystemScheduler to ensure correct behavior.
1 parent b67f4f0 commit 6c35984

7 files changed

Lines changed: 339 additions & 11 deletions

File tree

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- 📦 轻量级:零依赖,易于集成
1111
- ⚡ 内存高效:连续内存布局,优化的迭代性能
1212
- 🎣 生命周期钩子:支持组件和通配符关系的事件监听
13+
- 🔄 系统调度:支持系统依赖关系和拓扑排序执行
1314

1415
## 安装
1516

@@ -139,7 +140,7 @@ bun run examples/simple/demo.ts
139140
- `addComponent(entity, componentId, data)`: 向实体添加组件
140141
- `removeComponent(entity, componentId)`: 从实体移除组件
141142
- `createQuery(componentIds)`: 创建查询
142-
- `registerSystem(system)`: 注册系统
143+
- `registerSystem(system, dependencies?)`: 注册系统,可选指定依赖系统列表
143144
- `registerLifecycleHook(componentId, hook)`: 注册组件或通配符关系生命周期钩子
144145
- `unregisterLifecycleHook(componentId, hook)`: 注销组件或通配符关系生命周期钩子
145146
- `update(deltaTime)`: 更新世界
@@ -167,6 +168,17 @@ class MySystem implements System {
167168
}
168169
```
169170

171+
系统支持依赖关系排序,确保正确的执行顺序:
172+
173+
```typescript
174+
// 注册系统时指定依赖
175+
world.registerSystem(inputSystem);
176+
world.registerSystem(movementSystem, [inputSystem]); // movementSystem 依赖 inputSystem
177+
world.registerSystem(renderSystem, [movementSystem]); // renderSystem 依赖 movementSystem
178+
```
179+
180+
系统将按照拓扑排序执行,依赖系统始终在被依赖系统之前运行。
181+
170182
## 性能特点
171183

172184
- **Archetype 系统**:实体按组件组合分组,实现连续内存访问
@@ -199,6 +211,7 @@ src/
199211
├── query.ts # 查询系统
200212
├── query-filter.ts # 查询过滤器
201213
├── system.ts # 系统接口
214+
├── system-scheduler.ts # 系统调度器
202215
├── command-buffer.ts # 命令缓冲区
203216
├── types.ts # 类型定义
204217
├── utils.ts # 工具函数
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { component } from "../../src/entity";
2+
import type { Query } from "../../src/query";
3+
import type { System } from "../../src/system";
4+
import { World } from "../../src/world";
5+
6+
// 定义组件类型
7+
type Position = { x: number; y: number };
8+
type Velocity = { x: number; y: number };
9+
type Health = { value: number };
10+
11+
// 定义组件ID
12+
const PositionId = component<Position>();
13+
const VelocityId = component<Velocity>();
14+
const HealthId = component<Health>();
15+
16+
// 输入系统 - 处理用户输入
17+
class InputSystem implements System {
18+
update(world: World, deltaTime: number): void {
19+
console.log(`[InputSystem] Processing input at ${Date.now()}`);
20+
// 这里可以处理键盘/鼠标输入等
21+
}
22+
}
23+
24+
// 移动系统 - 依赖输入系统
25+
class MovementSystem implements System {
26+
private query: Query;
27+
28+
constructor(world: World) {
29+
this.query = world.createQuery([PositionId, VelocityId]);
30+
}
31+
32+
update(world: World, deltaTime: number): void {
33+
console.log(`[MovementSystem] Updating positions`);
34+
35+
this.query.forEach([PositionId, VelocityId], (entity, position, velocity) => {
36+
position.x += velocity.x * deltaTime;
37+
position.y += velocity.y * deltaTime;
38+
console.log(` Entity ${entity}: Position (${position.x.toFixed(2)}, ${position.y.toFixed(2)})`);
39+
});
40+
}
41+
}
42+
43+
// 伤害系统 - 依赖移动系统
44+
class DamageSystem implements System {
45+
private query: Query;
46+
47+
constructor(world: World) {
48+
this.query = world.createQuery([PositionId, HealthId]);
49+
}
50+
51+
update(world: World, deltaTime: number): void {
52+
console.log(`[DamageSystem] Applying damage based on position`);
53+
54+
this.query.forEach([PositionId, HealthId], (entity, position, health) => {
55+
// 根据位置计算伤害(示例逻辑)
56+
const damage = Math.abs(position.x) * 0.1;
57+
health.value -= damage;
58+
console.log(` Entity ${entity}: Health reduced by ${damage.toFixed(2)}, now ${health.value.toFixed(2)}`);
59+
});
60+
}
61+
}
62+
63+
// 渲染系统 - 依赖所有其他系统最后执行
64+
class RenderSystem implements System {
65+
private query: Query;
66+
67+
constructor(world: World) {
68+
this.query = world.createQuery([PositionId]);
69+
}
70+
71+
update(world: World, deltaTime: number): void {
72+
console.log(`[RenderSystem] Rendering entities`);
73+
74+
this.query.forEach([PositionId], (entity, position) => {
75+
console.log(` Rendering Entity ${entity} at (${position.x.toFixed(2)}, ${position.y.toFixed(2)})`);
76+
});
77+
}
78+
}
79+
80+
function main() {
81+
console.log("ECS Advanced Scheduling Demo - System Dependencies");
82+
console.log("=================================================");
83+
84+
const world = new World();
85+
86+
// 创建系统实例
87+
const inputSystem = new InputSystem();
88+
const movementSystem = new MovementSystem(world);
89+
const damageSystem = new DamageSystem(world);
90+
const renderSystem = new RenderSystem(world);
91+
92+
// 注册系统并指定依赖关系
93+
// 输入系统没有依赖
94+
world.registerSystem(inputSystem);
95+
96+
// 移动系统依赖输入系统
97+
world.registerSystem(movementSystem, [inputSystem]);
98+
99+
// 伤害系统依赖移动系统
100+
world.registerSystem(damageSystem, [movementSystem]);
101+
102+
// 渲染系统依赖伤害系统(确保所有更新都完成后才渲染)
103+
world.registerSystem(renderSystem, [damageSystem]);
104+
105+
// 创建一些实体
106+
const entity1 = world.createEntity();
107+
world.addComponent(entity1, PositionId, { x: 0, y: 0 });
108+
world.addComponent(entity1, VelocityId, { x: 2, y: 1 });
109+
world.addComponent(entity1, HealthId, { value: 100 });
110+
111+
const entity2 = world.createEntity();
112+
world.addComponent(entity2, PositionId, { x: 5, y: 3 });
113+
world.addComponent(entity2, VelocityId, { x: -1, y: 0.5 });
114+
world.addComponent(entity2, HealthId, { value: 80 });
115+
116+
// 运行几帧
117+
console.log("\n--- Frame 1 ---");
118+
world.update(1.0);
119+
120+
console.log("\n--- Frame 2 ---");
121+
world.update(1.0);
122+
123+
console.log("\nDemo completed!");
124+
}
125+
126+
if (import.meta.main) {
127+
main();
128+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ export * from "./world";
44
export * from "./archetype";
55
export * from "./query";
66
export * from "./system";
7+
export * from "./system-scheduler";
78
export * from "./types";

src/system-scheduler.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { describe, expect, it } from "bun:test";
2+
import { SystemScheduler } from "./system-scheduler";
3+
import type { System } from "./system";
4+
5+
describe("SystemScheduler", () => {
6+
it("should add and remove systems correctly", () => {
7+
const scheduler = new SystemScheduler();
8+
const systemA: System = { update: () => {} };
9+
const systemB: System = { update: () => {} };
10+
11+
scheduler.addSystem(systemA);
12+
scheduler.addSystem(systemB, [systemA]);
13+
14+
expect(scheduler.getExecutionOrder()).toEqual([systemA, systemB]);
15+
16+
scheduler.removeSystem(systemA);
17+
18+
// After removing systemA, systemB should still exist but without dependencies
19+
expect(scheduler.getExecutionOrder()).toEqual([systemB]);
20+
});
21+
22+
it("should clean up dependencies when removing systems", () => {
23+
const scheduler = new SystemScheduler();
24+
const systemA: System = { update: () => {} };
25+
const systemB: System = { update: () => {} };
26+
const systemC: System = { update: () => {} };
27+
28+
scheduler.addSystem(systemA);
29+
scheduler.addSystem(systemB, [systemA]);
30+
scheduler.addSystem(systemC, [systemB]);
31+
32+
// Verify initial state
33+
expect(scheduler.getExecutionOrder()).toEqual([systemA, systemB, systemC]);
34+
35+
// Remove systemB
36+
scheduler.removeSystem(systemB);
37+
38+
// Now systemC should no longer depend on systemB
39+
// Since systemC's dependencies were cleaned up, it should execute independently
40+
const order = scheduler.getExecutionOrder();
41+
expect(order).toContain(systemA);
42+
expect(order).toContain(systemC);
43+
expect(order).not.toContain(systemB);
44+
45+
// systemA should come before systemC in the execution order
46+
const aIndex = order.indexOf(systemA);
47+
const cIndex = order.indexOf(systemC);
48+
expect(aIndex).toBeLessThan(cIndex);
49+
});
50+
51+
it("should handle removing non-existent systems gracefully", () => {
52+
const scheduler = new SystemScheduler();
53+
const systemA: System = { update: () => {} };
54+
const systemB: System = { update: () => {} };
55+
56+
scheduler.addSystem(systemA);
57+
58+
// Removing a system that was never added should not throw
59+
expect(() => scheduler.removeSystem(systemB)).not.toThrow();
60+
61+
// systemA should still be there
62+
expect(scheduler.getExecutionOrder()).toEqual([systemA]);
63+
});
64+
65+
it("should clear all systems", () => {
66+
const scheduler = new SystemScheduler();
67+
const systemA: System = { update: () => {} };
68+
const systemB: System = { update: () => {} };
69+
70+
scheduler.addSystem(systemA, [systemB]);
71+
scheduler.addSystem(systemB);
72+
73+
expect(scheduler.getExecutionOrder()).toHaveLength(2);
74+
75+
scheduler.clear();
76+
77+
expect(scheduler.getExecutionOrder()).toHaveLength(0);
78+
});
79+
});

src/system-scheduler.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type { System } from "./system";
2+
3+
/**
4+
* System Scheduler for managing system dependencies and execution order
5+
*/
6+
export class SystemScheduler<ExtraParams extends any[] = [deltaTime: number]> {
7+
private systems = new Map<System<ExtraParams>, System<ExtraParams>[]>();
8+
private allSystems = new Set<System<ExtraParams>>();
9+
10+
/**
11+
* Add a system with optional dependencies
12+
* @param system The system to add
13+
* @param dependencies Systems that this system depends on (must run before this system)
14+
*/
15+
addSystem(system: System<ExtraParams>, dependencies: System<ExtraParams>[] = []): void {
16+
this.systems.set(system, dependencies);
17+
this.allSystems.add(system);
18+
// Also add dependencies to the set
19+
for (const dep of dependencies) {
20+
this.allSystems.add(dep);
21+
}
22+
}
23+
24+
/**
25+
* Remove a system
26+
* @param system The system to remove
27+
*/
28+
removeSystem(system: System<ExtraParams>): void {
29+
this.systems.delete(system);
30+
this.allSystems.delete(system);
31+
32+
// Remove this system from all dependency lists
33+
for (const [sys, deps] of this.systems) {
34+
const index = deps.indexOf(system);
35+
if (index !== -1) {
36+
deps.splice(index, 1);
37+
}
38+
}
39+
}
40+
41+
/**
42+
* Get the execution order of systems based on dependencies
43+
* Uses topological sort
44+
*/
45+
getExecutionOrder(): System<ExtraParams>[] {
46+
const result: System<ExtraParams>[] = [];
47+
const visited = new Set<System<ExtraParams>>();
48+
const visiting = new Set<System<ExtraParams>>();
49+
50+
const visit = (system: System<ExtraParams>): void => {
51+
if (visited.has(system)) return;
52+
if (visiting.has(system)) {
53+
throw new Error("Circular dependency detected in system scheduling");
54+
}
55+
56+
visiting.add(system);
57+
58+
const dependencies = this.systems.get(system) || [];
59+
for (const dep of dependencies) {
60+
visit(dep);
61+
}
62+
63+
visiting.delete(system);
64+
visited.add(system);
65+
result.push(system);
66+
};
67+
68+
for (const system of this.allSystems) {
69+
if (!visited.has(system)) {
70+
visit(system);
71+
}
72+
}
73+
74+
return result;
75+
}
76+
77+
/**
78+
* Clear all systems and dependencies
79+
*/
80+
clear(): void {
81+
this.systems.clear();
82+
this.allSystems.clear();
83+
}
84+
}

src/world.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,30 @@ describe("World", () => {
224224

225225
expect(updateCalled).toBe(true);
226226
});
227+
228+
it("should execute systems in dependency order", () => {
229+
const world = new World();
230+
const executionOrder: string[] = [];
231+
232+
const systemA = {
233+
update: () => executionOrder.push("A"),
234+
};
235+
const systemB = {
236+
update: () => executionOrder.push("B"),
237+
};
238+
const systemC = {
239+
update: () => executionOrder.push("C"),
240+
};
241+
242+
// C depends on B, B depends on A
243+
world.registerSystem(systemA);
244+
world.registerSystem(systemB, [systemA]);
245+
world.registerSystem(systemC, [systemB]);
246+
247+
world.update(0.016);
248+
249+
expect(executionOrder).toEqual(["A", "B", "C"]);
250+
});
227251
});
228252

229253
describe("Query", () => {

0 commit comments

Comments
 (0)