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 [[TransformComponent.z|z-index]] [[TransformComponent.pos|position]], [[TransformComponent.rotation|rotation]], and [[TransformComponent.scale|scale]] of an entity.

Additionally it keeps track of the [[TransformComponent.coordPlane|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)
  })
)

Systems

Excalibur has a few built in systems that can be used

[[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())
  }
}