phaser-navmesh
Version:
A plugin for Phaser (v3) for fast pathfinding using navigation meshes
222 lines (196 loc) • 7.76 kB
text/typescript
import Phaser from "phaser";
import PhaserNavMesh from "./phaser-navmesh";
import { buildPolysFromGridMap } from "navmesh/src/map-parsers";
/**
* This class can create navigation meshes for use in Phaser 3. The navmeshes can be constructed
* from convex polygons embedded in a Tiled map. The class that conforms to Phaser 3's plugin
* structure.
*
* @export
* @class PhaserNavMeshPlugin
*/
export default class PhaserNavMeshPlugin extends Phaser.Plugins.ScenePlugin {
private phaserNavMeshes: Record<string, PhaserNavMesh> = {};
public constructor(
scene: Phaser.Scene,
pluginManager: Phaser.Plugins.PluginManager,
pluginKey: string
) {
super(scene, pluginManager, pluginKey);
}
/** Phaser.Scene lifecycle event */
public boot() {
const emitter = this.systems.events;
emitter.once("destroy", this.destroy, this);
}
/** Phaser.Scene lifecycle event - noop in this plugin, but still required. */
public init() {}
/** Phaser.Scene lifecycle event - noop in this plugin, but still required.*/
public start() {}
/** Phaser.Scene lifecycle event - will destroy all navmeshes created. */
public destroy() {
this.systems.events.off("boot", this.boot, this);
this.removeAllMeshes();
}
/**
* Remove all the meshes from the navmesh.
*/
public removeAllMeshes() {
const meshes = Object.values(this.phaserNavMeshes);
this.phaserNavMeshes = {};
meshes.forEach((m) => m.destroy());
}
/**
* Remove the navmesh stored under the given key from the plugin. This does not destroy the
* navmesh.
* @param key
*/
public removeMesh(key: string) {
if (this.phaserNavMeshes[key]) delete this.phaserNavMeshes[key];
}
/**
* This method attempts to automatically build a navmesh based on the give tilemap and tilemap
* layer(s). It attempts to respect the x/y position and scale of the layer(s). Important note: it
* doesn't support rotation/flip or multiple layers that have different positions/scales. This
* method is a bit experimental. It will generate a valid mesh, but it won't necessarily be
* optimal, so you may end up sometimes getting non-shortest paths.
*
* @param key Key to use when storing this navmesh within the plugin.
* @param tilemap The tilemap to use for building the navmesh.
* @param [tilemapLayers] An optional array of tilemap layers to use for building the mesh.
* @param [isWalkable] An optional function to use to test if a tile is walkable. Defaults to
* assuming non-colliding tiles are walkable.
* @param [shrinkAmount=0] Amount to "shrink" the mesh away from the tiles. This adds more
* polygons to the generated mesh, but can be helpful for preventing agents from getting caught on
* edges. This supports values between 0 and tileWidth/tileHeight (whichever dimension is
* smaller).
*/
public buildMeshFromTilemap(
key: string,
tilemap: Phaser.Tilemaps.Tilemap,
tilemapLayers?: Phaser.Tilemaps.TilemapLayer[],
isWalkable?: (tile: Phaser.Tilemaps.Tile) => boolean,
shrinkAmount = 0
) {
// Use all layers in map, or just the specified ones.
const dataLayers = tilemapLayers ? tilemapLayers.map((tl) => tl.layer) : tilemap.layers;
if (!isWalkable) isWalkable = (tile: Phaser.Tilemaps.Tile) => !tile.collides;
let offsetX = 0;
let offsetY = 0;
let scaleX = 1;
let scaleY = 1;
// Attempt to set position offset and scale from the 1st tilemap layer given.
if (tilemapLayers) {
const layer = tilemapLayers[0];
offsetX = layer.tileToWorldX(0);
offsetY = layer.tileToWorldY(0);
scaleX = layer.scaleX;
scaleY = layer.scaleY;
// Check and warn for layer settings that will throw off the calculation.
for (const layer of tilemapLayers) {
if (
offsetX !== layer.tileToWorldX(0) ||
offsetY !== layer.tileToWorldY(0) ||
scaleX !== layer.scaleX ||
scaleY !== layer.scaleY
) {
console.warn(
`PhaserNavMeshPlugin: buildMeshFromTilemap reads position & scale from the 1st TilemapLayer. Layer index ${layer.layerIndex} has a different position & scale from the 1st TilemapLayer.`
);
}
if (layer.rotation !== 0) {
console.warn(
`PhaserNavMeshPlugin: buildMeshFromTilemap doesn't support TilemapLayer with rotation. Layer index ${layer.layerIndex} is rotated.`
);
}
}
}
// Check and warn about DataLayer that have x/y position from Tiled. In the future, these could
// be supported if A) only one colliding layer is offset, or B) the offset is a multiple of the
// tile size.
dataLayers.forEach((layer) => {
if (layer.x !== 0 || layer.y !== 0) {
console.warn(
`PhaserNavMeshPlugin: buildMeshFromTilemap doesn't support layers with x/y positions from Tiled.`
);
}
});
// Build 2D array of walkable tiles across all given layers.
const walkableAreas: boolean[][] = [];
for (let ty = 0; ty < tilemap.height; ty += 1) {
const row: boolean[] = [];
for (let tx = 0; tx < tilemap.width; tx += 1) {
let walkable = true;
for (const layer of dataLayers) {
const tile = layer.data[ty][tx];
if (tile && !isWalkable(tile)) {
walkable = false;
break;
}
}
row.push(walkable);
}
walkableAreas.push(row);
}
let polygons = buildPolysFromGridMap(
walkableAreas,
tilemap.tileWidth,
tilemap.tileHeight,
(bool) => bool,
shrinkAmount
);
// Offset and scale each polygon if necessary.
if (scaleX !== 1 && scaleY !== 1 && offsetX !== 0 && offsetY !== 0) {
polygons = polygons.map((poly) =>
poly.map((p) => ({ x: p.x * scaleX + offsetX, y: p.y * scaleY + offsetY }))
);
}
const mesh = new PhaserNavMesh(this, this.scene, key, polygons, 0);
this.phaserNavMeshes[key] = mesh;
return mesh;
}
/**
* Load a navmesh from Tiled. Currently assumes that the polygons are squares! Does not support
* tilemap layer scaling, rotation or position.
* @param key Key to use when storing this navmesh within the plugin.
* @param objectLayer The ObjectLayer from a tilemap that contains the polygons that make up the
* navmesh.
* @param meshShrinkAmount The amount (in pixels) that the navmesh has been shrunk around
* obstacles (a.k.a the amount obstacles have been expanded)
*/
public buildMeshFromTiled(
key: string,
objectLayer: Phaser.Tilemaps.ObjectLayer,
meshShrinkAmount = 0
) {
if (this.phaserNavMeshes[key]) {
console.warn(`NavMeshPlugin: a navmesh already exists with the given key: ${key}`);
return this.phaserNavMeshes[key];
}
if (!objectLayer || objectLayer.objects.length === 0) {
console.warn(
`NavMeshPlugin: The given tilemap object layer is empty or undefined: ${objectLayer}`
);
}
const objects = objectLayer.objects ?? [];
// Loop over the objects and construct a polygon - assumes a rectangle for now!
// TODO: support layer position, scale, rotation
const polygons = objects.map((obj) => {
const h = obj.height ?? 0;
const w = obj.width ?? 0;
const left = obj.x ?? 0;
const top = obj.y ?? 0;
const bottom = top + h;
const right = left + w;
return [
{ x: left, y: top },
{ x: left, y: bottom },
{ x: right, y: bottom },
{ x: right, y: top },
];
});
const mesh = new PhaserNavMesh(this, this.scene, key, polygons, meshShrinkAmount);
this.phaserNavMeshes[key] = mesh;
return mesh;
}
}