-
Notifications
You must be signed in to change notification settings - Fork 41
/
Copy pathphaser-navmesh-plugin.ts
221 lines (196 loc) · 7.57 KB
/
phaser-navmesh-plugin.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
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 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;
}
}