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.
- package/lib/clock.d.ts +2 -2
- package/lib/ecsInjector.d.ts +25 -7
- package/lib/ecsInjector.js +75 -26
- package/lib/ecsInjector.js.map +1 -1
- package/lib/entity.d.ts +7 -2
- package/lib/entity.js +58 -3
- package/lib/entity.js.map +1 -1
- package/lib/gameEngine.d.ts +3 -1
- package/lib/gameEngine.js +43 -39
- package/lib/gameEngine.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +2 -1
- package/lib/index.js.map +1 -1
- package/lib/injector.d.ts +20 -20
- package/lib/injector.js.map +1 -1
- package/lib/scope/scopeContext.d.ts +15 -3
- package/lib/scope/scopeContext.js +27 -0
- package/lib/scope/scopeContext.js.map +1 -1
- package/lib/systemContext.d.ts +3 -3
- package/lib/systemContext.js.map +1 -1
- package/lib/types.d.ts +7 -7
- package/lib/types.js.map +1 -1
- package/package.json +10 -10
- package/src/ecsInjector.ts +92 -30
- package/src/entity.ts +61 -6
- package/src/gameEngine.ts +44 -39
- package/src/index.ts +1 -1
- package/src/injector.ts +1 -0
- package/src/scope/scopeContext.ts +38 -1
- package/src/systemContext.ts +3 -2
- package/src/types.ts +1 -0
package/src/ecsInjector.ts
CHANGED
|
@@ -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
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
162
|
-
const
|
|
163
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/src/systemContext.ts
CHANGED
|
@@ -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:
|
|
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
|
|