Input

Excalibur offers several modes of input for your games.

The Engine.input property can be inspected during Actor.update or other areas of the game. This allows you to respond to any type of user input without writing complex input event code yourself.

Learn more about Keyboard, Mouse and Touch, and Controller support.

Inspecting engine input

Access Engine.input to see if any input is being tracked during the current update frame.

class Player extends ex.Actor {
  public update(engine, delta) {
    if (
      engine.input.keyboard.isKeyDown(ex.Input.Keys.W) ||
      engine.input.gamepads.at(0).getAxes(ex.Input.Axes.LeftStickY) > 0.5
    ) {
      player._moveForward()
    }
  }
}

Keyboard

Keyboard input is accessible through engine.input.keyboard. You can inspect whether a button was just pressed or released this frame, or if the key is currently being held down. Common keys are held in the Keys enumeration but you can pass any character code to the methods.

Excalibur subscribes to the browser events and keeps track of what keys are currently held, released, or pressed. A key can be held for multiple frames, but a key cannot be pressed or released for more than one subsequent update frame.

Inspecting the keyboard

You can inspectengine.input.keyboard to see what the state of the keyboard is during an update.

It is recommended that keyboard actions that directly effect actors be queried, instead of subscribed to:

class Player extends ex.Actor {
  public update(engine, delta) {
    if (
      engine.input.keyboard.isHeld(ex.Input.Keys.W) ||
      engine.input.keyboard.isHeld(ex.Input.Keys.Up)
    ) {
      player._moveForward()
    }

    if (engine.input.keyboard.wasPressed(ex.Input.Keys.Right)) {
      player._fire()
    }
  }
}

Checking whether keys are pressed or released during the update frame makes your game logic easier to follow and is less prone to tracking bugs since Excalibur tracks keyboard input on your behalf.

Keyboard Events

If you need more complex logic or if you need to be notified when input was processed, you can subscribe to keyboard events through engine.input.keyboard.on. A KeyEvent object is passed to your handler which offers information about the key that was part of the event.

  • press - When a key was just pressed this frame
  • release - When a key was just released this frame
  • hold - Whenever a key is in the down position
engine.input.keyboard.on("press", (evt: KeyEvent) => {...});
engine.input.keyboard.on("release", (evt: KeyEvent) => {...});
engine.input.keyboard.on("hold", (evt: KeyEvent) => {...});

Mouse and Touch

Excalibur handles mouse and touch input using a Pointers API that closely follows the W3C Pointer Events spec. Excalibur normalizes mouse and touch events to a PointerEvent that your game can subscribe to and handle (engine.input.pointers`).

There is always at least one Pointer available (Pointers.primary) and you can request multiple pointers to support multi-touch scenarios.

Since Pointers.primary normalizes both mouse and touch events, your game automatically supports touch for the primary pointer by default. When you handle the events, you can customize what your game does based on the type of pointer, if applicable.

For performance reasons,

actors do not automatically capture pointer events

until they are opted-in

.

Pointer Events

You can subscribe to pointer events through engine.input.pointers.on. A PointerEvent object is passed to your handler which offers information about the pointer input being received.

  • down - When a pointer is pressed down (any mouse button or finger press)
  • up - When a pointer is lifted
  • move - When a pointer moves (be wary of performance issues when subscribing to this)
  • cancel - When a pointer event is canceled for some reason
engine.input.pointers.primary.on('down', function (evt) {})
engine.input.pointers.primary.on('up', function (evt) {})
engine.input.pointers.primary.on('move', function (evt) {})
engine.input.pointers.primary.on('cancel', function (evt) {})

Wheel Event

You can also subscribe to the mouse wheel event through engine.input.pointers.on. A WheelEvent object is passed to your handler which offers information about the wheel event being received.

  • wheel - When a mousewheel is activated (trackpad scroll or mouse wheel)
engine.input.pointers.on('wheel', function (evt) {})

Last position querying

If you don't wish to subscribe to events, you can also access the Pointer.lastPagePos, Pointer.lastScreenPos or Pointer.lastWorldPos coordinates (Vector) on the pointer you're targeting.

engine.input.pointers.primary.lastPagePos
engine.input.pointers.primary.lastScreenPos
engine.input.pointers.primary.lastWorldPos

Note that the value may be null if the Pointer was not active the last frame.

Pointer scope (window vs. canvas)

You have the option to handle all pointer events in the browser by setting EngineOptions.pointerScope to PointerScope.Document. If this is enabled,

Excalibur will handle every pointer event in the browser. This is useful for handling complex input and having control over every interaction.

You can also use PointerScope.Canvas to only scope event handling to the game canvas. This is useful if you don't care about events that occur outside the game.

One real-world example is dragging and gestures. Sometimes a player will drag their finger outside your game and then into it, expecting it to work. If PointerScope is set to Canvas this will not work. If it is set to Document, it will.

Responding to input

The primary pointer can be a mouse, stylus, or single finger touch event. You can inspect what type of pointer it is from the PointerEvent handled.

engine.input.pointers.primary.on('down', function (pe) {
  if (pe.pointerType === ex.Input.PointerType.Mouse) {
    ex.Logger.getInstance().info('Mouse event:', pe)
  } else if (pe.pointerType === ex.Input.PointerType.Touch) {
    ex.Logger.getInstance().info('Touch event:', pe)
  }
})

Multiple Pointers (Multi-Touch)

When there is more than one pointer detected on the screen, this is considered multi-touch. For example, pressing one finger, then another, will create two pointers. If you lift a finger, the first one remains and the second one disappears.

You can handle multi-touch by subscribing to however many pointers you would like to support. If a pointer doesn't yet exist, it will be created. You do not need to check if a pointer exists. If it does exist, it will propagate events, otherwise it will remain idle.

Excalibur does not impose a limit to the amount of pointers you can subscribe to, so by all means, support all 10 fingers.

Note: There is no way to identify touches after they happen; you can only know that there are n touches on the screen at once.

function paint(color) {
  // create a handler for the event
  return function (pe) {
    if (pe.pointerType === ex.Input.PointerType.Touch) {
      engine.canvas.fillStyle = color
      engine.canvas.fillRect(pe.x, pe.y, 5, 5)
    }
  }
}
engine.input.pointers.at(0).on('move', paint('blue')) // 1st finger
engine.input.pointers.at(1).on('move', paint('red')) // 2nd finger
engine.input.pointers.at(2).on('move', paint('green')) // 3rd finger

Actor pointer events

By default, actors do not participate in pointer events. In other words, when you "click" an Actor, it will not throw an event for that Actor, only a generic pointer event for the game. This is to keep performance high and allow actors to "opt-in" to handling pointer events. Actors will automatically opt-in if a pointer related event handler is set on them actor.on("pointerdown", () => {}) for example.

To opt-in manually, set Actor.enableCapturePointer to true and the Actor will start publishing pointerup and pointerdown events. pointermove events will not be published by default due to performance implications. If you want an actor to receive move events, set CapturePointerConfig.captureMoveEvents to true.

Actor pointer events will be prefixed with pointer.

const player = new ex.Actor()
// enable propagating pointer events
player.enableCapturePointer = true
// enable move events, warning: performance intensive!
player.capturePointer.captureMoveEvents = true
// subscribe to input
player.on('pointerup', function (ev) {
  player.logger.info('Player selected!', ev)
})

Actor Events

Actors have the following extra events you can subscribe to:

  • pointerenter - When a pointer enters the bounds of an actor
  • pointerleave - When a pointer leaves the bounds of an actor
  • pointerdragstart - When a pointer starts a drag on an actor
  • pointerdragmove - When a pointer drags an actor
  • pointerdragend - When a pointer ends a drag on an actor

Gamepads and Controllers

You can query any Gamepads that are connected or listen to events ("button" and "axis").

You must opt-in to controller support (Gamepads.enabled) because it is a polling-based API, so we have to check it each update frame. If an gamepad related event handler is set, you will automatically opt-in to controller polling.

HTML5 Gamepad API only supports a maximum of 4 gamepads. You can access them using the Gamepads.at method. If a Gamepad is not connected, it will simply not throw events.

Gamepad Filtering

Different browsers/devices are sometimes loose about the devices they consider Gamepads, you can set minimum device requirements with engine.input.gamepads.setMinimumGamepadConfiguration so that undesired devices are not reported to you (Touchpads, Mice, Web Cameras, etc.).

// ensures that only gamepads with at least 4 axis and 8 buttons are reported for events
engine.input.gamepads.setMinimumGamepadConfiguration({
  axis: 4,
  buttons: 8,
})

Gamepad Events

You can subscribe to gamepad connect and disconnect events through engine.input.gamepads.on.

A GamepadConnectEvent or GamepadDisconnectEvent will be passed to you.

  • connect - When a gamepad connects it will fire this event and pass a GamepadConnectEvent with a reference to the gamepad.
  • disconnect - When a gamepad disconnects it will fire this event and pass a GamepadDisconnectEvent

Once you have a reference to a gamepad you may listen to changes on that gamepad with .on. A GamepadButtonEvent or GamepadAxisEvent will be passed to you.

  • button - Whenever a button is pressed on the game
  • axis - Whenever an axis
engine.input.gamepads.on('connect', (ce: ex.Input.GamepadConnectEvent) => {
  const newPlayer = CreateNewPlayer() // pseudo-code for new player logic on gamepad connection
  console.log('Gamepad connected', ce)
  ce.gamepad.on('button', (be: ex.GamepadButtonEvent) => {
    if (be.button === ex.Input.Buttons.Face1) {
      newPlayer.jump()
    }
  })

  ce.gamepad.on('axis', (ae: ex.GamepadAxisEvent) => {
    if (ae.axis === ex.Input.Axis.LeftStickX && ae.value > 0.5) {
      newPlayer.moveRight()
    }
  })
})

Responding to button input

Gamepad buttons typically have values between 0 and 1, however depending on the sensitivity of the controller, even if a button is idle it could have a very tiny value. For this reason, you can pass in a threshold to several methods to customize how sensitive you want to be in detecting button presses.

You can inspect any connected Gamepad using Gamepad.isButtonPressed, Gamepad.getButton, or you can subscribe to the button event published on the Gamepad which passes a GamepadButtonEvent to your handler.

// enable gamepad support
engine.input.gamepads.enabled = true
// query gamepad on update
engine.on('update', function (ev) {
  // access any gamepad by index
  if (engine.input.gamepads.at(0).isButtonPressed(ex.Input.Buttons.Face1)) {
    ex.Logger.getInstance().info('Controller A button pressed')
  }
  // query individual button
  if (engine.input.gamepads.at(0).getButton(ex.Input.Buttons.DpadLeft) > 0.2) {
    ex.Logger.getInstance().info('Controller D-pad left value is > 0.2')
  }
})
// subscribe to button events
engine.input.gamepads.at(0).on('button', function (ev) {
  ex.Logger.getInstance().info(ev.button, ev.value)
})

Responding to axis input

Gamepad axes typically have values between -1 and 1, but even idle sticks can still propagate very small values depending on the quality and age of a controller. For this reason, you can set Gamepads.MinAxisMoveThreshold to set the (absolute) threshold after which Excalibur will start publishing axis events. By default it is set to a value that normally will not throw events if a stick is idle. You can query axes via Gamepad.getAxes or by subscribing to the axis event on Gamepad which passes a GamepadAxisEvent to your handler.

// enable gamepad support
engine.input.gamepads.enabled = true;
// query gamepad on update
engine.on('update', function(ev) {
  // access any gamepad by index
  const axisValue =  = engine.input.gamepads.at(0).getAxes(ex.Input.Axes.LeftStickX));
  if (axisValue > 0.5) {
    ex.Logger.getInstance().info('Move right', axisValue);
  }
});
// subscribe to axis events
engine.input.gamepads.at(0).on('axis', function(ev) {
  ex.Logger.getInstance().info(ev.axis, ev.value);
});