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