phaser-matter-collision-plugin
Version:
A plugin for making it easier to manage collisions with Phaser + Matter.js
323 lines (240 loc) • 13 kB
Markdown
# Phaser Matter Collision Plugin 💥 <!-- omit in toc -->
A plugin for making it easier to manage collisions with the [Phaser](https://phaser.io/) game engine and the [Matter.js](http://brm.io/matter-js/) physics engine.
Matter is one of the cool physics engine choices you have in Phaser 3. Phaser has a thin wrapper over Matter's API, so you need to dig into Matter's native collision event system if you want to detect and respond to collisions. That system just gives you a dump of all the pairs of bodies that collided in a tick of the engine. This plugin wraps up that collision logic in a more useful way:
```js
const player = this.matter.add.sprite(0, 0, "player");
const trapDoor = this.matter.add.sprite(200, 0, "door");
this.matterCollision.addOnCollideStart({
objectA: player,
objectB: trapDoor,
callback: () => console.log("Player touched door!")
});
```
Or in a slightly more complicated example:
[](https://raw.githubusercontent.com/mikewesthad/phaser-matter-collision-plugin/master/doc-source-assets/collision-simple-demo.gif)
_See interactive versions of that example on CodeSandbox in [JavaScript](https://codesandbox.io/s/0o0917m23l) or [TypeScript](https://codesandbox.io/s/my3oyyqj39)._
If you are reading this on Github or NPM, check out the HTML documentation [here](https://www.mikewesthad.com/phaser-matter-collision-plugin/docs/manual/README.html).
- [Installation](#installation)
- [As a Module](#as-a-module)
- [As a Script](#as-a-script)
- [Usage](#usage)
- [Initial Setup](#initial-setup)
- [Usage in Scene](#usage-in-scene)
- [Tracking Collisions](#tracking-collisions)
- [Stop Tracking Collisions](#stop-tracking-collisions)
- [Examples](#examples)
- [Tips](#tips)
- [Changelog](#changelog)
- [Development](#development)
- [Scripts](#scripts)
- [Tests](#tests)
- [New Releases](#new-releases)
## Installation
You can install this plugin locally as a module using your bundler of choice, or globally as a script.
### As a Module
Install via npm:
```
npm install --save phaser-matter-collision-plugin
```
Then import the plugin into your project:
```js
import PhaserMatterCollisionPlugin from "phaser-matter-collision-plugin";
```
See [usage](#usage) for how to use the plugin.
### As a Script
Grab the desired version from the [releases](https://github.com/mikewesthad/phaser-matter-collision-plugin/releases) page and include it as a script in your HTML. E.g. it might look like:
```html
<script src="./phaser-matter-collision-plugin.min.js"></script>
```
Or use the jsdelivr CDN:
```html
<script src="//cdn.jsdelivr.net/npm/phaser-matter-collision-plugin"></script>
```
Now you can use the global `PhaserMatterCollisionPlugin`. See [usage](#usage) for how to use the plugin.
## Usage
### Initial Setup
When setting up your game config, add the plugin:
```js
const config = {
// ...
physics: {
default: "matter"
},
// Install the scene plugin
plugins: {
scene: [
{
plugin: PhaserMatterCollisionPlugin, // The plugin class
key: "matterCollision", // Where to store in Scene.Systems, e.g. scene.sys.matterCollision
mapping: "matterCollision" // Where to store in the Scene, e.g. scene.matterCollision
}
]
}
};
const game = new Phaser.Game(config);
```
Now, within a scene, you can use `this.matterCollision` to access the plugin instance.
### Usage in Scene
#### Tracking Collisions
The plugin has `addOnCollideStart`, `addOnCollideActive` and `addOnCollideEnd` methods which allow you to listen to collisions between "objects" in your scene. Those objects can be: a native Matter body, a tile, a Matter sprite, any object with a `body` property, or an array of any of those.
For example, game object vs game object collisions:
```js
const player = this.matter.add.sprite(0, 0, "player");
const trapDoor = this.matter.add.image(200, 0, "door");
this.matterCollision.addOnCollideStart({
objectA: player,
objectB: trapDoor,
callback: function(eventData) {
// This function will be invoked any time the player and trap door collide.
const { bodyA, bodyB, gameObjectA, gameObjectB, pair } = eventData;
// bodyA & bodyB are the Matter bodies of the player and door respectively.
// gameObjectA & gameObjectB are the player and door respectively.
// pair is the raw Matter pair data.
},
context: this // Optional context to apply to the callback function.
});
```
If you omit the `objectB` property, you'll get all collisions involving `objectA`:
```js
const player = this.matter.add.sprite(0, 0, "player");
this.matterCollision.addOnCollideStart({
objectA: player,
callback: eventData => {
const { bodyB, gameObjectB } = eventData;
console.log("Player touched something.");
// bodyB will be the matter body that the player touched.
// gameObjectB will be the game object that owns bodyB, or undefined if there's no game object
// (e.g. the player hitting an invisible Matter body acting as a wall).
}
});
```
Game object vs Matter sensor:
```js
const player = this.matter.add.sprite(0, 0, "player");
const sensor = this.matter.world.add.rectangle(100, 0, 50, 50, { isStatic: true, isSensor: true });
this.matterCollision.addOnCollideStart({
objectA: player,
objectB: sensor,
callback: eventData => console.log("Player touched hidden sensor")
});
```
Game object vs array of objects:
```js
const player = this.matter.add.sprite(0, 0, "player");
const enemy1 = this.matter.add.sprite(100, 0, "enemy");
const enemy2 = this.matter.add.sprite(200, 0, "enemy");
const enemy3 = this.matter.add.sprite(300, 0, "enemy");
this.matterCollision.addOnCollideStart({
objectA: player,
objectB: [enemy1, enemy2, enemy3],
callback: eventData => {
console.log("Player hit an enemy");
// eventData.gameObjectB will be the specific enemy that was hit!
}
});
```
Or, array vs array:
```js
this.matterCollision.addOnCollideStart({
objectA: [player1, player2],
objectB: [enemy1, enemy2, enemy3],
callback: eventData => {
console.log("A player hit an enemy");
// eventData.gameObjectA will be the specific player involved in the collision
// eventData.gameObjectB will be the specific enemy involved in the collision
}
});
```
You can listen for collisions vs a single tile (or an array of tiles), but it's likely more useful to do something like:
```js
this.matterCollision.addOnCollideStart({
objectA: player,
callback: eventData => {
const { bodyB, gameObjectB } = eventData;
if (gameObjectB instanceof Phaser.Tilemaps.Tile) {
// Now you know that gameObjectB is a Tile, so you can check the index, properties, etc.
if (gameObjectB.properties.isDeadly) console.log("Stepped on deadly tile");
else if (gameObjectB.index === 32) console.log("Stepped on the tile with index 32");
}
}
});
```
The plugin also exposes two sets of events via the `this.matterCollision.events` event emitter:
- "collisionstart", "collisionactive", "collisionend" - these match the Matter events. They emit a single parameter `event`. Aside from the normal Matter data in `event`, each pair in `event.pairs` has a `gameObjectA` and `gameObjectB` property that points to the game object that owns each body (if one exists).
- "paircollisionstart", "paircollisionactive", "paircollisionend" - these are similar to the above, except they fire once for each pair. They have one parameter that looks like this: `{ bodyA, bodyB, gameObjectA, gameObjectB, pair }`
You can listen to them via `this.matterCollision.events.on("collisionstart", ...)`.
#### Stop Tracking Collisions
You can stop tracking a collision via `removeOnCollideStart`, `removeOnCollideActive` and `removeOnCollideEnd`. They take the same parameters as `addOnCollideStart`. E.g.:
```js
function onCollide() {
console.log("A player hit an enemy");
}
// ... after addOnCollideStart has been used
this.matterCollision.removeOnCollideStart({
objectA: [player1, player2],
objectB: [enemy1, enemy2, enemy3],
callback: onCollide
});
```
In addition, the addOnCollide methods will also return a function that automatically unsubscribes from the collision event (which can be useful if you use arrow functions):
```js
const unsubscribe = this.matterCollision.addOnCollideStart({
objectA: player,
objectB: trapDoor,
callback: eventData => {
// Do something, like dropping the door out underneath the player
// Then unsubscribe so this callback is never called again
unsubscribe();
}
});
```
If you want to remove all listeners that have been added - not just one colliding pair - there are also the following methods:
- `removeAllCollideStartListeners`
- `removeAllCollideActiveListeners`
- `removeAllCollideEndListeners`
- `removeAllCollideListeners` - removes start, active and end listeners
## Examples

This repo includes two example projects:
- [JavaScript](./workspaces/javascript-example)
- [TypeScript](./workspaces/typescript-example)
You can also check out versions of those example projects on CodeSandbox:
- [JavaScript](https://codesandbox.io/s/0o0917m23l)
- [TypeScript](https://codesandbox.io/s/my3oyyqj39)
You can also poke around the ["tests" folder](https://github.com/mikewesthad/phaser-matter-collision-plugin/tree/master/tests) of this repository for usage examples.
## Tips
- Trying to apply a force during collision? See ['s](https://github.com/alrdebugne) note in this [issue](https://github.com/mikewesthad/phaser-matter-collision-plugin/issues/5#issuecomment-752780477) on how to do that.
## Changelog
- 1.0.0 (2021-08-06)
- Conversion to TypeScript.
- Update all dependencies.
- Add named export for PhaserMatterCollisionPlugin (in addition to existing default export).
- Fix removeOnCollideXX methods so that all config parameters are used when matching.
- 0.10.2 (2019-02-19)
- Bug: fixed a bug in `addOnCollide()` that threw an error when the user didn't supply a callback, from [](https://github.com/jvanroose).
- 0.10.1 (2018-11-17)
- Bug: fixed a bug in the removeOnCollideXXXX methods of the plugin, from [](https://github.com/Fantasix).
- 0.0.0 - 0.9.2 (2018-08-17)
- Initial releases.
## Development
To set up the project for local development make sure you have node and npm installed. You can grab them bundled together [here](https://nodejs.org/en/). Clone/download the repository and run `npm i` from a terminal within the folder.
### Scripts
The project is controlled by npm scripts and uses cypress & jest for testing. Cypress is used for end-to-end verification that the plugin works as expected with Phaser. Jest is used for unit testing the plugin (via heavy mocking since Phaser headless mode is not complete).
- The `watch` and `build` tasks will build the plugin source in library/ or the projects in tests/
- The `serve` task opens the whole project (starting at the root) in a server
- The `dev` task will build & watch the library, tests and open up the server. This is useful for creating tests and updating the library.
- The `dev:cypress` task will build & watch the library & tests, as well as open up cypress in headed mode. This is useful for checking out individual tests and debugging them.
- The `test:cypress` task will build the tests and run cypress in headless mode to check all end-to-end tests.
- The `test:jest` will run the jest tests.
### Tests
The cypress tests rely on a particular structure:
- Each test game inside of "tests/" should have an "index.html" file as the entry point. "src/js/index.js" will be compiled to "build/js/index.js" by webpack. (Cypress doesn't support `type="module"` on scripts, so this is necessary if we need modules.)
- Each test has access to `test-utils.js` which provides `startTest`, `passTest` and `failTest` methods. Call `startTest` at the beginning and pass/fail when the test passes/fails. This manipulates in the DOM in a way that cypress is expecting.
- Each test in "cypress/integration/" simply loads up the specified URL and waits for it to pass or timeout. (Technically, startTest and failTest are ignored, but they are useful for visual inspection of a test.)
The jest unit tests rely on a simple mocking of Phaser and Matter. They are stored inside "src/". Once Phaser headless is available, this testing structure could be re-evaluated.
### New Releases
To prepare a new release, verify that all tests pass with `yarn test:jest` and `yarn test:cypress`, update the changelog, and then:
```
yarn npm login
yarn npm publish
```