mani-game-engine 1.0.0-pre.36 → 1.0.0-pre.38

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.
@@ -9,6 +9,7 @@ type EntityClass = Class;
9
9
 
10
10
  type SystemResolvers<T extends Class> = [T, ResolverFunction[]]
11
11
  type ComponentDependency = Dependency & { kind: 'component'; index: number; type: Class; };
12
+ type DynamicComponentDependency = Dependency & { kind: 'dynamic'; index: number; type: Class; };
12
13
  type EntityDependency = Dependency & { kind: 'entity'; index: number; };
13
14
  type ContextDependency = Dependency & { kind: 'context'; index: number; };
14
15
  type SignalDependency = Dependency & { kind: 'signal'; index: number; id: ID; };
@@ -16,9 +17,12 @@ type EntitySignalDependency = Dependency & { kind: 'entitySignal'; index: number
16
17
 
17
18
  type EntityResolverContext = ResolverContext & { entityClass: Object; };
18
19
 
19
- export const entityComponents = new Map<EntityClass, Map<ComponentClass, string>>();
20
-
21
20
  export const GetComponent = createDependencyAnnotation((type, index): ComponentDependency => ({kind: 'component', type, index}));
21
+ export const GetDynamicComponent = createDependencyAnnotation((type, index): DynamicComponentDependency => ({
22
+ kind: 'dynamic',
23
+ type,
24
+ index,
25
+ }));
22
26
  export const GetEntity = createDependencyAnnotation((_type, index): EntityDependency => ({kind: 'entity', index}));
23
27
  export const GetContext = createDependencyAnnotation((_type, index): ContextDependency => ({kind: 'context', index}));
24
28
  export const GetSignal = (id: ID) => createDependencyAnnotation((_type, index): SignalDependency => ({kind: 'signal', index, id}));
@@ -33,7 +37,7 @@ export const EntityComponent = (target: object, propertyKey: string): any => {
33
37
  if (componentClass === Object) {
34
38
  throw new Error(`Object component type not allowed. Forgot to specify type of ${entityClass.name}.${propertyKey}?`);
35
39
  }
36
- const componentSet = putIfAbsent(entityComponents, entityClass, () => new Map<ComponentClass, string>());
40
+ const componentSet = putIfAbsent(EcsInjector.entityComponents, entityClass, () => new Map<ComponentClass, string>());
37
41
  componentSet.set(componentClass, propertyKey);
38
42
  };
39
43
 
@@ -49,17 +53,18 @@ export const OnSignal = (id: ID) => (target: any, propertyKey: string, descripto
49
53
  }
50
54
  };
51
55
 
52
- const getComponentDependencies = (system: Class) => {
53
- const dependencies = Injector.dependencyMap.get(system);
54
- return dependencies
55
- ? dependencies.filter((dependency): dependency is ComponentDependency => dependency.kind === 'component')
56
- : [];
56
+ const extractDependenciesWithType = <T extends Dependency>(type: string) => {
57
+ const result = new Map<Class, T[]>();
58
+ Injector.dependencyMap.forEach((dependency, system) => {
59
+ putIfAbsent(result, system, () => [...dependency.filter((dependency) => dependency.kind === type)]);
60
+ });
61
+ return result;
57
62
  };
58
63
 
59
64
  const getEntityClassesForComponentDependencies = (componentTypes: Class[]): Class[] => {
60
65
  // TODO: refactor class, use map, filter etc...
61
66
  const result = [];
62
- for (const [entityClass, componentMap] of entityComponents) {
67
+ for (const [entityClass, componentMap] of EcsInjector.entityComponents) {
63
68
  let allDependenciesMet = true;
64
69
  for (const componentType of componentTypes) {
65
70
  if (!componentMap.has(componentType)) {
@@ -81,10 +86,21 @@ const componentResolver = (context: ResolverContext, dependency: Dependency): Re
81
86
  throw new Error(`Could not resolve Component ${type.name}. @GetComponent only allowed in system scope.`);
82
87
  }
83
88
  const entityClass = (context as EntityResolverContext).entityClass;
84
- const key = entityComponents.get(<any>entityClass as Class)!.get((dependency as ComponentDependency).type);
89
+ const key = EcsInjector.entityComponents.get(<any>entityClass as Class)!.get((dependency as ComponentDependency).type);
85
90
  return (context: SystemContext) => (context.entity as any)[key!];
86
91
  };
87
92
 
93
+ const dynamicComponentResolver = (context: ResolverContext, dependency: Dependency): ResolverFunction => {
94
+ const kind = context.kind;
95
+ const type = context.type;
96
+ if (kind !== 'system') {
97
+ throw new Error(`Could not resolve Component ${type.name}. @GetComponent only allowed in system scope.`);
98
+ }
99
+ // const entityClass = (context as EntityResolverContext).entityClass;
100
+ // const key = entityComponents.get(<any>entityClass as Class)!.get((dependency as ComponentDependency).type);
101
+ return (context: SystemContext) => context.entity.getDynamicComponent((dependency as DynamicComponentDependency).type);
102
+ };
103
+
88
104
  const entityResolver = ({type, kind}: ResolverContext, _dependency: Dependency): ResolverFunction => {
89
105
  if (kind !== 'system') {
90
106
  throw new Error(`Could not resolve Entity in ${type.name}. @GetEntity only allowed in system scope.`);
@@ -109,22 +125,31 @@ const contextResolver = ({type, kind}: ResolverContext, _dependency: Dependency)
109
125
  // const isSignalResolver = (dependency: Dependency): dependency is SignalDependency => dependency.kind === 'signal';
110
126
 
111
127
  export class EcsInjector<SystemClass extends Class = Class> extends Injector {
112
- // protected readonly entitySystemMap = new Map<EntityClass, SystemClass[]>();
128
+ static warnNoMatchingEntities = true;
129
+ static entityComponents = new Map<EntityClass, Map<ComponentClass, string>>();
130
+ protected componentDependencyMap = extractDependenciesWithType<ComponentDependency>('component');
131
+ protected dynamicComponentDependencyMap = extractDependenciesWithType<DynamicComponentDependency>('dynamic');
113
132
  protected entitySystemMap: Map<Class, SystemClass[]>;
114
- protected entitySystemResolverTuples: Map<Class, SystemResolvers<SystemClass>[]>;
133
+ protected dynamicComponentSystemMap: Map<Class, SystemClass[]>;
134
+ protected entitySystemResolverTuples: Map<Class, Map<SystemClass, ResolverFunction[]>>;
115
135
  private signalMap: Map<ID, Signal>;
116
136
 
117
137
  constructor(parent?: EcsInjector<SystemClass>) {
118
138
  super(parent);
119
139
  this.signalMap = parent ? parent.signalMap : new Map<ID, Signal>();
140
+
141
+ // TODO: do we need to have this maps in every child injector?
120
142
  this.entitySystemMap = new Map<EntityClass, SystemClass[]>();
121
- this.entitySystemResolverTuples = new Map<EntityClass, SystemResolvers<SystemClass>[]>();
143
+ this.dynamicComponentSystemMap = new Map<EntityClass, SystemClass[]>();
144
+ this.entitySystemResolverTuples = new Map<Class, Map<SystemClass, ResolverFunction[]>>();
145
+
122
146
  this.map(EcsInjector).toValue(this);
123
147
 
124
148
  if (!parent) {
125
149
  // only add extension resolvers to the main/parent injector
126
150
  this.addExtensionResolver('entity', entityResolver);
127
151
  this.addExtensionResolver('component', componentResolver);
152
+ this.addExtensionResolver('dynamic', dynamicComponentResolver);
128
153
  this.addExtensionResolver('context', contextResolver);
129
154
 
130
155
  const signalResolver = ({kind}: ResolverContext, dependency: Dependency): ResolverFunction => {
@@ -143,12 +168,19 @@ export class EcsInjector<SystemClass extends Class = Class> extends Injector {
143
168
 
144
169
  registerSystem<T extends SystemClass>(systemClass: T) {
145
170
  // TODO: check if system is already mapped
146
- const componentDependencies = getComponentDependencies(systemClass).map(dependency => dependency.type);
171
+ const componentDependencies = this.componentDependencyMap.get(systemClass)?.map(it => it.type) || [];
172
+ const dynamicComponentDependencies = this.dynamicComponentDependencyMap.get(systemClass) || [];
173
+
174
+ for (const dynamicComponentDependency of dynamicComponentDependencies) {
175
+ const systemClassesForDynamicComponent = putIfAbsent(this.dynamicComponentSystemMap, dynamicComponentDependency.type, () => [] as SystemClass[]);
176
+ systemClassesForDynamicComponent.push(systemClass);
177
+ }
178
+
147
179
  if (componentDependencies.length === 0) {
148
180
  throw new Error(`${systemClass.name} needs at least one component dependency.`);
149
181
  }
150
182
  const entityClasses = getEntityClassesForComponentDependencies(componentDependencies);
151
- if (entityClasses.length === 0) {
183
+ if (entityClasses.length === 0 && EcsInjector.warnNoMatchingEntities) {
152
184
  console.warn(`System '${systemClass.name}' has no matching entities.`);
153
185
  return;
154
186
  }
@@ -158,23 +190,39 @@ export class EcsInjector<SystemClass extends Class = Class> extends Injector {
158
190
  }
159
191
  }
160
192
 
161
- createSystems(entity: Object): SystemContext[] {
162
- const systemResolverTuples = putIfAbsent(this.entitySystemResolverTuples, entity.constructor, (): SystemResolvers<SystemClass>[] => {
163
- const systems = this.entitySystemMap.get(entity.constructor as Class);
164
- if (!systems) {
165
- return [];
166
- }
193
+ createSystemsForDynamicComponents(dynamicComponentClass: Class, entity: Entity) {
194
+ const potentialSystems = this.getSystemResolverTuples(entity);
195
+ const systems = this.dynamicComponentSystemMap.get(dynamicComponentClass)!;
167
196
 
168
- const result: SystemResolvers<SystemClass>[] = [];
169
- for (const systemClass of systems) {
170
- result.push([systemClass, this.createResolverArray({type: systemClass, kind: 'system', entityClass: entity.constructor})]);
197
+ const finalSystemResolverEntries = new Map<SystemClass, ResolverFunction[]>();
198
+ for (const system of systems) {
199
+ const resolverTuples = potentialSystems.get(system);
200
+ if (resolverTuples) {
201
+ finalSystemResolverEntries.set(system, resolverTuples);
171
202
  }
172
- return result;
173
- });
174
- const systemInstances: SystemContext[] = [];
175
- for (const [system, resolver] of systemResolverTuples) {
203
+ }
204
+
205
+ return this.createSystems(finalSystemResolverEntries, entity);
206
+ }
207
+
208
+ createSystemsForEntity(entity: Entity): Set<SystemContext> {
209
+ const systemResolverTuples = this.getSystemResolverTuples(entity);
210
+ return this.createSystems(systemResolverTuples, entity);
211
+ }
212
+
213
+ private createSystems(systemResolverMap: Map<SystemClass, ResolverFunction[]>, entity: Entity) {
214
+ const systemInstances = new Set<SystemContext>();
215
+ for (const [system, resolver] of systemResolverMap) {
176
216
  const args = new Array(resolver.length);
177
217
 
218
+ const dynamicDependencies = this.dynamicComponentDependencyMap.get(system) || [];
219
+ const hasUnfulfilledDynamicDependencies = dynamicDependencies.some((dependency, index) => {
220
+ return !entity.hasDynamicComponentClass(dependency.type);
221
+ });
222
+ if (hasUnfulfilledDynamicDependencies) {
223
+ continue;
224
+ }
225
+
178
226
  const systemContext = new SystemContext<InstanceType<SystemClass>>(entity);
179
227
 
180
228
  for (let i = 0; i < args.length; i++) {
@@ -187,18 +235,32 @@ export class EcsInjector<SystemClass extends Class = Class> extends Injector {
187
235
  const entitySignals = entitySignalHandlers.get(system);
188
236
  if (entitySignals?.length) {
189
237
  for (const [propertyKey, id] of entitySignals) {
190
- (entity as Entity).getSignal(id).add(newSystem[propertyKey], newSystem);
238
+ entity.getSignal(id).add(newSystem[propertyKey], newSystem);
191
239
  }
192
240
  }
193
241
 
194
242
  // friend class :)
195
243
  (systemContext as any).system = newSystem;
196
244
 
197
- systemInstances.push(systemContext);
245
+ systemInstances.add(systemContext);
198
246
  }
199
247
  return systemInstances;
200
248
  }
201
249
 
250
+ private getSystemResolverTuples(entity: Entity) {
251
+ return putIfAbsent(this.entitySystemResolverTuples, entity.constructor, (): Map<SystemClass, ResolverFunction[]> => {
252
+ const systems = this.entitySystemMap.get(entity.constructor as Class);
253
+ if (!systems) return [] as any;
254
+
255
+ // const result: SystemResolvers<SystemClass>[] = [];
256
+ const result = new Map<SystemClass, ResolverFunction[]>();
257
+ for (const systemClass of systems) {
258
+ result.set(systemClass, this.createResolverArray({type: systemClass, kind: 'system', entityClass: entity.constructor}));
259
+ }
260
+ return result;
261
+ });
262
+ }
263
+
202
264
  getSignal<T>(id: ID): Signal<T> {
203
265
  return putIfAbsent(this.signalMap, id, () => new Signal<any>());
204
266
  }
package/src/entity.ts CHANGED
@@ -1,14 +1,13 @@
1
1
  import {GameEngine} from './gameEngine';
2
2
  import {Signal} from 'mani-signal';
3
3
  import {Class} from './types';
4
- import {entityComponents} from './ecsInjector';
5
4
  import {ID, putIfAbsent} from './injector';
5
+ import {EcsInjector} from './ecsInjector';
6
6
 
7
7
  let idCounter = 0;
8
8
 
9
9
  export class Entity {
10
10
 
11
- readonly id: number;
12
11
  readonly children = new Set<Entity>();
13
12
  private gameEngine?: GameEngine;
14
13
 
@@ -20,12 +19,64 @@ export class Entity {
20
19
  private _detached = false;
21
20
  private readonly signalMap: Map<ID, Signal> = new Map<ID, Signal>();
22
21
 
23
- get detached(): boolean { return this._detached; }
22
+ private dynamicComponents = new Map<Class, InstanceType<any>>();
23
+
24
+ addDynamicComponent<T extends Class>(component: InstanceType<T>) {
25
+ if (this.hasDynamicComponentClass(component.constructor)) {
26
+ throw new Error('component of this class already added');
27
+ }
28
+ this.dynamicComponents.set(component.constructor as T, component);
29
+
30
+ if (!this.gameEngine) return;
31
+
32
+ const systemContexts = this.gameEngine.injector.createSystemsForDynamicComponents(component.constructor, this);
33
+
34
+ if (!systemContexts) return;
35
+ const preparePromises = [];
36
+ for (const context of systemContexts) {
37
+ context.system.onPrepare && preparePromises.push(context.system.onPrepare());
38
+ }
39
+ for (const context of systemContexts) {
40
+ this.gameEngine['entitySystemMap'].get(this)?.add(context);
41
+ }
42
+
43
+ if (preparePromises.length === 0) {
44
+ this.gameEngine['startSystems'](systemContexts, () => {});
45
+ } else {
46
+ Promise.all(preparePromises).then(() => this.gameEngine?.['startSystems'](systemContexts));
47
+ }
48
+
49
+ }
50
+
51
+ hasDynamicComponentClass<T extends Class>(componentClass: T): boolean {
52
+ return this.dynamicComponents.has(componentClass);
53
+ }
54
+ removeDynamicComponentClass<T extends Class>(componentClass: T) {
55
+ const systemContexts = this.gameEngine?.['entitySystemMap'].get(this);
56
+ systemContexts?.forEach(systemContext => {
57
+ const systemToDependencies = this.gameEngine?.injector['dynamicComponentDependencyMap'];
58
+ const dependencies = systemToDependencies?.get((systemContext.system as any).constructor);
59
+ if (!dependencies) return;
60
+ if (dependencies.some(dependency => dependency.type === componentClass)) {
61
+ this.gameEngine?.['disposeContext']?.(systemContext);
62
+ systemContexts?.delete(systemContext);
63
+ }
64
+ });
65
+ if (!this.dynamicComponents.delete(componentClass)) {
66
+ throw new Error('component not found: ' + JSON.stringify(componentClass));
67
+ }
68
+ }
24
69
 
25
- constructor() {
26
- this.id = idCounter++;
70
+ removeDynamicComponent<T extends Class>(component: InstanceType<T>) {
71
+ this.removeDynamicComponentClass(component.constructor as T);
27
72
  }
28
73
 
74
+ getDynamicComponent<T extends Class>(componentClass: T): InstanceType<T> | undefined {
75
+ return this.dynamicComponents.get(componentClass);
76
+ }
77
+
78
+ get detached(): boolean { return this._detached; }
79
+
29
80
  async onBeforeRemove?(): Promise<void>;
30
81
 
31
82
  getSignal<T>(id: ID): Signal<T> {
@@ -46,7 +97,7 @@ export class Entity {
46
97
  }
47
98
 
48
99
  getComponent<T extends Class>(componentClass: T): InstanceType<T> | undefined {
49
- const componentMap = entityComponents.get(this.constructor as Class);
100
+ const componentMap = EcsInjector.entityComponents.get(this.constructor as Class);
50
101
  if (!componentMap) {
51
102
  throw new Error(`No components in entity of type '${this.constructor.name}'.`);
52
103
  }
@@ -128,4 +179,8 @@ export class Entity {
128
179
  }
129
180
  return target;
130
181
  }
182
+
183
+ hasDynamicComponents() {
184
+ return this.dynamicComponents.size > 0;
185
+ }
131
186
  }
package/src/gameEngine.ts CHANGED
@@ -17,7 +17,7 @@ const defaultOptions = {
17
17
  export type GameEngineOptions = Partial<typeof defaultOptions>;
18
18
  declare global {
19
19
  interface GameEngineActions {
20
- 'testAction': string
20
+ 'testAction': string;
21
21
  }
22
22
  }
23
23
 
@@ -50,7 +50,8 @@ export class GameEngine {
50
50
  private readonly onLateUpdateSet = new Set<System>();
51
51
  private readonly onRenderUpdateSet = new Set<System>();
52
52
 
53
- private readonly entitySystemMap = new Map<Entity, SystemContext[]>();
53
+ // TODO: for performance, we could store the systems directly in the entity
54
+ private readonly entitySystemMap = new Map<Entity, Set<SystemContext>>();
54
55
 
55
56
  private readonly serviceClasses = new Set<Class<Service>>();
56
57
  private readonly services: Service[] = [];
@@ -346,19 +347,7 @@ export class GameEngine {
346
347
  this.entitySystemMap.delete(entity);
347
348
  if (systemContexts) {
348
349
  for (const context of systemContexts) {
349
- context.dispose();
350
- const system = context.system;
351
- this.unmapAnnotatedSignals(system);
352
- system.onEnd && system.onEnd();
353
- system.onPreFixedUpdate && this.onPreFixedUpdateSet.delete(system);
354
- system.onFixedUpdate && this.onFixedUpdateSet.delete(system);
355
- system.onUpdate && this.onUpdateSet.delete(system);
356
- system.onLateUpdate && this.onLateUpdateSet.delete(system);
357
- system.onPhysicsUpdate && this.onPhysicsUpdateSet.delete(system);
358
- system.onPrePhysicsUpdate && this.onPrePhysicsUpdateSet.delete(system);
359
- system.onPostPhysicsUpdate && this.onPostPhysicsUpdateSet.delete(system);
360
- system.onLateFixedUpdate && this.onLateFixedUpdateSet.delete(system);
361
- system.onRender && this.onRenderUpdateSet.delete(system);
350
+ this.disposeContext(context);
362
351
  }
363
352
  }
364
353
  entity.onRemove.dispatch();
@@ -370,45 +359,27 @@ export class GameEngine {
370
359
  this.removeQueue = [];
371
360
 
372
361
  for (const [entity, resolve] of this.addQueue) {
362
+
373
363
  const preparePromises = [];
374
- const nexSystemContexts: SystemContext[] = [];
364
+ const nextSystemContexts: SystemContext[] = [];
375
365
  const entities = entity.getAllEntities();
376
366
  for (const entity of entities) {
377
367
  if ((<any>entity).gameEngine) throw new Error('Entity already added to a gameEngine');
378
368
  (<any>entity).gameEngine = this;
379
- const systemContexts = this.injector.createSystems(entity);
369
+ const systemContexts = this.injector.createSystemsForEntity(entity);
380
370
  for (const context of systemContexts) {
381
371
  // TODO: implement synced mode where async onPrepares aren't allowed
382
372
  context.system.onPrepare && preparePromises.push(context.system.onPrepare());
383
- nexSystemContexts.push(context);
373
+ nextSystemContexts.push(context);
384
374
  }
385
375
  this.entitySystemMap.set(entity, systemContexts);
386
376
  }
387
377
 
388
- const startSystems = () => {
389
- // TODO: check if a "global" method is faster than this inner method
390
- for (const context of nexSystemContexts) {
391
- const system = context.system;
392
- this.mapAnnotatedSignals(system);
393
- system.onStart && system.onStart();
394
- system.onPreFixedUpdate && this.onPreFixedUpdateSet.add(system);
395
- system.onFixedUpdate && this.onFixedUpdateSet.add(system);
396
- system.onUpdate && this.onUpdateSet.add(system);
397
- system.onLateUpdate && this.onLateUpdateSet.add(system);
398
- system.onPhysicsUpdate && this.onPhysicsUpdateSet.add(system);
399
- system.onPrePhysicsUpdate && this.onPrePhysicsUpdateSet.add(system);
400
- system.onPostPhysicsUpdate && this.onPostPhysicsUpdateSet.add(system);
401
- system.onLateFixedUpdate && this.onLateFixedUpdateSet.add(system);
402
- system.onRender && this.onRenderUpdateSet.add(system);
403
- }
404
- resolve();
405
- };
406
-
407
378
  // Promise.all doesnt resolve immediately when there are no promises bu
408
379
  if (preparePromises.length === 0) {
409
- startSystems();
380
+ this.startSystems(nextSystemContexts, resolve);
410
381
  } else {
411
- Promise.all(preparePromises).then(startSystems);
382
+ Promise.all(preparePromises).then(() => this.startSystems(nextSystemContexts, resolve));
412
383
  }
413
384
  }
414
385
  this.addQueue = [];
@@ -419,6 +390,40 @@ export class GameEngine {
419
390
  });
420
391
  this.onPrepare.dispatch();
421
392
  }
393
+
394
+ private startSystems(contexts: Iterable<SystemContext>, then?: () => void) {
395
+ for (const context of contexts) {
396
+ const system = context.system;
397
+ this.mapAnnotatedSignals(system);
398
+ system.onStart && system.onStart();
399
+ system.onPreFixedUpdate && this.onPreFixedUpdateSet.add(system);
400
+ system.onFixedUpdate && this.onFixedUpdateSet.add(system);
401
+ system.onUpdate && this.onUpdateSet.add(system);
402
+ system.onLateUpdate && this.onLateUpdateSet.add(system);
403
+ system.onPhysicsUpdate && this.onPhysicsUpdateSet.add(system);
404
+ system.onPrePhysicsUpdate && this.onPrePhysicsUpdateSet.add(system);
405
+ system.onPostPhysicsUpdate && this.onPostPhysicsUpdateSet.add(system);
406
+ system.onLateFixedUpdate && this.onLateFixedUpdateSet.add(system);
407
+ system.onRender && this.onRenderUpdateSet.add(system);
408
+ }
409
+ then?.();
410
+ };
411
+
412
+ private disposeContext(context: SystemContext<System>) {
413
+ context.dispose();
414
+ const system = context.system;
415
+ this.unmapAnnotatedSignals(system);
416
+ system.onEnd && system.onEnd();
417
+ system.onPreFixedUpdate && this.onPreFixedUpdateSet.delete(system);
418
+ system.onFixedUpdate && this.onFixedUpdateSet.delete(system);
419
+ system.onUpdate && this.onUpdateSet.delete(system);
420
+ system.onLateUpdate && this.onLateUpdateSet.delete(system);
421
+ system.onPhysicsUpdate && this.onPhysicsUpdateSet.delete(system);
422
+ system.onPrePhysicsUpdate && this.onPrePhysicsUpdateSet.delete(system);
423
+ system.onPostPhysicsUpdate && this.onPostPhysicsUpdateSet.delete(system);
424
+ system.onLateFixedUpdate && this.onLateFixedUpdateSet.delete(system);
425
+ system.onRender && this.onRenderUpdateSet.delete(system);
426
+ }
422
427
  }
423
428
  //
424
429
  // type RunSpread<T> = T extends undefined
package/src/index.ts CHANGED
@@ -17,7 +17,7 @@ export {
17
17
  Scope, SCOPE_CONTEXT, ScopeContext, OnScopeSignal, scopeSignalHandlers, ScopeSignalOptions, ScopeMapping,
18
18
  } from './scope/scopeContext';
19
19
  export {SystemContext, OnEntitySignal} from './systemContext';
20
- export {EcsInjector, EntityComponent, GetComponent, GetEntity, GetContext, GetSignal, OnSignal, GetEntitySignal} from './ecsInjector';
20
+ export {EcsInjector, EntityComponent, GetComponent,GetDynamicComponent, GetEntity, GetContext, GetSignal, OnSignal, GetEntitySignal, } from './ecsInjector';
21
21
 
22
22
  // intellij doesnt auto resolve imports if we just export * from an external dependency
23
23
 
package/src/injector.ts CHANGED
@@ -73,6 +73,7 @@ export const createDependencyAnnotation = (cb: (type: any, index: number, depend
73
73
  throw new Error('Could not inject class in itself.');
74
74
  }
75
75
  const depList = putIfAbsent(Injector.dependencyMap, dependantType, (): Dependency[] => []);
76
+
76
77
  depList.push(cb(type, index, dependantType));
77
78
  };
78
79
 
@@ -74,6 +74,13 @@ class ScopeStack {
74
74
  export type ScopeMapping = (params: {
75
75
  injector: EcsInjector,
76
76
  registerScopeService: (serviceClass: Class) => void,
77
+ onEnter: Signal<ScopeContext>,
78
+ onExit: Signal<ScopeContext>,
79
+ onSubReturn: Signal<ScopeContext>,
80
+ onSubExit: Signal<ScopeContext>,
81
+ onActivate: Signal<ScopeContext>,
82
+ onDeactivate: Signal<ScopeContext>,
83
+
77
84
  }) => void
78
85
 
79
86
  interface AddScopeSignalOptions extends ScopeSignalOptions {
@@ -93,6 +100,13 @@ export class ScopeContext {
93
100
  private closed = false;
94
101
  private muteKeepAliveSignals = false;
95
102
 
103
+ readonly onEnter = new Signal<ScopeContext>();
104
+ readonly onExit = new Signal<ScopeContext>();
105
+ readonly onSubReturn = new Signal<ScopeContext>();
106
+ readonly onSubExit = new Signal<ScopeContext>();
107
+ readonly onActivate = new Signal<ScopeContext>();
108
+ readonly onDeactivate = new Signal<ScopeContext>();
109
+
96
110
  constructor(injector: EcsInjector, scopeClass: Class, mapping?: ScopeMapping, private parent?: ScopeContext) {
97
111
  if (!parent) {
98
112
  this.stack = new ScopeStack(this);
@@ -115,6 +129,12 @@ export class ScopeContext {
115
129
  this.injector.map(serviceClass).toSingleton();
116
130
  this.mapServiceSignals(this.injector.get(serviceClass));
117
131
  },
132
+ onEnter: this.onEnter,
133
+ onExit: this.onExit,
134
+ onSubReturn: this.onSubReturn,
135
+ onSubExit: this.onSubExit,
136
+ onActivate: this.onActivate,
137
+ onDeactivate: this.onDeactivate,
118
138
  });
119
139
 
120
140
  this.scope = this.injector.get(scopeClass) as Scope;
@@ -122,7 +142,9 @@ export class ScopeContext {
122
142
  this.updateSignalBindings();
123
143
  // 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)
124
144
  this.scope.onEnter?.();
145
+ this.onEnter.dispatch(this);
125
146
  this.scope.onActivate?.();
147
+ this.onActivate.dispatch(this);
126
148
  }
127
149
 
128
150
  get isRoot() {
@@ -150,7 +172,9 @@ export class ScopeContext {
150
172
  const doChange = () => {
151
173
 
152
174
  this.activeContext.scope.onSubExit?.();
175
+ this.activeContext.onSubExit.dispatch(this);
153
176
  this.activeContext.scope.onDeactivate?.();
177
+ this.activeContext.onDeactivate.dispatch(this);
154
178
 
155
179
  newContext = new ScopeContext(this.activeContext.injector, scopeClass, mapping, this.activeContext);
156
180
 
@@ -162,7 +186,7 @@ export class ScopeContext {
162
186
  this.stack.ongoingChange = false;
163
187
  }
164
188
  };
165
- // TODO: this queued changes arent somewhat experimental
189
+ // TODO: this queued changes are somewhat experimental
166
190
  if (!this.stack.ongoingChange) {
167
191
  this.stack.ongoingChange = true;
168
192
  doChange();
@@ -313,10 +337,23 @@ export class ScopeContext {
313
337
  this.detachSignalBindings();
314
338
  this.detachServiceBindings();
315
339
  this.scope.onDeactivate?.();
340
+ this.onDeactivate.dispatch(this);
341
+
316
342
  this.scope.onExit?.();
343
+ this.onExit.dispatch(this);
317
344
  this.parent.updateSignalBindings();
318
345
  this.parent.scope.onSubReturn?.();
346
+ this.onSubReturn.dispatch(this);
319
347
  this.parent.scope.onActivate?.();
348
+ this.parent.onActivate.dispatch(this);
320
349
  this.closed = true;
350
+
351
+ this.onEnter.detachAll();
352
+ this.onExit.detachAll();
353
+ this.onSubReturn.detachAll();
354
+ this.onSubExit.detachAll();
355
+ this.onActivate.detachAll();
356
+ this.onDeactivate.detachAll();
357
+
321
358
  }
322
359
  }
@@ -1,5 +1,5 @@
1
1
  import {System} from './types';
2
- import {GameEngine, ID, putIfAbsent, Signal, SignalBinding, SignalCallback} from './index';
2
+ import {Entity, GameEngine, ID, putIfAbsent, Signal, SignalBinding, SignalCallback} from './index';
3
3
 
4
4
  export const entitySignalHandlers = new Map<Object, [string, ID][]>();
5
5
  export const OnEntitySignal = (id: ID) => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
@@ -15,7 +15,7 @@ export class SystemContext<T extends System = System> {
15
15
  readonly system!: T;
16
16
  private signalBindings: SignalBinding[] = [];
17
17
 
18
- constructor(readonly entity: Object) {
18
+ constructor(readonly entity: Entity) {
19
19
  }
20
20
 
21
21
  // TODO: is this a bit hacky?
@@ -33,5 +33,6 @@ export class SystemContext<T extends System = System> {
33
33
 
34
34
  dispose() {
35
35
  for (const binding of this.signalBindings) binding.detach();
36
+
36
37
  }
37
38
  }
package/src/types.ts CHANGED
@@ -14,6 +14,7 @@ export interface System {
14
14
  onPostPhysicsUpdate?(time: number, deltaTime: number): void;
15
15
  onLateFixedUpdate?(time: number, deltaTime: number): void;
16
16
  onRender?(time: number, deltaTime: number, alpha: number): void;
17
+ // new(...args: any[]): System;
17
18
  // new(): GameSystem;
18
19
  }
19
20