mani-game-engine 1.0.0-pre.10 → 1.0.0-pre.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/gameEngine.ts CHANGED
@@ -1,415 +1,613 @@
1
1
  import {Action, Class, EngineSignals, GameEnginePlugin, OnFixedUpdateParams, OnUpdateParams, Service, System} from './types';
2
2
  import {Signal, SignalBinding} from 'mani-signal';
3
- import {Entity} from './entity';
4
- import {ID, putIfAbsent} from 'mani-injector';
3
+ import {Entity, RemoveOptions} from './entity';
5
4
  import {EcsInjector, signalHandlers, staticSignalHandlers} from './ecsInjector';
6
5
  import {Clock, GameClock} from './clock';
7
6
  import {SystemContext} from './systemContext';
7
+ import {ID, putIfAbsent} from './injector';
8
8
 
9
9
  type AddEntry = [Entity, () => void];
10
- type RemoveEntry = [Entity, () => void];
10
+
11
+ type RemoveEntry = { entity: Entity; resolve: () => void; options: RemoveOptions };
11
12
 
12
13
  const defaultOptions = {
13
- fixedDeltaTime: 1 / 60,
14
- plugins: [] as GameEnginePlugin[],
14
+ fixedDeltaTime: 1 / 60,
15
+ plugins: [] as GameEnginePlugin[],
15
16
  };
16
17
 
17
18
  export type GameEngineOptions = Partial<typeof defaultOptions>;
18
19
  declare global {
19
- interface GameEngineActions {
20
- 'testAction': string
21
- }
20
+ interface GameEngineActions {
21
+ 'testAction': string;
22
+ }
22
23
  }
23
24
 
24
25
  type GameEngineActionParams<K extends keyof GameEngineActions> = GameEngineActions[K];
25
26
  type GameAction<K extends keyof GameEngineActions> = (engine: GameEngine, params: GameEngineActionParams<K>) => Promise<any> | any;
26
27
 
28
+ let id = 0;
27
29
  export class GameEngine {
28
-
29
- readonly onPrepare: Signal;
30
- readonly onStart: Signal;
31
- readonly onRender: Signal<OnUpdateParams>;
32
- readonly onUpdate: Signal<OnUpdateParams>;
33
- readonly onFixedUpdate: Signal<OnFixedUpdateParams>;
34
- readonly onPrePhysicsUpdate: Signal<OnFixedUpdateParams>;
35
- readonly onPostPhysicsUpdate: Signal<OnFixedUpdateParams>;
36
- readonly onLateFixedUpdate: Signal<OnFixedUpdateParams>;
37
- readonly onLateUpdate: Signal<OnUpdateParams>;
38
-
39
- private readonly onEarlyUpdateSet = new Set<System>();
40
- private readonly onUpdateSet = new Set<System>();
41
- private readonly onFixedUpdateSet = new Set<System>();
42
- private readonly onPreFixedUpdateSet = new Set<System>();
43
- private readonly onPhysicsUpdateSet = new Set<System>();
44
- private readonly onPostPhysicsUpdateSet = new Set<System>();
45
- private readonly onLateFixedUpdateSet = new Set<System>();
46
- private readonly onLateUpdateSet = new Set<System>();
47
- private readonly onRenderUpdateSet = new Set<System>();
48
-
49
- private readonly entitySystemMap = new Map<Entity, SystemContext[]>();
50
-
51
- private readonly serviceClasses = new Set<Class<Service>>();
52
- private readonly services: Service[] = [];
53
-
54
- private framePromise!: Promise<any>;
55
- private resolveFrame: () => void = () => undefined;
56
-
57
- private addQueue: AddEntry[] = [];
58
- private removeQueue: RemoveEntry[] = [];
59
-
60
- private _started = false;
61
-
62
- get started(): boolean { return this._started; }
63
-
64
- private _frameCount = 0;
65
- get frameCount(): number { return this._frameCount; }
66
-
67
- private _fixedFrameCount = 0;
68
- get fixedFrameCount(): number { return this._fixedFrameCount; }
69
-
70
- get numEntities(): number { return this.entitySystemMap.size;}
30
+ readonly id = id++;
31
+ readonly onPrepare: Signal;
32
+ readonly onStart: Signal;
33
+ readonly onEnd: Signal;
34
+ readonly onRender: Signal<OnUpdateParams>;
35
+ readonly onUpdate: Signal<OnUpdateParams>;
36
+ readonly onFixedUpdate: Signal<OnFixedUpdateParams>;
37
+ readonly onPrePhysicsUpdate: Signal<OnFixedUpdateParams>;
38
+ readonly onPhysicsUpdate: Signal<OnFixedUpdateParams>;
39
+ readonly onPostPhysicsUpdate: Signal<OnFixedUpdateParams>;
40
+ readonly onLateFixedUpdate: Signal<OnFixedUpdateParams>;
41
+ readonly onLateUpdate: Signal<OnUpdateParams>;
42
+ readonly onAddEntity: Signal<Entity>;
43
+ readonly onRemoveEntity: Signal<Entity>;
44
+
45
+ private readonly onEarlyUpdateSet = new Set<System>();
46
+ private readonly onUpdateSet = new Set<System>();
47
+ private readonly onFixedUpdateSet = new Set<System>();
48
+ private readonly onPreFixedUpdateSet = new Set<System>();
49
+ private readonly onPhysicsUpdateSet = new Set<System>();
50
+ private readonly onPrePhysicsUpdateSet = new Set<System>();
51
+ private readonly onPostPhysicsUpdateSet = new Set<System>();
52
+ private readonly onLateFixedUpdateSet = new Set<System>();
53
+ private readonly onLateUpdateSet = new Set<System>();
54
+ private readonly onRenderUpdateSet = new Set<System>();
55
+ private readonly onAddEntitySet = new Set<System>();
56
+ private readonly onRemoveEntitySet = new Set<System>();
57
+
58
+ // TODO: for performance, we could store the systems directly in the entity
59
+ private readonly entitySystemMap = new Map<Entity, Set<SystemContext>>();
60
+
61
+ private readonly registeredServices = new Set<Class<Service>>();
62
+ private readonly services: Service[] = [];
63
+ private readonly systemChildInjectors = new Map<object, EcsInjector>();
64
+
65
+ private framePromise!: Promise<any>;
66
+ private resolveFrame: () => void = () => undefined;
67
+
68
+ private addQueue: AddEntry[] = [];
69
+ private removeQueue: RemoveEntry[] = [];
70
+
71
+ private _started = false;
72
+
73
+ get started(): boolean { return this._started; }
74
+
75
+ private _frameCount = 0;
76
+ get frameCount(): number { return this._frameCount; }
77
+
78
+ private _fixedFrameCount = 0;
79
+ get fixedFrameCount(): number { return this._fixedFrameCount; }
80
+
81
+ get numEntities(): number { return this.entitySystemMap.size;}
71
82
 
72
83
  // TODO inject clock into game engine
73
84
  // TODO inject space into game engine
74
85
 
75
- constructor(
76
- readonly injector: EcsInjector<Class<System>>,
77
- readonly clock: Clock,
78
- ) {
79
- this.injector.map(GameEngine).toValue(this);
80
-
81
- this.onPrepare = this.injector.getSignal(EngineSignals.OnPrepare);
82
- this.onStart = this.injector.getSignal(EngineSignals.OnStart);
83
- this.onRender = this.injector.getSignal(EngineSignals.OnRender);
84
- this.onFixedUpdate = this.injector.getSignal(EngineSignals.OnFixedUpdate);
85
- this.onUpdate = this.injector.getSignal(EngineSignals.OnUpdate);
86
- this.onLateUpdate = this.injector.getSignal(EngineSignals.OnLateUpdate);
87
- this.onPrePhysicsUpdate = this.injector.getSignal(EngineSignals.OnPostPhysics);
88
- this.onPostPhysicsUpdate = this.injector.getSignal(EngineSignals.OnPostPhysics);
89
- this.onLateFixedUpdate = this.injector.getSignal(EngineSignals.onLateFixedUpdate);
90
-
91
- this.setupClock();
92
- this.prepareFrame();
93
- }
94
-
95
- static createDefault(options?: GameEngineOptions) {
96
- const opts = {...defaultOptions, ...options};
97
- const clock = new GameClock({
98
- fixedDeltaTime: opts.fixedDeltaTime,
99
- autoStart: false,
100
- });
101
- const injector = new EcsInjector<Class<System>>();
102
- const engine = new GameEngine(injector, clock);
103
- engine.installPlugins(opts.plugins);
104
- return engine;
105
- }
106
-
107
- static createDefaultWithInjector(injector: EcsInjector<Class<System>>, options?: GameEngineOptions) {
108
- const opts = {...defaultOptions, ...options};
109
- const clock = new GameClock({
110
- fixedDeltaTime: opts.fixedDeltaTime,
111
- autoStart: false,
112
- });
113
- const engine = new GameEngine(injector, clock);
114
- engine.installPlugins(opts.plugins);
115
- return engine;
116
- }
117
-
118
- addService(service: Service): void {
119
- this.mapAnnotatedSignals(service);
120
- service.onStart && service.onStart();
121
- service.onPreFixedUpdate && this.onPreFixedUpdateSet.add(service);
122
- service.onFixedUpdate && this.onFixedUpdateSet.add(service);
123
- service.onUpdate && this.onUpdateSet.add(service);
124
- service.onLateUpdate && this.onLateUpdateSet.add(service);
125
- service.onPhysicsUpdate && this.onPhysicsUpdateSet.add(service);
126
- service.onPostPhysicsUpdate && this.onPostPhysicsUpdateSet.add(service);
127
- service.onLateFixedUpdate && this.onLateFixedUpdateSet.add(service);
128
- service.onRender && this.onRenderUpdateSet.add(service);
129
- this.services.push(service);
86
+ constructor(
87
+ readonly injector: EcsInjector<Class<System>>,
88
+ readonly clock: Clock,
89
+ ) {
90
+ this.injector.map(GameEngine).toValue(this);
91
+
92
+ this.onPrepare = this.injector.getSignal(EngineSignals.OnPrepare);
93
+ this.onStart = this.injector.getSignal(EngineSignals.OnStart);
94
+ this.onEnd = this.injector.getSignal(EngineSignals.OnEnd);
95
+ this.onRender = this.injector.getSignal(EngineSignals.OnRender);
96
+ this.onFixedUpdate = this.injector.getSignal(EngineSignals.OnFixedUpdate);
97
+ this.onUpdate = this.injector.getSignal(EngineSignals.OnUpdate);
98
+ this.onLateUpdate = this.injector.getSignal(EngineSignals.OnLateUpdate);
99
+ this.onPrePhysicsUpdate = this.injector.getSignal(EngineSignals.OnPrePhysicsUpdate);
100
+ this.onPhysicsUpdate = this.injector.getSignal(EngineSignals.OnPhysicsUpdate);
101
+ this.onPostPhysicsUpdate = this.injector.getSignal(EngineSignals.OnPostPhysicsUpdate);
102
+ this.onLateFixedUpdate = this.injector.getSignal(EngineSignals.OnLateFixedUpdate);
103
+
104
+ this.onAddEntity = this.injector.getSignal(EngineSignals.OnAddEntity);
105
+ this.onRemoveEntity = this.injector.getSignal(EngineSignals.OnRemoveEntity);
106
+
107
+ this.setupClock();
108
+ this.prepareFrame();
109
+ }
110
+
111
+ static createDefault(options?: GameEngineOptions) {
112
+ const opts = {...defaultOptions, ...options};
113
+ const clock = new GameClock({
114
+ fixedDeltaTime: opts.fixedDeltaTime,
115
+ autoStart: false,
116
+ });
117
+ const injector = new EcsInjector<Class<System>>();
118
+ const engine = new GameEngine(injector, clock);
119
+ engine.installPlugins(opts.plugins);
120
+ return engine;
121
+ }
122
+
123
+ static createDefaultWithInjector(injector: EcsInjector<Class<System>>, options?: GameEngineOptions) {
124
+ const opts = {...defaultOptions, ...options};
125
+ const clock = new GameClock({
126
+ fixedDeltaTime: opts.fixedDeltaTime,
127
+ autoStart: false,
128
+ });
129
+ const engine = new GameEngine(injector, clock);
130
+ engine.installPlugins(opts.plugins);
131
+ return engine;
132
+ }
133
+
134
+ // TODO: test
135
+ addServiceClass<T extends Class<Service>>(serviceClass: T, mapping?: (injector: EcsInjector) => void): InstanceType<T> {
136
+ let childInjector: EcsInjector | undefined;
137
+ if (mapping) {
138
+ childInjector = this.injector.createChild();
139
+ mapping(childInjector);
130
140
  }
131
-
132
- private installPlugins(plugins: GameEnginePlugin[]) {
133
- plugins.forEach(plugin => {
134
- plugin.onPrepare && plugin.onPrepare(this);
135
- });
136
- plugins.forEach(plugin => {
137
- plugin.onStart && plugin.onStart(this);
138
- });
141
+ this.mapStaticAnnotatedSignals(serviceClass);
142
+ const service = childInjector?.createInstance(serviceClass) || this.injector.createInstance(serviceClass);
143
+ this.addService(service);
144
+ if (childInjector) {
145
+ this.systemChildInjectors.set(service, childInjector);
139
146
  }
140
-
141
- registerSystem(systemClass: Class<System>): void {
142
- this.mapStaticAnnotatedSignals(systemClass);
143
- if (this._started) throw new Error('Register Systems before engine is started');
144
- this.injector.registerSystem(systemClass);
147
+ return service;
148
+ }
149
+
150
+ private addService(service: Service): void {
151
+ this.mapAnnotatedSignals(service);
152
+ service.onStart && service.onStart();
153
+ service.onEarlyUpdate && this.onEarlyUpdateSet.add(service);
154
+ service.onPreFixedUpdate && this.onPreFixedUpdateSet.add(service);
155
+ service.onFixedUpdate && this.onFixedUpdateSet.add(service);
156
+ service.onUpdate && this.onUpdateSet.add(service);
157
+ service.onLateUpdate && this.onLateUpdateSet.add(service);
158
+ service.onPrePhysicsUpdate && this.onPrePhysicsUpdateSet.add(service);
159
+ service.onPhysicsUpdate && this.onPhysicsUpdateSet.add(service);
160
+ service.onPostPhysicsUpdate && this.onPostPhysicsUpdateSet.add(service);
161
+ service.onLateFixedUpdate && this.onLateFixedUpdateSet.add(service);
162
+ service.onRender && this.onRenderUpdateSet.add(service);
163
+ service.onAddEntity && this.onAddEntitySet.add(service);
164
+ service.onRemoveEntity && this.onRemoveEntitySet.add(service);
165
+ this.services.push(service);
166
+ }
167
+
168
+ removeService(service: Service): void {
169
+ const index = this.services.indexOf(service);
170
+ if (index === -1) return;
171
+ this.services.splice(index, 1);
172
+ this.unmapAnnotatedSignals(service);
173
+ service.onEarlyUpdate && this.onEarlyUpdateSet.delete(service);
174
+ service.onPreFixedUpdate && this.onPreFixedUpdateSet.delete(service);
175
+ service.onFixedUpdate && this.onFixedUpdateSet.delete(service);
176
+ service.onUpdate && this.onUpdateSet.delete(service);
177
+ service.onLateUpdate && this.onLateUpdateSet.delete(service);
178
+ service.onPrePhysicsUpdate && this.onPrePhysicsUpdateSet.delete(service);
179
+ service.onPhysicsUpdate && this.onPhysicsUpdateSet.delete(service);
180
+ service.onPostPhysicsUpdate && this.onPostPhysicsUpdateSet.delete(service);
181
+ service.onLateFixedUpdate && this.onLateFixedUpdateSet.delete(service);
182
+ service.onRender && this.onRenderUpdateSet.delete(service);
183
+ service.onAddEntity && this.onAddEntitySet.delete(service);
184
+ service.onRemoveEntity && this.onRemoveEntitySet.delete(service);
185
+ service.onEnd && service.onEnd();
186
+ const childInjector = this.systemChildInjectors.get(service);
187
+ if (childInjector) {
188
+ childInjector.dispose();
189
+ this.systemChildInjectors.delete(service);
145
190
  }
146
-
147
- registerService(serviceClass: Class<Service>): void {
148
- this.mapStaticAnnotatedSignals(serviceClass);
149
- if (this._started) throw new Error('Register Services before engine is started');
150
- this.injector.map(serviceClass).toSingleton();
151
- this.serviceClasses.add(serviceClass);
191
+ }
192
+
193
+ installPlugins(plugins: GameEnginePlugin[]) {
194
+ plugins.forEach(plugin => {
195
+ plugin.onPrepare && plugin.onPrepare(this);
196
+ });
197
+ plugins.forEach(plugin => {
198
+ plugin.onStart && plugin.onStart(this);
199
+ });
200
+ }
201
+
202
+ registerSystem(systemClass: Class<System>): void {
203
+ this.mapStaticAnnotatedSignals(systemClass);
204
+ if (this._started) throw new Error('Register Systems before engine is started');
205
+ this.injector.registerSystem(systemClass);
206
+ }
207
+
208
+ registerService(serviceClass: Class<Service>): void {
209
+ this.mapStaticAnnotatedSignals(serviceClass);
210
+ if (this._started) throw new Error('Register Services before engine is started');
211
+ this.injector.map(serviceClass).toSingleton();
212
+ this.registeredServices.add(serviceClass);
213
+ }
214
+
215
+ getService<T extends Class<Service>>(serviceClass: T): InstanceType<T> {
216
+ return this.injector.get(serviceClass);
217
+ }
218
+
219
+ start() {
220
+ this._started = true;
221
+ //TODO: throw when registering services or systems when engine is running
222
+ for (const serviceClass of this.registeredServices) {
223
+ const service = this.injector.get(serviceClass);
224
+ this.addService(service);
152
225
  }
153
-
154
- getService<T extends Class<Service>>(serviceClass: T): InstanceType<T> {
155
- return this.injector.get(serviceClass);
226
+ this.clock.start();
227
+ this.onStart.dispatch();
228
+ }
229
+
230
+ private setupClock() {
231
+ this.clock.onPrepare.add(this.prepareFrame, {context: this});
232
+
233
+ this.clock.onEarlyUpdate.add(({time, deltaTime}) => {
234
+ for (const system of this.onEarlyUpdateSet) {
235
+ system.onEarlyUpdate!(time, deltaTime);
236
+ }
237
+ });
238
+
239
+ this.clock.onFixedUpdate.add((params) => {
240
+ const {time, deltaTime} = params;
241
+ for (const system of this.onPreFixedUpdateSet) {
242
+ system.onPreFixedUpdate!(time, deltaTime);
243
+ }
244
+ this.onFixedUpdate.dispatch(params);
245
+ for (const system of this.onFixedUpdateSet) {
246
+ system.onFixedUpdate!(time, deltaTime);
247
+ }
248
+ this.onPrePhysicsUpdate.dispatch(params);
249
+ for (const system of this.onPrePhysicsUpdateSet) {
250
+ system.onPrePhysicsUpdate!(time, deltaTime);
251
+ }
252
+ this.onPhysicsUpdate.dispatch(params);
253
+ for (const system of this.onPhysicsUpdateSet) {
254
+ system.onPhysicsUpdate!(time, deltaTime);
255
+ }
256
+ this.onPostPhysicsUpdate.dispatch(params);
257
+ for (const system of this.onPostPhysicsUpdateSet) {
258
+ system.onPostPhysicsUpdate!(time, deltaTime);
259
+ }
260
+ this.onLateFixedUpdate.dispatch(params);
261
+ for (const system of this.onLateFixedUpdateSet) {
262
+ system.onLateFixedUpdate!(time, deltaTime);
263
+ }
264
+ this._fixedFrameCount++;
265
+ });
266
+
267
+ // TODO: update from outside
268
+ this.clock.onUpdate.add(onUpdateParams => {
269
+ const {time, deltaTime, alpha} = onUpdateParams;
270
+ this.onUpdate.dispatch(onUpdateParams);
271
+ for (const system of this.onUpdateSet) {
272
+ system.onUpdate!(time, deltaTime, alpha);
273
+ }
274
+ //TODO: merge with framerate to single signal?
275
+ this.onLateUpdate.dispatch(onUpdateParams);
276
+ for (const system of this.onLateUpdateSet) {
277
+ system.onLateUpdate!(time, deltaTime, alpha);
278
+ }
279
+ this.onRender.dispatch(onUpdateParams);
280
+ for (const system of this.onRenderUpdateSet) {
281
+ system.onRender!(time, deltaTime, alpha);
282
+ }
283
+ this._frameCount++;
284
+ });
285
+ }
286
+
287
+ add(entity: Entity) {
288
+ return new Promise<void>(resolve => this.addQueue.push([entity, resolve]));
289
+ }
290
+
291
+ addAll(entity: Entity[]) {
292
+ return Promise.all(entity.map(entity => new Promise<void>(resolve => this.addQueue.push([entity, resolve]))));
293
+ }
294
+
295
+ waitFrame() {
296
+ return this.framePromise;
297
+ }
298
+
299
+ async remove(entity: Entity, options: RemoveOptions = {dispose: true}) {
300
+ if ((<any>entity).markRemoval) return;
301
+ (<any>entity).markRemoval = true;
302
+ return new Promise<void>(resolve => this.removeQueue.push({entity, resolve, options}));
303
+ }
304
+
305
+ async removeAll(entities: Entity[]) {
306
+ return Promise.all(entities.map(entity => this.remove(entity)));
307
+ }
308
+
309
+ stop() {
310
+ // this.injector.dispose();
311
+ this._started = false;
312
+ this.clock.stop();
313
+ }
314
+
315
+ runAction<T extends Action>(action: T): ReturnType<T> {
316
+ return action(this);
317
+ }
318
+
319
+ getSignal<T>(signalId: ID): Signal<T> {
320
+ return this.injector.getSignal<T>(signalId);
321
+ }
322
+
323
+ private signalMappings = new Map<Class, [string, Signal][]>();
324
+ private staticSignalMappings = new Map<Class, [string, Signal][]>();
325
+ private signalMapBindings = new Map<Object, SignalBinding[]>();
326
+
327
+ // TODO: staticSignalMapBindings needed if we want to unregister Services or Systems
328
+ private staticSignalMapBindings = new Map<Class, SignalBinding[]>();
329
+
330
+ private mapStaticAnnotatedSignals(target: Class) {
331
+ let staticSignalMap = this.getStaticSignalMap(target);
332
+ if (staticSignalMap.length === 0) return;
333
+
334
+ const bindings = [];
335
+ for (const [key, signal] of staticSignalMap) {
336
+ bindings.push(signal.add((<any>target)[key], {context: target}));
156
337
  }
157
-
158
- start() {
159
- this._started = true;
160
- //TODO: throw when registering services or systems when engine is running
161
- for (const serviceClass of this.serviceClasses) {
162
- const service = this.injector.get(serviceClass);
163
- this.addService(service);
164
- }
165
- this.clock.start();
166
- this.onStart.dispatch();
338
+ // Store bindings to allow unregistering Services or Systems
339
+ this.staticSignalMapBindings.set(target, bindings);
340
+ }
341
+
342
+ private unmapStaticAnnotatedSignals(target: Class) {
343
+ const bindings = this.staticSignalMapBindings.get(target);
344
+ if (!bindings) return;
345
+ for (const binding of bindings) {
346
+ binding.detach();
167
347
  }
348
+ this.staticSignalMapBindings.delete(target);
349
+ }
168
350
 
169
- private setupClock() {
170
- this.clock.onPrepare.add(this.prepareFrame, this);
171
-
172
- this.clock.onEarlyUpdate.add(({time, deltaTime}) => {
173
- for (const system of this.onEarlyUpdateSet) {
174
- system.onEarlyUpdate!(time, deltaTime);
175
- }
176
- });
177
-
178
- this.clock.onFixedUpdate.add((params) => {
179
- const {time, deltaTime} = params;
180
- for (const system of this.onPreFixedUpdateSet) {
181
- system.onPreFixedUpdate!(time, deltaTime);
182
- }
183
- for (const system of this.onFixedUpdateSet) {
184
- system.onFixedUpdate!(time, deltaTime);
185
- }
186
- this.onFixedUpdate.dispatch(params);
187
- for (const system of this.onPhysicsUpdateSet) {
188
- system.onPhysicsUpdate!(time, deltaTime);
189
- }
190
- this.onPostPhysicsUpdate.dispatch(params);
191
- for (const system of this.onPostPhysicsUpdateSet) {
192
- system.onPostPhysicsUpdate!(time, deltaTime);
193
- }
194
- this.onLateFixedUpdate.dispatch(params);
195
- for (const system of this.onLateFixedUpdateSet) {
196
- system.onLateFixedUpdate!(time, deltaTime);
197
- }
198
- this._fixedFrameCount++;
199
- });
200
-
201
- // TODO: update from outside
202
- this.clock.onUpdate.add(onUpdateParams => {
203
- const {time, deltaTime, alpha} = onUpdateParams;
204
- for (const system of this.onUpdateSet) {
205
- system.onUpdate!(time, deltaTime, alpha);
206
- }
207
- //TODO: merge with framerate to single signal?
208
-
209
- this.onUpdate.dispatch(onUpdateParams);
210
- for (const system of this.onLateUpdateSet) {
211
- system.onLateUpdate!(time, deltaTime, alpha);
212
- }
213
- this.onLateUpdate.dispatch(onUpdateParams);
214
-
215
- for (const system of this.onRenderUpdateSet) {
216
- system.onRender!(time, deltaTime, alpha);
217
- }
218
- this.onRender.dispatch(onUpdateParams);
219
- this._frameCount++;
220
- });
221
- }
351
+ private mapAnnotatedSignals(target: Object) {
352
+ const signalMap = this.getSignalMap(target.constructor);
353
+ if (signalMap.length === 0) return;
222
354
 
223
- add(entity: Entity) {
224
- return new Promise<void>(resolve => this.addQueue.push([entity, resolve]));
355
+ const bindings = [];
356
+ for (const [key, signal] of signalMap) {
357
+ bindings.push(signal.add((<any>target)[key], {context: target}));
225
358
  }
226
-
227
- waitFrame() {
228
- return this.framePromise;
359
+ this.signalMapBindings.set(target, bindings);
360
+ }
361
+
362
+ private unmapAnnotatedSignals(target: Object) {
363
+ const signalBindings = this.signalMapBindings.get(target);
364
+ if (!signalBindings) return;
365
+ for (const binding of signalBindings) {
366
+ binding.detach();
229
367
  }
230
-
231
- async remove(entity: Entity) {
232
- if ((<any>entity).markRemoval) return;
233
- (<any>entity).markRemoval = true;
234
- return new Promise<void>(resolve => this.removeQueue.push([entity, resolve]));
368
+ }
369
+
370
+ private getSignalMap(target: Function) {
371
+ // TODO: we could speed thinks up by introducing "invisible" flags on the systems/services to avoid a map check
372
+ return putIfAbsent(this.signalMappings, target, (): [string, Signal][] => {
373
+ const handlers = signalHandlers.get(target);
374
+ if (!handlers) return [];
375
+ const result: [string, Signal][] = [];
376
+ for (const [key, signalId] of handlers) {
377
+ result.push([key, this.getSignal(signalId)]);
378
+ }
379
+ return result;
380
+ });
381
+ }
382
+
383
+ private getStaticSignalMap(target: Class) {
384
+ // TODO: we could speed thinks up by introducing "invisible" flags on the systems/services to avoid a map check
385
+ return putIfAbsent(this.staticSignalMappings, target, (): [string, Signal][] => {
386
+ const handlers = staticSignalHandlers.get(target);
387
+ if (!handlers) return [];
388
+ const result: [string, Signal][] = [];
389
+ for (const [key, signalId] of handlers) {
390
+ result.push([key, this.getSignal(signalId)]);
391
+ }
392
+ return result;
393
+ });
394
+ }
395
+
396
+ dispose() {
397
+ this.clock.stop();
398
+ this.clock.dispose();
399
+ for (const systems of this.entitySystemMap.values()) {
400
+ for (const context of systems) {
401
+ context.system.onEnd && context.system.onEnd();
402
+ this.unmapAnnotatedSignals(context.system);
403
+ }
235
404
  }
236
-
237
- stop() {
238
- // this.injector.dispose();
239
- this.clock.stop();
405
+ for (const service of this.services) {
406
+ service.onEnd && service.onEnd();
407
+ this.unmapAnnotatedSignals(service);
240
408
  }
409
+ this.onEnd.dispatch();
410
+
411
+ this.onAddEntity.detachAll();
412
+ this.onRemoveEntity.detachAll();
413
+ this.onPrepare.detachAll();
414
+ this.onStart.detachAll();
415
+ this.onEnd.detachAll();
416
+ this.onRender.detachAll();
417
+ this.onUpdate.detachAll();
418
+ this.onLateUpdate.detachAll();
419
+ this.onFixedUpdate.detachAll();
420
+ this.onPrePhysicsUpdate.detachAll();
421
+ this.onPhysicsUpdate.detachAll();
422
+ this.onPostPhysicsUpdate.detachAll();
423
+ this.onLateFixedUpdate.detachAll();
424
+ this.onEarlyUpdateSet.clear();
425
+ this.onPhysicsUpdateSet.clear();
426
+ this.onPrePhysicsUpdateSet.clear();
427
+ this.onPostPhysicsUpdateSet.clear();
428
+ this.onLateFixedUpdateSet.clear();
429
+ this.onLateUpdateSet.clear();
430
+ this.onUpdateSet.clear();
431
+ this.onFixedUpdateSet.clear();
432
+ this.onRenderUpdateSet.clear();
433
+ this.onAddEntitySet.clear();
434
+ this.onRemoveEntitySet.clear();
435
+ }
436
+
437
+ private prepareFrame() {
438
+ this.deferSignal = new Signal();
439
+ for (const {entity, resolve, options} of this.removeQueue) {
440
+ const entities = entity.getAllEntities();
441
+ for (const entity of entities) {
442
+ (<any>entity).markRemoval = false;
443
+ if (!(<any>entity).gameEngine) {
444
+ console.log('Entity not active', entity);
445
+ // throw new Error('Entity not active');
446
+ }
447
+ (<any>entity).gameEngine = undefined;
448
+
449
+ const systemContexts = this.entitySystemMap.get(entity);
450
+ this.entitySystemMap.delete(entity);
451
+ if (systemContexts) {
452
+ for (const context of systemContexts) {
453
+ this.disposeContext(context);
454
+ }
455
+ }
456
+ entity.onRemove.dispatch();
457
+ if (options.dispose) {
458
+ entity.onRemove.detachAll();
459
+ entity._signalMap.forEach(signal => signal.detachAll());
460
+ }
241
461
 
242
- runAction<T extends Action>(action: T): ReturnType<T> {
243
- return action(this);
244
- }
462
+ for (const system of this.onRemoveEntitySet) {
463
+ system.onRemoveEntity!(entity);
464
+ }
465
+ this.onRemoveEntity.dispatch(entity);
466
+ }
245
467
 
246
- getSignal<T>(signalId: ID): Signal<T> {
247
- return this.injector.getSignal<T>(signalId);
468
+ resolve();
248
469
  }
249
-
250
- private signalMappings = new Map<Class, [string, Signal][]>();
251
- private staticSignalMappings = new Map<Class, [string, Signal][]>();
252
- private signalMapBindings = new Map<Object, SignalBinding[]>();
253
-
254
- // TODO: staticSignalMapBindings needed if we want to unregister Services or Systems
255
-
256
- private mapStaticAnnotatedSignals(target: Class) {
257
- let staticSignalMap = this.getStaticSignalMap(target);
258
- if (staticSignalMap.length === 0) return;
259
-
260
- const bindings = [];
261
- for (const [key, signal] of staticSignalMap) {
262
- bindings.push(signal.add((<any>target)[key], target));
470
+ this.removeQueue = [];
471
+
472
+ for (const [entity, resolve] of this.addQueue) {
473
+ const preparePromises = [];
474
+ const nextSystemContexts: SystemContext[] = [];
475
+ const entities = entity.getAllEntities();
476
+ for (const entity of entities) {
477
+ if ((<any>entity).gameEngine) throw new Error('Entity already added to a gameEngine');
478
+ (<any>entity).gameEngine = this;
479
+ (<any>entity)._detached = false;
480
+ const systemContexts = this.injector.createSystemsForEntity(entity);
481
+ for (const context of systemContexts) {
482
+ // TODO: implement synced mode where async onPrepares aren't allowed
483
+ context.system.onPrepare && preparePromises.push(context.system.onPrepare());
484
+ nextSystemContexts.push(context);
263
485
  }
264
- // TODO: ignore bindings for now (store them if we implement unregister Service or Systems
265
- }
266
-
267
- private mapAnnotatedSignals(target: Object) {
268
- const signalMap = this.getSignalMap(target.constructor);
269
- if (signalMap.length === 0) return;
270
-
271
- const bindings = [];
272
- for (const [key, signal] of signalMap) {
273
- bindings.push(signal.add((<any>target)[key], target));
486
+ this.entitySystemMap.set(entity, systemContexts);
487
+ // we have to check if the entity is already in the map, because a system might have added dynamic components that also added to this
488
+ this.addSystemContextSetToMap(entity, systemContexts);
489
+
490
+
491
+ const entityWasAdded = () => {
492
+ this.onAddEntity.dispatch(entity);
493
+ for (const system of this.onAddEntitySet) {
494
+ system.onAddEntity!(entity);
495
+ }
496
+ entity.onAdd.dispatch();
497
+ resolve();
498
+ };
499
+
500
+ // Promise.all doesnt resolve immediately when there are no promises bu
501
+ if (preparePromises.length === 0) {
502
+ this.startSystems(nextSystemContexts, entityWasAdded);
503
+ } else {
504
+ //TODO: this doesnt start the systems in the right life cycle?
505
+ Promise.all(preparePromises).then(() => this.startSystems(nextSystemContexts, entityWasAdded));
274
506
  }
275
- this.signalMapBindings.set(target, bindings);
507
+ }
276
508
  }
277
-
278
- private unmapAnnotatedSignals(target: Object) {
279
- const signalBindings = this.signalMapBindings.get(target);
280
- if (!signalBindings) return;
281
- for (const binding of signalBindings) {
282
- binding.detach();
283
- }
509
+ this.addQueue = [];
510
+ // create promise for next frame
511
+
512
+ // the next frame promise should work inside the first frame promise resolve function
513
+ const previousResolveFrame = this.resolveFrame;
514
+ this.framePromise = new Promise<void>(resolve => {
515
+ this.resolveFrame = resolve;
516
+ });
517
+ previousResolveFrame();
518
+ this.onPrepare.dispatch();
519
+
520
+ this.deferSignal.dispatch();
521
+ this.deferSignal = undefined;
522
+ }
523
+
524
+ /**
525
+ * if deferSignal is set, the callback will be called when the signal is dispatched
526
+ * is used for example to add a dynamic component to a system within the constructor of another system
527
+ */
528
+ deferSignal: Signal | undefined;
529
+ maybeDefer<T>(callback: () => T) {
530
+ if (this.deferSignal) {
531
+ this.deferSignal.addOnce(callback);
532
+ } else {
533
+ callback();
284
534
  }
535
+ }
285
536
 
286
- private getSignalMap(target: Function) {
287
- // TODO: we could speed thinks up by introducing "invisible" flags on the systems/services to avoid a map check
288
- return putIfAbsent(this.signalMappings, target, (): [string, Signal][] => {
289
- const handlers = signalHandlers.get(target);
290
- if (!handlers) return [];
291
- const result: [string, Signal][] = [];
292
- for (const [key, signalId] of handlers) {
293
- result.push([key, this.getSignal(signalId)]);
294
- }
295
- return result;
296
- });
297
- }
298
537
 
299
- private getStaticSignalMap(target: Class) {
300
- // TODO: we could speed thinks up by introducing "invisible" flags on the systems/services to avoid a map check
301
- return putIfAbsent(this.staticSignalMappings, target, (): [string, Signal][] => {
302
- const handlers = staticSignalHandlers.get(target);
303
- if (!handlers) return [];
304
- const result: [string, Signal][] = [];
305
- for (const [key, signalId] of handlers) {
306
- result.push([key, this.getSignal(signalId)]);
307
- }
308
- return result;
309
- });
538
+ private addSystemContextSetToMap (entity: Entity, contexts: Set<SystemContext>) {
539
+ const systemContexts = this.entitySystemMap.get(entity);
540
+ if (!systemContexts) {
541
+ this.entitySystemMap.set(entity, contexts);
542
+ } else {
543
+ for (const context of contexts) {
544
+ systemContexts.add(context);
545
+ }
310
546
  }
311
-
312
- dispose() {
313
- this.clock.stop();
314
- for (const systems of this.entitySystemMap.values()) {
315
- for (const system of systems) {
316
- system.system.onEnd && system.system.onEnd();
317
- }
318
- }
319
- for (const service of this.services) {
320
- service.onEnd && service.onEnd();
321
- }
547
+ }
548
+
549
+ private startSystems(contexts: Iterable<SystemContext>, then?: () => void) {
550
+ for (const context of contexts) {
551
+ const system = context.system;
552
+ this.mapAnnotatedSignals(system);
553
+ system.onStart && system.onStart();
554
+ system.onEarlyUpdate && this.onEarlyUpdateSet.add(system);
555
+ system.onPreFixedUpdate && this.onPreFixedUpdateSet.add(system);
556
+ system.onFixedUpdate && this.onFixedUpdateSet.add(system);
557
+ system.onUpdate && this.onUpdateSet.add(system);
558
+ system.onLateUpdate && this.onLateUpdateSet.add(system);
559
+ system.onPhysicsUpdate && this.onPhysicsUpdateSet.add(system);
560
+ system.onPrePhysicsUpdate && this.onPrePhysicsUpdateSet.add(system);
561
+ system.onPostPhysicsUpdate && this.onPostPhysicsUpdateSet.add(system);
562
+ system.onLateFixedUpdate && this.onLateFixedUpdateSet.add(system);
563
+ system.onRender && this.onRenderUpdateSet.add(system);
564
+ system.onAddEntity && this.onAddEntitySet.add(system);
565
+ system.onRemoveEntity && this.onRemoveEntitySet.add(system);
322
566
  }
323
-
324
- private prepareFrame() {
325
-
326
- for (const [entity, resolve] of this.removeQueue) {
327
- const entities = entity.getAllEntities();
328
- for (const entity of entities) {
329
- (<any>entity).markRemoval = false;
330
- if (!(<any>entity).gameEngine) {
331
- console.log(entity);
332
- // throw new Error('Entity not active');
333
- }
334
- (<any>entity).gameEngine = undefined;
335
-
336
- const systemContexts = this.entitySystemMap.get(entity);
337
- this.entitySystemMap.delete(entity);
338
- if (systemContexts) {
339
- for (const context of systemContexts) {
340
- context.dispose();
341
- const system = context.system;
342
- this.unmapAnnotatedSignals(system);
343
- system.onEnd && system.onEnd();
344
- system.onPreFixedUpdate && this.onPreFixedUpdateSet.delete(system);
345
- system.onFixedUpdate && this.onFixedUpdateSet.delete(system);
346
- system.onEarlyUpdate && this.onEarlyUpdateSet.delete(system);
347
- system.onUpdate && this.onUpdateSet.delete(system);
348
- system.onLateUpdate && this.onLateUpdateSet.delete(system);
349
- system.onPhysicsUpdate && this.onPhysicsUpdateSet.delete(system);
350
- system.onPostPhysicsUpdate && this.onPostPhysicsUpdateSet.delete(system);
351
- system.onLateFixedUpdate && this.onLateFixedUpdateSet.delete(system);
352
- system.onRender && this.onRenderUpdateSet.delete(system);
353
- }
354
- }
355
- entity.onRemove.dispatch();
356
- entity.onRemove.detachAll();
357
- }
358
-
359
- resolve();
360
- }
361
- this.removeQueue = [];
362
-
363
- for (const [entity, resolve] of this.addQueue) {
364
- const preparePromises = [];
365
- const nexSystemContexts: SystemContext[] = [];
366
- const entities = entity.getAllEntities();
367
- for (const entity of entities) {
368
- if ((<any>entity).gameEngine) throw new Error('Entity already added to a gameEngine');
369
- (<any>entity).gameEngine = this;
370
- const systemContexts = this.injector.createSystems(entity);
371
- for (const context of systemContexts) {
372
- // TODO: implement synced mode where async onPrepares aren't allowed
373
- context.system.onPrepare && preparePromises.push(context.system.onPrepare());
374
- nexSystemContexts.push(context);
375
- }
376
- this.entitySystemMap.set(entity, systemContexts);
377
- }
378
-
379
- const startSystems = () => {
380
- // TODO: check if a "global" method is faster than this inner method
381
- for (const context of nexSystemContexts) {
382
- const system = context.system;
383
- this.mapAnnotatedSignals(system);
384
- system.onStart && system.onStart();
385
- system.onPreFixedUpdate && this.onPreFixedUpdateSet.add(system);
386
- system.onFixedUpdate && this.onFixedUpdateSet.add(system);
387
- system.onEarlyUpdate && this.onEarlyUpdateSet.add(system);
388
- system.onUpdate && this.onUpdateSet.add(system);
389
- system.onLateUpdate && this.onLateUpdateSet.add(system);
390
- system.onPhysicsUpdate && this.onPhysicsUpdateSet.add(system);
391
- system.onPostPhysicsUpdate && this.onPostPhysicsUpdateSet.add(system);
392
- system.onLateFixedUpdate && this.onLateFixedUpdateSet.add(system);
393
- system.onRender && this.onRenderUpdateSet.add(system);
394
- }
395
- resolve();
396
- };
397
-
398
- // Promise.all doesnt resolve immediately when there are no promises bu
399
- if (preparePromises.length === 0) {
400
- startSystems();
401
- } else {
402
- Promise.all(preparePromises).then(startSystems);
403
- }
404
- }
405
- this.addQueue = [];
406
- // create promise for next frame
407
- this.resolveFrame();
408
- this.framePromise = new Promise<void>(resolve => {
409
- this.resolveFrame = resolve;
567
+ then?.();
568
+ };
569
+
570
+ getAllEntities(): Entity[] {
571
+ return Array.from(this.entitySystemMap.keys());
572
+ }
573
+
574
+ removeAllEntities() {
575
+ return this.removeAll(this.getAllEntities());
576
+ }
577
+
578
+ private disposeContext(context: SystemContext<System>) {
579
+ const system = context.system;
580
+
581
+ // if onEnd returns a promise, we need to wait for it to finish before we can dispose the system
582
+ const onEndPromise = system.onEnd && system.onEnd();
583
+ if (onEndPromise instanceof Promise) {
584
+ onEndPromise
585
+ .then(() => {
586
+ context.dispose();
587
+ })
588
+ .catch((error) => {
589
+ console.error('Error during system.onEnd():', error);
590
+ // Dispose the context even if onEnd() fails
591
+ context.dispose();
410
592
  });
411
- this.onPrepare.dispatch();
593
+ } else {
594
+ context.dispose();
412
595
  }
596
+
597
+ this.unmapAnnotatedSignals(system);
598
+ system.onEarlyUpdate && this.onEarlyUpdateSet.delete(system);
599
+ system.onPreFixedUpdate && this.onPreFixedUpdateSet.delete(system);
600
+ system.onFixedUpdate && this.onFixedUpdateSet.delete(system);
601
+ system.onUpdate && this.onUpdateSet.delete(system);
602
+ system.onLateUpdate && this.onLateUpdateSet.delete(system);
603
+ system.onPhysicsUpdate && this.onPhysicsUpdateSet.delete(system);
604
+ system.onPrePhysicsUpdate && this.onPrePhysicsUpdateSet.delete(system);
605
+ system.onPostPhysicsUpdate && this.onPostPhysicsUpdateSet.delete(system);
606
+ system.onLateFixedUpdate && this.onLateFixedUpdateSet.delete(system);
607
+ system.onRender && this.onRenderUpdateSet.delete(system);
608
+ system.onAddEntity && this.onAddEntitySet.delete(system);
609
+ system.onRemoveEntity && this.onRemoveEntitySet.delete(system);
610
+ }
413
611
  }
414
612
  //
415
613
  // type RunSpread<T> = T extends undefined
@@ -421,4 +619,3 @@ export class GameEngine {
421
619
  // export function createGameAction<K extends keyof GameEngineActions>(id: K, action: GameAction<K>): GameActionMapping {
422
620
  // return {id, action};
423
621
  // }
424
- //