mani-game-engine 1.0.0-pre.7 → 1.0.0-pre.70

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,27 +189,35 @@ 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
  }
@@ -163,24 +227,25 @@ export class ScopeContext {
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[] {
@@ -292,14 +361,31 @@ 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.onSubReturn.dispatch(this);
302
379
  this.parent.scope.onActivate?.();
380
+ this.parent.onActivate.dispatch(this);
303
381
  this.closed = true;
382
+
383
+ this.onEnter.detachAll();
384
+ this.onExit.detachAll();
385
+ this.onSubReturn.detachAll();
386
+ this.onSubExit.detachAll();
387
+ this.onActivate.detachAll();
388
+ this.onDeactivate.detachAll();
389
+
304
390
  }
305
391
  }
@@ -1,11 +1,27 @@
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
+
4
+ export const entitySignalHandlers = new Map<Object, [string, ID][]>();
5
+ export const OnEntitySignal = (id: ID) => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
6
+ if (target instanceof Function) {
7
+ throw new Error('only allowed on non static methods');
8
+ } else {
9
+ const mappingList = putIfAbsent(entitySignalHandlers, target.constructor, (): [string, ID][] => []);
10
+ mappingList.push([propertyKey, id]);
11
+ }
12
+ };
3
13
 
4
14
  export class SystemContext<T extends System = System> {
5
15
  readonly system!: T;
16
+ readonly onDispose = new Signal();
6
17
  private signalBindings: SignalBinding[] = [];
7
18
 
8
- constructor(readonly entity: Object) {
19
+ constructor(readonly entity: Entity) {
20
+ }
21
+
22
+ // TODO: is this a bit hacky?
23
+ addSignalById<S>(signalId: string | symbol, callback: SignalCallback<S>, thisArg?: unknown) {
24
+ this.signalBindings.push(((this.entity as any).gameEngine as GameEngine).getSignal<S>(signalId).add(callback, thisArg));
9
25
  }
10
26
 
11
27
  addSignal<S>(signal: Signal<S>, callback: SignalCallback<S>, thisArg?: unknown) {
@@ -18,5 +34,7 @@ export class SystemContext<T extends System = System> {
18
34
 
19
35
  dispose() {
20
36
  for (const binding of this.signalBindings) binding.detach();
37
+ this.onDispose.dispatch();
38
+ this.onDispose.detachAll();
21
39
  }
22
40
  }
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,21 @@ 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
  };
96
+
97
+ export interface Disposable {
98
+ onDispose(): void;
99
+ }