---
description: A TypeScript library for Entity Component System (ECS)
image: https://hackmd.io/screenshot.png
tags: ecs,playground,official
---
# ECS
ECS is a small, fast, tree-shakeable, and flexible TypeScript library for the entity-component system.
---
<a href="https://github.com/G43riko/polayground-nx/tree/development/libs/ecs/core#readme">
<img align="left" alt="Github" width="26px" src="https://raw.githubusercontent.com/github/explore/master/topics/github/github.png" />
</a>
<a href="https://www.npmjs.com/package/@g43/ecs">
<img align="left" alt="Github" width="26px" src="https://raw.githubusercontent.com/github/explore/master/topics/npm/npm.png"/>
</a>
---
[Components and systems](/ZX7vQMgdRZeQgOtfXdjoLA)
Inspiration:
https://github.com/libgdx/ashley
> [TOC]
## Creation
### Engine
#### Create Engine
### Component
#### Create Component
#### Add Component to Entity
```plantuml
-> Entity++: addComponent(ComponentInstance)
Entity -> EcsMarker: extractEngineFromEntity(Entity)
Entity -> EcsMarker: assertComponentInstance(ComponentInstance)
Entity -> EcsMarker: assignComponentToEntity(ComponentInstance, Entity)
Entity -> ComponentHolder: addComponent(ComponentInstance)
<- Entity--: ComponentInstance
```
### Entity
#### Create Entity in Engine
```plantuml
-> Engine++: createEntity(EntityType)
Engine -> Engine: createEntityInjector(EcsComponent[])
Engine -> Engine: setInjector(EntityInjector)
Engine -> EcsHolder: createEntity(EntityType)
EcsHolder -> Engine: EntityInstance
Engine -> Engine: setInjector(undefined)
Engine -> EcsMarker: markInstanceAsCreateByEngine(EntityInstance)
Engine -> Engine: addEntity(EntityInstance)
<- Engine--: EntityInstance
```
#### Create Entity in EcsHolder
```plantuml
-> EcsHolder ++: createEntity(EntityType)
EcsHolder -> EcsHolder ++: createEntityInternally(EntityType)
EcsHolder -> EcsHolder: getCurrentEntityInjector(EcsComponent[])
EcsHolder -> EcsHolder ++: instantiateEntity(EntityType, EntityInjector)
EcsHolder -> EntityInstance **: new()
'EcsHolder -> EcsMarker: assertEntityInstance(result)
EcsHolder -> EcsMarker: markInstanceAsCreatedByEcs(result)
EcsHolder -> EntityInjector: addAllComponentToEntity(result);
EcsHolder -> EcsHolder: getAllComponentDependencies()
EcsHolder -> EcsHolder: setBindingToEntity()
<-EcsHolder --: EntityInstance
```
#### Add Entity to Engine
```plantuml
-> Engine++: addEntity(EntityInstance)
Engine-> Engine: addSingleEntityInternally(EntityInstance)
== Checks ==
Engine -> EcsMarker: assertEntityInstance(EntityInstance)
Engine -> Engine: assertEntityDoesNotExist(EntityInstance)
opt EcsSettings.ENTITY_DEPENDENCIES_CHECK === EcsSettingDependencyCheck.ADDED
Engine -> EcsHolder: checkAllComponentDecoratorDependencies(EntityInstance)
Engine -> EcsMarker:getEntityParams(EntityInstance)
EcsMarker -> Engine: EntityData
Engine -> EcsHolder: checkEntityDecoratorDependencies(EntityData)
end
Engine -> EntityHolder: addEntity(EntityInstance)
== Internal updates ==
EntityHolder -> Engine ++: addEntityInternally(EntityInstance)
Engine -> EcsMarker: setEngineToEntity(EntityInstance, Engine)
Engine -> Engine: notifyAllFamiliesAboutEntity(EntityInstance)
Engine -> Engine: notifyAllSystemsAboutEntity(EntityInstance)
Engine -> EntityInstance: onAddToEngine(Engine)
Engine -> Engine: onEntityAdded(SystemInstance)
<- Engine--: EntityInstance
```
### System
#### Create System in Engine
```plantuml
-> Engine: createSystem(SystemType)
Engine -> Engine: setInjector(EngineInjector)
Engine -> EcsHolder: createSystem(SystemType)
EcsHolder -> Engine: SystemInstance
Engine -> Engine: setInjector(undefined)
Engine -> EcsMarker: markInstanceAsCreateByEngine(SystemInstance)
Engine -> Engine: addSystem(SystemInstance)
<- Engine--: SystemInstance
```
#### Create System in EcsHolder
```plantuml
-> EcsHolder: createSystem(SystemType)
EcsHolder -> EcsMarker: assertIsSystem(SystemType)
EcsHolder -> SystemInstance **: new()
<- EcsHolder: SystemInstance
```
#### Add system to Engine
```plantuml
-> Engine++: addSystem(SystemInstance)
Engine -> EcsMarker: assertSystemInstance(SystemInstance)
Engine -> SystemHolder: add(SystemInstance)
SystemHolder -> Engine++: addSystemInternally(SystemInstance)
Engine -> EcsMarker: getSystemData(SystemInstance)
EcsMarker -> Engine: SystemData
alt Data has family
Engine -> EcsHolder: createFamily(SystemInstance, FamilyData)
end
alt Data has throttle update
Engine -> Engine: setThrottleUpdate(SystemInstance, ThrottleUpdateData)
end
alt EcsSettings.systemDependenciesCheck === EcsSettingDependencyCheck.ADDED
Engine -> Engine: checkAllDependencies(SystemInstance[])
end
Engine -> EcsMarker: getSystemBindingsData(SystemInstance)
EcsMarker -> Engine: SystemBindingsData
alt has SystemBindingsData
Engine -> Engine: bindSystemsToSystem(SystemInstance, SystemInstance[])
Engine -> Engine: bindEngineToSystem(SystemInstance)
end
alt typeof system.onEntityAdded === "function"
Engine -> SystemInstance: onEntityAdded(allEntitiesFromSystem)
end
Engine -> Engine: assignFamiliesToSystem(SystemInstance)
Engine -> SystemInstance: onAddToEngine(Engine)
Engine -> Engine: onSystemAdded(SystemInstance)
<- Engine--: SystemInstance
```
## API
### EcsHolder
### Functions
#### injectSystem(Type<EcsSystemInstance>)
#### injectComponent(Type<unknown>)
#### injectEngine()
### Class decorators
#### EcsSystem
| Name | Type | Description |
| ------- | ----------- | ---------------- |
| require | `Type<any>[]` | list of required systems |
#### EcsComponent
| Name | Type | Description |
| ------- | ----------- | ---------------- |
| dependencies | `Type<any>[]` | list of required components |
| global| boolean | if true, this component can be injected via `injectComponent` into any entity
#### EcsEngine
| Name | Type | Description |
| ------- | ----------- | ---------------- |
| systems | `Type<any>[]` | List of systems to be created automatically with the engine |
#### EcsEntity
| Name | Type | Description |
| ------- | ----------- | ---------------- |
| components | `Type<any>[]` | List of components for creation |
| dependencies | `Type<any>[]` | List of required components |
### Property decorators
#### EcsBindSystem
can be used in **system** or **engine**
| Name | Type | Description |
| ------- | ----------- | ---------------- |
| system | `Type<any>` | |
| optional | `boolean` | |
#### EcsBindFamily
can be used in **system** or **engine**
| Name | Type | Description |
| ------- | ----------- | ---------------- |
| required | `Type<any>[]` | |
| except | `Type<any>[]` | |
| optional | `boolean` | |
| required | `boolean` | |
| count | `number` | |
| minCount | `number` | |
| maxCount | `number` | |
#### EcsBindEngine
can be used in **system**
#### EcsBindComponent
can be used in **entity**
| Name | Type | Description |
| ------- | ----------- | ---------------- |
| component | `Type<any>` | |
| optional | `boolean` | |
## Usage
### Entities
#### Basics
Entities can be created by calling the method `createEntity`.
```typescript
const entity = Ecs.createEntity();
```
Each entity can contains multiple components.
Components are added to entity with method `add`
```typescript
entity.add(ComponentA);
entity.add(ComponentB);
```
Entity can be created directly with some components
```typescript
const entity = Ecs.createEntity(ComponentA, ComponentB)
```
If these components require parameters, you have to create them using the method `createComponent`.
```typescript
// Create with parameterized components
const entity = Ecs.createEntity(
Ecs.createComponent(ComponentA, param),
Ecs.createComponent(ComponentB, paramA, paramB),
)
```
#### Custom entity
Sometimes you want to add custom logic to an entity. In that case, you can create your own entity class which needs to inherit from `EcsCustomEntity` and annotate it with the `EcsEntity` decorator.
```typescript
@EcsEntity()
class MyEntity extends EcsCustomEntity {
// ...custom functionality
}
const entity = Ecs.createEntity(MyEntity);
```
Sometimes you need to pass parameters to the entity's constructor. You can achieve it by passing an array with the entity and parameters instead of the entity.
```typescript
@EcsEntity()
class MyEntity extends EcsCustomEntity {
public constructor(paramA: ParamaA, paramB: ParamB) {}
}
const entity = Ecs.createEntity([MyEntity, paramA, paramB]);
```
:::warning
Notice that a custom entity with parameters is wrapped in an array because the next parameters in `createEntity` are components.
`Ecs.createEntity([MyEntity, paramA, paramB], Comp1, Comp2);`
:::
#### Entity's static components
When you want to create an entity like a prototype and you want to add the same components to it, you can define them in the entity's decorator. These components will be created automatically with the entity.
```typescript
@EcsEntity({
components: [ComponentA, ComponentB, ComponentC]
})
class MyEntity extend EcsCustomEntity {}
const entity = Ecs.createEntity(MyEntity);
```
If some of these components require parameters, you can easily provide them.
Don't forget that these parameters must be static and the same for each component attached to this entity.
```typescript
@EcsEntity({
components: [ComponentA, [ComponentB, idB], ComponentC]
})
```
#### Bind components
For easier access to an entity's components, you can bind them to its properties.
```typescript
class HealthComponent {
public constructor(
public readonly maxHealth: number,
public health = maxHealth,
) {}
}
@EcsEntity()
class Person extends EcsCustomEntity {
@EcsBindComponent(HealthComponent)
public readonly healthComponent!: HealthComponent;
public isAlive(): boolean {
return this.healthComponent.health > 0;
}
}
const entityA = Ecs.createEntity(Person);
// throws error because Person requires health Component
const entityB = Ecs.createEntity(Person, Ecs.createComponent(HealthComponent));
// OK
```
All bound components are by default required so if you want to make a component optional, you can mark binding as dynamic by passing a second parameter.
```typescript
@EcsEntity()
class Person extends EcsCustomEntity {
@EcsBindComponent(HealthComponent, true)
public readonly healthComponent?: HealthComponent;
}
const entityA = Ecs.createEntity(Person); // OK
entityA.healthComponent; // undefined
entityA.addComponent(HealthComponent)
entityA.healthComponent; // HealthComponent
```
##### Inject components
Components also have an alternative way of injecting via the injectComponent function
```typescript
@EcsEntity()
class Person extends EcsCustomEntity {
public readonly healthComponent = injectComponent(HealthComponent);
}
```
:::warning
Components are injected only if there are
- passed as parameter to `Ecs.createEntity` or `Engine.createEntity` methods
- declared in `EcsEntity` decorator
- marked as global in `EcsComponent` decorator
- created using EcsEntityInstance.createComponent()
```typescript
@EcsComponent({global: true})
class ComponentC {}
@EcsEntity({components: [ComponentA]})
class Entity extends EcsCustomEntity {
public readonly componentA = injectComponent(ComponentA);
public readonly componentB = injectComponent(ComponentB);
public readonly componentC = injectComponent(ComponentC);
}
const entity1 = Ecs.createEntity(Entity);
entity1.componentA; // exists;
entity1.componentB; // undefined;
entity1.componentC; // exists;
const entity2 = Ecs.createEntity(Entity, ComponentB);
entity2.componentA; // exists;
entity2.componentB; // exists;
entity2.componentC; // exists;
:::
#### Components usage
Components can be added to an entity even when the entity is already created. All components can be retrieved from an entity or removed from an entity.
```typescript
// Adding
entity.addComponent(ComponentA);
entity.addComponent(ComponentB, ComponentC);
entity.addComponent([ComponentC, paramA, paramB], ComponentD)
// Retrieving
const componentA = entity.getComponent(ComponentA);
const [compB, compA] = entity.getComponent(ComponentB, ComponentC);
// Removing
entity.removeComponent(ComponentA);
entity.removeComponent(ComponentB, ComponentC);
```
#### Lifetime
When your entity is ready, you can add it to the engine using the method `addEntity`. When the entity is no longer needed, you can remove it by calling the method `removeEntity`.
```typescript
// Adding
engine.addEntity(entityA);
engine.addEntity(entityB, entityC);
// Removing
engine.removeEntity(entityA);
engine.removeEntity(entityB, entityC);
```
### Systems
#### Creating
A system can be created by calling the method `createSystem`.
```typescript
@EcsSystem()
class SystemA {}
const system = Ecs.createSystem(SystemA)
```
You can pass constructor parameters as the next parameters in the method `createSystem`.
```typescript
const system = Ecs.createSystem(SystemA, paramA, paramB);
```
#### Bind another system
For easier access to another system, you can bind it to a property using the decorator `EcsBindSystem`.
```typescript
@EcsSystem()
class SystemA extends EcsSystem {
@EcsBindSystem(SystemB)
public readonly systemB: SystemB;
}
```
By default, the engine throws an error if the system is not available when the system is added to the engine. You can set the required flag to false to make the system optional.
```typescript
@EcsSystem()
class SystemA extends EcsSystem {
@BindSystem(SystemB, false)
public readonly systemB?: SystemB;
}
```
##### Inject another system
Alternatively, it is possible to assign the system via the inject function. However,
```typescript
@EcsSystem()
class SystemA extends EcsSystem {
public readonly systemA = injectSystem(SystemA); // SystemA | undefined
public readonly systemB = injectSystem.required(SystemB); // SystemB
}
```
:::warning
`injectSystem` function has its limitation and the system needs to be created by the `EcsEngine.createSystem` method
:::
#### Accessing engine
Sometimes you also want to access the engine. You can achieve it easily by using the `EcsBindEngine` decorator and bind the engine to some property.
```typescript
@EcsSystem()
class SystemA extends EcsSystem {
@BindEngine()
public readonly engine: EcsEngine;
}
```
The property is dynamic so its value is undefined when the system hasn't been added to the engine yet.
```typescript
const system = Ecs.createSystem(SystemA);
console.log(system.engine); // undefined;
engine.addSystem(system);
console.log(system.engine); // EcsEngine;
engine.removeSystem(system);
console.log(system.engine); // undefined;
```
#### Inject engine
For the bind engine there is also an alternate
```typescript
@EcsSystem()
class SystemA extends EcsSystem {
public readonly engine = injectEngine()
}
```
:::warning
`injectEngine` function is static so it doesn't change when system is removed from engine. Also system using this function must be created using `EcsEngine.createSystem` method
:::
#### Hooks
Systems provide multiple hooks to handle different engine actions.
##### System events
```typescript
@EcsSystem()
class System extends EcsSystem {
public onAddToEngine(engine: EscEngine): void {}
public onRemoveFromEngine(engine: EscEngine): void {}
}
```
##### Entity events
```typescript
@EcsSystem()
class System extends EcsSystem {
public onEntityAddedToEngine(entity: EcsEntityInstance): void {}
public onEntityRemovedFromEngine(entity: EcsEntityInstance): void {}
}
```
#### Iterate over entities
You can implement the `processEntity` method for accessing each entity in each tick.
```typescript
@EcsSystem()
class System extends EcsIteratingSystem {
public processEntity(entity: EcsEntity, delta: number): void {}
}
```
If you need to have entities sorted, pass the `sorter` parameter into the property decorator.
```typescript
@EcsSystem({
sorter: (a, b) => 0
})
```
And if the sorter is, for example, an instance parameter, you can pass it as a property and define its name in the `EcsSystem` decorator.
```typescript
@EcsSystem({
sorter: "customSorter",
})
class System extends EcsSystem {
public constructor(
public readonly customSorter: (a: EcsEntity, b: EcsEntity) => number,
) {}
public processEntity(entity: EcsEntity, delta: number): void {}
}
```
#### Interval
If you don't want to update the system every tick, you can use the `interval` parameter. You need to implement the `intervalUpdate` method instead of `update`. The defined value means the number of milliseconds between calling the `intervalUpdate` method.
```typescript
@EcsSystem({
interval: 1000,
})
class System extends EcsIteratingSystem {
public intervalUpdate(index: number): void {
...
}
}
```
### Engine
#### Basic usage
```typescript
const engine = Ecs.createEngine();
engine.addSystem(systemA);
engine.addSystem(systemB);
engine.start(fps);
setTimeout(() => engine.stop(), duration);
// or use shortcut
engine.startFor(fps, duration)
// or start conditionaly
engine.startWhile(fps, check); // check: (ticks, elapsedTime) => boolean;
// or only one tick
engine.update(delta);
engine.removeSystem(systemA);
engine.removeSystem(SystemB);
```
#### Custom engine
```typescript
class MyEngine extends EcsEngine {
}
const myEngine = Ecs.createEngine(MyEngine);
// If you need pass some parameter use class decorator
@EcsCustomEngine({
...
})
class MyEngine extends EcsEngine {}
```
### Components
#### Dependencies
Components can have implicit dependencies defined in the `EcsComponent` decorator.
```typescript
@EcsComponent([ComponentTwo]) // or @EcsComponent({dependencies: [ComponentTwo]})
class ComponentOne {
}
```
Each component has a reserved property `entity`. This property can be used to access the current component's entity.
```typescript
@EcsComponent()
class ComponentOne {
public readonly entity: EcsEntityInstance;
// this.entity.getComponent(ComponentOne) === this;
}
```
~~You can add key to the component to make them accessible easier~~
```typescript
@EcsComponent()
class ComponentOne {
public readonly key: "CompOne";
}
@EcsSystem()
class System {
@BindFamily([ComponentA])
public family: EcsFamily;
public update(delta: number): void {
this.family.iterareEntries((entry) => {
entry.entity; // entity
entry.CompOne; // ComponentOne
entry.entity.getComponent(ComponentOne); // ComponentOne
})
}
}
```
### Families
Families are collections of entities with specific components.
#### ~~Default family~~
```typescript
@EcsSystem({
family: {require: [Component]}
})
class SystemA extends EcsSystem {
public readonly family: EcsFamily;
}
```
#### ~~Custom family name~~
```typescript
@EcsSystem({
family: {
require: [Component],
propertyName: "components"
}
})
class SystemA extends EcsSystem {
public readonly components: EcsFamily;
}
```
#### EcsBindFamily decorator
```typescript
@EcsSystem()
class SystemA extends EcsSystem {
@EcsBindFamily([Component])
public readonly componentsB: EcsFamily;
@EcsBindFamily({require: [ComponentA]})
public readonly componentsA: EcsFamily;
}
```
#### Iterating entities
Default
```typescript
@EcsSystem()
class SystemA extends EcsSystem {
@EcsBindFamily([Component])
public readonly components: EcsFamily;
public update(delta: number): void {
this.components.entities.forEach(entity => entity.update(delta: number));
}
}
```
If you require some component from entities, you should use the method `iterateEntities`.
```typescript
@EcsSystem()
class System extends EcsSystem {
@EcsBindFamily([ComponentA, ComponentB])
public readonly components: EcsFamily;
public update(delta: number): void {
this.components.iterateEntities(
[ComponentB, ComponentA],
(entity, componentB, componentA) => {
...
}
);
}
}
```
#### Cardinalities
You can set cardinalities for families.
```typescript
@EcsSystem()
class SystemA extends EcsSystem {
@BindFamily({
require: [ComponentA],
minCount: 3,
maxCount: 5,
})
public readonly componentsA: EcsFamily;
@BindFamily({
require: [ComponentB],
count: 4,
})
public readonly componentsB: EcsFamily;
}
```
## Future
### EntityPrototype
```typescript
@EcsComponent()
class HealthComponent {
public constructor(
public readonly maxHealth: number
) {}
}
@EcsEntity()
export class Entity {
public readonly health: HealthComponent;
public constructor(maxHealth: number) {
this.health = createComponent(HealthComponent, maxHealth);
}
}
// alternative
@EcsEntity()
export class Entity {
public readonly health = createComponent(HealthComponent, this.initialMaxHealth);
public constructor(private readonly initialMaxHealth: number) {}
}
@EcsEntity()
export class Entity {
private readonly health = injectComponent(HealthComponent);
public static prepare(health: number): void {
createComponent(HealthComponent, health)
}
public constructor() {}
}
```
https://www.typescriptlang.org/play/?#code/JYOwLgpgTgZghgYwgAgCoE8AOEA8rkC8yAShAgPZQAmOAzmFKAOYA0yAriANYjkDuIAHyDkAbwBQyKchBwAthAD8ALmT1GIJgG5xk6SAh8AFADozcKE1qq4IdAG0AugEpVqHQF9xYLCgCSIPS2YMBwAEYANihEoqBB4KGQqqZmmBby1si2Di6EIgBu5MBUHjo+2MgB8SHhUQDymCHkshGEYnFgwYlKyWYmaVAZNnZOznnIhcWluuIIEXC0tMgAgmJ6UpjskcAIyFAQcFTNEejI5dHIAERVnSBIl+Jes-OLyABCUhKPunMLSwDCyAgAA9ICAqEtVl8vL9XgARIGgiDgyFraTITbbXYUQIMdgIMCUIyYiI7PYHI4gE4yeQQVTqZhjCTo9G0djYKBGZzrZBeGEvJYAUURYIhKzR0hJZKCIV2HS6cEgRjgTDpMnYcjC0FcEyKVDWPK86Kl2Oa6nxhM5JvJh2OpyoirgqlE+1tVNOsgU9IYzA8TJ5rPZ0C5hu+ME4BOAzWQCFdkButiQOD8kDkIuRYuWgiM5zcvmTqcEOpTEDTzOk+zA7CgIBkhjOvi5nnE4buTVrsYO8dxiYgAHVgGAABYABXScloywLpfTKLQ+c4PH4QmzucqqbYfQGQ2Q-zNeIJlDHgwUkCgtGnciLqgTdwgGGwl5E5aklertYMfAb2BS-XHtDGBZKh7O8H1wEsr2bVtI2jTtFQgW8kAHYdjwyN5L1nMUwJwRdeAEEQADJgOqUJIggVdfBvDdkC3f9VFQ09oAvCD7EueUEngy5HGvYjbiQbCIOfHk3xrOsv3OX9twnQClkQ+980E5sWwjdsYzjBCQKQwdR3-f4MJBUUlmw3Dl0I3iFTIijsCo0tN1SOjkAYiAz2Y1NWPYmpIC4ni5IEwtlLbKMO3UuTkJ0k9aD0iDMKMhduDwlcc0o9dbJo+yItUPdcSgC0j3HZymKfYtNPkx9BOQAKYOCrsNPiLSUN0-SkTnYz4tMqQiNvGoyIads4AibMeTXCC2B5WiMt3fccsPKAnJcp9xGKurSvAwsJVfZz3zE78IEk-8ZPM0CFMLZscXoLI2jg7tlqMZZnB0M6wGQMJLpCkqjDee7Zn3GNXpquSjH+L7HuQfUiCu2q+P7bSGMnIw4TYS4AFl0AAOVpS5gZ+i4IdCmH-zeIxBTYAAmABWL6fh+yB6FWcG3uWsLYf+eHEZRmkFExh7qYgegPnp-6SqZ3SidJimtCAA