ECS

Excalibur has a built in Entity Component System (ECS for short), which is a popular software technique in the video game industry for managing behavior in a game in a composable and reusable way. Many ECS implementations also focus on memory cache optimization techniques, that is not a goal of the current Excalibur implemenation.

What is an Entity-Component-System

The wikipedia article does a pretty good job of explaining the design pattern, but here is the gist:

  • Entity - Any "thing" in the game that has behavior, it has an id place to to put components.
  • Component - Any state for a particular entity, generally these are state only, but can contain small amounts of logic
  • System - Implementation of behavior given a set of components on an entity

Many of the features built in Excalibur are built using this, and users of Excalibur can make their own systems and/or replace existing excalibur systems.

Excalibur ECS Implementation

In addition to the familiar Entity, Component, and System, Excalibur also implements an ECS World where all of these pieces live and can interact together. Each Scene in Excalibur contains an ECS world, they are not shared between scenes.

Inside an ECS World there is an EntityManager, QueryManager, and SystemManager that handle all the details.

  • EntityManager - Manages all the entities known by the world, it is an Observer of entity component changes over time.
  • QueryManager - Manages all the entity queries, a Query allows a search of all known entities by a set of component types.
  • SystemManager - Manages all the systems known by the world, it updates a System in priority order (low to high) and gives them a set of entities that match types.

Excalibur Actor's are in fact Entity's in this ECS implementation. Actors come "pre-installed" with a set of components and features.

Built in Excalibur Components

TagComponent

Tag components are useful when you want to flag an entity with something, for example "offscreen". A good way to think about it is to use a flag for a non-default state.

const entity = new ex.Entity()
entity.addComponent(new ex.TagComponent('offscreen'))

console.log(entity.tags) // ['offscreen']

TransformComponent

The Excalibur TransformComponent component primarily keeps track of the z-index position, rotation, and scale of an entity.

Additionally it keeps track of the coordinate plane which is whether it draws to the CoordPlane.Screen or part of the CoordPlane.World. The default is drawing in CoordPlane.World which shifts the entities in "world space" by the position of the current Camera If you are drawing in CoordPlane.Screen the entity is not influenced by the camera, for example an entity at (0, 0) will always be at the top left of the screen regardless of what happens to the current Camera.

CanvasDrawComponent

The Excalibur CanvasDrawComponent component tells CanvasDrawingSystem how to draw to a HTML 2D Canvas.

const entity = new ex.Entity()
entity.addComponent(new TransformComponent())
entity.addComponent(
  new CanvasDrawComponent((ctx, delta) => {
    // draw stuff
    ctx.fillStyle = 'black'
    ctx.fillRect(0, 0, 200, 200)
  })
)

Built in Excalibur Systems

CanvasDrawingSystem

The Excalibur CanvasDrawingSystem takes any entity with a TransformComponent and a CanvasDrawComponent and draws it to the canvas using the 2D canvas api.

Implementing your own Components & Systems

To build your own component, extend the Excalibur Component abstract class and pick a unique type name (duplicate type names will cause problems).

For custom component types it is recommended you prefix your types, like type = 'myCustomPrefixTransform'

In this example, we create a "search" type component, that searches for a target position. Notice how this component implementation is mostly data.

class SearchComponent extends ex.Component<'search'> {
  public readonly type = 'search'
  constructor(public target: ex.Vector) {}
}

Actor comes prebuilt with other components and functionality, or build an empty Entity from scratch! In this example we use actor and add our new component in the onInitialize.

class Explorer extends ex.Actor {
  onInitialize() {
    // Excalibur Actors already has some components pre-installed
    // Like this one that keeps track of the position -> `this.addComponent(new ex.TransformComponent);`
    // And this one that knows how to draw -> `this.addComponent(new ex.CanvasDrawComponent((ctx, delta) => this.draw(ctx, delta)));`

    // Add your new component in
    this.addComponent(new SearchComponent(ex.vec(100, 100)))
  }
}

To implement this behavior as a system, extend the Excalibur System abstract class and pick the types this system should operate on.

class SearchSystem extends ex.System<
  ex.TransformComponent | SearchAIComponent
> {
  // Types need to be listed as a const literal
  public readonly types = ['transform', 'search'] as const

  // Run this system in the "update" phase
  public systemType = System.Update

  private _searchSpeed = 10 // pixels/sec

  public update(
    entities: Entity<ex.TransformComponent | SearchComponent>[],
    delta: number
  ) {
    for (let entity of entities) {
      const target = entity.components.search.target
      // ex.TransformComponent is a built in type
      const transform = entity.components.transform

      const motion = target.sub(transform.pos).size(this._searchSpeed)

      // Moves these entities towards the target at 10 pixels per second
      transform.pos = transform.pos.add(motion.scale(delta / 1000))
    }
  }
}

Any entity that has the new component attached will be processed by the new system once added to the world!

class MainLevel extends ex.Scene {
  onInitialize(engine: ex.Engine) {
    // Excalibur has some systems built in
    // If you want to opt out and define your own `this.world.clearSystems()` will remove them

    // Setup your new system in the ECS world and any others you want
    this.world.add(new SearchSystem())

    // Add your actors
    this.add(new Explorer())
  }
}