mani-game-engine 1.0.0-pre.4 → 1.0.0-pre.41

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,350 +1,463 @@
1
- import {Action, Class, GameEnginePlugin, OnFixedUpdateParams, OnUpdateParams, Service, System} from './types';
2
- import {Signal, SignalBinding} from 'mani-signal';
3
- import {Entity} from './entity';
4
- import {ID, putIfAbsent} from 'mani-injector';
5
- import {EcsInjector, signalHandlers, staticSignalHandlers} from './ecsInjector';
6
- import {Clock} from './clock';
7
-
8
- type AddEntry = [Entity[], () => void];
9
- type RemoveEntry = [Entity[], () => void];
10
-
11
- const defaultOptions = {
12
- fixedDeltaTime: 1 / 60,
13
- plugins: [] as GameEnginePlugin[],
14
- };
15
-
16
- export type GameEngineOptions = Partial<typeof defaultOptions>;
17
- declare global {
18
- interface GameEngineActions {
19
- 'testAction': string
20
- }
21
- }
22
-
23
- type GameEngineActionParams<K extends keyof GameEngineActions> = GameEngineActions[K];
24
- type GameAction<K extends keyof GameEngineActions> = (engine: GameEngine, params: GameEngineActionParams<K>) => Promise<any> | any;
25
-
26
- export class GameEngine {
27
- readonly onStart = new Signal();
28
- readonly onRender = new Signal();
29
- readonly onUpdate = new Signal<OnUpdateParams>();
30
- readonly onPostPhysics = new Signal<OnFixedUpdateParams>();
31
-
32
- private readonly onEarlyUpdateSet = new Set<System>();
33
- private readonly onUpdateSet = new Set<System>();
34
- private readonly onFixedUpdateSet = new Set<System>();
35
- private readonly onPreFixedUpdateSet = new Set<System>();
36
- private readonly onPhysicsUpdateSet = new Set<System>();
37
- private readonly onPostPhysicsUpdateSet = new Set<System>();
38
- private readonly onLateUpdateSet = new Set<System>();
39
- private readonly onRenderUpdateSet = new Set<System>();
40
-
41
- private readonly entitySystemMap = new Map<Object, System[]>();
42
-
43
- private readonly serviceClasses = new Set<Class<Service>>();
44
-
45
- private framePromise!: Promise<any>;
46
- private resolveFrame: () => void = () => undefined;
47
-
48
- private addQueue: AddEntry[] = [];
49
- private removeQueue: RemoveEntry[] = [];
50
-
51
- private _started = false;
52
- get started(): boolean { return this._started; }
53
-
54
- private _frameCount = 0;
55
- get frameCount(): number { return this._frameCount; }
56
-
57
- // TODO inject clock into game engine
58
- // TODO inject space into game engine
59
-
60
- static createDefault(options?: GameEngineOptions) {
61
- const opts = {...defaultOptions, ...options};
62
- const clock = new Clock({
63
- fixedDeltaTime: opts.fixedDeltaTime,
64
- autoStart: false
65
- });
66
- const injector = new EcsInjector<Class<System>>();
67
- const engine = new GameEngine(injector, clock);
68
- engine.installPlugins(opts.plugins);
69
- return engine;
70
- }
71
-
72
- constructor(
73
- readonly injector: EcsInjector<Class<System>>,
74
- readonly clock: Clock,
75
- ) {
76
- this.injector.map(GameEngine).toValue(this);
77
- this.setupClock();
78
- this.prepareFrame();
79
- }
80
-
81
- private setupClock() {
82
- this.clock.onPrepare.add(this.prepareFrame, this);
83
-
84
- this.clock.onEarlyUpdate.add(({time, deltaTime}) => {
85
- for (const system of this.onEarlyUpdateSet) {
86
- system.onEarlyUpdate!(time, deltaTime);
87
- }
88
- });
89
-
90
- this.clock.onFixedUpdate.add(({time, deltaTime}) => {
91
- for (const system of this.onPreFixedUpdateSet) {
92
- system.onPreFixedUpdate!();
93
- }
94
- for (const system of this.onFixedUpdateSet) {
95
- system.onFixedUpdate!(time, deltaTime);
96
- }
97
- for (const system of this.onPhysicsUpdateSet) {
98
- system.onPhysicsUpdate!(time, deltaTime);
99
- }
100
- this.onPostPhysics.dispatch({time, deltaTime});
101
- for (const system of this.onPostPhysicsUpdateSet) {
102
- system.onPostPhysicsUpdate!(time, deltaTime);
103
- }
104
- });
105
-
106
- // TODO: update from outside
107
- this.clock.onUpdate.add(({time, deltaTime, alpha}) => {
108
- for (const system of this.onUpdateSet) {
109
-
110
- system.onUpdate!(time, deltaTime, alpha);
111
- }
112
- //TODO: merge with framerate to single signal?
113
- this.onUpdate.dispatch({time, deltaTime, alpha});
114
- for (const system of this.onLateUpdateSet) {
115
- system.onLateUpdate!(time, deltaTime, alpha);
116
- }
117
-
118
- //
119
- // const drawTime = 1 / 30;
120
- // while (performance.now() < ts + drawTime * 1000) {
121
- //
122
- // }
123
- for (const system of this.onRenderUpdateSet) {
124
- system.onRender!();
125
- }
126
- this.onRender.dispatch();
127
- this._frameCount++;
128
- });
129
- }
130
-
131
- private installPlugins(plugins: GameEnginePlugin[]) {
132
- plugins.forEach(plugin => {
133
- plugin.onPrepare && plugin.onPrepare(this);
134
- });
135
- plugins.forEach(plugin => {
136
- plugin.onStart && plugin.onStart(this);
137
- });
138
- }
139
-
140
- registerSystem(systemClass: Class<System>): void {
141
- this.mapStaticAnnotatedSignals(systemClass);
142
- if (this._started) throw new Error('Register Systems before engine is started');
143
- this.injector.registerSystem(systemClass);
144
- }
145
-
146
- registerService(serviceClass: Class<Service>): void {
147
- this.mapStaticAnnotatedSignals(serviceClass);
148
- if (this._started) throw new Error('Register Services before engine is started');
149
- this.injector.map(serviceClass).toSingleton();
150
- this.serviceClasses.add(serviceClass);
151
- }
152
-
153
- getService<T extends Class<Service>>(serviceClass: T): InstanceType<T> {
154
- return this.injector.get(serviceClass);
155
- }
156
-
157
- start() {
158
- this._started = true;
159
- //TODO: throw when registering services or systems when engine is running
160
- for (const serviceClass of this.serviceClasses) {
161
- const service = this.injector.get(serviceClass);
162
- this.addService(service);
163
- }
164
- this.clock.start();
165
- this.onStart.dispatch();
166
- }
167
-
168
- addService(service: Service): void {
169
- this.mapAnnotatedSignals(service);
170
- service.onStart && service.onStart();
171
- service.onPreFixedUpdate && this.onPreFixedUpdateSet.add(service);
172
- service.onFixedUpdate && this.onFixedUpdateSet.add(service);
173
- service.onUpdate && this.onUpdateSet.add(service);
174
- service.onLateUpdate && this.onLateUpdateSet.add(service);
175
- service.onPhysicsUpdate && this.onPhysicsUpdateSet.add(service);
176
- service.onPostPhysicsUpdate && this.onPostPhysicsUpdateSet.add(service);
177
- service.onRender && this.onRenderUpdateSet.add(service);
178
- }
179
-
180
- addEntity(entity: Entity) {
181
- return new Promise(resolve => this.addQueue.push([entity.getAllChildren(), resolve]));
182
- }
183
-
184
- waitFrame() {
185
- return this.framePromise;
186
- }
187
-
188
- async removeEntity(entity: Entity) {
189
- if ((<any>entity).markRemoval) return;
190
- (<any>entity).markRemoval = true;
191
- return new Promise(resolve => this.removeQueue.push([entity.getAllChildren(), resolve]));
192
- }
193
-
194
- private prepareFrame() {
195
-
196
- for (const [entities, resolve] of this.removeQueue) {
197
- for (const entity of entities) {
198
- (<any>entity).markRemoval = false;
199
- if (!(<any>entity).gameEngine) throw new Error('Entity not active');
200
- (<any>entity).gameEngine = undefined;
201
-
202
- const systems = this.entitySystemMap.get(entity);
203
- this.entitySystemMap.delete(entity);
204
- if (systems) {
205
- for (const system of systems) {
206
- this.unmapAnnotatedSignals(system);
207
- system.onEnd && system.onEnd();
208
- system.onPreFixedUpdate && this.onPreFixedUpdateSet.delete(system);
209
- system.onFixedUpdate && this.onFixedUpdateSet.delete(system);
210
- system.onEarlyUpdate && this.onEarlyUpdateSet.delete(system);
211
- system.onUpdate && this.onUpdateSet.delete(system);
212
- system.onLateUpdate && this.onLateUpdateSet.delete(system);
213
- system.onPhysicsUpdate && this.onPhysicsUpdateSet.delete(system);
214
- system.onPostPhysicsUpdate && this.onPostPhysicsUpdateSet.delete(system);
215
- system.onRender && this.onRenderUpdateSet.delete(system);
216
- }
217
- }
218
- entity.onRemove.dispatch();
219
- }
220
-
221
- resolve();
222
- }
223
- this.removeQueue = [];
224
-
225
- for (const [entities, resolve] of this.addQueue) {
226
- const preparePromises = [];
227
- const newSystems: System[] = [];
228
- for (const entity of entities) {
229
- if ((<any>entity).gameEngine) throw new Error('Entity already added to a gameEngine');
230
- (<any>entity).gameEngine = this;
231
- const systems = this.injector.createSystems(entity);
232
- for (const system of systems) {
233
- system.onPrepare && preparePromises.push(system.onPrepare());
234
- newSystems.push(system);
235
- }
236
- this.entitySystemMap.set(entity, systems);
237
- }
238
-
239
- const startSystems = () => {
240
- // TODO: check if a "global" method is faster than this inner method
241
- for (const system of newSystems) {
242
- this.mapAnnotatedSignals(system);
243
- system.onStart && system.onStart();
244
- system.onPreFixedUpdate && this.onPreFixedUpdateSet.add(system);
245
- system.onFixedUpdate && this.onFixedUpdateSet.add(system);
246
- system.onEarlyUpdate && this.onEarlyUpdateSet.add(system);
247
- system.onUpdate && this.onUpdateSet.add(system);
248
- system.onLateUpdate && this.onLateUpdateSet.add(system);
249
- system.onPhysicsUpdate && this.onPhysicsUpdateSet.add(system);
250
- system.onPostPhysicsUpdate && this.onPostPhysicsUpdateSet.add(system);
251
- system.onRender && this.onRenderUpdateSet.add(system);
252
- }
253
- resolve();
254
- };
255
-
256
- // Promise.all doesnt resolve immediately when there are no promises bu
257
- if (preparePromises.length === 0) {
258
- startSystems();
259
- } else {
260
- Promise.all(preparePromises).then(startSystems);
261
- }
262
- }
263
- this.addQueue = [];
264
- // create promise for next frame
265
- this.resolveFrame();
266
- this.framePromise = new Promise<any>(resolve => {
267
- this.resolveFrame = resolve;
268
- });
269
- }
270
-
271
- runAction<T extends Action>(action: T): ReturnType<T> {
272
- return action(this);
273
- }
274
-
275
- getSignal<T>(signalId: ID): Signal<T> {
276
- return this.injector.getSignal<T>(signalId);
277
- }
278
-
279
- private signalMappings = new Map<Class, [string, Signal][]>();
280
- private staticSignalMappings = new Map<Class, [string, Signal][]>();
281
- private signalMapBindings = new Map<Object, SignalBinding[]>();
282
- // TODO: staticSignalMapBindings needed if we want to unregister Services or Systems
283
-
284
- private mapStaticAnnotatedSignals(target: Class) {
285
- let staticSignalMap = this.getStaticSignalMap(target);
286
- if (staticSignalMap.length === 0) return;
287
-
288
- const bindings = [];
289
- for (const [key, signal] of staticSignalMap) {
290
- bindings.push(signal.add((<any>target)[key], target));
291
- }
292
- // TODO: ignore bindings for now (store them if we implement unregister Service or Systems
293
- }
294
-
295
- private mapAnnotatedSignals(target: Object) {
296
- const signalMap = this.getSignalMap(target.constructor);
297
- if (signalMap.length === 0) return;
298
-
299
- const bindings = [];
300
- for (const [key, signal] of signalMap) {
301
- bindings.push(signal.add((<any>target)[key], target));
302
- }
303
- this.signalMapBindings.set(target, bindings);
304
- }
305
-
306
- private unmapAnnotatedSignals(target: Object) {
307
- const signalBindings = this.signalMapBindings.get(target);
308
- if (!signalBindings) return;
309
- for (const binding of signalBindings) {
310
- binding.detach();
311
- }
312
- }
313
-
314
- private getSignalMap(target: Function) {
315
- // TODO: we could speed thinks up by introducing "invisible" flags on the systems/services to avoid a map check
316
- return putIfAbsent(this.signalMappings, target, (): [string, Signal][] => {
317
- const handlers = signalHandlers.get(target);
318
- if (!handlers) return [];
319
- const result: [string, Signal][] = [];
320
- for (const [key, signalId] of handlers) {
321
- result.push([key, this.getSignal(signalId)]);
322
- }
323
- return result;
324
- });
325
- }
326
-
327
- private getStaticSignalMap(target: Class) {
328
- // TODO: we could speed thinks up by introducing "invisible" flags on the systems/services to avoid a map check
329
- return putIfAbsent(this.staticSignalMappings, target, (): [string, Signal][] => {
330
- const handlers = staticSignalHandlers.get(target);
331
- if (!handlers) return [];
332
- const result: [string, Signal][] = [];
333
- for (const [key, signalId] of handlers) {
334
- result.push([key, this.getSignal(signalId)]);
335
- }
336
- return result;
337
- });
338
- }
339
- }
340
- //
341
- // type RunSpread<T> = T extends undefined
342
- // ? []
343
- // : [T];
344
- //
345
- // export type GameActionMapping = { id: string, action: GameAction<any> }
346
- //
347
- // export function createGameAction<K extends keyof GameEngineActions>(id: K, action: GameAction<K>): GameActionMapping {
348
- // return {id, action};
349
- // }
350
- //
1
+ import {Action, Class, EngineSignals, GameEnginePlugin, OnFixedUpdateParams, OnUpdateParams, Service, System} from './types';
2
+ import {Signal, SignalBinding} from 'mani-signal';
3
+ import {Entity} from './entity';
4
+ import {EcsInjector, signalHandlers, staticSignalHandlers} from './ecsInjector';
5
+ import {Clock, GameClock} from './clock';
6
+ import {SystemContext} from './systemContext';
7
+ import {ID, putIfAbsent} from './injector';
8
+
9
+ type AddEntry = [Entity, () => void];
10
+ type RemoveEntry = [Entity, () => void];
11
+
12
+ const defaultOptions = {
13
+ fixedDeltaTime: 1 / 60,
14
+ plugins: [] as GameEnginePlugin[],
15
+ };
16
+
17
+ export type GameEngineOptions = Partial<typeof defaultOptions>;
18
+ declare global {
19
+ interface GameEngineActions {
20
+ 'testAction': string;
21
+ }
22
+ }
23
+
24
+ type GameEngineActionParams<K extends keyof GameEngineActions> = GameEngineActions[K];
25
+ type GameAction<K extends keyof GameEngineActions> = (engine: GameEngine, params: GameEngineActionParams<K>) => Promise<any> | any;
26
+
27
+ let id = 0;
28
+ export class GameEngine {
29
+ readonly id = id++;
30
+ readonly onPrepare: Signal;
31
+ readonly onStart: Signal;
32
+ readonly onEnd: Signal;
33
+ readonly onRender: Signal<OnUpdateParams>;
34
+ readonly onUpdate: Signal<OnUpdateParams>;
35
+ readonly onFixedUpdate: Signal<OnFixedUpdateParams>;
36
+ readonly onPrePhysicsUpdate: Signal<OnFixedUpdateParams>;
37
+ readonly onPhysicsUpdate: Signal<OnFixedUpdateParams>;
38
+ readonly onPostPhysicsUpdate: Signal<OnFixedUpdateParams>;
39
+ readonly onLateFixedUpdate: Signal<OnFixedUpdateParams>;
40
+ readonly onLateUpdate: Signal<OnUpdateParams>;
41
+ readonly onAddEntity: Signal<Entity>;
42
+ readonly onRemoveEntity: Signal<Entity>;
43
+
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
+
59
+ // TODO: for performance, we could store the systems directly in the entity
60
+ private readonly entitySystemMap = new Map<Entity, Set<SystemContext>>();
61
+
62
+ private readonly serviceClasses = new Set<Class<Service>>();
63
+ private readonly services: Service[] = [];
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
+
74
+ get started(): boolean { return this._started; }
75
+
76
+ private _frameCount = 0;
77
+ get frameCount(): number { return this._frameCount; }
78
+
79
+ private _fixedFrameCount = 0;
80
+ get fixedFrameCount(): number { return this._fixedFrameCount; }
81
+
82
+ get numEntities(): number { return this.entitySystemMap.size;}
83
+
84
+ // TODO inject clock into game engine
85
+ // TODO inject space into game engine
86
+
87
+ constructor(
88
+ readonly injector: EcsInjector<Class<System>>,
89
+ readonly clock: Clock,
90
+ ) {
91
+ this.injector.map(GameEngine).toValue(this);
92
+
93
+ this.onPrepare = this.injector.getSignal(EngineSignals.OnPrepare);
94
+ this.onStart = this.injector.getSignal(EngineSignals.OnStart);
95
+ this.onEnd = this.injector.getSignal(EngineSignals.OnEnd);
96
+ this.onRender = this.injector.getSignal(EngineSignals.OnRender);
97
+ this.onFixedUpdate = this.injector.getSignal(EngineSignals.OnFixedUpdate);
98
+ this.onUpdate = this.injector.getSignal(EngineSignals.OnUpdate);
99
+ this.onLateUpdate = this.injector.getSignal(EngineSignals.OnLateUpdate);
100
+ this.onPrePhysicsUpdate = this.injector.getSignal(EngineSignals.OnPrePhysicsUpdate);
101
+ this.onPhysicsUpdate = this.injector.getSignal(EngineSignals.OnPhysicsUpdate);
102
+ this.onPostPhysicsUpdate = this.injector.getSignal(EngineSignals.OnPostPhysicsUpdate);
103
+ this.onLateFixedUpdate = this.injector.getSignal(EngineSignals.onLateFixedUpdate);
104
+
105
+ this.onAddEntity = this.injector.getSignal(EngineSignals.OnAddEntity);
106
+ this.onRemoveEntity = this.injector.getSignal(EngineSignals.OnRemoveEntity);
107
+
108
+ this.setupClock();
109
+ this.prepareFrame();
110
+ }
111
+
112
+ static createDefault(options?: GameEngineOptions) {
113
+ const opts = {...defaultOptions, ...options};
114
+ const clock = new GameClock({
115
+ fixedDeltaTime: opts.fixedDeltaTime,
116
+ autoStart: false,
117
+ });
118
+ const injector = new EcsInjector<Class<System>>();
119
+ const engine = new GameEngine(injector, clock);
120
+ engine.installPlugins(opts.plugins);
121
+ return engine;
122
+ }
123
+
124
+ static createDefaultWithInjector(injector: EcsInjector<Class<System>>, options?: GameEngineOptions) {
125
+ const opts = {...defaultOptions, ...options};
126
+ const clock = new GameClock({
127
+ fixedDeltaTime: opts.fixedDeltaTime,
128
+ autoStart: false,
129
+ });
130
+ const engine = new GameEngine(injector, clock);
131
+ engine.installPlugins(opts.plugins);
132
+ return engine;
133
+ }
134
+
135
+ addService(service: Service): void {
136
+ this.mapAnnotatedSignals(service);
137
+ service.onStart && service.onStart();
138
+ service.onPreFixedUpdate && this.onPreFixedUpdateSet.add(service);
139
+ service.onFixedUpdate && this.onFixedUpdateSet.add(service);
140
+ service.onUpdate && this.onUpdateSet.add(service);
141
+ service.onLateUpdate && this.onLateUpdateSet.add(service);
142
+ service.onPrePhysicsUpdate && this.onPrePhysicsUpdateSet.add(service);
143
+ service.onPhysicsUpdate && this.onPhysicsUpdateSet.add(service);
144
+ service.onPostPhysicsUpdate && this.onPostPhysicsUpdateSet.add(service);
145
+ service.onLateFixedUpdate && this.onLateFixedUpdateSet.add(service);
146
+ service.onRender && this.onRenderUpdateSet.add(service);
147
+ service.onAddEntity && this.onAddEntitySet.add(service);
148
+ service.onRemoveEntity && this.onRemoveEntitySet.add(service);
149
+ this.services.push(service);
150
+ }
151
+
152
+ installPlugins(plugins: GameEnginePlugin[]) {
153
+ plugins.forEach(plugin => {
154
+ plugin.onPrepare && plugin.onPrepare(this);
155
+ });
156
+ plugins.forEach(plugin => {
157
+ plugin.onStart && plugin.onStart(this);
158
+ });
159
+ }
160
+
161
+ registerSystem(systemClass: Class<System>): void {
162
+ this.mapStaticAnnotatedSignals(systemClass);
163
+ if (this._started) throw new Error('Register Systems before engine is started');
164
+ this.injector.registerSystem(systemClass);
165
+ }
166
+
167
+ registerService(serviceClass: Class<Service>): void {
168
+ this.mapStaticAnnotatedSignals(serviceClass);
169
+ if (this._started) throw new Error('Register Services before engine is started');
170
+ this.injector.map(serviceClass).toSingleton();
171
+ this.serviceClasses.add(serviceClass);
172
+ }
173
+
174
+ getService<T extends Class<Service>>(serviceClass: T): InstanceType<T> {
175
+ return this.injector.get(serviceClass);
176
+ }
177
+
178
+ start() {
179
+ this._started = true;
180
+ //TODO: throw when registering services or systems when engine is running
181
+ for (const serviceClass of this.serviceClasses) {
182
+ const service = this.injector.get(serviceClass);
183
+ this.addService(service);
184
+ }
185
+ this.clock.start();
186
+ this.onStart.dispatch();
187
+ }
188
+
189
+ private setupClock() {
190
+ this.clock.onPrepare.add(this.prepareFrame, this);
191
+
192
+ this.clock.onEarlyUpdate.add(({time, deltaTime}) => {
193
+ for (const system of this.onEarlyUpdateSet) {
194
+ system.onEarlyUpdate!(time, deltaTime);
195
+ }
196
+ });
197
+
198
+ this.clock.onFixedUpdate.add((params) => {
199
+ const {time, deltaTime} = params;
200
+ for (const system of this.onPreFixedUpdateSet) {
201
+ system.onPreFixedUpdate!(time, deltaTime);
202
+ }
203
+ for (const system of this.onFixedUpdateSet) {
204
+ system.onFixedUpdate!(time, deltaTime);
205
+ }
206
+ this.onFixedUpdate.dispatch(params);
207
+ for (const system of this.onPhysicsUpdateSet) {
208
+ system.onPhysicsUpdate!(time, deltaTime);
209
+ }
210
+ this.onPhysicsUpdate.dispatch(params);
211
+ this.onPostPhysicsUpdate.dispatch(params);
212
+ for (const system of this.onPostPhysicsUpdateSet) {
213
+ system.onPostPhysicsUpdate!(time, deltaTime);
214
+ }
215
+ this.onLateFixedUpdate.dispatch(params);
216
+ for (const system of this.onLateFixedUpdateSet) {
217
+ system.onLateFixedUpdate!(time, deltaTime);
218
+ }
219
+ this._fixedFrameCount++;
220
+ });
221
+
222
+ // TODO: update from outside
223
+ this.clock.onUpdate.add(onUpdateParams => {
224
+ const {time, deltaTime, alpha} = onUpdateParams;
225
+ for (const system of this.onUpdateSet) {
226
+ system.onUpdate!(time, deltaTime, alpha);
227
+ }
228
+ //TODO: merge with framerate to single signal?
229
+
230
+ this.onUpdate.dispatch(onUpdateParams);
231
+ for (const system of this.onLateUpdateSet) {
232
+ system.onLateUpdate!(time, deltaTime, alpha);
233
+ }
234
+ this.onLateUpdate.dispatch(onUpdateParams);
235
+
236
+ for (const system of this.onRenderUpdateSet) {
237
+ system.onRender!(time, deltaTime, alpha);
238
+ }
239
+ this.onRender.dispatch(onUpdateParams);
240
+ this._frameCount++;
241
+ });
242
+ }
243
+
244
+ add(entity: Entity) {
245
+ return new Promise<void>(resolve => this.addQueue.push([entity, resolve]));
246
+ }
247
+
248
+ waitFrame() {
249
+ return this.framePromise;
250
+ }
251
+
252
+ async remove(entity: Entity) {
253
+ if ((<any>entity).markRemoval) return;
254
+ (<any>entity).markRemoval = true;
255
+ return new Promise<void>(resolve => this.removeQueue.push([entity, resolve]));
256
+ }
257
+
258
+ stop() {
259
+ // this.injector.dispose();
260
+ this.clock.stop();
261
+ }
262
+
263
+ runAction<T extends Action>(action: T): ReturnType<T> {
264
+ return action(this);
265
+ }
266
+
267
+ getSignal<T>(signalId: ID): Signal<T> {
268
+ return this.injector.getSignal<T>(signalId);
269
+ }
270
+
271
+ private signalMappings = new Map<Class, [string, Signal][]>();
272
+ private staticSignalMappings = new Map<Class, [string, Signal][]>();
273
+ private signalMapBindings = new Map<Object, SignalBinding[]>();
274
+
275
+ // TODO: staticSignalMapBindings needed if we want to unregister Services or Systems
276
+
277
+ private mapStaticAnnotatedSignals(target: Class) {
278
+ let staticSignalMap = this.getStaticSignalMap(target);
279
+ if (staticSignalMap.length === 0) return;
280
+
281
+ const bindings = [];
282
+ for (const [key, signal] of staticSignalMap) {
283
+ bindings.push(signal.add((<any>target)[key], target));
284
+ }
285
+ // TODO: ignore bindings for now (store them if we implement unregister Service or Systems
286
+ }
287
+
288
+ private mapAnnotatedSignals(target: Object) {
289
+ const signalMap = this.getSignalMap(target.constructor);
290
+ if (signalMap.length === 0) return;
291
+
292
+ const bindings = [];
293
+ for (const [key, signal] of signalMap) {
294
+ bindings.push(signal.add((<any>target)[key], target));
295
+ }
296
+ this.signalMapBindings.set(target, bindings);
297
+ }
298
+
299
+ private unmapAnnotatedSignals(target: Object) {
300
+ const signalBindings = this.signalMapBindings.get(target);
301
+ if (!signalBindings) return;
302
+ for (const binding of signalBindings) {
303
+ binding.detach();
304
+ }
305
+ }
306
+
307
+ private getSignalMap(target: Function) {
308
+ // TODO: we could speed thinks up by introducing "invisible" flags on the systems/services to avoid a map check
309
+ return putIfAbsent(this.signalMappings, target, (): [string, Signal][] => {
310
+ const handlers = signalHandlers.get(target);
311
+ if (!handlers) return [];
312
+ const result: [string, Signal][] = [];
313
+ for (const [key, signalId] of handlers) {
314
+ result.push([key, this.getSignal(signalId)]);
315
+ }
316
+ return result;
317
+ });
318
+ }
319
+
320
+ private getStaticSignalMap(target: Class) {
321
+ // TODO: we could speed thinks up by introducing "invisible" flags on the systems/services to avoid a map check
322
+ return putIfAbsent(this.staticSignalMappings, target, (): [string, Signal][] => {
323
+ const handlers = staticSignalHandlers.get(target);
324
+ if (!handlers) return [];
325
+ const result: [string, Signal][] = [];
326
+ for (const [key, signalId] of handlers) {
327
+ result.push([key, this.getSignal(signalId)]);
328
+ }
329
+ return result;
330
+ });
331
+ }
332
+
333
+ dispose() {
334
+ this.clock.stop();
335
+ this.clock.dispose();
336
+ for (const systems of this.entitySystemMap.values()) {
337
+ for (const system of systems) {
338
+ system.system.onEnd && system.system.onEnd();
339
+ }
340
+ }
341
+ for (const service of this.services) {
342
+ service.onEnd && service.onEnd();
343
+ }
344
+ this.onEnd.dispatch();
345
+ }
346
+
347
+ private prepareFrame() {
348
+ for (const [entity, resolve] of this.removeQueue) {
349
+ const entities = entity.getAllEntities();
350
+ for (const entity of entities) {
351
+ (<any>entity).markRemoval = false;
352
+ if (!(<any>entity).gameEngine) {
353
+ console.log(entity);
354
+ // throw new Error('Entity not active');
355
+ }
356
+ (<any>entity).gameEngine = undefined;
357
+
358
+ const systemContexts = this.entitySystemMap.get(entity);
359
+ this.entitySystemMap.delete(entity);
360
+ if (systemContexts) {
361
+ for (const context of systemContexts) {
362
+ this.disposeContext(context);
363
+ }
364
+ }
365
+ entity.onRemove.dispatch();
366
+ entity.onRemove.detachAll();
367
+
368
+ for (const system of this.onRemoveEntitySet) {
369
+ system.onRemoveEntity!(entity);
370
+ }
371
+ }
372
+
373
+ resolve();
374
+ }
375
+ this.removeQueue = [];
376
+
377
+ for (const [entity, resolve] of this.addQueue) {
378
+
379
+ const preparePromises = [];
380
+ const nextSystemContexts: SystemContext[] = [];
381
+ const entities = entity.getAllEntities();
382
+ for (const entity of entities) {
383
+ if ((<any>entity).gameEngine) throw new Error('Entity already added to a gameEngine');
384
+ (<any>entity).gameEngine = this;
385
+ const systemContexts = this.injector.createSystemsForEntity(entity);
386
+ for (const context of systemContexts) {
387
+ // TODO: implement synced mode where async onPrepares aren't allowed
388
+ context.system.onPrepare && preparePromises.push(context.system.onPrepare());
389
+ nextSystemContexts.push(context);
390
+ }
391
+ this.entitySystemMap.set(entity, systemContexts);
392
+ }
393
+
394
+ const entityWasAdded = ()=> {
395
+ for (const system of this.onAddEntitySet) {
396
+ system.onAddEntity!(entity);
397
+ }
398
+ resolve();
399
+ }
400
+
401
+ // Promise.all doesnt resolve immediately when there are no promises bu
402
+ if (preparePromises.length === 0) {
403
+ this.startSystems(nextSystemContexts, entityWasAdded);
404
+ } else {
405
+ Promise.all(preparePromises).then(() => this.startSystems(nextSystemContexts, entityWasAdded));
406
+ }
407
+ }
408
+ this.addQueue = [];
409
+ // create promise for next frame
410
+ this.resolveFrame();
411
+ this.framePromise = new Promise<void>(resolve => {
412
+ this.resolveFrame = resolve;
413
+ });
414
+ this.onPrepare.dispatch();
415
+ }
416
+
417
+ private startSystems(contexts: Iterable<SystemContext>, then?: () => void) {
418
+ for (const context of contexts) {
419
+ const system = context.system;
420
+ this.mapAnnotatedSignals(system);
421
+ system.onStart && system.onStart();
422
+ system.onPreFixedUpdate && this.onPreFixedUpdateSet.add(system);
423
+ system.onFixedUpdate && this.onFixedUpdateSet.add(system);
424
+ system.onUpdate && this.onUpdateSet.add(system);
425
+ system.onLateUpdate && this.onLateUpdateSet.add(system);
426
+ system.onPhysicsUpdate && this.onPhysicsUpdateSet.add(system);
427
+ system.onPrePhysicsUpdate && this.onPrePhysicsUpdateSet.add(system);
428
+ system.onPostPhysicsUpdate && this.onPostPhysicsUpdateSet.add(system);
429
+ system.onLateFixedUpdate && this.onLateFixedUpdateSet.add(system);
430
+ system.onRender && this.onRenderUpdateSet.add(system);
431
+ system.onAddEntity && this.onAddEntitySet.add(system);
432
+ system.onRemoveEntity && this.onRemoveEntitySet.add(system);
433
+ }
434
+ then?.();
435
+ };
436
+
437
+ private disposeContext(context: SystemContext<System>) {
438
+ context.dispose();
439
+ const system = context.system;
440
+ this.unmapAnnotatedSignals(system);
441
+ system.onEnd && system.onEnd();
442
+ system.onPreFixedUpdate && this.onPreFixedUpdateSet.delete(system);
443
+ system.onFixedUpdate && this.onFixedUpdateSet.delete(system);
444
+ system.onUpdate && this.onUpdateSet.delete(system);
445
+ system.onLateUpdate && this.onLateUpdateSet.delete(system);
446
+ system.onPhysicsUpdate && this.onPhysicsUpdateSet.delete(system);
447
+ system.onPrePhysicsUpdate && this.onPrePhysicsUpdateSet.delete(system);
448
+ system.onPostPhysicsUpdate && this.onPostPhysicsUpdateSet.delete(system);
449
+ system.onLateFixedUpdate && this.onLateFixedUpdateSet.delete(system);
450
+ system.onRender && this.onRenderUpdateSet.delete(system);
451
+ }
452
+ }
453
+ //
454
+ // type RunSpread<T> = T extends undefined
455
+ // ? []
456
+ // : [T];
457
+ //
458
+ // export type GameActionMapping = { id: string, action: GameAction<any> }
459
+ //
460
+ // export function createGameAction<K extends keyof GameEngineActions>(id: K, action: GameAction<K>): GameActionMapping {
461
+ // return {id, action};
462
+ // }
463
+ //