Actors

Basic actors

For quick and dirty games, you can just create an instance of an Actor and manipulate it directly.

Actors (and other entities) must be added to a Scene to be drawn and updated on-screen.

const player = new ex.Actor()

// move the player
player.vel.x = 5

// add player to the current scene
game.add(player)

game.add is a convenience method for adding an actor to the current scene. The equivalent verbose call is game.currentScene.add.

Custom actors

For "real-world" games, you'll want to extend the Actor class. This gives you much greater control and encapsulates logic for that actor.

class Player extends ex.Actor {
  public health: number = 100
  public ammo: number = 20

  constructor() {
    super({ x: 10, y: 10 })
  }

  shoot() {
    if (this.ammo < 1) {
      return
    }

    this.ammo -= 1
  }
}

Custom actors make it easier to hook into the actor lifecycle and encapsulate the actor's state better than a basic actor.

Actor lifecycle

An actor has a basic lifecycle that dictates how it is initialized, updated, and drawn. Once an actor is part of a scene, it will follow this lifecycle.

Actor Lifecycle

Updating actors

In most games, things are happening on screen: the background is parallaxing, your hero responds to input, or enemies shoot bullets. In Excalibur, the logic that updates game state is run during the update loop. Actors are a way to encapsulate that logic, such as a Player or Enemy or MenuButton. Actors can be pretty much anything!

Initialization

You should override the Actor.onInitialize method to perform any startup logic for an actor (such as configuring state). onInitialize gets called once before the first frame an actor is drawn/updated. It is passed an instance of Engine to access global state or perform coordinate math.

This is the recommended way to manage startup logic for actor, not the constructor since you don't incur the cost of initialization until an actor is ready to be updated in the game.

class Player extends ex.Actor {
  public level = 1
  public endurance = 0
  public fortitude = 0

  constructor() {
    super({ x: 50, y: 50 })
  }

  public onInitialize(engine: ex.Engine) {
    this.endurance = 20
    this.fortitude = 16
  }

  public getMaxHealth() {
    return 0.4 * this.endurance + 0.9 * this.fortitude + this.level * 1.2
  }
}

There are three ways to hook into the update loop of an actor: Actor.onPreUpdate, Actor.update and Actor.onPostUpdate. Actors (and other entities in Excalibur) all have "core" logic that runs in the update or draw loop. The pre- and post-method hooks allow you to choose when you want to run logic in each phase. Normally you will run logic in the "post" hook but sometimes you may want to completely override the core logic or run logic that uses state that was updated before the core logic runs.

All update methods are passed an instance of the Excalibur engine, which can be used to perform coordinate math or access global state. It is also passed delta which is the time in milliseconds since the last frame, which can be used to perform time-based movement or time-based math (such as a timer).

Reference Actor lifecycle for a breakdown of each phase and when things are executed.

Pre-update

Override the Actor.onPreUpdate method to update the state of your actor before Actor.update.

Important: This logic will run before the core Excalibur update logic runs, so you may not have the latest transform matrix applied or other positional information updated. Essentially you will be working with the last frame's state.

class Player extends Actor {
  /**
   * Runs before "core" update logic, before this frame is updated
   */
  public onPreUpdate(engine: ex.Engine, delta: number) {
    // update velocity
    this.vel.setTo(-1, 0)
  }
}

Core update

Actor.update is the core update logic to prepare the frame. This is an advanced method override. You can take over the whole update loop by overriding this method. Use super.update(engine, delta) to invoke the core logic or leave it out to completely customize the update logic.

/**
 * DANGER: This is for advanced users to totally override update logic.
 */
public update(ctx: CanvasRenderingContext2D, delta: number) {

  // Invoke the core logic
  // Or leave it out to completely override
  super.update(engine, delta);

  // Perform custom update logic
  // ...
}

Post-update

Actor.onPostUpdate is called after Actor.update to prepare state for the next frame. Things that need to be updated include state, drawing, or position.

This is the recommended method to override for adding update logic to your actors since it runs after Excalibur has done all the update logic for the frame and before things get drawn to the screen.

class Player extends Actor {
  /**
   * RECOMMENDED: Runs after "core" update logic, before the next frame.
   * Usually this is what you want!
   */
  public onPostUpdate(engine: ex.Engine, delta: number) {
    // check if player died
    if (this.health <= 0) {
      this.kill()
      return
    }
  }
}

Drawing actors

Actors by default have no associated drawings, meaning that they will be rendered without any graphics unless you've assigned a default Actor.color or added a drawing. If an actor has a color set, it will draw a box in that color. This is useful only at the beginning of development when you're just tinkering but for most games you'll need to add sprites, animations, and other drawings.

Working with textures & sprites

Think of a texture as the raw image file that will be loaded into Excalibur. In order for it to be drawn it must be converted to a Sprite.

A common usage is to load a Texture and convert it to a sprite for an actor. If you are using the asset loader to pre-load assets, you can use Actor.addDrawing to add a texture directly, which will internally convert it to a sprite on your behalf. You can also create a sprite from a Texture to create a sprite instance to pass to addDrawing.

// assume Resources.TxPlayer is a 80x80 png image

public onInitialize(engine: ex.Engine) {

   // set as the "default" drawing
   this.addDrawing(Resources.TxPlayer);
}

Working with animations

A sprite sheet holds a collection of sprites from a single Texture. Use SpriteSheet.getAnimationForAll to quickly generate an Animation.

// assume Resources.TxPlayerIdle is a texture containing several frames of an animation

public onInitialize(engine: ex.Engine) {

   // create a SpriteSheet for the animation
   const playerIdleSheet = new ex.SpriteSheet(Resources.TxPlayerIdle, 5, 1, 80, 80);

   // create an animation
   const playerIdleAnimation = playerIdleSheet.getAnimationForAll(engine, 120);

   // the first drawing is always the current
   this.addDrawing("idle", playerIdleAnimation);
}

When adding an animation, you may want to have an "idle" or static animation added first (or you can explicitly set the current drawing using Actor.setDrawing during initialization).

Drawing hooks

Like the update loop, the draw loop has hooks you can override to perform custom drawing. Override the Actor.onPreDraw, Actor.draw, or Actor.onPostDraw methods to customize the draw logic at different points in the loop.

Reference Actor lifecycle for a breakdown of each phase and when things are executed.

When using the drawing hooks you can draw complex shapes or to use the raw Canvas API.

Pre-draw

Actor.onPreDraw is run before the core draw logic to prepare the frame.

Important: This runs before Excalibur has run all its draw logic to apply effects, transform information, etc. so you essentially are working with the last frame's draw state.

/**
 * ADVANCED: This is run before Actor.draw core logic.
 */
public onPreDraw(ctx: CanvasRenderingContext2D, delta: number) {
   // custom drawing
   ctx.lineTo(...);
}

Core draw

Actor.draw is the core draw logic to prepare the frame. This is an advanced method override. You can take over the whole draw loop by overriding this method. Use super.draw(ctx, delta) to invoke the core logic or leave it out to completely customize the draw logic.

/**
 * DANGER: This is for advanced users to totally override draw logic.
 */
public draw(ctx: CanvasRenderingContext2D, delta: number) {

  // Invoke the core logic
  // Or leave it out to completely override
  super.draw(ctx, delta);

  // custom drawing
  ctx.lineTo(...);
}

Post-draw

Actor.onPostDraw is run after Actor.draw and will draw in the current frame.

This is the recommended method to override since Excalibur has run its core draw logic and you can now customize what gets drawn during the current frame.

/**
 * RECOMMENDED: This is run at the end of the draw loop. Usually
 * this is what you want!
 */
public onPostDraw(ctx: CanvasRenderingContext2D, delta: number) {

  /* perform custom draw */
  ctx.lineTo(...);
}

Debug draw

Actor.debugDraw provides a way to customize the way Excalibur shows actors when debugging is enabled. Override it to add or replace any draw logic with your own:

/**
 * This is run at the end of the draw loop
 */
public debugDraw(ctx: CanvasRenderingContext2D) {

   // Call core logic
   super.debugDraw(ctx, delta);

   // Add custom debug drawing logic
   ctx.lineTo(...);
}

Adding actors to the scene

For an Actor to be drawn and updated, it needs to be part of the "scene graph". The Engine provides several easy ways to quickly add/remove actors from the current scene.

const game   = new ex.Engine(...);
const player = new ex.Actor();
const enemy  = new ex.Actor();
// add them to the "root" scene
game.add(player);
game.add(enemy);
// start game
game.start();

You can also add actors to a Scene instance specifically.

const game = new ex.Engine()
const level1 = new ex.Scene()
const player = new ex.Actor()
const enemy = new ex.Actor()
// add actors to level1
level1.add(player)
level1.add(enemy)
// add level1 to the game
game.add('level1', level1)
// start the game
game.start()
// after player clicks start game, for example
game.goToScene('level1')

Collision Detection

By default Actors do not participate in collisions. If you wish to make an actor participate, you need to switch from the default prevent collision to active, fixed, or passive collision type.

For more information on collisions, see Physics.

public Player extends ex.Actor {
   constructor() {
      super();
      // set preferred CollisionType
      this.body.collider.type = ex.CollisionType.Active;
   }
}

// or set the collisionType

const actor = new ex.Actor();
actor.body.collider.type = ex.CollisionType.Active;

Traits

Traits describe actor behavior that occurs every update. If you wish to build a generic behavior without needing to extend every actor you can do it with a trait, a good example of this may be plugging in an external collision detection library like Box2D or PhysicsJS by wrapping it in a trait. Removing traits can also make your actors more efficient.

Default traits provided by Excalibur are pointer capture, tile map collision, and offscreen culling.

Actions

Actions can be chained together and can be set to repeat, or can be interrupted to change.

Actor actions are available off of Actor.actions.

Chaining Actions

You can chain actions to create a script because the action methods return the context, allowing you to build a queue of actions that get executed as part of an ActionQueue.

class Enemy extends ex.Actor {
  public patrol() {
    // clear existing queue
    this.actions.clearActions()
    // guard a choke point
    // move to 100, 100 and take 1.2s
    // wait for 3s
    // move back to 0, 100 and take 1.2s
    // wait for 3s
    // repeat
    this.actions
      .moveTo(100, 100, 1200)
      .delay(3000)
      .moveTo(0, 100, 1200)
      .delay(3000)
      .repeatForever()
  }
}

Example: Follow a Path

You can use Actor.actions.moveTo to move to a specific point, allowing you to chain together actions to form a path.

This example has a Ship follow a path that it guards by spawning at the start point, moving to the end, then reversing itself and repeating that forever.

public Ship extends ex.Actor {
  public onInitialize() {
    const path = [
      new ex.Vector(20, 20),
      new ex.Vector(50, 40),
      new ex.Vector(25, 30),
      new ex.Vector(75, 80)
    ];
    // spawn at start point
    this.x = path[0].x;
    this.y = path[0].y;
    // create action queue
    // forward path (skip first spawn point)
    for (let i = 1; i < path.length; i++) {
      this.actions.moveTo(path[i].x, path[i].y, 300);
    }

    // reverse path (skip last point)
    for (let i = path.length - 2; i >= 0; i--) {
      this.actions.moveTo(path[i].x, path[i].y, 300);
    }
    // repeat
    this.actions.repeatForever();
  }
}

While this is a trivial example, the Action API allows complex routines to be programmed for Actors. For example, using the Tiled Map Editor you can create a map that uses polylines to create paths, load in the JSON using a Generic Resource, create a TileMap, and spawn ships programmatically while utilizing the polylines to automatically generate the actions needed to do pathing.

Custom Actions

The API does allow you to implement new actions by implementing the Action interface, but this will be improved in future versions as right now it is meant for the Excalibur team and can be advanced to implement.

You can manually manipulate an Actor's ActionQueue using Actor.actionQueue. For example, using ActionQueue.add for custom actions.

Future Plans

The Excalibur team is working on extending and rebuilding the Action API in future versions to support multiple timelines/scripts, better eventing, and a more robust API to allow for complex and customized actions.