Eva框架的ECS架构解析

ECS 架构简单来说就是用实体来组合组件,用组件来保存数据,用系统来进行运算。单一的实体没有任何意义,只有在组合组件之后这个实体才有意义。组件只能用于保存数据,不能自己进行任何的运算。系统只负责运算,不会持久化保存任何数据。这样就把数据和逻辑分离开来,通过组合的方式实现特定的功能,实现数据和逻辑的解耦,以及逻辑与逻辑之间的解耦。

简介

ECS 架构(Entity - Component - System)简介

  • Entity(实体)
    • 实体是游戏对象或者模拟世界中的一个对象。它可以是一个角色、一个道具、一个建筑等。例如在一个角色扮演游戏中,玩家角色、怪物、宝箱等都是实体。实体本身没有太多的行为和属性定义,它主要是一个 “容器”,用来挂载各种组件。
    • 实体通常通过一个唯一的标识符来进行区分,这样在系统处理大量实体时可以准确地定位和操作特定的实体。
  • Component(组件)
    • 组件是用来存储数据的。比如位置组件可以存储实体在三维空间中的坐标(x,y,z);速度组件可以存储实体的移动速度大小和方向;渲染组件可以存储实体的外观信息,如模型、纹理等。
    • 一个实体可以挂载多个组件。例如一个游戏中的汽车实体,它可能挂载有位置组件(用于记录汽车在场景中的位置)、速度组件(用于控制汽车的行驶速度)、模型组件(用于渲染汽车的外观)、碰撞组件(用于检测汽车与其他物体的碰撞)等。
  • System(系统)
    • 系统负责处理组件中的数据,实现游戏的逻辑。例如,一个移动系统会读取实体的速度组件和位置组件,根据速度和时间间隔来更新位置组件中的坐标,从而实现实体的移动。
    • 渲染系统会读取实体的渲染组件,将实体的模型和纹理等信息绘制到屏幕上。系统通常是在游戏的主循环中被调用,按照一定的顺序处理所有相关的实体和组件。

ECS 架构的优点

  • 高度解耦
    • 组件与实体解耦:实体只作为组件的容器,组件之间相互独立。这样在开发过程中,可以方便地添加、删除和修改组件,而不会对实体本身的结构产生太大影响。例如,要给一个游戏角色添加一个新的技能,只需要添加一个技能组件到该角色实体上,而不需要修改角色实体的其他部分。
    • 系统与组件解耦:系统只关心它所处理的组件类型,而不依赖于具体的实体。一个系统可以处理所有挂载了特定类型组件的实体。比如,移动系统只需要处理挂载了速度组件和位置组件的实体,不管这个实体是玩家角色、怪物还是道具。这种解耦方式使得代码的可维护性和可扩展性大大提高。
  • 性能优化优势
    • 数据局部性好:由于相同类型的组件数据存储在一起,在系统处理这些数据时,可以利用缓存的高效性。例如,在处理位置组件时,由于所有位置组件的数据在内存中是连续存储的,CPU 可以更快地读取和处理这些数据,减少了内存访问的开销。
    • 并行处理能力强:系统之间相对独立,在多核处理器环境下,可以将不同的系统分配到不同的核心上进行并行处理。例如,渲染系统可以在一个核心上处理实体的渲染,同时物理系统可以在另一个核心上处理实体的碰撞检测等物理行为,从而提高了游戏的整体性能。
  • 代码复用性高
    • 组件和系统都可以被复用。一旦开发了一个通用的组件,如基本的物理碰撞组件,就可以在多个不同的实体上使用。同样,一个系统,如简单的 AI 行为系统,也可以被应用到各种不同类型的实体上,只要这些实体挂载了该系统所需的组件。

架构图

画板

简单来说,就是Ticker会在每一帧调用每个system的更新函数,每个system的更新函数只会执行指定的component的更新逻辑

Eva的ECS实现

Eva的ECS实现和正统的不太一样

Eva是自己实现的tikcer,通过requestFrameIdleCallback,然后把pixi的ticker停掉了

每次ticker就会遍历所有system的update和lateUpdate

每次ticker也会调用gameObjectLoop,调用所有gameObject的所有component的update方法

Ticker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import { UpdateParams } from '../core/Component';
import Timeline from '../timeline/index';

interface TickerOptions {
autoStart?: boolean;
frameRate?: number;
}

/** Default Ticker Options */
const defaultOptions: Partial<TickerOptions> = {
autoStart: true,
frameRate: 60,
};

/**
* Timeline tool
*/
class Ticker {
/** Whether or not ticker should auto start */
autoStart: boolean;

/** FPS, The number of times that raf method is called per second */
frameRate: number;

/** Global Timeline **/
private timeline: Timeline;

/** Time between two frame */
private _frameDuration: number;

/** Ticker is a function will called in each raf */
private _tickers: Set<unknown>;

/** raf handle id */
_requestId: number;

/** Last frame render time */
private _lastFrameTime: number;

/** Frame count since from ticker beigning */
private _frameCount: number;

// private _activeWithPause: boolean;

/** Main ticker method handle */
private _ticker: (time?: number) => void;

/** Represents the status of the Ticker, If ticker has started, the value is true */
private _started: boolean;

/**
* @param autoStart - auto start game
* @param frameRate - game frame rate
*/
constructor(options?: TickerOptions) {
options = Object.assign({}, defaultOptions, options);

this._frameCount = 0;
this._frameDuration = 1000 / options.frameRate;
this.autoStart = options.autoStart;
this.frameRate = options.frameRate;

this.timeline = new Timeline({ originTime: 0, playbackRate: 1.0 });
this._lastFrameTime = this.timeline.currentTime;

this._tickers = new Set();
this._requestId = null;

this._ticker = () => {
if (this._started) {
this._requestId = requestAnimationFrame(this._ticker);
this.update();
}
};

if (this.autoStart) {
this.start();
}
}

/** Main loop, all _tickers will called in this method */
update() {
const currentTime = this.timeline.currentTime;

const durationTime = currentTime - this._lastFrameTime;
if (durationTime >= this._frameDuration) {
const frameTime = currentTime - (durationTime % this._frameDuration);
const deltaTime = frameTime - this._lastFrameTime;
this._lastFrameTime = frameTime;

const options: UpdateParams = {
deltaTime,
time: frameTime,
currentTime: frameTime,
frameCount: ++this._frameCount,
fps: Math.round(1000 / deltaTime),
};

this._tickers.forEach((func) => {
if (typeof func === 'function') {
func(options);
}
});
}
}

/** Add ticker function */
add(fn) {
this._tickers.add(fn);
}

/** Remove ticker function */
remove(fn) {
this._tickers.delete(fn);
}

/** Start main loop */
start() {
if (this._started) return;
this._started = true;
this.timeline.playbackRate = 1.0;
this._requestId = requestAnimationFrame(this._ticker);
}

/** Pause main loop */
pause() {
this._started = false;
this.timeline.playbackRate = 0;
}
setPlaybackRate(rate: number) {
this.timeline.playbackRate = rate;
}

Entity/GameObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
import Scene from '../game/Scene';
import Transform, { TransformParams } from './Transform';
import Component, { ComponentConstructor, ComponentParams, getComponentName } from './Component';
import { observer, observerAdded, observerRemoved } from './observer';

let _id = 0;
/** Generate unique id for gameObject */
function getId() {
return ++_id;
}

/**
* GameObject is a general purpose object. It consists of a unique id and components.
* @public
*/
class GameObject {
/** Name of this gameObject */
private _name: string;

/** Scene is an abstraction, represent a canvas layer */
private _scene: Scene;

/** A key-value map for components on this gameObject */
private _componentCache: Record<string, Component<ComponentParams>> = {};

/** Identifier of this gameObject */
public id: number;

/** Components apply to this gameObject */
public components: Component<ComponentParams>[] = [];

/** GameObject has been destroyed */
public destroyed = false;

/** Static GameObject will not run update and lateUpdate */
public isStatic = false;

/**
* Consruct a new gameObject
* @param name - the name of this gameObject
* @param obj - optional transform parameters for default Transform component
*/
constructor(name: string, obj?: TransformParams) {
this._name = name;
this.id = getId();
this.addComponent(new Transform(obj));
}

/**
* Get default transform component
* @returns transform component on this gameObject
* @readonly
*/
get transform(): Transform {
return this.getComponent(Transform);
}

/**
* Get parent gameObject
* @returns parent gameObject
* @readonly
*/
get parent(): GameObject {
return this.transform && this.transform.parent && this.transform.parent.gameObject;
}

/**
* Get the name of this gameObject
* @readonly
*/
get name() {
return this._name;
}

set scene(val: Scene) {
if (this._scene === val) return;
const scene = this._scene;
this._scene = val;
if (this.transform && this.transform.children) {
for (const child of this.transform.children) {
child.gameObject.scene = val;
}
}
if (val) {
val.addGameObject(this);
} else {
scene && scene.removeGameObject(this);
}
}

/**
* Get the scene which this gameObject added on
* @returns scene
* @readonly
*/
get scene() {
return this._scene;
}

/**
* Add child gameObject
* @param gameObject - child gameobject
*/
addChild(gameObject: GameObject) {
if (!gameObject || !gameObject.transform || gameObject === this) return;

if (!(gameObject instanceof GameObject)) {
throw new Error('addChild only receive GameObject');
}

if (!this.transform) {
throw new Error(`gameObject '${this.name}' has been destroy`);
}
gameObject.transform.parent = this.transform;
gameObject.scene = this.scene;
}

/**
* Remove child gameObject
* @param gameObject - child gameobject
*/
removeChild(gameObject: GameObject): GameObject {
if (!(gameObject instanceof GameObject) || !gameObject.parent || gameObject.parent !== this) {
return gameObject;
}

gameObject.transform.parent = null;
gameObject.scene = null;
return gameObject;
}

/**
* Add component to this gameObject
* @remarks
* If component has already been added on a gameObject, it will throw an error
* @param C - component instance or Component class
*/
addComponent<T extends Component<ComponentParams>>(C: T): T;
addComponent<T extends Component<ComponentParams>>(
C: ComponentConstructor<T>,
obj?: ComponentParams,
): T;
addComponent<T extends Component<ComponentParams>>(
C: T | ComponentConstructor<T>,
obj?: ComponentParams,
): T {
if (this.destroyed) return;
const componentName = getComponentName(C);
if (this._componentCache[componentName]) return;

let component;
if (C instanceof Function) {
component = new C(obj);
} else if (C instanceof Component) {
component = C;
} else {
throw new Error('addComponent recieve Component and Component Constructor');
}
if (component.gameObject) {
throw new Error(`component has been added on gameObject ${component.gameObject.name}`);
}

component.gameObject = this;
component.init && component.init(component.__componentDefaultParams);

// 这里不用在意,与ECS无关,是Eva的另外一套机制
observerAdded(component, component.name);
observer(component, component.name);

this.components.push(component);
this._componentCache[componentName] = component;

component.awake && component.awake();

return component;
}

/**
* Remove component on this gameObject
* @remarks
* default Transform component can not be removed, if the paramter represent a transform component, an error will be thrown.
* @param c - one of the compnoentName, component instance, component Class
* @returns
*/
removeComponent<T extends Component<ComponentParams>>(c: string): T;
removeComponent<T extends Component<ComponentParams>>(c: T): T;
removeComponent<T extends Component<ComponentParams>>(c: ComponentConstructor<T>): T;
removeComponent<T extends Component<ComponentParams>>(
c: string | T | ComponentConstructor<T>,
): T {
let componentName: string;
if (typeof c === 'string') {
componentName = c;
} else if (c instanceof Component) {
componentName = c.name;
} else if (c.componentName) {
componentName = c.componentName;
}

if (componentName === 'Transform') {
throw new Error("Transform can't be removed");
}

return this._removeComponent(componentName);
}

private _removeComponent<T extends Component>(componentName: string) {
const index = this.components.findIndex(({ name }) => name === componentName);
if (index === -1) return;

const component = this.components.splice(index, 1)[0] as T;
delete this._componentCache[componentName];
delete component.__componentDefaultParams;
component.onDestroy && component.onDestroy();
// 这里不用在意,与ECS无关,是Eva的另外一套机制
observerRemoved(component, componentName);
component.gameObject = undefined;
return component;
}

/**
* Get component on this gameObject
* @param c - one of the compnoentName, component instance, component Class
* @returns
*/
getComponent<T extends Component<ComponentParams>>(c: ComponentConstructor<T>): T;
getComponent<T extends Component>(c: string): T;
getComponent<T extends Component>(c: string | ComponentConstructor<T>): T {
let componentName: string;
if (typeof c === 'string') {
componentName = c;
} else if (c instanceof Component) {
componentName = c.name;
} else if (c.componentName) {
componentName = c.componentName;
}
if (typeof this._componentCache[componentName] !== 'undefined') {
return this._componentCache[componentName] as T;
} else {
return;
}
}

/**
* Remove this gameObject on its parent
* @returns return this gameObject
*/
remove() {
if (this.parent) return this.parent.removeChild(this);
}

/** Destory this gameObject */
destroy() {
if (!this.transform) {
console.error('Cannot destroy gameObject that have already been destroyed.');
return;
}
Array.from(this.transform.children).forEach(({ gameObject }) => {
gameObject.destroy();
});
this.remove();
this.transform.clearChildren();
for (const key in this._componentCache) {
this._removeComponent(key);
}
this.components.length = 0;
this.destroyed = true;
}
}

export default GameObject;

Component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import EventEmitter from 'eventemitter3';
import GameObject from './GameObject';

/** frame info pass to `Component.update` method */
export interface UpdateParams {
/** delta time from last frame */
deltaTime: number;

/** frame count since game begining */
frameCount: number;

/** current timestamp */
time: number;

/** current timestamp */
currentTime: number;

/** fps at current frame */
fps: number;
}

/** type of Component is function */
export type ComponentType = typeof Component;

/**
* Get component name from component instance or Component class
* @param component - component instance or Component class
* @returns component' name
* @example
* ```typescript
* import { Transform } from 'eva.js'
*
* assert(getComponentName(Transform) === 'Transform')
* assert(getComponentName(new Transform()) === 'Transform')
* ```
*/
export function getComponentName<T extends Component<ComponentParams>>(
component: T | ComponentConstructor<T>,
): string {
if (component instanceof Component) {
return component.name;
} else if (component instanceof Function) {
return component.componentName;
}
}

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ComponentParams {}

export interface ComponentConstructor<T extends Component<ComponentParams>> {
componentName: string;
new (params?: ComponentParams): T;
}

/**
* Component contain raw data apply to gameObject and how it interacts with the world
* @public
*/
class Component<T extends ComponentParams = object> extends EventEmitter {
/** Name of this component */
static componentName: string;

/** Name of this component */
public readonly name: string;

/**
* Represents the status of the component, If component has started, the value is true
* @defaultValue false
*/
started = false;

/**
* gameObject which this component had added on
* @remarks
* Component can only be added on one gameObject, otherwise an error will be thrown,
* see {@link https://eva.js.org/#/tutorials/gameObject} for more details
*/
gameObject: GameObject;

/** Default paramaters for this component */
__componentDefaultParams: T;

constructor(params?: T) {
super();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.name = this.constructor.componentName;
this.__componentDefaultParams = params;
}

/**
* Called during component construction
* @param params - optional initial parameters
* @override
*/
init?(params?: T): void;

/**
* Called when component is added to a gameObject
* @override
*/
awake?(): void;

/**
* Called after all component's `awake` method has been called
* @override
*/
start?(): void;

/**
* Called in every tick, change self property or other component property
* @param frame - frame info about this tick
* @override
*/
update?(frame: UpdateParams): void;

/**
* Called after all gameObject's `update` method has been called
* @param frame - frame info about this tick
* @override
*/
lateUpdate?(frame: UpdateParams): void;

/**
* Called before game runing or every time game paused
* @virtual
* @override
*/
onResume?(): void;

/**
* Called while the game paused.
* @override
*/
onPause?(): void;

/**
* Called while component be destroyed.
* @override
*/
onDestroy?(): void;
}

export default Component;

System

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import { PureObserverInfo } from './observer';
import { UpdateParams } from './Component';
import ComponentObserver from './ComponentObserver';
import Game from '../game/Game';

export interface SystemConstructor<T extends System = System> {
systemName: string;
observerInfo: PureObserverInfo;
new (params?: any): T;
}
/**
* Each System runs continuously and performs global actions on every Entity that possesses a Component of the same aspect as that System.
* @public
*/
class System<T extends object = object> {
/** System name */
static systemName: string;
name: string;

/**
* The collection of component properties observed by the System. System will respond to these changes
* @example
* ```typescript
* // TestSystem will respond to changes of `size` and `position` property of the Transform component
* class TestSystem extends System {
* static observerInfo = {
* Transform: [{ prop: 'size', deep: true }, { prop: 'position', deep: true }]
* }
* }
* ```
*/
static observerInfo: PureObserverInfo;

/** Component Observer */
componentObserver: ComponentObserver;

/** Game instance */
game: Game;

/** Represents the status of the component, if component has started, the value is true */
started = false;

/** Default paramaters for this system */
__systemDefaultParams: T;

constructor(params?: T) {
this.componentObserver = new ComponentObserver();
this.__systemDefaultParams = params;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.name = this.constructor.systemName;
}

/**
* Called when system is added to a gameObject
* @remarks
* The difference between init and awake is that `init` method recieves params.
* Both of those methods are called early than `start` method.
* Use this method to prepare data, ect.
* @param param - optional params
* @override
*/
init?(param?: T): void;

/**
* Calleen system installed
* @override
*/
awake?(): void;

/**
* Called after all system `awake` method has been called
* @override
*/
start?(): void;

/**
* Called in each tick
* @example
* ```typescript
* // run TWEEN `update` method in main requestAnimationFrame loop
* class TransitionSystem extends System {
* update() {
* TWEEN.update()
* }
* }
* ```
* @param e - info about this tick
* @override
*/
update?(e: UpdateParams): void;

/**
* Called after all system have called the `update` method
* @param e - info about this tick
* @override
*/
lateUpdate?(e: UpdateParams): void;

/**
* Called before game runing or every time game paused
* @override
*/
onResume?(): void;

/**
* Called while the game paused
* @override
*/
onPause?(): void;

/**
* Called while the system be destroyed.
* @override
*/
onDestroy?(): void;

/** Default destory method */
destroy() {
this.componentObserver = null;
this.__systemDefaultParams = null;
this.onDestroy?.();
}
}

export default System;

Game

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
class Game extends EventEmitter {
_scene: Scene;
canvas: HTMLCanvasElement;

/**
* State of game
* @defaultValue false
*/
playing = false;
started = false;
multiScenes: Scene[] = [];

/**
* Ticker
*/
ticker: Ticker;

/** Systems alled to this game */
systems: System[] = [];

constructor({
systems,
onInit,
frameRate = 60,
autoStart = true,
needScene = true,
}: GameParams = {}) {
super();
// if (window.__EVA_INSPECTOR_ENV__) {
// window.__EVA_GAME_INSTANCE__ = this;
// }
this.ticker = new Ticker({ autoStart: false, frameRate });
this.initTicker();

this.init(systems).then(() => {
if (needScene) {
this.loadScene(new Scene('scene'));
}

if (autoStart) {
this.start();
}

onInit?.();
});
}

private async init(systems: System[]) {
for (const system of systems) {
await this.addSystem(system);
}
}

/**
* Get scene on this game
*/
get scene() {
return this._scene;
}

set scene(scene: Scene) {
this._scene = scene;
}

get gameObjects() {
return getAllGameObjects(this);
}

get nonStaticGameObject() {
return this.gameObjects.filter((g) => !g.isStatic);
}

async addSystem<T extends System>(S: T): Promise<T>;
async addSystem<T extends System>(
S: SystemConstructor<T>,
obj?: ConstructorParameters<SystemConstructor<T>>,
): Promise<T>;

/**
* Add system
* @param S - system instance or system Class
* @typeParam T - system which extends base `System` class
* @typeparam U - type of system class
*/
async addSystem<T extends System>(
S: T | SystemConstructor<T>,
obj?: ConstructorParameters<SystemConstructor<T>>,
): Promise<T> {
let system;
if (S instanceof Function) {
system = new S(obj);
} else if (S instanceof System) {
system = S;
} else {
console.warn('can only add System');
return;
}

const hasTheSystem = this.systems.find((item) => {
return item.constructor === system.constructor;
});
if (hasTheSystem) {
console.warn(`${system.constructor.systemName} System has been added`);
return;
}

system.game = this;
system.init && (await system.init(system.__systemDefaultParams));

setSystemObserver(system, system.constructor);
initObserver(system.constructor);

try {
system.awake && (await system.awake());
} catch (e) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
console.error(`${system.constructor.systemName} awake error`, e);
}

this.systems.push(system);
return system;
}

/**
* Remove system from this game
* @param system - one of system instance / system Class or system name
*/
removeSystem<S extends System>(system: S | SystemConstructor<S> | string) {
if (!system) return;

let index = -1;
if (typeof system === 'string') {
index = this.systems.findIndex((s) => s.name === system);
} else if (system instanceof Function) {
index = this.systems.findIndex((s) => s.constructor === system);
} else if (system instanceof System) {
index = this.systems.findIndex((s) => s === system);
}

if (index > -1) {
this.systems[index].destroy && this.systems[index].destroy();
this.systems.splice(index, 1);
}
}

/**
* Get system
* @param S - system class or system name
* @returns system instance
*/
getSystem<T extends System>(S: SystemConstructor<T> | string): T {
return this.systems.find((system) => {
if (typeof S === 'string') {
return system.name === S;
} else {
return system instanceof S;
}
}) as T;
}

/** Pause game */
pause() {
if (!this.playing) return;
this.playing = false;
this.ticker.pause();
this.triggerPause();
}

/** Start game */
start() {
if (this.playing) return;
this.playing = true;
this.started = true;
this.ticker.start();
PIXITicker.shared.maxFPS = 60;
}

/** Resume game */
resume() {
if (this.playing) return;
this.playing = true;
this.ticker.start();
this.triggerResume();
}

/**
* add main render method to ticker
* @remarks
* the method added to ticker will called in each requestAnimationFrame,
* 1. call update method on all gameObject
* 2. call lastUpdate method on all gameObject
* 3. call update method on all system
* 4. call lastUpdate method on all system
*/
initTicker() {
this.ticker.add((e) => {
this.scene && gameObjectLoop(e, this.nonStaticGameObject);
for (const system of this.systems) {
try {
triggerStart(system);
system.update && system.update(e);
} catch (e) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
console.error(`${system.constructor.systemName} update error`, e);
}
}
for (const system of this.systems) {
try {
system.lateUpdate && system.lateUpdate(e);
} catch (e) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
console.error(`${system.constructor.systemName} lateUpdate error`, e);
}
}
});
}

/** Call onResume method on all gameObject's, and then call onResume method on all system */
triggerResume() {
gameObjectResume(this.gameObjects);
for (const system of this.systems) {
try {
system.onResume && system.onResume();
} catch (e) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
console.error(`${system.constructor.systemName}, onResume error`, e);
}
}
}

/** Call onPause method on all gameObject */
triggerPause() {
gameObjectPause(this.gameObjects);

for (const system of this.systems) {
try {
system.onPause && system.onPause();
} catch (e) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
console.error(`${system.constructor.systemName}, onPause error`, e);
}
}
}

// TODO: call system destroy method
/** remove all system on this game */
destroySystems() {
for (const system of [...this.systems]) {
this.removeSystem(system);
}
this.systems.length = 0;
}

/** Destroy game instance */
destroy() {
this.removeAllListeners();
this.pause();
this.scene.destroy();
this.destroySystems();
this.ticker = null;
this.scene = null;
this.canvas = null;
this.multiScenes = null;
}

loadScene({ scene, mode = LOAD_SCENE_MODE.SINGLE, params = {} }: LoadSceneParams) {
if (!scene) {
return;
}
switch (mode) {
case LOAD_SCENE_MODE.SINGLE:
this.scene = scene;
break;

case LOAD_SCENE_MODE.MULTI_CANVAS:
this.multiScenes.push(scene);
break;
}
this.emit('sceneChanged', { scene, mode, params });
}
}

其他用途

ECS架构也可以用于消息通信,比如把消息体封装进一个Component中,这样其对应的System就会在下一个Ticker获得该Component并进行处理