mani-game-engine 1.0.0-pre.9 → 1.0.0-pre.91
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/LICENSE +1 -1
- package/lib/clock.d.ts +26 -3
- package/lib/clock.js +94 -20
- package/lib/clock.js.map +1 -1
- package/lib/ecsInjector.d.ts +31 -12
- package/lib/ecsInjector.js +125 -46
- package/lib/ecsInjector.js.map +1 -1
- package/lib/entity.d.ts +23 -4
- package/lib/entity.js +131 -8
- package/lib/entity.js.map +1 -1
- package/lib/gameEngine.d.ts +29 -6
- package/lib/gameEngine.js +253 -78
- package/lib/gameEngine.js.map +1 -1
- package/lib/index.d.ts +7 -7
- package/lib/index.js +35 -10
- package/lib/index.js.map +1 -1
- package/lib/injector.d.ts +125 -0
- package/lib/injector.js +305 -0
- package/lib/injector.js.map +1 -0
- package/lib/scope/scopeContext.d.ts +34 -11
- package/lib/scope/scopeContext.js +97 -31
- package/lib/scope/scopeContext.js.map +1 -1
- package/lib/systemContext.d.ts +12 -5
- package/lib/systemContext.js +43 -6
- package/lib/systemContext.js.map +1 -1
- package/lib/types.d.ts +26 -16
- package/lib/types.js +12 -3
- package/lib/types.js.map +1 -1
- package/lib/utils/map2k.js +5 -1
- package/lib/utils/map2k.js.map +1 -1
- package/package.json +12 -15
- package/src/clock.ts +183 -91
- package/src/ecsInjector.ts +130 -36
- package/src/entity.ts +219 -83
- package/src/gameEngine.ts +555 -361
- package/src/index.ts +22 -7
- package/src/injector.ts +410 -0
- package/src/scope/scopeContext.ts +122 -35
- package/src/systemContext.ts +50 -14
- package/src/types.ts +20 -10
package/src/entity.ts
CHANGED
|
@@ -1,116 +1,252 @@
|
|
|
1
1
|
import {GameEngine} from './gameEngine';
|
|
2
2
|
import {Signal} from 'mani-signal';
|
|
3
3
|
import {Class} from './types';
|
|
4
|
-
import {
|
|
4
|
+
import {ID, putIfAbsent} from './injector';
|
|
5
|
+
import {EcsInjector} from './ecsInjector';
|
|
5
6
|
|
|
6
7
|
let idCounter = 0;
|
|
7
8
|
|
|
8
9
|
export class Entity {
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
readonly children = new Set<Entity>();
|
|
12
|
+
readonly gameEngine?: GameEngine;
|
|
13
|
+
readonly onAdd = new Signal();
|
|
14
|
+
|
|
15
|
+
get isAdded() {
|
|
16
|
+
return !!this.gameEngine;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// TODO: use single rootEntity class instead of undefined?
|
|
20
|
+
private _parent?: Entity;
|
|
21
|
+
readonly parentChanged = new Signal();
|
|
22
|
+
readonly onRemove = new Signal();
|
|
23
|
+
private _detached = false;
|
|
24
|
+
readonly _signalMap: Map<ID, Signal> = new Map<ID, Signal>();
|
|
25
|
+
|
|
26
|
+
// this is filled by the injector
|
|
27
|
+
private _components?: Map<Class, InstanceType<any>>;
|
|
28
|
+
get components(): ReadonlyMap<Class, InstanceType<any>> {
|
|
29
|
+
if (!this._components) {
|
|
30
|
+
this._components = new Map<Class, InstanceType<any>>();
|
|
31
|
+
EcsInjector.entityComponents.get(this.constructor as Class)!.forEach((field, componentClass) =>
|
|
32
|
+
this._components!.set(componentClass, (this as any)[field]));
|
|
33
|
+
}
|
|
34
|
+
return this._components;
|
|
35
|
+
}
|
|
13
36
|
|
|
14
|
-
|
|
15
|
-
private _parent?: Entity;
|
|
16
|
-
readonly parentChanged = new Signal();
|
|
17
|
-
readonly onRemove = new Signal();
|
|
18
|
-
private markRemoval = false;
|
|
37
|
+
private _dynamicComponents = new Map<Class, InstanceType<any>>();
|
|
19
38
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
39
|
+
readonly dynamicComponentAdded = new Signal<InstanceType<any>>();
|
|
40
|
+
readonly dynamicComponentRemoved = new Signal<InstanceType<any>>();
|
|
23
41
|
|
|
24
|
-
|
|
42
|
+
get dynamicComponents(): ReadonlyMap<Class, InstanceType<any>> {
|
|
43
|
+
return this._dynamicComponents;
|
|
44
|
+
}
|
|
25
45
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
46
|
+
// TODO: move the logic to gameEngine?
|
|
47
|
+
addDynamicComponent<T extends Class>(component: InstanceType<T>): this {
|
|
48
|
+
if (this.hasDynamicComponentClass(component.constructor)) {
|
|
49
|
+
throw new Error('component of this class already added');
|
|
50
|
+
}
|
|
51
|
+
if (!this.gameEngine) {
|
|
52
|
+
this._dynamicComponents.set(component.constructor as T, component);
|
|
53
|
+
this.dynamicComponentAdded.dispatch(component);
|
|
54
|
+
return this;
|
|
33
55
|
}
|
|
34
56
|
|
|
35
|
-
|
|
36
|
-
|
|
57
|
+
this.gameEngine.maybeDefer(() => {
|
|
58
|
+
this._dynamicComponents.set(component.constructor as T, component);
|
|
59
|
+
this.dynamicComponentAdded.dispatch(component);
|
|
60
|
+
// it could be that we are within the system creation loop (component is added within a constructor of a system)
|
|
61
|
+
const systemContexts = this.gameEngine!.injector.createSystemsForDynamicComponents(component.constructor, this);
|
|
62
|
+
|
|
63
|
+
if (!systemContexts) return this;
|
|
64
|
+
const preparePromises = [];
|
|
65
|
+
for (const context of systemContexts) {
|
|
66
|
+
context.system.onPrepare && preparePromises.push(context.system.onPrepare());
|
|
67
|
+
}
|
|
68
|
+
for (const context of systemContexts) {
|
|
69
|
+
this.gameEngine!['entitySystemMap'].get(this)?.add(context);
|
|
70
|
+
// (this.gameEngine as any).addSystemContextSetToMap(this, systemContexts);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (preparePromises.length === 0) {
|
|
74
|
+
this.gameEngine => {});
|
|
75
|
+
} else {
|
|
76
|
+
Promise.all(preparePromises).then(() => this.gameEngine?.['startSystems'](systemContexts));
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
hasDynamicComponentClass<T extends Class>(componentClass: T): boolean {
|
|
83
|
+
return this._dynamicComponents.has(componentClass);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// TODO: move the logic to gameEngine?
|
|
87
|
+
removeDynamicComponentClass<T extends Class>(componentClass: T) {
|
|
88
|
+
if (!this.gameEngine) {
|
|
89
|
+
if (!this._dynamicComponents.has(componentClass)) {
|
|
90
|
+
throw new Error('component not found: ' + JSON.stringify(componentClass));
|
|
91
|
+
}
|
|
92
|
+
this._dynamicComponents.delete(componentClass);
|
|
93
|
+
this.dynamicComponentRemoved.dispatch(this._dynamicComponents.get(componentClass)!);
|
|
94
|
+
return;
|
|
37
95
|
}
|
|
38
96
|
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
97
|
+
this.gameEngine.maybeDefer(() => {
|
|
98
|
+
const systemContexts = this.gameEngine?.['entitySystemMap'].get(this);
|
|
99
|
+
systemContexts?.forEach(systemContext => {
|
|
100
|
+
const systemToDependencies = this.gameEngine?.injector['dynamicComponentDependencyMap'];
|
|
101
|
+
const dependencies = systemToDependencies?.get((systemContext.system as any).constructor);
|
|
102
|
+
if (!dependencies) return;
|
|
103
|
+
if (dependencies.some(dependency => dependency.type === componentClass)) {
|
|
104
|
+
this.gameEngine?.['disposeContext']?.(systemContext);
|
|
105
|
+
systemContexts?.delete(systemContext);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
const component = this._dynamicComponents.get(componentClass);
|
|
109
|
+
if (!this._dynamicComponents.delete(componentClass)) {
|
|
110
|
+
throw new Error('component not found: ' + JSON.stringify(componentClass));
|
|
43
111
|
}
|
|
44
|
-
|
|
45
|
-
|
|
112
|
+
this.dynamicComponentRemoved.dispatch(component);
|
|
113
|
+
},
|
|
114
|
+
);}
|
|
115
|
+
|
|
116
|
+
removeDynamicComponent<T extends Class>(component: InstanceType<T>) {
|
|
117
|
+
this.removeDynamicComponentClass(component.constructor as T);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
getDynamicComponent<T extends Class>(componentClass: T): InstanceType<T> | undefined {
|
|
121
|
+
return this._dynamicComponents.get(componentClass);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
getDynamicComponentOrThrow<T extends Class>(componentClass: T): InstanceType<T> {
|
|
125
|
+
const component = this._dynamicComponents.get(componentClass);
|
|
126
|
+
if (!component) {
|
|
127
|
+
throw new Error(`DynamicComponent '${componentClass.name}' not found in entity of type '${this.constructor.name}'.`);
|
|
46
128
|
}
|
|
129
|
+
return component;
|
|
130
|
+
}
|
|
47
131
|
|
|
48
|
-
|
|
49
|
-
if (entity.parent === this) {
|
|
50
|
-
throw new Error('Entity is already a child');
|
|
51
|
-
}
|
|
52
|
-
if (entity === this) {
|
|
53
|
-
throw new Error('Could not add Entity as a child of itself.');
|
|
54
|
-
}
|
|
132
|
+
get detached(): boolean { return this._detached; }
|
|
55
133
|
|
|
56
|
-
|
|
57
|
-
entity.parent.children.delete(entity);
|
|
58
|
-
}
|
|
59
|
-
this.children.add(entity);
|
|
60
|
-
entity.setParent(this);
|
|
61
|
-
|
|
62
|
-
if (entity.gameEngine) {
|
|
63
|
-
if (this.gameEngine !== entity.gameEngine) {
|
|
64
|
-
throw new Error('cant add an active to a passive entity');
|
|
65
|
-
}
|
|
66
|
-
} else {
|
|
67
|
-
if (this.gameEngine) {
|
|
68
|
-
|
|
69
|
-
await this.gameEngine.add(entity);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
134
|
+
async onBeforeRemove?(): Promise<void>;
|
|
72
135
|
|
|
73
|
-
|
|
136
|
+
getSignal<T>(id: ID): Signal<T> {
|
|
137
|
+
return putIfAbsent(this._signalMap, id, () => new Signal<any>());
|
|
138
|
+
}
|
|
74
139
|
|
|
75
|
-
|
|
140
|
+
private setParent(parent: Entity | undefined) {
|
|
141
|
+
if (parent === this._parent) {
|
|
142
|
+
//TODO: i think this check can be removed
|
|
143
|
+
throw Error('same parent');
|
|
144
|
+
}
|
|
145
|
+
this._parent = parent;
|
|
146
|
+
this.parentChanged.dispatch();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
get parent(): Entity | undefined {
|
|
150
|
+
return this._parent;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
getComponent<T extends Class>(componentClass: T): InstanceType<T> | undefined {
|
|
154
|
+
const componentMap = EcsInjector.entityComponents.get(this.constructor as Class);
|
|
155
|
+
if (!componentMap) {
|
|
156
|
+
throw new Error(`No components in entity of type '${this.constructor.name}'.`);
|
|
157
|
+
}
|
|
158
|
+
const key = componentMap.get(componentClass as T);
|
|
159
|
+
return key && (<any>this)[key];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
getComponentOrThrow<T extends Class>(componentClass: T): InstanceType<T> {
|
|
163
|
+
const componentMap = EcsInjector.entityComponents.get(this.constructor as Class);
|
|
164
|
+
if (!componentMap) {
|
|
165
|
+
throw new Error(`No components in entity of type '${this.constructor.name}'.`);
|
|
166
|
+
}
|
|
167
|
+
const key = componentMap.get(componentClass as T);
|
|
168
|
+
if (!key) {
|
|
169
|
+
throw new Error(`Component '${componentClass.name}' not found in entity of type '${this.constructor.name}'.`);
|
|
170
|
+
}
|
|
171
|
+
return (<any>this)[key];
|
|
172
|
+
}
|
|
76
173
|
|
|
77
|
-
|
|
174
|
+
async add(entity: Entity) {
|
|
175
|
+
if (entity.parent === this) {
|
|
176
|
+
throw new Error('Entity is already a child');
|
|
177
|
+
}
|
|
178
|
+
if (entity === this) {
|
|
179
|
+
throw new Error('Could not add Entity as a child of itself.');
|
|
78
180
|
}
|
|
79
181
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
182
|
+
if (entity.parent) {
|
|
183
|
+
entity.parent.children.delete(entity);
|
|
184
|
+
}
|
|
185
|
+
this.children.add(entity);
|
|
186
|
+
entity.setParent(this);
|
|
187
|
+
|
|
188
|
+
if (entity.gameEngine) {
|
|
189
|
+
if (this.gameEngine !== entity.gameEngine) {
|
|
190
|
+
throw new Error('cant add an active to a passive entity');
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
if (this.gameEngine) {
|
|
194
|
+
|
|
195
|
+
await this.gameEngine.add(entity);
|
|
196
|
+
}
|
|
88
197
|
}
|
|
89
198
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (!this.gameEngine) {
|
|
95
|
-
throw new Error('entity isn\'t attached to a parent or the game engine');
|
|
96
|
-
}
|
|
97
|
-
await this.gameEngine.remove(this);
|
|
98
|
-
}
|
|
199
|
+
// TODO: systems should be informed so they can do stuff (renderer system should add object 3d etc...)
|
|
200
|
+
|
|
201
|
+
if (this.gameEngine && this.gameEngine !== entity.gameEngine) {
|
|
202
|
+
|
|
99
203
|
}
|
|
204
|
+
}
|
|
100
205
|
|
|
101
|
-
|
|
102
|
-
|
|
206
|
+
async remove(entity: Entity) {
|
|
207
|
+
if (!this.children.delete(entity)) {
|
|
208
|
+
throw new Error('Could not remove. Entity is not a child.');
|
|
209
|
+
}
|
|
210
|
+
entity.setParent(undefined);
|
|
211
|
+
if (this.gameEngine) {
|
|
212
|
+
return await this.gameEngine.remove(entity);
|
|
103
213
|
}
|
|
214
|
+
entity._signalMap.forEach(signal => signal.detachAll());
|
|
215
|
+
}
|
|
104
216
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
*/
|
|
109
|
-
getAllEntities(target: Entity[] = []) {
|
|
110
|
-
target.push(this);
|
|
111
|
-
for (const child of this.children) {
|
|
112
|
-
child.getAllEntities(target);
|
|
113
|
-
}
|
|
114
|
-
return target;
|
|
217
|
+
async detach() {
|
|
218
|
+
if (this._detached) {
|
|
219
|
+
throw new Error('entity already detached');
|
|
115
220
|
}
|
|
221
|
+
this._detached = true;
|
|
222
|
+
// TODO: mark for detach and prevent another call, because this.gameEngine is set to undefined until next frame
|
|
223
|
+
if (this.parent) {
|
|
224
|
+
await this.parent.remove(this);
|
|
225
|
+
} else {
|
|
226
|
+
if (!this.gameEngine) {
|
|
227
|
+
throw new Error('entity isn\'t attached to a parent or the game engine');
|
|
228
|
+
}
|
|
229
|
+
await this.gameEngine.remove(this);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
has(entity: Entity) {
|
|
234
|
+
return this.children.has(entity);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* this includes the entity itself
|
|
239
|
+
* @param target
|
|
240
|
+
*/
|
|
241
|
+
getAllEntities(target: Entity[] = []) {
|
|
242
|
+
target.push(this);
|
|
243
|
+
for (const child of this.children) {
|
|
244
|
+
child.getAllEntities(target);
|
|
245
|
+
}
|
|
246
|
+
return target;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
hasDynamicComponents() {
|
|
250
|
+
return this._dynamicComponents.size > 0;
|
|
251
|
+
}
|
|
116
252
|
}
|