mani-game-engine 1.0.0-pre.8 → 1.0.0-pre.80

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.
@@ -1,6 +1,6 @@
1
1
  import {Class, EcsInjector, Signal, SignalBinding, SignalCallback} from '../index';
2
- import {ID, putIfAbsent} from 'mani-injector';
3
2
  import {signalHandlers} from '../ecsInjector';
3
+ import {ID, putIfAbsent} from '../injector';
4
4
 
5
5
  export type ScopeSignalOptions = { keepAlive?: boolean | Scope[], group?: string }
6
6
 
@@ -15,6 +15,10 @@ export const OnScopeSignal = (id: ID, options?: ScopeSignalOptions) => (target:
15
15
  }
16
16
  };
17
17
 
18
+ export const SCOPE_CONTEXT = {
19
+ log: true,
20
+ };
21
+
18
22
  export interface Scope {
19
23
  [propName: string]: any; // disable weak type detection
20
24
  onEnter?(): void;
@@ -46,11 +50,12 @@ class ScopeSignalBinding {
46
50
  this.signalBinding.detach();
47
51
  }
48
52
  }
49
-
53
+ // TODO: rename to ScopeStack to something like InternalScopeTreeSingleton with an interface
50
54
  class ScopeStack {
51
55
  readonly stack: ScopeContext[] = [];
52
- queuedScopeChanges: Function[] = [];
56
+ queuedScopeChanges: (() => Promise<void>)[] = [];
53
57
  ongoingChange = false;
58
+ target?: Scope;
54
59
 
55
60
  constructor(readonly rootScope: ScopeContext) {
56
61
  this.stack.push(rootScope);
@@ -63,21 +68,32 @@ class ScopeStack {
63
68
  get activeContext() {
64
69
  return this.stack[this.stack.length - 1];
65
70
  }
66
-
67
71
  }
68
72
 
69
73
  export type ScopeMapping = (params: {
70
74
  injector: EcsInjector,
71
75
  registerScopeService: (serviceClass: Class) => void,
72
- }) => void
76
+ onEnter: Signal<ScopeContext>,
77
+ onExit: Signal<ScopeContext>,
78
+ onSubReturn: Signal<ScopeContext>,
79
+ onSubExit: Signal<ScopeContext>,
80
+ onActivate: Signal<ScopeContext>,
81
+ onDeactivate: Signal<ScopeContext>,
82
+
83
+ }) => any | Promise<void>;
73
84
 
74
85
  interface AddScopeSignalOptions extends ScopeSignalOptions {
75
86
  context?: unknown;
76
87
  }
77
88
 
89
+ export type ScopeChangeParams = {
90
+ from: ScopeContext;
91
+ to: ScopeContext;
92
+ }
93
+
78
94
  // TODO: maybe move Signal resolver to Injector and dont use EcsInjector here?
79
- export class ScopeContext {
80
- readonly scope: Scope;
95
+ export class ScopeContext<T extends Class = Class<Scope>> {
96
+ readonly scope!: InstanceType<T>;
81
97
  readonly injector: EcsInjector;
82
98
  private readonly stack: ScopeStack; // TODO: naming
83
99
  private readonly signalBindings: ScopeSignalBinding[] = [];
@@ -88,41 +104,81 @@ export class ScopeContext {
88
104
  private closed = false;
89
105
  private muteKeepAliveSignals = false;
90
106
 
91
- constructor(injector: EcsInjector, scopeClass: Class, mapping?: ScopeMapping, private parent?: ScopeContext) {
107
+ readonly onEnter = new Signal<ScopeContext>();
108
+ readonly onExit = new Signal<ScopeContext>();
109
+ readonly onSubReturn = new Signal<ScopeContext>();
110
+ readonly onSubExit = new Signal<ScopeContext>();
111
+ readonly onActivate = new Signal<ScopeContext>();
112
+ readonly onDeactivate = new Signal<ScopeContext>();
113
+ private readonly creationTime: DOMHighResTimeStamp;
114
+ readonly onScopeChange: Signal<ScopeChangeParams>;
115
+
116
+ get runtime() {
117
+ return (performance.now() - this.creationTime) / 1000;
118
+ }
119
+
120
+ constructor(injector: EcsInjector, scopeClass: T, mapping?: ScopeMapping, private parent?: ScopeContext, resolve?: () => void) {
121
+ this.creationTime = performance.now();
92
122
  if (!parent) {
93
123
  this.stack = new ScopeStack(this);
124
+ this.onScopeChange = new Signal<ScopeChangeParams>();
94
125
 
95
126
  } else {
96
127
  this.stack = parent.stack;
128
+ this.onScopeChange = parent.onScopeChange;
97
129
  this.stack.stack.push(this);
98
130
  }
99
131
 
132
+ // TODO: disable this for the rootscope?
100
133
  this.injector = injector.createChild();
134
+
101
135
  //TODO: i think it would really be better if we implement an injector.injectIntoUnmapped() instead of mapping here
102
136
  this.injector.map(scopeClass).toSingleton();
103
137
  this.injector.map(ScopeContext).toValue(this);
104
138
 
105
- mapping?.({
139
+ const result = mapping?.({
106
140
  injector: this.injector,
107
141
  registerScopeService: serviceClass => {
108
142
  this.injector.map(serviceClass).toSingleton();
109
143
  this.mapServiceSignals(this.injector.get(serviceClass));
110
144
  },
145
+ onEnter: this.onEnter,
146
+ onExit: this.onExit,
147
+ onSubReturn: this.onSubReturn,
148
+ onSubExit: this.onSubExit,
149
+ onActivate: this.onActivate,
150
+ onDeactivate: this.onDeactivate,
111
151
  });
112
152
 
113
- this.scope = this.injector.get(scopeClass) as Scope;
114
- this.mapScopeSignals();
115
- this.updateSignalBindings();
116
- // TODO: maybe the scope needs to prepare resources or something before it can react to signals, so we should move onEnter bevore the signal mapping and make it async (not working in constructor though)
117
- this.scope.onEnter?.();
118
- this.scope.onActivate?.();
153
+ const completeScopeChange = () => {
154
+ (this.scope as Scope) = this.injector.get(scopeClass);
155
+ this.mapScopeSignals();
156
+ this.updateSignalBindings();
157
+ // TODO: maybe the scope needs to prepare resources or something before it can react to signals, so we should move onEnter bevore the signal mapping and make it async (not working in constructor though)
158
+ this.scope.onEnter?.();
159
+ this.onEnter.dispatch(this);
160
+ this.scope.onActivate?.();
161
+ this.onActivate.dispatch(this);
162
+ resolve?.();
163
+ };
164
+
165
+ if (result instanceof Promise) {
166
+ result.then(completeScopeChange);
167
+ } else {
168
+ completeScopeChange();
169
+ }
119
170
  }
120
171
 
121
- get isRoot() {
172
+ get isRoot(): boolean {
122
173
  return this === this.stack.rootScope;
123
174
  }
124
175
 
125
- get isActive() {
176
+ // TODO: test this
177
+ get target() {
178
+ return this.stack.target;
179
+ }
180
+
181
+ get isActive(): boolean {
126
182
  return this === this.stack.activeContext;
127
183
  }
128
184
 
@@ -133,54 +189,63 @@ export class ScopeContext {
133
189
  * @param scopeClass
134
190
  * @param mapping
135
191
  */
136
- enterScope(scopeClass: Class, mapping?: ScopeMapping): ScopeContext | undefined {
137
- let newContext;
138
- const doChange = () => {
139
-
192
+ async enterScope(scopeClass: Class, mapping?: ScopeMapping) {
193
+ let newContext: ScopeContext | undefined;
194
+ const doChange = async () => {
195
+ const oldContext = this.activeContext;
140
196
  this.activeContext.scope.onSubExit?.();
197
+ this.activeContext.onSubExit.dispatch(this);
141
198
  this.activeContext.scope.onDeactivate?.();
199
+ this.activeContext.onDeactivate.dispatch(this);
142
200
 
143
- newContext = new ScopeContext(this.activeContext.injector, scopeClass, mapping, this.activeContext);
201
+ await new Promise<void>(resolve => {
202
+ newContext = new ScopeContext(this.activeContext.injector, scopeClass, mapping, this.activeContext, resolve);
203
+ });
144
204
 
145
205
  this.logStack();
206
+ this.onScopeChange.dispatch({
207
+ from: oldContext,
208
+ to: newContext!,
209
+ })
146
210
  const nextChange = this.stack.queuedScopeChanges.shift();
147
211
  if (nextChange) {
148
- nextChange();
212
+ await nextChange();
149
213
  } else {
150
214
  this.stack.ongoingChange = false;
151
215
  }
152
216
  };
153
- // TODO: this queued changes arent somewhat experimental
217
+ // TODO: this queued changes are somewhat experimental
154
218
  if (!this.stack.ongoingChange) {
155
219
  this.stack.ongoingChange = true;
156
- doChange();
220
+ await doChange();
157
221
  } else {
158
222
  this.stack.queuedScopeChanges.push(doChange);
159
223
  }
160
- return newContext;
224
+ return newContext!;
161
225
  }
162
226
 
163
227
  /**
164
228
  * Exits this scope (and all open subscopes=), if a scope class is given. all scopes from the stack are closed until after the scope with the given class
165
229
  */
166
- exitScope(target?: Scope) {
167
- const doChange = () => {
230
+ async exitScope(target?: Scope) {
231
+ const doChange = async () => {
168
232
  if (this.closed) throw new Error(`Scope already closed`);
169
233
  // TODO: check if target is in stack?
170
- if (!target) target = this.scope.constructor;
234
+ this.stack.target = target || this.scope.constructor;
171
235
  while (true) {
172
236
  const ctx = this.stack.stack.pop();
173
237
  if (!ctx) {
174
238
  throw new Error('no scope in stack');
175
239
  }
176
240
  ctx!.exitThis();
177
- if (ctx!.scope.constructor === target) {
241
+ if (ctx!.scope.constructor === this.stack.target) {
178
242
  break;
179
243
  }
180
244
  }
245
+ this.stack.target = undefined;
181
246
  const nextChange = this.stack.queuedScopeChanges.shift();
182
247
  if (nextChange) {
183
- nextChange();
248
+ await nextChange();
184
249
  } else {
185
250
  this.stack.ongoingChange = false;
186
251
  }
@@ -188,13 +253,14 @@ export class ScopeContext {
188
253
  };
189
254
  if (!this.stack.ongoingChange) {
190
255
  this.stack.ongoingChange = true;
191
- doChange();
256
+ await doChange();
192
257
  } else {
193
258
  this.stack.queuedScopeChanges.push(doChange);
194
259
  }
195
260
  }
196
261
 
197
262
  closeSubScopes() {
263
+ this.stack.target = this.scope.constructor;
198
264
  while (!this.isActive) {
199
265
  const ctx = this.stack.stack.pop();
200
266
  if (!ctx) {
@@ -202,10 +268,13 @@ export class ScopeContext {
202
268
  }
203
269
  ctx.exitThis();
204
270
  }
271
+ this.stack.target = undefined;
205
272
  }
206
273
 
207
274
  private logStack() {
208
- console.debug('%c' + this.stack.stack.map(c => c.scope.constructor.name).join(' -> '), 'color:yellow');
275
+ if (SCOPE_CONTEXT.log) {
276
+ console.debug('%c' + this.stack.stack.map(c => c.scope.constructor.name).join(' -> '), 'color:yellow');
277
+ }
209
278
  }
210
279
 
211
280
  getStackClasses(): Scope[] {
@@ -213,7 +282,7 @@ export class ScopeContext {
213
282
  }
214
283
 
215
284
  addScopeSignal<T>(signal: Signal<T>, callback: SignalCallback<T>, params?: AddScopeSignalOptions) {
216
- const signalBinding = new ScopeSignalBinding(signal.add(callback, params?.context), params);
285
+ const signalBinding = new ScopeSignalBinding(signal.add(callback, {context:params?.context}), params);
217
286
  this.signalBindings.push(signalBinding);
218
287
  }
219
288
 
@@ -274,7 +343,7 @@ export class ScopeContext {
274
343
 
275
344
  for (const [field, id] of handlers) {
276
345
  const signal = this.injector.getSignal(id);
277
- const signalBinding = signal.add((<any>scopeService) [field], scopeService);
346
+ const signalBinding = signal.add((<any>scopeService) [field], {context:scopeService});
278
347
  this.serviceBindings.push(signalBinding);
279
348
  }
280
349
  }
@@ -292,14 +361,32 @@ export class ScopeContext {
292
361
  if (!this.parent) {
293
362
  throw new Error('can\'t exit root scope?!');
294
363
  }
364
+
295
365
  this.logStack();
296
366
  this.detachSignalBindings();
297
367
  this.detachServiceBindings();
298
368
  this.scope.onDeactivate?.();
369
+ this.onDeactivate.dispatch(this);
370
+
299
371
  this.scope.onExit?.();
372
+ this.onExit.dispatch(this);
373
+ this.onScopeChange.dispatch({from: this, to: this.parent});
374
+ // this.injector._dispose();
375
+
300
376
  this.parent.updateSignalBindings();
301
377
  this.parent.scope.onSubReturn?.();
378
+ this.parent.onSubReturn.dispatch(this);
379
+ this.onSubReturn.dispatch(this);
302
380
  this.parent.scope.onActivate?.();
381
+ this.parent.onActivate.dispatch(this);
303
382
  this.closed = true;
383
+
384
+ this.onEnter.detachAll();
385
+ this.onExit.detachAll();
386
+ this.onSubReturn.detachAll();
387
+ this.onSubExit.detachAll();
388
+ this.onActivate.detachAll();
389
+ this.onDeactivate.detachAll();
390
+
304
391
  }
305
392
  }
@@ -1,22 +1,58 @@
1
1
  import {System} from './types';
2
- import {Signal, SignalBinding, SignalCallback} from './index';
2
+ import {Entity, GameEngine, ID, putIfAbsent, Signal, SignalBinding, SignalCallback} from './index';
3
+ import {AddSignalOptions} from 'mani-signal/src/signal';
4
+
5
+ export const entitySignalHandlers = new Map<Object, [string, ID][]>();
6
+ export const OnEntitySignal = (id: ID) => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
7
+ if (target instanceof Function) {
8
+ throw new Error('only allowed on non static methods');
9
+ } else {
10
+ const mappingList = putIfAbsent(entitySignalHandlers, target.constructor, (): [string, ID][] => []);
11
+ mappingList.push([propertyKey, id]);
12
+ }
13
+ };
3
14
 
4
15
  export class SystemContext<T extends System = System> {
5
- readonly system!: T;
6
- private signalBindings: SignalBinding[] = [];
16
+ readonly system!: T;
17
+ readonly onDispose = new Signal();
18
+ private readonly abortController = new AbortController();
19
+ get abortSignal() {
20
+ return this.abortController.signal;
21
+ }
22
+ private signalBindings = new Set<SignalBinding>();
23
+
24
+ constructor(readonly entity: Entity) {
25
+ }
26
+
27
+ // TODO: is this a bit hacky?
28
+ addSignalById<S>(signalId: string | symbol, callback: SignalCallback<S>, options: AddSignalOptions = {}) {
29
+ this.signalBindings.add(((this.entity as any).gameEngine as GameEngine).getSignal<S>(signalId).add(callback, options));
30
+ }
7
31
 
8
- constructor(readonly entity: Object) {
9
- }
32
+ // returns a function that can be called to remove the signal
33
+ addSignal<S>(signal: Signal<S>, callback: SignalCallback<S>, options: AddSignalOptions = {}) {
34
+ const binding = signal.add(callback, options);
35
+ this.signalBindings.add(binding);
36
+ return () => {
37
+ binding.detach();
38
+ this.signalBindings.delete(binding);
39
+ };
40
+ }
10
41
 
11
- addSignal<S>(signal: Signal<S>, callback: SignalCallback<S>, thisArg?: unknown) {
12
- this.signalBindings.push(signal.add(callback, thisArg));
13
- }
42
+ addSignalOnce<S>(signal: Signal<S>, callback: SignalCallback<S>, options: AddSignalOptions = {}) {
43
+ const binding = signal.addOnce(callback, options);
44
+ this.signalBindings.add(binding);
45
+ return () => {
46
+ binding.detach();
47
+ this.signalBindings.delete(binding);
48
+ };
14
49
 
15
- addSignalOnce<S>(signal: Signal<S>, callback: SignalCallback<S>, thisArg?: unknown) {
16
- this.signalBindings.push(signal.addOnce(callback, thisArg));
17
- }
50
+ }
18
51
 
19
- dispose() {
20
- for (const binding of this.signalBindings) binding.detach();
21
- }
52
+ dispose() {
53
+ this.abortController.abort();
54
+ for (const binding of this.signalBindings) binding.detach();
55
+ this.onDispose.dispatch();
56
+ this.onDispose.detachAll();
57
+ }
22
58
  }
package/src/types.ts CHANGED
@@ -1,20 +1,23 @@
1
1
  import {GameEngine} from './gameEngine';
2
+ import {Entity} from './entity';
2
3
 
3
4
  export interface System {
4
5
  [propName: string]: any;
5
6
  onPrepare?(): Promise<void>;
6
7
  onStart?(): void;
7
- onEnd?(): void;
8
- onPreFixedUpdate?(): void;
9
- onEarlyUpdate?(time: number, deltaTime: number): void;
8
+ onEnd?(): void | Promise<void>;
9
+ onPreFixedUpdate?(time: number, deltaTime: number): void;
10
10
  onFixedUpdate?(time: number, deltaTime: number): void;
11
- onPrePhysicUpdate?(time: number, deltaTime: number): void;
12
11
  onPhysicsUpdate?(time: number, deltaTime: number): void;
13
12
  onUpdate?(time: number, deltaTime: number, alpha: number): void;
14
13
  onLateUpdate?(time: number, deltaTime: number, alpha: number): void;
14
+ onPrePhysicsUpdate?(time: number, deltaTime: number): void;
15
15
  onPostPhysicsUpdate?(time: number, deltaTime: number): void;
16
16
  onLateFixedUpdate?(time: number, deltaTime: number): void;
17
17
  onRender?(time: number, deltaTime: number, alpha: number): void;
18
+ onAddEntity?(entity: Entity): void;
19
+ onRemoveEntity?(entity: Entity): void;
20
+ // new(...args: any[]): System;
18
21
  // new(): GameSystem;
19
22
  }
20
23
 
@@ -27,15 +30,17 @@ export interface Service {
27
30
  onPrepare?(): Promise<void>;
28
31
  onStart?(): void;
29
32
  onEnd?(): void;
30
- onPreFixedUpdate?(): void;
33
+ onPreFixedUpdate?(time: number, deltaTime: number): void;
31
34
  onPostPhysicsUpdate?(time: number, deltaTime: number): void;
32
35
  onLateFixedUpdate?(time: number, deltaTime: number): void;
33
36
  onFixedUpdate?(time: number, deltaTime: number): void;
34
- onPrePhysicUpdate?(time: number, deltaTime: number): void;
35
37
  onPhysicsUpdate?(time: number, deltaTime: number): void;
38
+ onPrePhysicsUpdate?(time: number, deltaTime: number): void;
36
39
  onUpdate?(time: number, deltaTime: number, alpha: number): void;
37
40
  onLateUpdate?(time: number, deltaTime: number, alpha: number): void;
38
- onRender?(): void;
41
+ onRender?(time: number, deltaTime: number, alpha: number): void;
42
+ onAddEntity?(entity: Entity): void;
43
+ onRemoveEntity?(entity: Entity): void;
39
44
  // new(): GameSystem;
40
45
  }
41
46
 
@@ -74,12 +79,17 @@ export type Class<T = any> = { new(...args: any[]): T; }
74
79
 
75
80
  export const EngineSignals = {
76
81
  OnStart: Symbol('OnStart'),
82
+ OnEnd: Symbol('OnEnd'),
77
83
  OnUpdate: Symbol('OnUpdate'),
78
84
  OnLateUpdate: Symbol('OnLateUpdate'),
79
85
  OnFixedUpdate: Symbol('OnFixedUpdate'),
80
- OnPostPhysics: Symbol('OnPostPhysics'),
81
- onLateFixedUpdate: Symbol('OnPrepare'),
86
+ OnPreFixedUpdate: Symbol('OnPreFixedUpdate'),
87
+ OnPrePhysicsUpdate: Symbol('OnPrePhysicsUpdate'),
88
+ OnPhysicsUpdate: Symbol('OnPhysicsUpdate'),
89
+ OnPostPhysicsUpdate: Symbol('OnPostPhysicsUpdate'),
90
+ onLateFixedUpdate: Symbol('onLateFixedUpdate'),
82
91
  OnRender: Symbol('OnRender'),
83
92
  OnPrepare: Symbol('OnPrepare'),
84
-
93
+ OnAddEntity: Symbol('OnAddEntity'),
94
+ OnRemoveEntity: Symbol('OnRemoveEntity'),
85
95
  };