@perplexdotgg/mecs 0.1.0 → 0.2.0

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/README.md CHANGED
@@ -5,25 +5,32 @@
5
5
 
6
6
  MECS is an [ECS](https://en.wikipedia.org/wiki/Entity_component_system) library for Typescript and Javascript games and simulations, built around [Monomorph](http://codeberg.org/perplexdotgg/monomorph), a library for performant objects. The API is inspired by [Miniplex](https://github.com/hmans/miniplex), though the implementation is quite different.
7
7
 
8
- MECS focuses on the developer's experience in their IDE, especially around auto-complete and compile-type type-checking, while maintaining very high performance, about as high as you can achieve with an [AoS design](https://en.wikipedia.org/wiki/AoS_and_SoA).
8
+ Although using Monomorph classes as components gives you some extra features, it is not a requirement or dependency of MECS. You can use primitives (number, boolean, etc) or your own classes, and customize their lifecycle with component config objects. See the "[Non-Monomorph Components](#non-monomorph-components)" and "[Customizing Components](#customizing-components)" sections below for more details.
9
+
10
+ MECS focuses on the developer's experience in their IDE, especially around auto-complete and compile-time type-checking, while maintaining very high performance, about as high as you can achieve with an [AoS design](https://en.wikipedia.org/wiki/AoS_and_SoA).
9
11
 
10
12
  <details>
11
- <summary>Why not use SoA design?</summary>
12
- While SoA can achieve better performance in the best cases, it tends to be unwieldy, especially for nested fields, and generally ECS implementations lack auto-complete for components and properties.
13
+ <summary>Side note: Why not use SoA design?</summary>
14
+ While SoA can achieve better performance in the best cases, it tends to be unwieldy, especially for nested fields, and generally ECS implementations lack auto-complete for components and properties. For larger game projecting, remembering all component names and their properties is quite difficult. Using JS getters with SoA (to achieve auto complete) gives much worse overall performance than AoS.
13
15
  </details>
14
16
 
17
+ ---
18
+
15
19
  ## Features
16
- - Entities and monomorph-based components automatically gain **object-pooling** features, eliminating most garbage collection through automatic object re-use. Just create and destroy entities and components, and the pooling and re-use is taken care of.
17
- - Simple and minimal: There are only entities, components and queries
20
+ - Automatically generates highly optimized JS class code for your specific entity schema, at start up
21
+ - Entities and monomorph-based components automatically gain **object-pooling** features, eliminating most garbage collection ("Major GC") through automatic object re-use. Just create and destroy entities and components, and the pooling and re-use is taken care of.
22
+ - Simple and minimal API: There are only entities, components and queries
18
23
  - Queries, aka archetypes, automatically match and track entities based on which components they have, or don't have
19
24
  - Strong compile-time type safety if you use Typescript (recommended)
20
25
  - Coming soon: automatic serialization/deserialization (to/from number arrays) of entities (fully or a subset of components) to create snapshots
21
26
 
22
- MECS creates a single entity pool, or world, when it creates your entity class from the schema you provide.
27
+ For convenience, MECS automatically puts entities in a central pool by default, so they can be re-used. However you can also create multiple pools for an entity class, or have multiple entity classes with different schemas and pools.
23
28
 
24
- MECS has no built-in concept of "systems" in the ECS sense, instead it provides queries to help you quickly write efficient code that processes specific archetypes of entities. Your systems can just be functions that iterate over entities matching a query, and your systems can also subscribe to entities newly matching, or no longer matching, a query
29
+ MECS has no built-in concept of "systems" in the ECS sense, instead it provides queries to help you quickly write efficient code that processes specific archetypes of entities. Your systems can just be functions that iterate over entities matching a query, and your systems can also subscribe to entities newly matching, or no longer matching, a query.
25
30
 
26
- MECS has been created primarily around having a single entity schema/pool where all entities _can_ have all possible components types, but most entities only have a subset of them. If all your entities have all the same components, it is likely simpler to use [Monomorph](http://codeberg.org/perplexdotgg/monomorph) on its own.
31
+ ### When to use MECS over Monomorph alone
32
+
33
+ MECS has been created primarily around having a single entity schema/pool, where most entities only have a subset of the possible components. For example, some but not all of your entities might be visible, they may or may not have physics properties, some might be controlled by input, and so on. If all your entities have all the same components, it may be simpler to use [Monomorph](http://codeberg.org/perplexdotgg/monomorph) on its own.
27
34
 
28
35
  ## Usage
29
36
 
@@ -36,20 +43,17 @@ npm install @perplexdotgg/mecs
36
43
 
37
44
  ### Defining an Entity schema to create your Entity class
38
45
 
39
- The following is Typescript code. The Javascript version is below this.
46
+ The following is JavaScript code. The Typescript version is below this.
40
47
 
41
- ```ts
48
+ ```js
42
49
  import { createClass } from 'monomorph';
43
- import { createEntityClass, type QueryMap, type VirtualComponent } from '@perplexdotgg/mecs';
50
+ import { createEntityClass } from '@perplexdotgg/mecs';
44
51
 
45
52
  // imagine some monomorph classes in your project
46
53
  // see the monomorph docs for more on those:
47
54
  // https://codeberg.org/perplexdotgg/monomorph
48
- class Projectile extends createClass<Projectile, typeof projectileProps>(projectileProps) {}
49
- class Transform extends createClass<Transform, typeof transformProps>(transformProps) {}
50
-
51
- // and some non-monomorph class
52
- class MyClass {}
55
+ class Projectile extends createClass(projectileProps) {}
56
+ class Transform extends createClass(transformProps) {}
53
57
 
54
58
  // define all the components you want entities to potentially have
55
59
  const components = {
@@ -57,20 +61,15 @@ const components = {
57
61
  localTransform: Transform,
58
62
  globalTransform: Transform,
59
63
  Projectile, // this is shorthand for: projectile: Projectile
64
+ };
60
65
 
61
- // for non-monomorph classes, pass a key with value null.
62
- // this syntax allows for the correct types to be expected and inferred.
63
- // you are responsible for creating and managing
64
- // this field, including any needed clean-up before it is removed
65
- myClass: null! as VirtualComponent<MyClass>,
66
- } as const;
67
-
66
+ // define queries you want to do. these always stay up-to-date, efficiently
68
67
  const queries = {
69
68
  projectiles: {
70
69
  // an entity must have all of these components to match this query
71
70
  with: ['globalTransform', 'projectile'],
72
71
  // AND it can have none of these components to match this query
73
- without: ['myClass'],
72
+ without: ['localTransform'],
74
73
 
75
74
  // listeners can added now, or later (see the Queries docs below)
76
75
  afterEntityAdded: (entity) => {
@@ -79,22 +78,24 @@ const queries = {
79
78
  beforeEntityRemoved: (entity) => {
80
79
  // this is called when an entity no longer matches this query,
81
80
  // just before a relevant component is removed from the entity
81
+ // and/or before an entity is destroyed
82
82
  },
83
83
  },
84
84
  query2: {
85
85
  with: ['localTransform', 'globalTransform'],
86
86
  without: [],
87
87
  },
88
- } as const satisfies QueryMap<Entity, typeof components>;
88
+ };
89
89
 
90
- let debugLogging: boolean | undefined = false;
90
+ // additional Entity class options, see "Entity Class Options" below
91
+ const extraOptions = {};
91
92
 
92
- // automatically create our entity class from the schemas above
93
+ // automatically create our entity class from the schemas above.
93
94
  // note that there are two function calls after createEntityClass
94
- // to circumvent a typescript limitation, see https://github.com/microsoft/TypeScript/issues/10571
95
- class Entity extends createEntityClass<Entity>(debugLogging)(components, queries) {
95
+ // to circumvent a typescript limitation (sorry, vanilla JS folks!)
96
+ class Entity extends createEntityClass(extraOptions)(components, queries) {
96
97
 
97
- // most methods will likely exist on your components directly,
98
+ // most methods will likely exist on your components rather than your entities,
98
99
  // but you can also extend the generated entity class
99
100
  myEntityMethod() {
100
101
  // your code here! use 'this' like you would expect
@@ -106,20 +107,18 @@ class Entity extends createEntityClass<Entity>(debugLogging)(components, queries
106
107
  ```
107
108
 
108
109
  <details>
109
- <summary>Javascript version</summary>
110
+ <summary>Typescript version</summary>
110
111
 
111
- ```js
112
+ ```ts
112
113
  import { createClass } from 'monomorph';
113
- import { createEntityClass } from '@perplexdotgg/mecs';
114
+ import { createEntityClass, type QueryMap, type ComponentMap, type ComponentConfig } from '@perplexdotgg/mecs';
114
115
 
115
- // imagine some monomorph classes in your project
116
- // see the monomorph docs for more on those:
116
+ // imagine some monomorph classes in your project, see the monomorph docs for more on those:
117
117
  // https://codeberg.org/perplexdotgg/monomorph
118
- class Projectile extends createClass(projectileProps) {}
119
- class Transform extends createClass(transformProps) {}
118
+ class Projectile extends createClass<Projectile, typeof projectileProps>(projectileProps) {}
119
+ class Transform extends createClass<Transform, typeof transformProps>(transformProps) {}
120
120
 
121
- // and some non-monomorph class
122
- class MyClass {}
121
+ // non-monomorph components are also supported, see "Non-Monomorph Components" below
123
122
 
124
123
  // define all the components you want entities to potentially have
125
124
  const components = {
@@ -127,19 +126,15 @@ const components = {
127
126
  localTransform: Transform,
128
127
  globalTransform: Transform,
129
128
  Projectile, // this is shorthand for: projectile: Projectile
129
+ } as const satisfies ComponentMap;
130
130
 
131
- // for non-monomorph classes, pass a key with value null.
132
- // you are responsible for creating and managing
133
- // this field, including any needed clean-up before it is removed
134
- myClass: null,
135
- };
136
-
131
+ // define queries you want to do. these always stay up-to-date, efficiently
137
132
  const queries = {
138
133
  projectiles: {
139
134
  // an entity must have all of these components to match this query
140
135
  with: ['globalTransform', 'projectile'],
141
136
  // AND it can have none of these components to match this query
142
- without: ['myClass'],
137
+ without: ['localTransform'],
143
138
 
144
139
  // listeners can added now, or later (see the Queries docs below)
145
140
  afterEntityAdded: (entity) => {
@@ -154,16 +149,17 @@ const queries = {
154
149
  with: ['localTransform', 'globalTransform'],
155
150
  without: [],
156
151
  },
157
- };
152
+ } as const satisfies QueryMap<Entity, typeof components>;
158
153
 
159
- let debugLogging = false;
154
+ // additional Entity class options, see "Entity Class Options" below
155
+ const extraOptions = {};
160
156
 
161
- // automatically create our entity class from the schemas above
157
+ // automatically create our entity class from the schemas above.
162
158
  // note that there are two function calls after createEntityClass
163
- // to circumvent a typescript limitation (sorry, vanilla JS folks!)
164
- class Entity extends createEntityClass(debugLogging)(components, queries) {
159
+ // to circumvent a typescript limitation ([TS issue 10571](https://github.com/microsoft/TypeScript/issues/10571))
160
+ class Entity extends createEntityClass<Entity>(extraOptions)(components, queries) {
165
161
 
166
- // most methods will likely exist on your components directly,
162
+ // most methods will likely exist on your components rather than your entities,
167
163
  // but you can also extend the generated entity class
168
164
  myEntityMethod() {
169
165
  // your code here! use 'this' like you would expect
@@ -180,14 +176,14 @@ Like monomorph's create method, entities accept the same object representations
180
176
 
181
177
  ```ts
182
178
  const entity1 = Entity.create({
183
- localTransform: {
184
- position: { x: 0, y: 0, z: 0, },
185
- orientation: { x: 0, y: 0, z: 0, w: 1 },
179
+ globalTransform: {
180
+ position: [0, 0, 0],
181
+ orientation: [0, 0, 0],
186
182
  scale: 1,
187
183
  },
188
184
  projectile: {
189
185
  speed: 4,
190
- direction: { x: 1, y: 0, z: 0 },
186
+ direction: [1, 0, 0],
191
187
  },
192
188
  });
193
189
 
@@ -200,9 +196,6 @@ const entity2 = Entity.create({
200
196
  speed: 4,
201
197
  direction: { x: 1, y: 0, z: 0 },
202
198
  },
203
-
204
- // you are responsible for instantiating your custom classes
205
- myClass: new MyClass(),
206
199
  });
207
200
 
208
201
  // destroy() to return the entity to the pool, it will automatically be re-used later
@@ -210,11 +203,105 @@ const entity2 = Entity.create({
210
203
  // before the component(s) needed for the query are deleted
211
204
  // each destroyed component also gets recycled in its own component pool
212
205
  entity2.destroy();
206
+ ```
207
+
208
+ ### Entity Class Options
209
+ When creating your entity class, you can pass as an optional parameter to `createEntityClass`, an object with the following properties:
210
+ | Option | Type | Default | Description |
211
+ | --- | --- | --- | --- |
212
+ | `skipSafetyChecks` | `boolean` | `false` | Set to `true` to skip safety checks when adding/removing components, improving performance. It is recommended to keep this `false` during development and `true` in production builds. |
213
+ | `logCode` | `boolean` | `false` | Set to `true` to `console.log` the code that was automatically generated for the base Entity class. |
214
+ | `logComponents` | `boolean` | `false` | Set to `true` to `console.log` every time a component is added or removed from an entity. (note: this affects the generated Entity code) |
215
+ | `logQueries` | `boolean` | `false` | Set to `true` to `console.log` every time an entity is added or removed from a query. (note: this affects the generated Entity code) |
216
+
217
+
218
+ ### Non-Monomorph Components
219
+ For components that are not monomorph classes, such as your own classes or primitives like numbers and booleans,
220
+ you can quickly define them with a default value (such as `null`) as the component config. You are then responsible for creating and managing these components, including any necessary cleanup before they are removed. Below is a simple example; see the "[Customizing Components](#customizing-components)" section below for more advanced options.
221
+
222
+ This is the Javascript versiom, the Typescript version is below.
223
+
224
+ ```js
225
+ // some non-monomorph class
226
+ class MyClass {}
227
+
228
+ const components = {
229
+ // allows entity.addComponent('isVisible') without specifying the second parameter, in this case
230
+ // entity.isVisible will be true. this is how you can use components as simple flags
231
+ isVisible: true,
232
+
233
+ // for non-monomorph classes, you can use a key with value null, or a default value of your choice.
234
+ // you are responsible for creating and managing this field, including any needed clean-up
235
+ // before it is removed. the `null` here is the default value that will be used when
236
+ // entity.addComponent('myClass') is called without a second parameter, and it is also the value set on
237
+ // the entity when the component is removed
238
+ myClass: null,
239
+
240
+ // you have much more control over the component's lifecycle, by passing a component config object,
241
+ // see the "Customizing Components" section below for details
242
+ myCustomComponent: {
243
+ // component config options here, see "Customizing Components" below
244
+ },
245
+
246
+ } as const satisfies ComponentMap;
247
+
248
+ const queries = {};
249
+
250
+ class Entity extends createEntityClass()(components, queries) {}
251
+
252
+ const entity = Entity.create({
253
+ myClass: new MyClass(),
254
+ isVisible: true,
255
+ });
256
+ // now entity.myClass is an instance of MyClass, and entity.isVisible is true
257
+
258
+ entity.destroy();
259
+ // now entity.myClass is null, but note that entity.isVisible is still true, based on the value in the `components` config above
260
+ ```
261
+
262
+ <details>
263
+ <summary>TypeScript Version</summary>
264
+
265
+ ```ts
266
+ // some non-monomorph class
267
+ class MyClass {}
268
+
269
+ const components = {
270
+ // allows entity.addComponent('isVisible') without specifying the second parameter, in this case
271
+ // entity.isVisible will be true. this is how you can use components as simple flags
272
+ isVisible: true as unknown as ComponentConfig<true>,
273
+
274
+ // for non-monomorph classes, you can use a key with value null, or a default value of your choice.
275
+ // you are responsible for creating and managing this field, including any needed clean-up
276
+ // before it is removed. the `null` here is the default value that will be used when
277
+ // entity.addComponent('myClass') is called without a second parameter, and it is also the value set on
278
+ // the entity when the component is removed
279
+ myClass: null as unknown as ComponentConfig<MyClass>,
280
+
281
+ // you have much more control over the component's lifecycle, by passing a component config object,
282
+ // see the "Customizing Components" section below for details
283
+ myCustomComponent: {
284
+ // config options here, see below
285
+ } as const satisfies ComponentConfig<TheComponentType, AcceptedInputTypes>,
286
+
287
+ } as const satisfies ComponentMap;
288
+
289
+ const queries = {};
290
+
291
+ class Entity extends createEntityClass<Entity>()(components, queries) {}
292
+
293
+ const entity = Entity.create({
294
+ myClass: new MyClass(),
295
+ isVisible: true,
296
+ });
297
+ // now entity.myClass is an instance of MyClass, and entity.isVisible is true
213
298
 
214
- // note: if you need to do a cleanup on your custom class (like MyClass above),
215
- // use a query's beforeEntityRemoved method/listeners for that purpose
299
+ entity.destroy();
300
+ // now entity.myClass is null, and entity.isVisible is still true, based on the values in the `components` object above
216
301
  ```
217
302
 
303
+ </details>
304
+
218
305
  ### Accessing components and methods
219
306
  ```js
220
307
  // calling component methods
@@ -223,6 +310,7 @@ entity1.projectile.checkCollisions();
223
310
  // calling entity methods
224
311
  entity1.myEntityMethod();
225
312
 
313
+ // adding amd accessing a monomorph-based component using json notation
226
314
  entity1.addComponent('globalTransform', {
227
315
  position: { x: 1, y: 2, z: 3, },
228
316
  orientation: { x: 0, y: 0, z: 0, w: 1 },
@@ -232,15 +320,15 @@ entity1.globalTransform.position.x += 5;
232
320
 
233
321
  // add a component, using another component as input data
234
322
  entity2.addComponent('localTransform', entity1.localTransform);
235
- // ^^ they will not be the same objects, they will just have the same data
236
- // this is also true for nested objects
323
+ // ^^ they will not be the same objects, they will just have the same data,
324
+ // this is also true for nested objects. i.e:
325
+ // entity2.localTransform will be a deep copy of entity1.localTransform
237
326
 
238
327
  // remove a component
239
328
  entity1.removeComponent('projectile');
240
329
 
241
- // checking if a component exists on an entity, two ways to do so:
242
- if (entity1.globalTransform) { /* */ }
243
- if (entity1.hasCompoonent('globalTransform')) { /* */ }
330
+ // checking if a component exists on an entity:
331
+ if (entity1.hasComponent('globalTransform')) { /* */ }
244
332
 
245
333
  ```
246
334
 
@@ -287,9 +375,132 @@ Entity.queries.projectiles.beforeEntityRemoved.removeListener(someListener);
287
375
 
288
376
  ```
289
377
 
378
+ ### Customizing Components
379
+ For both monomorph and non-monomorph types, you can pass a component config object, allowing you to customize the component's lifecycle. A brief example is below with a few of the options, for many more option examples, see the tests in
380
+ [`tests/customComponents.test.ts`](https://codeberg.org/perplexdotgg/mecs/src/branch/main/tests/customComponents.test.ts).
381
+
382
+ This is the Javascript version, the Typescript version is below.
383
+
384
+ ```js
385
+ class MyClass {}
386
+
387
+ const components = {
388
+ myClass: {
389
+ // called after the component is added to an entity
390
+ afterComponentAdded: (myClassInstance, entity, componentKey) => {
391
+ // do any additional actions on myClassInstance here
392
+ // componentKey will be the string 'myClass' in this case
393
+ },
394
+
395
+ // called before the component is removed from an entity
396
+ beforeComponentRemoved: (myClassInstance, entity, componentKey) => {
397
+ // do any needed cleanup on myClassInstance here
398
+ },
399
+
400
+ // this function will be called to process the input data when adding this component
401
+ processDataOnAdd: (data) => {
402
+ if (data === null) {
403
+ // provide a default value if needed
404
+ data = new MyClass();
405
+ }
406
+ // modify and return the data as needed
407
+ return data;
408
+ },
409
+ },
410
+
411
+ myCustomMonomorphComponent: {
412
+ monomorphClass: SomeMonomorphClass,
413
+
414
+ // all other config options are available, see the tests for more examples
415
+ },
416
+ };
417
+
418
+ const queries = {};
419
+
420
+ class Entity extends createEntityClass()(components, queries) {}
421
+
422
+ const entity = Entity.create({
423
+ myClass: null, // this will automatically be processed by processDataOnAdd
424
+ });
425
+
426
+ // entity.myClass is now an instance of MyClass
427
+
428
+ const entity2 = Entity.create({
429
+ myClass: new MyClass(), // this still works, because of the (data === null) check in processDataOnAdd
430
+ });
431
+ ```
432
+
433
+ <details>
434
+ <summary>TypeScript Version</summary>
435
+
436
+ ```ts
437
+ class MyClass {}
438
+
439
+ const components = {
440
+ myClass: {
441
+ // called after the component is added to an entity
442
+ afterComponentAdded: (myClassInstance, entity, componentKey) => {
443
+ // do any additional actions on myClassInstance here
444
+ // componentKey will be the string 'myClass' in this case
445
+ },
446
+
447
+ // called before the component is removed from an entity
448
+ beforeComponentRemoved: (myClassInstance, entity, componentKey) => {
449
+ // do any needed cleanup on myClassInstance here
450
+ },
451
+
452
+ // this function will be called to process the input data when adding this component
453
+ processDataOnAdd: (data) => {
454
+ if (data === null) {
455
+ // provide a default value if needed
456
+ data = new MyClass();
457
+ }
458
+ // modify and return the data as needed
459
+ return data;
460
+ },
461
+ // ComponentConfig<MyClass, MyClass | null> means that entity.myClass will be of type MyClass,
462
+ // and when adding the component, the input data can be either MyClass or null
463
+ } as const satisfies ComponentConfig<MyClass, MyClass | null>,
464
+
465
+ myCustomMonomorphComponent: {
466
+ monomorphClass: SomeMonomorphClass,
467
+
468
+ // all other config options are available, see the tests for more examples
469
+ } as const satisfies ComponentConfig<SomeMonomorphClass>,
470
+ } as const satisfies ComponentMap;
471
+
472
+ const queries = {};
473
+
474
+ class Entity extends createEntityClass<Entity>()(components, queries) {}
475
+
476
+ const entity = Entity.create({
477
+ myClass: null, // this will automatically be processed by processDataOnAdd
478
+ });
479
+
480
+ // entity.myClass is now an instance of MyClass
481
+
482
+ const entity2 = Entity.create({
483
+ myClass: new MyClass(), // this still works
484
+ });
485
+ ```
486
+
487
+ </details>
488
+
489
+ | Option | Type | Description |
490
+ | --- | --- | --- |
491
+ | `monomorphClass` | A [Monomorph](http://codeberg.org/perplexdotgg/monomorph) class | If you want to customize the component lifecycle for a monomorph class, set this to the monomorphClass. Otherwise do not set this field. |
492
+ | `afterComponentAdded` | Function: `(componentInstance, entity, componentKey) => void` | A function that will be called after the component is added. This will happen before any query membership-related triggers occur. |
493
+ | `afterComponentAddedCode` | String or Function that returns a string | Like the above, but you provide the code that will be injected directly into the generated Entity code for this component. See the [tests](https://codeberg.org/perplexdotgg/mecs/src/branch/main/tests/customComponents.test.ts) for more details. |
494
+ | `beforeComponentRemoved` | Function: `(componentInstance, entity, componentKey) => void` | A function that will be called before the component is removed. This will happen after any query membership-related triggers occur. |
495
+ | `beforeComponentRemovedCode` | String or Function that returns a string | Like the above, but you provide the code that will be injected directly into the generated Entity code for this component. See the [tests](https://codeberg.org/perplexdotgg/mecs/src/branch/main/tests/customComponents.test.ts) for more details. |
496
+ | `processDataOnAdd` | Function: `(data) => processedData` | A function that will be called to process the input data when adding this component. The returned value will be used as the component's value for non-monomorph components. For monomorph components, the processed value will be passed to the class's create method. |
497
+ | `processDataOnAddCode` | String or Function that returns a string | Like the above, but you provide the code that will be injected directly into the generated Entity code for this component. See the [tests](https://codeberg.org/perplexdotgg/mecs/src/branch/main/tests/customComponents.test.ts) for more details. |
498
+ | `nullComponent` | Function: `(entity, componentKey) => void` | By default, i.e. when this option is not specified, the component property on the entity gets set to `null` when the component is removed from the entity. To override that behavior, you can provide a function that does whatever you prefer (pass a noop function to leave the value as is) |
499
+ | `nullComponentCode` | String or Function that returns a string | Like the above, but you provide the code that will be injected directly into the generated Entity code for this component. See the [tests](https://codeberg.org/perplexdotgg/mecs/src/branch/main/tests/customComponents.test.ts) for more details. |
500
+ | `beforeEntityClassCode` | String or Function that returns a string | Code that will be injected at the start of the generated code, before the Entity class's code. Useful for defining helper functions or values needed by all the options above. See the [tests](https://codeberg.org/perplexdotgg/mecs/src/branch/main/tests/customComponents.test.ts) for more details. |
501
+
290
502
  ## Roadmap
291
503
 
292
- - Integrating the upcoming version of monomorph where non-monomorth classes are better supported (e.g. to allow a component to contain a reference to an html element)
293
504
  - Example project/template coming soon
294
505
  - Basic benchmarks
295
506
  - Entity/component data debugging tool(s)
package/build/index.d.ts CHANGED
@@ -1,18 +1,48 @@
1
1
  import { InputType } from 'monomorph';
2
2
  import { MonomorphClass } from 'monomorph';
3
3
  import { MonomorphInstance } from 'monomorph';
4
- import { PartialRecursive } from 'monomorph';
5
4
  import { WithPool } from 'monomorph';
6
5
 
7
- export declare type ComponentInput<X> = X extends MonomorphClass<infer P, infer I, infer M> ? M extends MonomorphInstance<infer iP, infer iI> ? InputType<M> : never : X extends VirtualComponent<infer T> ? T : X;
6
+ export declare type ComponentConfig<ComponentType, ComponentInputType extends any = InputType<ComponentType> extends never ? ComponentType : InputType<ComponentType>> = {
7
+ monomorphClass?: ComponentType extends MonomorphClass<any, any, any> ? ComponentType : ComponentType extends MonomorphInstance<infer P, infer I> ? MonomorphClass<P, I, ComponentType> : never;
8
+ afterComponentAdded?: (componentInstance: ComponentType extends abstract new (...args: any) => any ? InstanceType<ComponentType> : ComponentType, entity: any, componentKey: string) => void;
9
+ afterComponentAddedCode?: string | ComponentConfigFunction<ComponentType, ComponentInputType>;
10
+ beforeComponentRemoved?: (componentInstance: ComponentType extends abstract new (...args: any) => any ? InstanceType<ComponentType> : ComponentType, entity: any, componentKey: string) => void;
11
+ beforeComponentRemovedCode?: string | ComponentConfigFunction<ComponentType, ComponentInputType>;
12
+ processDataOnAdd?: (data: ComponentInputType) => ComponentInputType;
13
+ processDataOnAddCode?: string | ((options: {
14
+ dataVariableName: string;
15
+ componentConfigReference: string;
16
+ }) => string);
17
+ nullComponent?: (entity: any, componentKey: string) => void;
18
+ nullComponentCode?: string | ComponentConfigFunction<ComponentType, ComponentInputType>;
19
+ beforeEntityClassCode?: string | ComponentConfigBeforeEntityClassFunction<ComponentType, ComponentInputType>;
20
+ };
21
+
22
+ export declare type ComponentConfigBeforeEntityClassFunction<ComponentType, ComponentInputType> = ((options: {
23
+ componentKey: string;
24
+ componentFlagValue: bigint;
25
+ componentIndex: number;
26
+ componentConfig: ComponentConfig<ComponentType, ComponentInputType>;
27
+ }) => string);
28
+
29
+ export declare type ComponentConfigFunction<ComponentType, ComponentInputType> = ((options: {
30
+ entityVariableName: string;
31
+ componentKey: string;
32
+ componentFlagValue: bigint;
33
+ componentIndex: number;
34
+ componentConfig: ComponentConfig<ComponentType, ComponentInputType>;
35
+ }) => string);
36
+
37
+ export declare type ComponentInput<X> = X extends MonomorphClass<infer P, infer I, infer M> ? M extends MonomorphInstance<infer iP, infer iI> ? InputType<M> : never : X extends ComponentConfig<infer CT, infer I> ? I : X;
8
38
 
9
- export declare type ComponentMap = Record<string, any>;
39
+ export declare type ComponentMap = Record<string, ComponentConfig<any, any> | MonomorphClass<any, any, any> | MonomorphClass<any, any, any>>;
10
40
 
11
41
  declare type ComponentMapClassProperties<CM> = {
12
- [K in keyof CM as LowercaseFirstLetter<K>]?: CM[K] extends MonomorphClass<infer P, infer I, infer M> ? M : CM[K];
42
+ [K in keyof CM as LowercaseFirstLetter<K>]?: CM[K] extends MonomorphClass<infer P, infer I, infer M> ? M : CM[K] extends ComponentConfig<infer MC, infer I> ? MC extends MonomorphClass<infer P, infer iI, infer M> ? M : MC : CM[K];
13
43
  };
14
44
 
15
- export declare function createEntityClass<C>(debug?: boolean): <CM extends ComponentMap, QM extends QueryMap<C, CM>, I extends EntityInput<CM> = EntityInput<CM>>(componentMap: CM, queries?: QM) => EntityClassWithStatics<C, CM, QM, I>;
45
+ export declare function createEntityClass<C>(options?: EntityCodeGenerationOptions): <CM extends ComponentMap, QM extends QueryMap<C, CM>, I extends EntityInput<CM> = EntityInput<CM>>(componentMap: CM, queries?: QM) => EntityClassWithStatics<C, CM, QM, I>;
16
46
 
17
47
  export declare type CreateEntityFunction<C, CM extends ComponentMap, I extends EntityInput<CM>> = (data?: I, pool?: EntityPoolClass<C>) => EntityInstanceWithPool<CM, I, C>;
18
48
 
@@ -20,12 +50,12 @@ declare type ElementOfArray<T> = T extends (infer E)[] ? E : never;
20
50
 
21
51
  declare interface EntityBaseProperties<CM extends ComponentMap, I extends EntityInput<CM>, QM extends QueryMap<any, CM>> {
22
52
  componentMap: CM;
23
- addComponent: <CCK extends LowercaseFirstLetter<keyof CM>, CC extends CM[OriginalComponentKey<CCK, CM>] = CM[OriginalComponentKey<CCK, CM>]>(key: CCK, data: ComponentInput<CC>) => void;
53
+ addComponent: <CCK extends LowercaseFirstLetter<keyof CM>, CC extends CM[OriginalComponentKey<CCK, CM>] = CM[OriginalComponentKey<CCK, CM>]>(...a: CC extends {
54
+ monomorphClass: any;
55
+ } ? (undefined extends ComponentInput<CC['monomorphClass']> ? [key: CCK, data?: ComponentInput<CC['monomorphClass']>] : [key: CCK, data: ComponentInput<CC['monomorphClass']>]) : (undefined extends ComponentInput<CC> ? [key: CCK, data?: ComponentInput<CC>] : [key: CCK, data: ComponentInput<CC>])) => void;
24
56
  removeComponent: <CCK extends LowercaseFirstLetter<keyof CM>>(key: CCK) => void;
25
57
  hasComponent: <CCK extends LowercaseFirstLetter<keyof CM>>(key: CCK) => boolean;
26
58
  clone(): this;
27
- copy(other: this): this;
28
- set(data: PartialRecursive<I>): this;
29
59
  destroy(): void;
30
60
  isDestroyed(): boolean;
31
61
  }
@@ -37,18 +67,47 @@ export declare type EntityClassWithStatics<C, CM extends ComponentMap, QM extend
37
67
  Pool: NoInfer<EntityPoolClass<C>>;
38
68
  };
39
69
 
70
+ declare type EntityCodeGenerationOptions = {
71
+ /**
72
+ * If true, logs all the generated class code for the entity class
73
+ */
74
+ logCode?: boolean;
75
+ /**
76
+ * If true, logs when components are added to or removed from entities
77
+ */
78
+ logComponents?: boolean;
79
+ /**
80
+ * If true, logs when entities are added to or removed from queries
81
+ */
82
+ logQueries?: boolean;
83
+ /**
84
+ * If true, skips safety checks such as trying to add a component that an entity already has
85
+ * It is recommended that you set to true in production to improve performance, while leave false
86
+ * during development to catch unintended mistakes, or missing hasComponent() checks in your code
87
+ */
88
+ skipSafetyChecks?: boolean;
89
+ };
90
+
40
91
  export declare interface EntityConstructor<QM extends QueryMap<any, CM>, E extends EntityInstance<CM, I, QM>, I extends EntityInput<CM>, CM extends ComponentMap> {
41
92
  new (data: I): E;
42
93
  componentMap: CM;
43
94
  pool: EntityPoolClass<E>;
44
95
  componentsWithEntities: {
45
- [K in keyof CM as CM[K] extends MonomorphClass<infer P, infer I, infer M> ? LowercaseFirstLetter<K> : never]: {
46
- [Symbol.iterator](): IterableIterator<[WithPool<InstanceType<CM[K]>>, E]>;
96
+ [K in keyof CM as CM[K] extends MonomorphClass<infer P, infer I, infer M> | {
97
+ monomorphClass: MonomorphClass<infer P, infer I, infer M>;
98
+ } ? LowercaseFirstLetter<K> : never]: {
99
+ [Symbol.iterator](): IterableIterator<[WithPool<InstanceType<CM[K] extends MonomorphClass<any, any, any> ? CM[K] : CM[K] extends {
100
+ monomorphClass: MonomorphClass<any, any, any>;
101
+ } ? CM[K]['monomorphClass'] : never>>, E]>;
47
102
  };
48
103
  };
49
104
  components: {
50
- [K in keyof CM as CM[K] extends MonomorphClass<infer P, infer I, infer M> ? LowercaseFirstLetter<K> : never]: {
51
- [Symbol.iterator](): IterableIterator<WithPool<InstanceType<CM[K]>>>;
105
+ [K in keyof CM as CM[K] extends MonomorphClass<infer P, infer I, infer M> | {
106
+ monomorphClass: MonomorphClass<infer P, infer I, infer M>;
107
+ } ? LowercaseFirstLetter<K> : never]: {
108
+ [Symbol.iterator](): IterableIterator<WithPool<InstanceType<CM[K] extends MonomorphClass<any, any, any> ? CM[K] : CM[K] extends {
109
+ monomorphClass: MonomorphClass<any, any, any>;
110
+ } ? CM[K]['monomorphClass'] : never>>>;
52
111
  };
53
112
  };
54
113
  queries: {
@@ -67,10 +126,12 @@ export declare interface EntityConstructor<QM extends QueryMap<any, CM>, E exten
67
126
  }
68
127
 
69
128
  export declare type EntityInput<CM extends ComponentMap> = Partial<{
70
- [K in keyof CM as LowercaseFirstLetter<K>]: ComponentInput<CM[K]>;
129
+ [K in keyof CM as LowercaseFirstLetter<K>]: CM[K] extends {
130
+ monomorphClass: any;
131
+ } ? ComponentInput<CM[K]['monomorphClass']> : ComponentInput<CM[K]>;
71
132
  }>;
72
133
 
73
- export declare type EntityInstance<CM extends ComponentMap, I extends EntityInput<CM>, QM extends QueryMap<any, CM>> = EntityBaseProperties<CM, I, QM> & ComponentMapClassProperties<CM>;
134
+ export declare type EntityInstance<CM extends ComponentMap, I extends EntityInput<CM>, QM extends QueryMap<any, CM>> = ComponentMapClassProperties<CM> & EntityBaseProperties<CM, I, QM>;
74
135
 
75
136
  export declare type EntityInstanceWithPool<CM extends ComponentMap, I extends EntityInput<CM>, E> = NoInfer<E & {
76
137
  pool: EntityPoolClass<E> | null;
@@ -99,7 +160,7 @@ export declare interface EntityPoolClass<M> {
99
160
  create: (data?: M extends EntityInstance<infer CM, infer I, any> ? EntityInput<CM> : undefined) => M extends EntityInstanceWithPool<infer CM, infer I, any> ? EntityInstanceWithPool<CM, I, M> : never;
100
161
  }
101
162
 
102
- export declare function getEntityClassCode<C, CM extends ComponentMap>(componentMap: CM, queries?: QueryMap<C, CM>, debug?: boolean): string;
163
+ export declare function getEntityClassCode<C, CM extends ComponentMap>(componentMap: CM, queries?: QueryMap<C, CM>, options?: EntityCodeGenerationOptions): string;
103
164
 
104
165
  declare type LowercaseFirstLetter<S> = S extends `${infer FirstLetter}${infer Rest}` ? `${Lowercase<FirstLetter>}${Rest}` : S;
105
166
 
@@ -116,8 +177,6 @@ export declare type QueryMap<C, CM extends ComponentMap> = Record<string, QueryC
116
177
 
117
178
  declare type UppercaseFirstLetter<S> = S extends `${infer FirstLetter}${infer Rest}` ? `${Uppercase<FirstLetter>}${Rest}` : S;
118
179
 
119
- export declare type VirtualComponent<T> = T;
120
-
121
180
  export declare type WithComponent<C extends {
122
181
  componentMap: any;
123
182
  }, KCM extends LowercaseFirstLetter<keyof C['componentMap']>> = Writeable<Omit<C, KCM> & {
package/build/mecs.js CHANGED
@@ -2,21 +2,23 @@ function lowercaseFirstLetter(str) {
2
2
  return str.charAt(0).toLowerCase() + str.slice(1);
3
3
  }
4
4
  function isMonomorphClass(x) {
5
- return typeof x === "function" && "serializedSize" in x;
5
+ return typeof x === "function" && "serializedSize" in x || typeof x === "object" && "monomorphClass" in x;
6
6
  }
7
- function getEntityClassCode(componentMap, queries, debug) {
7
+ function getEntityClassCode(componentMap, queries, options) {
8
8
  let monomorphReferenceCode = "";
9
9
  let poolCreationCode = "";
10
10
  let poolMapCreationCode = "const componentPools = {\n";
11
11
  let componentToEntityCode = "";
12
+ let referenceComponentFunctionsCode = "";
12
13
  let componentIndicesCode = "";
13
14
  let addComponentFunctionsCode = "";
14
15
  let removeComponentFunctionsCode = "";
16
+ let nullComponentCode = "";
15
17
  let componentFlagsCode = "";
16
18
  let createFromDataCode = "";
17
19
  let createFromDataCodeAllNulls = "";
18
20
  let destroyCode = "";
19
- let componentIteratorFunctionsCode = "";
21
+ let componentWithEntityIteratorFunctionsCode = "";
20
22
  const componentFlags = [];
21
23
  const componentIndices = {};
22
24
  const monomorphsAlreadyAdded = /* @__PURE__ */ new Set();
@@ -27,7 +29,7 @@ function getEntityClassCode(componentMap, queries, debug) {
27
29
  const isMonomorph = isMonomorphClass(component);
28
30
  componentIndices[componentKey] = componentIndex;
29
31
  componentIndicesCode += `
30
- ${componentKey}: ${componentIndex},
32
+ ['${componentKey}', ${componentIndex}],
31
33
  `;
32
34
  componentFlags.push(componentFlagValue);
33
35
  componentFlagsCode += `${componentFlagValue}n, `;
@@ -41,43 +43,131 @@ function getEntityClassCode(componentMap, queries, debug) {
41
43
  createFromDataCodeAllNulls += `
42
44
  this.${componentKey} = null;
43
45
  `;
46
+ destroyCode += `
47
+ if ((thisComponentFlags & ${componentFlagValue}n) !== 0n) {
48
+ this.removeComponent('${componentKey}', updateQueryMemberships);
49
+ }
50
+ `;
51
+ let processedDataCode = "data";
52
+ let afterComponentAddedCode = "";
53
+ let beforeComponentRemovedCode = "";
54
+ if (typeof component === "object" && component !== null) {
55
+ if ("beforeEntityClassCode" in component) {
56
+ if (typeof component.beforeEntityClassCode === "string") {
57
+ referenceComponentFunctionsCode += component.beforeEntityClassCode;
58
+ } else {
59
+ referenceComponentFunctionsCode += component.beforeEntityClassCode({
60
+ componentKey,
61
+ componentFlagValue,
62
+ componentIndex,
63
+ componentConfig: component
64
+ });
65
+ }
66
+ }
67
+ if ("afterComponentAdded" in component) {
68
+ referenceComponentFunctionsCode += `
69
+ const ${componentKey}OnAdded = componentMap.${key}.afterComponentAdded;
70
+ `;
71
+ afterComponentAddedCode = `
72
+ ${componentKey}OnAdded(entity.${componentKey}, entity, '${componentKey}');
73
+ `;
74
+ } else if ("afterComponentAddedCode" in component) {
75
+ if (typeof component.afterComponentAddedCode === "string") {
76
+ afterComponentAddedCode = component.afterComponentAddedCode;
77
+ } else {
78
+ afterComponentAddedCode = component.afterComponentAddedCode({
79
+ entityVariableName: "entity",
80
+ componentKey,
81
+ componentFlagValue,
82
+ componentIndex,
83
+ componentConfig: component
84
+ });
85
+ }
86
+ }
87
+ if ("beforeComponentRemoved" in component) {
88
+ referenceComponentFunctionsCode += `
89
+ const ${componentKey}OnRemoved = componentMap.${key}.beforeComponentRemoved;
90
+ `;
91
+ beforeComponentRemovedCode = `
92
+ ${componentKey}OnRemoved(entity.${componentKey}, entity, '${componentKey}');
93
+ `;
94
+ } else if ("beforeComponentRemovedCode" in component) {
95
+ if (typeof component.beforeComponentRemovedCode === "string") {
96
+ beforeComponentRemovedCode = component.beforeComponentRemovedCode;
97
+ } else {
98
+ beforeComponentRemovedCode = component.beforeComponentRemovedCode({
99
+ entityVariableName: "entity",
100
+ componentKey,
101
+ componentFlagValue,
102
+ componentIndex,
103
+ componentConfig: component
104
+ });
105
+ }
106
+ }
107
+ if ("processDataOnAdd" in component) {
108
+ referenceComponentFunctionsCode += `
109
+ const ${componentKey}ProcessDataOnAdd = componentMap.${key}.processDataOnAdd;
110
+ `;
111
+ processedDataCode = `${componentKey}ProcessDataOnAdd(data)`;
112
+ } else if ("processDataOnAddCode" in component) {
113
+ if (typeof component.processDataOnAddCode === "string") {
114
+ processedDataCode = component.processDataOnAddCode;
115
+ } else {
116
+ processedDataCode = component.processDataOnAddCode({ dataVariableName: "data", componentConfigReference: "componentMap." + key });
117
+ }
118
+ }
119
+ if ("nullComponent" in component) {
120
+ referenceComponentFunctionsCode += `
121
+ const ${componentKey}NullComponent = componentMap.${key}.nullComponent;
122
+ `;
123
+ nullComponentCode = `
124
+ ${componentKey}NullComponent(entity, '${componentKey}');
125
+ `;
126
+ } else if ("nullComponentCode" in component) {
127
+ if (typeof component.nullComponentCode === "string") {
128
+ nullComponentCode = component.nullComponentCode;
129
+ } else {
130
+ nullComponentCode = component.nullComponentCode({
131
+ entityVariableName: "entity",
132
+ componentKey,
133
+ componentFlagValue,
134
+ componentIndex,
135
+ componentConfig: component
136
+ });
137
+ }
138
+ } else {
139
+ nullComponentCode = `
140
+ entity.${componentKey} = null;
141
+ `;
142
+ }
143
+ } else if (!isMonomorph) {
144
+ processedDataCode = "data ?? componentMap." + key;
145
+ if (typeof component === "number" || typeof component === "string" || typeof component === "boolean" || component === null || component === void 0) {
146
+ nullComponentCode = `
147
+ entity.${componentKey} = ${component === void 0 ? "undefined" : JSON.stringify(component)};
148
+ `;
149
+ }
150
+ }
44
151
  if (isMonomorph) {
45
- if (!monomorphsAlreadyAdded.has(component.name)) {
152
+ const monomorphClassName = ("monomorphClass" in component ? component.monomorphClass : component).name;
153
+ if (!monomorphsAlreadyAdded.has(monomorphClassName)) {
46
154
  monomorphReferenceCode += `
47
- const ${component.name} = componentMap.${key};
155
+ const ${monomorphClassName} = componentMap.${key}${"monomorphClass" in component ? ".monomorphClass" : ""};
48
156
  `;
49
- monomorphsAlreadyAdded.add(component.name);
157
+ monomorphsAlreadyAdded.add(monomorphClassName);
50
158
  }
51
159
  poolCreationCode += `
52
- const ${componentKey}Pool = new ${component.name}.Pool();
160
+ const ${componentKey}Pool = new ${monomorphClassName}.Pool();
53
161
  const ${componentKey}PoolArray = ${componentKey}Pool.array;
54
162
  `;
55
163
  poolMapCreationCode += `
56
164
  ${componentKey}: ${componentKey}Pool,
57
165
  `;
58
- addComponentFunctionsCode += `
59
- (data, entity) => {
60
- const instance = ${component.name}.create(data, ${componentKey}Pool);
61
- ${componentKey}ComponentToEntity[instance.index] = entity;
62
- return instance;
63
- },
64
- `;
65
- removeComponentFunctionsCode += `
66
- instance => {
67
- ${componentKey}ComponentToEntity[instance.index] = null;
68
- instance.destroy();
69
- },
70
- `;
71
- destroyCode += `
72
- if ((this.componentFlags & ${componentFlagValue}n) !== 0n) {
73
- this.removeComponent('${componentKey}', updateQueryMemberships);
74
- }
75
- `;
76
166
  componentToEntityCode += `
77
167
  const ${componentKey}ComponentToEntity = [null];
78
168
  ${componentKey}ComponentToEntity.pop();
79
169
  `;
80
- componentIteratorFunctionsCode += `
170
+ componentWithEntityIteratorFunctionsCode += `
81
171
  ${componentKey}: {
82
172
  [Symbol.iterator]() {
83
173
  const resultArray = [null, null];
@@ -113,12 +203,60 @@ function getEntityClassCode(componentMap, queries, debug) {
113
203
  }
114
204
  },
115
205
  `;
206
+ addComponentFunctionsCode += `
207
+ (data, entity, componentKey, updateQueryMemberships) => {
208
+ entity.componentFlags |= ${componentFlagValue}n;
209
+ entity.${componentKey} = ${monomorphClassName}.create(${processedDataCode}, ${componentKey}Pool);
210
+ ${componentKey}ComponentToEntity[entity.${componentKey}.index] = entity;
211
+
212
+ ${afterComponentAddedCode}
213
+
214
+ if (updateQueryMemberships) {
215
+ entity.updateQueryMemberships();
216
+ }
217
+ },
218
+ `;
219
+ removeComponentFunctionsCode += `
220
+ (instance, entity, componentKey, updateQueryMemberships) => {
221
+ const newFlagValue = entity.componentFlags & ~${componentFlagValue}n;
222
+ if (updateQueryMemberships) {
223
+ entity.updateQueryMemberships(newFlagValue);
224
+ }
225
+
226
+ ${beforeComponentRemovedCode}
227
+
228
+ ${componentKey}ComponentToEntity[instance.index] = null;
229
+ instance.destroy();
230
+
231
+ entity.componentFlags = newFlagValue;
232
+ ${nullComponentCode};
233
+ },
234
+ `;
116
235
  } else {
117
236
  addComponentFunctionsCode += `
118
- null,
237
+ (data, entity, componentKey, updateQueryMemberships) => {
238
+ entity.componentFlags |= ${componentFlagValue}n;
239
+ entity.${componentKey} = ${processedDataCode};
240
+
241
+ ${afterComponentAddedCode}
242
+
243
+ if (updateQueryMemberships) {
244
+ entity.updateQueryMemberships();
245
+ }
246
+ },
119
247
  `;
120
248
  removeComponentFunctionsCode += `
121
- null,
249
+ (currentComponentValue, entity, componentKey, updateQueryMemberships) => {
250
+ const newFlagValue = entity.componentFlags & ~${componentFlagValue}n;
251
+ if (updateQueryMemberships) {
252
+ entity.updateQueryMemberships(newFlagValue);
253
+ }
254
+
255
+ ${beforeComponentRemovedCode}
256
+
257
+ entity.componentFlags = newFlagValue;
258
+ ${nullComponentCode};
259
+ },
122
260
  `;
123
261
  }
124
262
  componentFlagValue <<= 1n;
@@ -173,13 +311,10 @@ function getEntityClassCode(componentMap, queries, debug) {
173
311
  updateQueryMembershipsCode += `
174
312
  if (
175
313
  this.queryIndices[${queryIndex}] === -1
176
- && (this.componentFlags & ${queryWithFlag}n) === ${queryWithFlag}n
177
- && (this.componentFlags & ~${queryWithoutFlag}n) === this.componentFlags
314
+ && (thisComponentFlags & ${queryWithFlag}n) === ${queryWithFlag}n
315
+ && (thisComponentFlags & ~${queryWithoutFlag}n) === thisComponentFlags
178
316
  ) {
179
- ${!debug ? "" : `
180
- console.log('adding entity to query "${queryName}"', this);
181
- `}
182
- this.componentFlags |= ${queryWithFlag}n;
317
+ // thisComponentFlags |= ${queryWithFlag}n;
183
318
  if (queryEntityFreeIndices${queryIndex}.length > 0) {
184
319
  const index = queryEntityFreeIndices${queryIndex}.pop();
185
320
  this.queryIndices[${queryIndex}] = index;
@@ -191,28 +326,19 @@ function getEntityClassCode(componentMap, queries, debug) {
191
326
 
192
327
  afterEntityAdded${queryIndex}(this);
193
328
 
194
- ${!debug ? "" : `
329
+ ${!options?.logQueries ? "" : `
195
330
  console.log('added entity to query "${queryName}"', this);
196
331
  `}
197
332
  } else if (
198
333
  this.queryIndices[${queryIndex}] !== -1
199
334
  && (
200
- (this.componentFlags & ${queryWithFlag}n) !== ${queryWithFlag}n
201
- || (this.componentFlags & ~${queryWithoutFlag}n) !== this.componentFlags
335
+ (thisComponentFlags & ${queryWithFlag}n) !== ${queryWithFlag}n
336
+ || (thisComponentFlags & ~${queryWithoutFlag}n) !== thisComponentFlags
202
337
  )
203
338
  ) {
204
339
 
205
340
  if (queryEntityRemovedListeners${queryIndex}.length > 0) {
206
- if (componentFlagThatWasRemoved !== undefined) {
207
- // before calling the listeners, toggle the components that were removed, if any
208
- // this is so that in the listener, the entity's hasComponent() calls correctly
209
- // show that the component is still there, since the removal hasn't happened yet
210
- this.componentFlags &= ~componentFlagThatWasRemoved;
211
- beforeEntityRemoved${queryIndex}(this);
212
- this.componentFlags &= ~componentFlagThatWasRemoved;
213
- } else {
214
- beforeEntityRemoved${queryIndex}(this);
215
- }
341
+ beforeEntityRemoved${queryIndex}(this);
216
342
  }
217
343
 
218
344
  const index = this.queryIndices[${queryIndex}];
@@ -220,7 +346,7 @@ function getEntityClassCode(componentMap, queries, debug) {
220
346
  queryEntities${queryIndex}[index] = null;
221
347
  queryEntityFreeIndices${queryIndex}.push(index);
222
348
 
223
- ${!debug ? "" : `
349
+ ${!options?.logQueries ? "" : `
224
350
  console.log('removed entity from query "${queryName}"', this);
225
351
  `}
226
352
  }
@@ -298,9 +424,10 @@ function getEntityClassCode(componentMap, queries, debug) {
298
424
  ${poolCreationCode}
299
425
  ${poolMapCreationCode}
300
426
  ${componentToEntityCode}
301
- const componentIndices = {
427
+ ${referenceComponentFunctionsCode}
428
+ const componentIndices = new Map([
302
429
  ${componentIndicesCode}
303
- };
430
+ ]);
304
431
  const addComponentFunctions = [
305
432
  ${addComponentFunctionsCode}
306
433
  ];
@@ -445,41 +572,36 @@ function getEntityClassCode(componentMap, queries, debug) {
445
572
  }
446
573
 
447
574
  addComponent(key, data, updateQueryMemberships = true) {
448
- const componentIndex = componentIndices[key];
449
- const fn = addComponentFunctions[componentIndex];
450
- this[key] = fn ? fn(data, this) : data;
575
+ ${options?.skipSafetyChecks ? "" : `
576
+ if ((this.componentFlags & componentFlags[componentIndices.get(key)]) !== 0n) {
577
+ console.error('this entity:', this);
578
+ throw new Error('DEV-only check: Tried to add component "'+key+'" that this entity already has');
579
+ }
580
+ `}
451
581
 
452
- this.componentFlags |= componentFlags[componentIndex];
582
+ (addComponentFunctions[componentIndices.get(key)])(data, this, key, updateQueryMemberships);
453
583
 
454
- if (updateQueryMemberships) {
455
- this.updateQueryMemberships();
456
- }
584
+ ${!options?.logComponents ? "" : `
585
+ console.log('added component "'+key+'" to entity, updateQueryMemberships = ', updateQueryMemberships, 'entity = ', this);
586
+ `}
457
587
  }
458
588
 
459
589
  removeComponent(key, updateQueryMemberships = true) {
460
- const componentIndex = componentIndices[key];
461
- const componentFlag = componentFlags[componentIndex];
462
- ${!debug ? "" : `
590
+ const componentIndex = componentIndices.get(key);
591
+ ${options?.skipSafetyChecks ? "" : `
463
592
  if ((this.componentFlags & componentFlags[componentIndex]) === 0n) {
464
593
  console.error('this entity:', this);
465
594
  throw new Error('DEV-only check: Tried to remove component "'+key+'" that this entity does not have');
466
595
  }
467
596
  `}
468
- this.componentFlags &= ~componentFlag;
469
-
470
- if (updateQueryMemberships) {
471
- this.updateQueryMemberships(componentFlag);
472
- }
473
-
474
- const fn = removeComponentFunctions[componentIndex];
475
- if (fn) {
476
- fn(this[key], this, key);
477
- }
478
-
479
- this[key] = null;
597
+ (removeComponentFunctions[componentIndex])(this[key], this, key, updateQueryMemberships);
598
+ ${!options?.logComponents ? "" : `
599
+ console.log('removed component "'+key+'" from entity', this);
600
+ `}
480
601
  }
481
602
 
482
603
  destroy(updateQueryMemberships = true) {
604
+ const thisComponentFlags = this.componentFlags;
483
605
  ${destroyCode}
484
606
  if (this.version & 1 === 1) {
485
607
  ${""}
@@ -493,10 +615,10 @@ function getEntityClassCode(componentMap, queries, debug) {
493
615
  }
494
616
 
495
617
  hasComponent(key) {
496
- return (this.componentFlags & componentFlags[componentIndices[key]]) !== 0n;
618
+ return (this.componentFlags & componentFlags[componentIndices.get(key)]) !== 0n;
497
619
  }
498
620
 
499
- updateQueryMemberships(componentFlagThatWasRemoved) {
621
+ updateQueryMemberships(thisComponentFlags = this.componentFlags) {
500
622
  ${updateQueryMembershipsCode}
501
623
  }
502
624
 
@@ -813,9 +935,10 @@ function getEntityClassCode(componentMap, queries, debug) {
813
935
 
814
936
  theClass.queries = queries;
815
937
  theClass.componentToEntity = componentToEntity;
938
+ theClass.componentIndices = componentIndices;
816
939
  theClass.components = componentPools;
817
940
  theClass.componentsWithEntities = {
818
- ${componentIteratorFunctionsCode}
941
+ ${componentWithEntityIteratorFunctionsCode}
819
942
  };
820
943
 
821
944
  theClass.create = function(data, updateQueryMemberships = true) {
@@ -837,13 +960,13 @@ function getEntityClassCode(componentMap, queries, debug) {
837
960
 
838
961
  return theClass;
839
962
  `;
840
- if (debug) {
963
+ if (options?.logCode) {
841
964
  console.log("code", code);
842
965
  }
843
966
  return code;
844
967
  }
845
- function createEntityClass(debug) {
846
- return (componentMap, queries) => new Function("componentMap", "queryMap", getEntityClassCode(componentMap, queries, debug))(componentMap, queries);
968
+ function createEntityClass(options) {
969
+ return (componentMap, queries) => new Function("componentMap", "queryMap", getEntityClassCode(componentMap, queries, options))(componentMap, queries);
847
970
  }
848
971
  export {
849
972
  createEntityClass,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@perplexdotgg/mecs",
3
- "version": "0.1.0",
4
- "description": "MECS - Monomorph ECS - A high-performance Entity Component System for TypeScript and JavaScript, designed for games and simulations.",
3
+ "version": "0.2.0",
4
+ "description": "MECS - Monomorph ECS - A high-performance Entity Component System for TypeScript and JavaScript projects, designed for games and simulations.",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://codeberg.org/perplexdotgg/mecs"
@@ -26,11 +26,17 @@
26
26
  "test": "vitest",
27
27
  "bench": "vitest bench"
28
28
  },
29
- "dependencies": {
30
- "monomorph": "^1.2.0"
29
+ "peerDependencies": {
30
+ "monomorph": "^1.5.0"
31
+ },
32
+ "peerDependenciesMeta": {
33
+ "monomorph": {
34
+ "optional": true
35
+ }
31
36
  },
32
37
  "devDependencies": {
33
38
  "@types/node": "^25.0.1",
39
+ "monomorph": "^1.5.0",
34
40
  "ts-node": "^10.9.2",
35
41
  "tslib": "^2.8.1",
36
42
  "typescript": "^5.9.3",