Physics

Excalibur comes built in with two physics simulations.

  • "Arcade" style physics which is good for basic collision detection for non-rotated rectangular areas.
    • Example: platformers, tile based games, top down, etc
  • "Realistic" style physics which is good for rigid body games where realistic collisions are desired
    • Example: block stacking, angry bird's style games, etc

Arcade

Arcade physics simulation is on by default, but can be enabled explicitly at any time with ex.Physics.useArcadePhysics()

Physics.useArcadePhysics();

const game = new Engine({...});

Realistic

Realistic physics are not on by default, but can be enabled explicitly at any time with ex.Physics.useRealisticPhysics()

Physics.useRealisticPhysics();

const game = new Engine({...});

Example

ex.Physics.useRealisticPhysics();
ex.Physics.acc = ex.vec(0, 300);
const game = new ex.Engine({
    width: 600,
    height: 400,
    displayMode: ex.DisplayMode.FitScreen
});

const box = new ex.Actor({
    pos: ex.vec(game.halfDrawWidth, -100),
    width: 50,
    height: 50,
    rotation: Math.PI / 3,
    color: ex.Color.Red,
    collisionType: ex.CollisionType.Active
});

const trianglePoints = [ex.vec(-20, 20), ex.vec(0, -20), ex.vec(20, 20)];
const triangle = new ex.Actor({
    pos: ex.vec(game.halfDrawWidth, 100),
    rotation: Math.PI / 3,
    collider: new ex.PolygonCollider({points: trianglePoints}),
    collisionType: ex.CollisionType.Active
});
const triangleGraphic = new ex.Polygon({points: trianglePoints, color: ex.Color.Green});
triangle.graphics.use(triangleGraphic);

const circle = new ex.Actor({
    pos: ex.vec(game.halfDrawWidth + 20, -200),
    radius: 30,
    color: ex.Color.Yellow,
    collisionType: ex.CollisionType.Active
});

const ground = new ex.Actor({
    pos: ex.vec(game.halfDrawWidth, game.drawHeight),
    width: game.drawWidth,
    height: 100,
    color: ex.Color.DarkGray,
    collisionType: ex.CollisionType.Fixed
});
// start-snippet{collision}
game.start().then(() => {
    game.currentScene.add(box);
    game.currentScene.add(circle);
    game.currentScene.add(triangle);
    game.currentScene.add(ground);
    game.currentScene.camera.pos = ex.vec(game.halfDrawWidth, game.halfDrawHeight);
});

Other Physics Settings

See Physics documentation for more

  • Turn off excalibur physics ex.Physics.enabled = false
  • Global acceleration applied to active objects ex.Physics.acc = ex.vec(0, 100)
  • Fast moving object detection (continuous collision) ex.Physics.checkForFastBodies = true
  • Physics realistic passes, more passes mean higher quality simulation at the expense of cpu
    • Realistic solver position iterations, improves overlap resolution ex.Physics.positionIterations = 3
    • Realistic solver velocity iterations, improves response and stability ex.Physics.velocityIterations = 8

Using Physics with Actors or Entities

The physics simulation has 2 major pieces for actors

  1. BodyComponent - Controls the motion and qualities of the physics simulation
  2. ColliderComponent - Stores the geometry of any collider and performs overlap testing

Actors

Actors come out of the box with both of these components, an implicitly created BodyComponent and a ColliderComponent of a box that matches the specified width and height, or a circle

const actor = new ex.Actor({
  pos: ex.vec(200, 200),
  width: 100,
  height: 100,
  collisionType: ex.CollisionType.Active,
})

const builtInBox = actor.collider.get()

Entities

Reminder entities don't have anything pre-built, all Actors are Entities, but with the common built in features included.

const entity = new ex.Entity([
  new TransformComponent(),
  new BodyComponent(),
  new ColliderComponent(),
])

const tx = entity.get(TransformComponent)

const body = entity.get(BodyComponent)

const collider = entity.get(ColliderComponent)

Colliders

Colliders are abstractions over geometry in Excalibur, they implement the Collider interface and know how to detect intersecions with other colliders, test ray casts, check point containment, etc.

Colliders attached to an Entity will have a Entity.owner populated.

Keep in mind colliders are relative to the owner TransformComponent, they only represent geometry relative to an entity TransformComponent. Colliders can be shifted using the Collider.offset.

Colliders don't have a position in the world without an Actor/Entity with a position

CircleCollider

The CircleCollider defines a circular geometry and can be created and added to an actor

// Actors have a built in circle collider if radius is set
const actorWithCircleCollider = new ex.Actor({
  pos: ex.vec(5, 5),
  radius: 10,
})

// Alternatively you can define and set a collider yourself

const circle = new ex.CircleCollider({
  radius: 10, // 10 pixel radius
})

// or

const circle = ex.Shape.Circle(10) // 10 pixel radius

const actor = new ex.Actor({
  pos: ex.vec(100, 100),
  collider: circle,
})

// Change the collider afterwards
actor.collider.set(circle)

Box and PolygonCollider

The PolygonCollider allows the definition of arbitrary convex polygons. Convex meaning there are no dents in the shape, for example "Pac-Man" would be a non-convex shape.

A box collider is created using the PolygonCollider, there is a Shape.Box helper to build these.

// Actors have a built in box collider if width/height are set
const actorWithBoxCollider = new ex.Actor({
  pos: ex.vec(100, 100),
  width: 100,
  height: 10,
})

// Alternatively you can define and set a collider yourself
const box = ex.Shape.Box(100, 10)

const actor = new ex.Actor({
  pos: ex.vec(100, 100),
  collider: box,
})

A polygon collider can be defined using a set of points specified in clockwise order (also known as "winding").

const triangle = new ex.PolygonCollider({
  points: [ex.vec(-100, 0), ex.vec(0, -50), ex.vec(100, 0)],
})

EdgeCollider

An EdgeCollider can be used to define a single line collider.

They are useful for creating walls, barriers, or platforms in your game.

Edge points are relative to the Entity position, so begin of (0, 0) means it starts right on the top of the Entity TransformComponent

const edge = new ex.EdgeCollider({
  begin: ex.vec(0, 0),
  end: ex.vec(100, 0),
})

const actor = new ex.Actor({
  pos: ex.vec(100, 100),
  collider: edge,
})

CompositeCollider

CompositeCollider can be used to attach multiple colliders to an entity at once. This can be useful when generating complex shapes, like for example a capsule collider for a player Actor.

// Capsule Collider
const capsule = new ex.CompositeCollider([
  ex.Shape.Circle(10, ex.vec(0, -20)),
  ex.Shape.Box(20, 40),
  ex.Shape.Circle(10, ex.vec(0, 20)),
])

const player = new ex.Actor({
  pos: ex.vec(100, 100),
  collider: capsule,
})

Collision Events

Collision Event Lifecycle

Collision Events Diagram

Collision Start "collisionstart"

The collisionstart event is fired when a physics body, usually attached to an actor, first starts colliding with another body, and will not fire again while in contact until the the pair separates and collides again.

Use cases for the collisionstart event may be detecting when an actor has touch a surface (like landing) or if a item has been touched and needs to be picked up.

actor.on('collisionstart', () => {...})
// or
actor.body.collider.on('collisionstart', () => {...})

Collision End "collisionend"

The collisionend event is fired when two physics bodies are no longer in contact. This event will not fire again until another collision and separation.

Use cases for the collisionend event might be to detect when an actor has left a surface (like jumping) or has left an area.

actor.on('collisionend', () => {...})
// or
actor.body.collider.on('collisionend', () => {...})

Pre Collision "precollision"

The precollision event is fired every frame where a collision pair is found and two bodies are intersecting.

This event is useful for building in custom collision resolution logic in Passive-Passive or Active-Passive scenarios. For example in a breakout game you may want to tweak the angle of ricochet of the ball depending on which side of the paddle you hit.

actor.on('precollision', () => {...})
// or
actor.body.collider.on('precollision', () => {...})

Post Collision "postcollision"

The postcollision event is fired for every frame where collision resolution was performed. Collision resolution is when two bodies influence each other and cause a response like bouncing off one another. It is only possible to have postcollision event in Active-Active and Active-Fixed type collision pairs.

Post collision would be useful if you need to know that collision resolution is happening or need to tweak the default resolution.

actor.on('postcollision', () => {...})
// or
actor.body.collider.on('postcollision', () => {...})

Collision Types

Colliders have the default collision type of CollisionType.PreventCollision, this is so colliders don't accidentally opt into something computationally expensive. In order for colliders to participate in collisions and the global physics system, colliders must have a collision type of CollisionType.Active or CollisionType.Fixed.

Prevent

Actors with the CollisionType.PreventCollision setting do not participate in any collisions and do not raise collision events.

Passive

Actors with the CollisionType.Passive setting only raise collision events, but are not influenced or moved by other actors and do not influence or move other actors.

Active

Actors with the CollisionType.Active setting raise collision events and participate in collisions with other actors and will be push or moved by actors sharing the CollisionType.Active or CollisionType.Fixed setting.

Fixed

Actors with the CollisionType.Fixed setting raise collision events and participate in collisions with other actors. Actors with the CollisionType.Fixed setting will not be pushed or moved by other actors sharing the CollisionType.Fixed.

Think of Fixed actors as "immovable/unstoppable" objects. If two CollisionType.Fixed actors meet they will not be pushed or moved by each other, they will not interact except to throw collision events.

Collision Type Behavior Matrix

This matrix shows what will happen with 2 actors of any collision type.

Collision TypePreventPassiveActiveFixed
PreventNoneNoneNoneNone
PassiveNoneEvents OnlyEvents OnlyEvents Only
ActiveNoneEvents OnlyResolution & EventsResolution & Events
FixedNoneEvents OnlyResolution & EventsNone
  • None = No collision resolution and no collision events
  • Events Only = No resolution is performed, only collision events are fired on colliders, except for postcollision which only fires if resolution was performed.
  • Resolution & Events = Collider positions are resolved according to their collision type and collision events are fired on both colliders

Collision Groups

Collision groups are useful when you want to filter the collision that are possible between colliders on a granular level above and beyond collision type. This idea is also known as collision layers, collision filters, or collision filtering.

Collision groups currently are a property on the BodyComponent because ??? TODO

// Create a group for each distinct category of "collidable" in your game
const playerGroup = ex.CollisionGroupManager.create('player')
const npcGroup = ex.CollisionGroupManager.create('npcGroup')
const floorGroup = ex.CollisionGroupManager.create('floorGroup')
const enemyGroup = ex.CollisionGroupManager.create('enemyGroup')

// Define your rules
const playersCanCollideWith = ex.CollisionGroup.collidesWith([
  playersGroup, // collide with other players
  floorGroup, // collide with the floor
  enemyGroup, // collide with enemies
])

const enemiesCanCollideWith = ex.CollisionGroup.collidesWith([
  playerGroup, // collide with players
  floorGroup, // collide with the floor
])

const npcGroupCanCollideWith = ex.CollisionGroup.collidesWith([
  floorGroup, // only collides with the floor
])

const player = new ex.Actor({
  collisionGroup: playersCanCollideWith,
})

const npc = new ex.Actor({
  collisionGroup: npcGroupCanCollideWith,
})

const enemy = new ex.Actor({
  collisionGroup: enemiesCanCollideWith,
})

const floor = new ex.Actor({
  pos: ex.vec(100, 400),
  width: 100,
  height: 20,
  collisionGroup: floorGroup,
})

Collision System Under the Hood

Example Active-Active/Active-Fixed scenario

// setup game
const game = new ex.Engine({
  width: 600,
  height: 400
});
// use rigid body
ex.Physics.collisionResolutionStrategy = ex.CollisionResolutionStrategy.RigidBody;
// set global acceleration simulating gravity pointing down
ex.Physics.acc.setTo(0, 700);

const block = new ex.Actor({
  pos: new ex.Vector(300, 0),
  width: 20,
  height: 20,
  color: ex.Color.Blue
});
block.body.useBoxCollider(); // useBoxCollision is the default, technically optional
block.body.collider.type = ex.CollisionType.Active;
game.add(block);

// or

const block = new ex.Actor({
    pos: new ex.Vector(300, 0),
    color: ex.Color.Blue,
    body: new ex.Body({
        collider: new ex.Collider({
            type: ex.CollisionType.Active,
            shape: ex.Shape.Box(20, 20)
        })
    })
});

const circle = new ex.Actor({
  x: 301,
  y: 100,
  width: 20,
  height: 20,
  color: ex.Color.Red
});
circle.body.useCircleCollider(10);
circle.body.collider.type = ex.CollisionType.Active;
game.add(circle);

// or

const circle = new ex.Actor({
    pos: new ex.Vector(301, 100),
    color: ex.Color.Red,
    body: new ex.Body({
        collider: new ex.Collider({
            shape: ex.Shape.Circle(10),
            type: ex.CollisionType.Active
        })
    })
});

const ground = new ex.Actor({
  x: 300,
  y: 380,
  width: 600,
  height: 10,
  color: ex.Color.Black;
});

ground.body.useBoxCollider(); // optional
ground.body.collider.type = ex.CollisionType.Fixed;

game.add(ground);
// start the game

game.start();