npm package: @perplexdotgg/mecs
Description: MECS - Monomorph ECS - A high-performance Entity Component System for TypeScript and JavaScript projects, designed for games and simulations.
Author: perplex.gg
Latest version: 0.5.1

[![npm version](https://img.shields.io/npm/v/@perplexdotgg/mecs.svg)](https://www.npmjs.com/package/@perplexdotgg/mecs)
[![repo](https://img.shields.io/badge/codeberg-repo-7d48ff?logo=codeberg&logoColor=ff7d48)](https://codeberg.org/perplexdotgg/mecs)
[![license](https://img.shields.io/npm/l/@perplexdotgg/mecs.svg?color=7d48ff)](http://codeberg.org/perplexdotgg/mecs/src/branch/main/LICENSE)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-7d48ff.svg)](https://codeberg.org/perplexdotgg/mecs/pulls)

<div style="display: flex;">
  <img src="./mecs.svg" width="152" height="64" style="margin-right: 16px;" />
  <h1>MECS - Monomorph Entity Component System</h1>
</div>

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.

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.

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).

<details>
  <summary>Side note: Why not use SoA design?</summary>
  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 projects, 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.
</details>

---

## Features
- Automatically generates highly optimized JS class code for your specific entity schema, at start up
- 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.
- Simple and minimal API: There are only entities, components and queries
- Queries, aka archetypes, automatically match and track entities based on which components they have, or don't have
- Strong compile-time type safety if you use Typescript (recommended)
- Coming soon: automatic serialization/deserialization (to/from number arrays) of entities (fully or a subset of components) to create snapshots

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.

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.

### When to use MECS over Monomorph alone

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.

## Usage

### Install into your project

```bash
npm install @perplexdotgg/mecs
```


### Defining an Entity schema to create your Entity class

The following is JavaScript code. The Typescript version is below this.

```js
import { createClass } from 'monomorph';
import { createEntityClass } from '@perplexdotgg/mecs';

// imagine some monomorph classes in your project
// see the monomorph docs for more on those:
// https://codeberg.org/perplexdotgg/monomorph
class Projectile extends createClass(projectileProps) {}
class Transform extends createClass(transformProps) {}

// define all the components you want entities to potentially have
const components = {
  // defining components when they are monomorph classes:
  localTransform: Transform,
  globalTransform: Transform,
  Projectile, // this is shorthand for: projectile: Projectile
};

// define queries you want to do. these always stay up-to-date, efficiently
const queries = {
  projectiles: {
    // an entity must have all of these components to match this query
    with: ['globalTransform', 'projectile'],
    // AND it can have none of these components to match this query
    without: ['localTransform'],

    // listeners can added now, or later (see the Queries docs below)
    afterEntityAdded: (entity) => {
      // this is called when an entity newly matches this query
    },
    beforeEntityRemoved: (entity) => {
      // this is called when an entity no longer matches this query,
      // just before a relevant component is removed from the entity
      // and/or before an entity is destroyed
    },
  },
  query2: {
    with: ['localTransform', 'globalTransform'],
    without: [],
  },
};

// additional Entity class options, see "Entity Class Options" below
const extraOptions = {};

// automatically create our entity class from the schemas above.
// note that there are two function calls after createEntityClass
// to circumvent a typescript limitation (sorry, vanilla JS folks!)
class Entity extends createEntityClass(extraOptions)(components, queries) {

  // most methods will likely exist on your components rather than your entities,
  // but you can also extend the generated entity class
  myEntityMethod() {
    // your code here! use 'this' like you would expect
    if (this.hasComponent('example')) {
      this.example.whatever = 5;
    }
  }
}
```

<details>
  <summary>Typescript version</summary>

```ts
import { createClass } from 'monomorph';
import { createEntityClass, type QueryMap, type ComponentMap, type ComponentConfig } from '@perplexdotgg/mecs';

// imagine some monomorph classes in your project, see the monomorph docs for more on those:
// https://codeberg.org/perplexdotgg/monomorph
class Projectile extends createClass<Projectile, typeof projectileProps>(projectileProps) {}
class Transform extends createClass<Transform, typeof transformProps>(transformProps) {}

// non-monomorph components are also supported, see "Non-Monomorph Components" below

// define all the components you want entities to potentially have
const components = {
  // defining components when they are monomorph classes:
  localTransform: Transform,
  globalTransform: Transform,
  Projectile, // this is shorthand for: projectile: Projectile
} as const satisfies ComponentMap;

// define queries you want to do. these always stay up-to-date, efficiently
const queries = {
  projectiles: {
    // an entity must have all of these components to match this query
    with: ['globalTransform', 'projectile'],
    // AND it can have none of these components to match this query
    without: ['localTransform'],

    // listeners can added now, or later (see the Queries docs below)
    // note that in this context, the entity type doesn't have Entity methods that you've added
    // if you need those, either ts-ignore when calling them here, or add listeners later in your system code using:
    // Entity.queries().yourQueryKey.afterEntityAdded.addListener((entity) => { /* your code here will get the right entity type */ }))
    afterEntityAdded: (entity) => {
      // this is called when an entity newly matches this query
    },
    beforeEntityRemoved: (entity) => {
      // this is called when an entity no longer matches this query,
      // just before a relevant component is removed from the entity
    },
  },
  query2: {
    with: ['localTransform', 'globalTransform'],
    without: [],
  },
} as const satisfies QueryMap<typeof components>;

// additional Entity class options, see "Entity Class Options" below
const extraOptions = {};

// automatically create our entity class from the schemas above.
// note that there are two function calls after createEntityClass
// to circumvent a typescript limitation ([TS issue 10571](https://github.com/microsoft/TypeScript/issues/10571))
class Entity extends createEntityClass<Entity>(extraOptions)(components, queries) {

  // most methods will likely exist on your components rather than your entities,
  // but you can also extend the generated entity class
  myEntityMethod() {
    // your code here! use 'this' like you would expect
    if (this.hasComponent('example')) {
      this.example.whatever = 5;
    }
  }
}
```
</details>

### Creating and destroying entities
Like monomorph's create method, entities accept the same object representations as inputs for each component. All components instances will be created automatically as needed

```ts
const entity1 = Entity.create({
  globalTransform: {
    position: [0, 0, 0],
    orientation: [0, 0, 0],
    scale: 1,
  },
  projectile: {
    speed: 4,
    direction: [1, 0, 0],
  },
});

// now entity1 is of the Entity class, and entity1.globalTransform is of the Transform class
// so you can call methods on it like so:
entity1.globalTransform.composeMatrix();

const entity2 = Entity.create({
  projectile: {
    speed: 4,
    direction: { x: 1, y: 0, z: 0 },
  },
});

// you can use destroy() to return an entity to the pool, it will automatically be re-used later
// this iterates on all components to remove them first, and triggers queries just
// before the component(s) needed for the query are deleted
// each destroyed component also gets recycled in its own component pool
entity2.destroy();
```

### Entity Class Options
When creating your entity class, you can pass as an optional parameter to `createEntityClass`, an object with the following properties:
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `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. |
| `logCode` | `boolean` | `false` | Set to `true` to `console.log` the code that was automatically generated for the base Entity class. |
| `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) |
| `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) |


### Non-Monomorph Components
For components that are not monomorph classes, such as your own classes or primitives like numbers and booleans,
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.

This is the Javascript versiom, the Typescript version is below.

```js
// some non-monomorph class
class MyClass {}

const components = {
  // allows entity.addComponent('isVisible') without specifying the second parameter, in this case
  // entity.isVisible will be true. this is how you can use components as simple flags
  isVisible: true,

  // for non-monomorph classes, you can use a key with value null, or a default value of your choice.
  // you are responsible for creating and managing this field, including any needed clean-up
  // before it is removed. the `null` here is the default value that will be used when
  // entity.addComponent('myClass') is called without a second parameter, and it is also the value set on
  // the entity when the component is removed
  myClass: null,

  // you have much more control over the component's lifecycle, by passing a component config object,
  // see the "Customizing Components" section below for details
  myCustomComponent: {
    // component config options here, see "Customizing Components" below
  },

};

const queries = {};

class Entity extends createEntityClass()(components, queries) {}

const entity = Entity.create({
  myClass: new MyClass(),
  isVisible: true,
});
// now entity.myClass is an instance of MyClass, and entity.isVisible is true

entity.destroy();
// now entity.myClass is null, but note that entity.isVisible is still true, based on the value in the `components` config above
```

<details>
  <summary>TypeScript Version</summary>

```ts
// some non-monomorph class
class MyClass {}

const components = {
  // allows entity.addComponent('isVisible') without specifying the second parameter, in this case
  // entity.isVisible will be true. this is how you can use components as simple flags
  isVisible: true as unknown as ComponentConfig<true>,

  // for non-monomorph classes, you can use a key with value null, or a default value of your choice.
  // you are responsible for creating and managing this field, including any needed clean-up
  // before it is removed. the `null` here is the default value that will be used when
  // entity.addComponent('myClass') is called without a second parameter, and it is also the value set on
  // the entity when the component is removed
  myClass: null as unknown as ComponentConfig<MyClass>,

  // you have much more control over the component's lifecycle, by passing a component config object,
  // see the "Customizing Components" section below for details
  myCustomComponent: {
    // config options here, see below
  } as const satisfies ComponentConfig<TheComponentType, AcceptedInputTypes>,

} as const satisfies ComponentMap;

const queries = {};

class Entity extends createEntityClass<Entity>()(components, queries) {}

const entity = Entity.create({
  myClass: new MyClass(),
  isVisible: true,
});
// now entity.myClass is an instance of MyClass, and entity.isVisible is true

entity.destroy();
// now entity.myClass is null, and entity.isVisible is still true, based on the values in the `components` object above
```

</details>

### Accessing components and methods
```js
// calling component methods
entity1.projectile.checkCollisions();

// calling entity methods
entity1.myEntityMethod();

// adding amd accessing a monomorph-based component using json notation
entity1.addComponent('globalTransform', {
  position: { x: 1, y: 2, z: 3, },
  orientation: { x: 0, y: 0, z: 0, w: 1 },
  scale: 1,
});
entity1.globalTransform.position.x += 5;

// add a component, using another component as input data
entity2.addComponent('localTransform', entity1.localTransform);
// ^^ they will not be the same objects, they will just have the same data,
// this is also true for nested objects. i.e:
// entity2.localTransform will be a deep copy of entity1.localTransform

// remove a component
entity1.removeComponent('projectile');

// checking if a component exists on an entity:
if (entity1.hasComponent('globalTransform')) { /* */ }

```

### Iterating on entities and components
```ts
// all entities
for (const entity of Entity.pool) {

}

// only for monomorph-class components, iterating all instances of a component
for (const transform of Entity.components.localTransform) {

}

// only for monomorph-class components, iterating all instances of a component,
// along with the entity for each
for (const [transform, entity] of Entity.componentsWithEntities.localTransform) {

}
```

### Iterating on queries, and adding/removing listeners
For defining queries, see the [entity schema definition section](#defining-an-entity-schema-to-create-your-entity-class) above
```ts
// all entities matching a query
for (const entity of Entity.queries.projectiles) {

}

// in addition to being able to define `afterEntityAdded` and `beforeEntityRemoved` functions in
// the schema, you can dynamically add a new listener for entities matching a query,
// useful if you want to put this logic in your system code for example
function afterProjectileExists(entity) {
  // do whatever
}

Entity.queries.projectiles.afterEntityAdded.addListener(afterProjectileExists);
// you can also remove listeners
Entity.queries.projectiles.afterEntityAdded.removeListener(afterProjectileExists);

Entity.queries.projectiles.beforeEntityRemoved.addListener(someListener);
Entity.queries.projectiles.beforeEntityRemoved.removeListener(someListener);

```

### Components that reference Entities
You can have components that reference other entities, by using Monomorph's `LazyReferenceType` and `LazyReferenceListType`. This will usually 'just work', but in some situations you may also need to use MECS's `LazyComponent`. See the Typescript tests in [`tests/componentsWithEntityReferences.test.ts`](https://codeberg.org/perplexdotgg/mecs/src/branch/main/tests/componentsWithEntityReferences.test.ts) and [`tests/lazyComponentsWithEntityReferences.test.ts`](https://codeberg.org/perplexdotgg/mecs/src/branch/main/tests/lazyComponentsWithEntityReferences.test.ts) for examples. Below is a simple Javascript example:

```js
const turretProps = {
  targetEntity: LazyReferenceType(() => Entity),

  // or for multiple referenced entities:
  targetEntities: LazyReferenceListType(() => Entity),
};
class Turret extends createClass(turretProps) {}

const components = {
  turret: Turret,
  // or the lazy loaded version:
  // turret: LazyComponent(() => Turret),

  // alternatively, you can use a component config object for further customization:
  turret: {
    monomorphClass: Turret,
    // or the lazy loaded version:
    // monomorphClass: LazyComponent(() => Turret),

    // other config options here, for example:
    afterComponentAdded: (turretInstance, entity, componentKey) => {
      if (turretInstance.targetEntity === null) {
        // example maybe automatically select a target in this situation
      }
    },
  },
};
class Entity extends createEntityClass()(components, queries) {}
```


### Customizing Components
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
[`tests/customComponents.test.ts`](https://codeberg.org/perplexdotgg/mecs/src/branch/main/tests/customComponents.test.ts).

This is the Javascript version, the Typescript version is below.

```js
class MyClass {}

const components = {
  myClass: {
    // called after the component is added to an entity
    afterComponentAdded: (myClassInstance, entity, componentKey) => {
      // do any additional actions on myClassInstance here
      // componentKey will be the string 'myClass' in this case
    },

    // called before the component is removed from an entity
    beforeComponentRemoved: (myClassInstance, entity, componentKey) => {
      // do any needed cleanup on myClassInstance here
    },

    // this function will be called to process the input data when adding this component
    processDataOnAdd: (data) => {
      if (data === null) {
        // provide a default value if needed
        data = new MyClass();
      }
      // modify and return the data as needed
      return data;
    },
  },

  myCustomMonomorphComponent: {
    monomorphClass: SomeMonomorphClass,

    // all other config options are available, see the tests for more examples
  },
};

const queries = {};

class Entity extends createEntityClass()(components, queries) {}

const entity = Entity.create({
  myClass: null, // this will automatically be processed by processDataOnAdd
});

// entity.myClass is now an instance of MyClass

const entity2 = Entity.create({
  myClass: new MyClass(), // this still works, because of the (data === null) check in processDataOnAdd
});
```

<details>
  <summary>TypeScript Version</summary>

```ts
class MyClass {}

const components = {
  myClass: {
    // called after the component is added to an entity
    afterComponentAdded: (myClassInstance, entity, componentKey) => {
      // do any additional actions on myClassInstance here
      // componentKey will be the string 'myClass' in this case
    },

    // called before the component is removed from an entity
    beforeComponentRemoved: (myClassInstance, entity, componentKey) => {
      // do any needed cleanup on myClassInstance here
    },

    // this function will be called to process the input data when adding this component
    processDataOnAdd: (data) => {
      if (data === null) {
        // provide a default value if needed
        data = new MyClass();
      }
      // modify and return the data as needed
      return data;
    },
    // ComponentConfig<MyClass, MyClass | null> means that entity.myClass will be of type MyClass,
    // and when adding the component, the input data can be either MyClass or null
  } as const satisfies ComponentConfig<MyClass, MyClass | null>,

  myCustomMonomorphComponent: {
    monomorphClass: SomeMonomorphClass,

    // all other config options are available, see the tests for more examples
  } as const satisfies ComponentConfig<SomeMonomorphClass>,
} as const satisfies ComponentMap;

const queries = {};

class Entity extends createEntityClass<Entity>()(components, queries) {}

const entity = Entity.create({
  myClass: null, // this will automatically be processed by processDataOnAdd
});

// entity.myClass is now an instance of MyClass

const entity2 = Entity.create({
  myClass: new MyClass(), // this still works
});
```

</details>

---

Here are all the available component config options:

| Option | Type | Description |
| --- | --- | --- |
| `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. |
| `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.  |
| `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. |
| `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. |
| `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. |
| `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. |
| `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. |
| `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) |
| `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. |
| `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. |

## Roadmap

- Example project/template coming soon
- Basic benchmarks
- Entity/component data debugging tool(s)
- Want to see something here? Please create an [issue on codeberg](https://codeberg.org/perplexdotgg/mecs/issues)

## How to contribute

If you like this project and would like to support our work, please consider contributing code via [pull requests](https://codeberg.org/perplexdotgg/mecs/pulls), or donating via [open collective](https://opencollective.com/perplexgg). Contributions are greatly appreciated!