Giter Club home page Giter Club logo

navmesh's Introduction

Navigation Meshes Overview

A JS plugin for fast pathfinding using navigation meshes, with optional wrappers for the Phaser v2 and Phaser v3 game engines.

Interactive demo

(Note: if you are viewing this on GitHub or NPM, you might want to check out the HTML documentation here.)

Table of Contents:

Introduction

Pathfinding is essentially the problem of solving a maze, finding a path between points while avoiding obstacles. When pathfinding in games, we need to:

  1. Represent the game world in a way that defines what areas are walkable.
  2. Search that representation for the shortest path.

When it comes to 2D pathfinding, a common approach is to represent the world using tiles (a grid) and then search for a path using the A* algorithm (e.g. Phaser AStar). If you have a 50 x 50 tile world, searching for a path involves searching through a representation of the world with up to 2500 locations/nodes (50 x 50 = 2500).

This plugin uses navigation meshes to simplify that search. Instead of representing the world as a grid of tiles, it represents the walkable areas of the world as a mesh. That means that the representation of the world has far fewer nodes, and hence, can be searched much faster than the grid approach. This approach is 5x - 150x faster than Phaser's A* plugin (see performance section), depending on the mesh.

The example map below (left) is a 30 x 30 map. As a grid, there are 900 nodes, but as a navmesh (right) there are 27 nodes (colored rectangles).

Installation

This repository contains 3 related JS packages:

  • navmesh - core logic, game-engine agnostic, usable outside of Phaser.
  • phaser-navmesh - Phaser v3 wrapper around navmesh that creates a Phaser 3 Scene plugin. Phaser 3 is expected to be a dependency in your project.
  • phaser2-navmesh - Phaser v2 wrapper around navmesh that creates a Phaser 2 game plugin. Phaser 2 or Phaser-ce is expected to be in the global scope.

You can use any of them as a script or as a module in your bundler of choice.

As a Script

You can drop in any of the transpiled code into your project as a standalone script. Download the version that you want from the GitHub release.

E.g. if you wanted phaser-navmesh, you would add this to your HTML:

<script src="phaser-navmesh.min.js"></script>

Inside of your own script, you can now use the global variable PhaserNavMeshPlugin (see library name in the above table), e.g.

const game = new Phaser.Game({
  type: Phaser.AUTO,
  width: 750,
  height: 750,
  plugins: {
    scene: [
      { key: "NavMeshPlugin", plugin: PhaserNavMeshPlugin, mapping: "navMeshPlugin", start: true }
    ]
  }
});

See usage for more information on how to use each of the three modules in this repository.

As a Module

Install the appropriate dependency:

  • npm install --save navmesh for usage outside of Phaser
  • npm install --save phaser-navmesh for Phaser 3
  • npm install --save phaser2-navmesh for Phaser 2

To use the transpiled and minified distribution of the library (recommended for most users):

// Phaser 3
import { PhaserNavMeshPlugin } from "phaser-navmesh";

// Phaser 2
import { Phaser2NavMeshPlugin } from "phaser2-navmesh";

// NavMesh (commonjs or es import)
const { NavMesh } = require("navmesh");
import { NavMesh } from "navmesh";

To use the raw TypeScript source code so you can optimize the bundle yourself:

import { PhaserNavMeshPlugin } from "phaser-navmesh/src";

Creating a Navigation Mesh

Creating a navigation mesh is the process of defining the walkable areas within you world as a series of polygons. If you are using a tilemap, then you can probably get away with just auto-generating the mesh using buildMeshFromTilemap in Phaser 3 (or if you are using NavMesh without the Phaser wrapper, see buildPolysFromGridMap).

If you've got a more complex situation, you can use a tilemap editor like Tiled to create your mesh and load it into the game. See guide.

(Note: the current version of the library only supports convex polygons. There are libraries like poly-decom.js for decomposing a concave polygon into easier to manage convex polygons. It's on the to do list to handle any polygon, but I've found that automatically decomposing polygons leads to worse performance than hand-mapping the levels with convex polygons.)

Usage

You can find code snippets for the different use cases below. You can also jump directly to a few example projects in this repository for:

navmesh (API reference)

If you don't need the Phaser wrappers, you can construct navmeshes directly from points using the navmesh package:

import { NavMesh } from "navmesh";

/*
  Imaging your game world has three walkable rooms, like this:

    +-----+-----+
    |     |     |
    |  1  |  2  |
    |     |     |
    +-----------+
          |     |
          |  3  |
          |     |
          +-----+
*/

// The mesh is represented as an array where each element contains the points for an indivdual
// polygon within the mesh.
const meshPolygonPoints = [
  [{ x: 0, y: 0 }, { x: 10, y: 0 }, { x: 10, y: 10 }, { x: 0, y: 10 }], // Polygon 1
  [{ x: 10, y: 0 }, { x: 20, y: 0 }, { x: 20, y: 10 }, { x: 10, y: 10 }], // Polygon 2
  [{ x: 10, y: 10 }, { x: 20, y: 10 }, { x: 20, y: 20 }, { x: 10, y: 20 }] // Polygon 3
];
const navMesh = new NavMesh(meshPolygonPoints);

// Find a path from the top left of room 1 to the bottom left of room 3
const path = navMesh.findPath({ x: 0, y: 0 }, { x: 10, y: 20 });
// ⮡  [{ x: 0, y: 0 }, { x: 10, y: 10 }, { x: 10, y: 20 }]

If your world is a grid (e.g. a tilemap), NavMesh also has a utility buildPolysFromGridMap that can automatically generate meshPolygonPoints from a 2D array.

phaser-navmesh (API reference)

If you are working with Phaser 3, you can use the phaser-navmesh package, which provides a Scene plugin. Play with a live example on CodeSandbox here, or peek at the examples in this repository for more complete usage.

import Phaser from "phaser";
import { PhaserNavMeshPlugin } from "phaser-navmesh";

const game = new Phaser.Game({
  type: Phaser.AUTO,
  parent: "game-container",
  width: 750,
  height: 750,
  plugins: {
    scene: [
      {
        key: "PhaserNavMeshPlugin", // Key to store the plugin class under in cache
        plugin: PhaserNavMeshPlugin, // Class that constructs plugins
        mapping: "navMeshPlugin", // Property mapping to use for the scene, e.g. this.navMeshPlugin
        start: true
      }
    ]
  },
  scene: {
    preload: preload,
    create: create
  }
});

function preload() {
  this.load.tilemapTiledJSON("map", "tilemaps/map.json");
  this.load.image("tiles", "tilemaps/tiles.png");
}

function create() {
  // Set up a tilemap with at least one layer
  const tilemap = this.add.tilemap("map");
  const tileset = tilemap.addTilesetImage("tiles", "tiles");
  const wallLayer = tilemap.createStaticLayer("walls", tileset);
  wallLayer.setCollisionByProperty({ collides: true }); // Or, however you set tiles to collide.

  // Automatically generate mesh from colliding tiles in a layer or layers: 
  const navMesh = this.navMeshPlugin.buildMeshFromTilemap("mesh", tilemap, [wallLayer]);
  const path = navMesh.findPath({ x: 0, y: 0 }, { x: 300, y: 400 });
  // ⮡  path will either be null or an array of Phaser.Geom.Point objects

  // Alternatively, you can load a navmesh created by hand in Tiled that is stored in an object 
  // layer. See the creating a navmesh guide for more info on this.
  // const objectLayer = tilemap.getObjectLayer("navmesh");
  // const navMesh = this.navMeshPlugin.buildMeshFromTiled("mesh1", objectLayer, 12.5);
}

The plugin comes with some methods for visually debugging your navmesh:

navMesh.enableDebug(); // Creates a Phaser.Graphics overlay on top of the screen
navMesh.debugDrawClear(); // Clears the overlay
// Visualize the underlying navmesh
navMesh.debugDrawMesh({
  drawCentroid: true,
  drawBounds: false,
  drawNeighbors: true,
  drawPortals: true
});
// Visualize an individual path
navMesh.debugDrawPath(path, 0xffd900);

phaser2-navmesh (API reference)

If you are working with Phaser 2, you can use the phaser2-navmesh package, which provides a game plugin. See this example for more complete usage. You can also look at the previous section for Phaser usage.

Performance Comparison

(Note: these comparisons were done in any earlier verison of the repo before Phaser v3 was released. The plugins tested haven't been released in v3 versions yet, so this section could use an update. That said, the results should be the same.)

Comparing this navmesh plugin against:

Performance depends on the size of the area that needs to be searched. Finding for a path between points that are 50 pixels away is (generally) going to be much faster than finding a path between points that are 5000 pixels away.

Details (see src/library/performance):

Performance Comparison, 100000 iterations, 30x30 tilemap

Short paths (150 - 500 pixel length)

    Average time per iteration:
        AStart Plugin: 0.02470ms
        EasyStar Plugin: 0.02876ms
        NavMesh Plugin: 0.00575ms

    Comparison:
        NavMesh is 4.30x faster than Phaser AStar
        NavMesh is 5.00x faster than EasyStar

Long paths (600 pixels and greater length), average time per iteration:

    Average time per iteration:
        AStart Plugin: 1.38710ms
        EasyStar Plugin: 0.15977ms
        NavMesh Plugin: 0.00738ms

    Comparison:
        NavMesh is 187.95x faster than Phaser AStar
        NavMesh is 21.65x faster than EasyStar

Community Examples

Development

Pull requests are welcome (see todos)! If you want to run this repo locally, make sure you have node installed. Download the repo, open a terminal in the repo folder and run:

npm i -g yarn
yarn
yarn bootstrap

This project uses lerna and yarn workspaces to manage multiple packages within one repository. npx yarn will pull the root dependencies (and install yarn if needed) and npm run bootstrap will use lerna & yarn to pull and link dependencies within "packages/". This project has the following packages:

  • navmesh - core logic, game-engine agnostic
  • phaser-navmesh - Phaser Plugin v3 wrapper around navmesh
  • phaser2-navmesh - Phaser Plugin v2 wrapper around navmesh

The project is controlled via npm scripts. The main ones to use:

  • yarn build:libs - will build production versions of the three libraries within "packages/".
  • yarn test - will run the automated tests against the libraries.
  • serve:examples - will build, serve and watch the three examples. Phaser 3 examples are at localhost::8080, Phaser 2 examples at localhost::8081 and node examples at localhost::8082.
  • yarn dev - watch the libraries & serve the examples. If you are working on the library, this is the easiest way to do "functional testing" by using the library in a game environment.
  • yarn dev:phaser3, yarn dev:phaser2, yarn dev:node - these will watch the relevant libraries and serve one the corresponding example. Useful for dev on a specific library.

A few tips, because Lerna + Yarn is complicated, and I keep forgetting these:

  • Running a command in a workspace:
    • yarn workspace <workspace_name> <command>
    • Example: yarn workspace navmesh add typescript --dev
  • Running a command in a set of workspaces:
    • lerna run --parallel watch --scope <scope>
    • Example: lerna run --parallel build --scope '{navmesh,phaser-navmesh,phaser-navmesh}'
  • Add a dev dependency to the root:
    • yarn add <dependency> --dev -W
  • Running a command in all workspaces:
    • yarn workspaces <command>
    • Example: yarn workspaces build
    • lerna run --parallel watch

Changelogs

References

Helpful resources used while building this plugin:

To Dos

  • Features
    • Allow non-square navmesh polygons from Tiled - ideally, any convex shape.
    • Reimplement the autotessalation version of the lib & try libtess in quad mode.
    • The astar heuristic & cost functions need another pass. They don't always produce the shortest path. Implement incomplete funneling while building the astar path?
    • The navmesh assumes any polygon can reach any other polygon. This probably should be extended to put connected polygons into groups like patroljs.
    • Better warnings for devs - warn on empty map, warn on disconnected map, warn if polygons are malformed.
    • Factor in the layer position / scale / rotation
  • Testing
    • Check against tilemap that is larger than the screen
  • Research
    • There are probably optimization tricks to do when dealing with certain types of shapes. E.g. we are using axis-aligned boxes for the polygons and it is dead simple to calculate if a point is inside one of those...
    • Investigate Points-of-Visibility pathfinding to compare speed

navmesh's People

Contributors

dependabot[bot] avatar herohan avatar ikiselev1989 avatar malahaas avatar mikewesthad avatar stevenr152 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

navmesh's Issues

Get all crossed polygons

Hi,
I am using NavMesh in an isometric game.
The isometric factor brings its share of issues, but I managed to build my grid and use it to get a path.

However, I have a problem. My character can only move in 2 dimensions North-South and East-West, so I have ton convert the path given by navmesh in N-S and E-W components. This work fine as long as the path given by NavMesh do not cross a polygon without 'stopping' by it.

Is there any way to add intermediate points to the path each time the path cross polygon limits ?

I think this could also be interesting in case you want to apply, let say, a speed modifiers, in specific polygons.

If you ever are interested in proper isometric implementation here is a link to our code that is open source https://gitlab.com/eternaltwin/mush/mush/-/tree/develop/App/src/game/scenes

TypeError: NavMesh.buildPolysFromGridMap is not a function

import * as NavMesh from 'navmesh'

const gridWidth = 5
const gridHeight = 4
const matrix2d =  new Array(gridHeight).fill(0).map(() => new Array(gridWidth).fill(0));
const meshPolygonPoints = NavMesh.buildPolysFromGridMap<number>(matrix2d)

Running this code gives the error:
TypeError: NavMesh.buildPolysFromGridMap is not a function

i think something woth the exports dosnt work

Phaser 3 support is broken

The example for the Phaser3 plugin no longer works as it seems like the plugin manager has been updated.

restricted movement

Hi!

Is it possible to add restrictions to the calculated path? I would like to mimic the behaviour of a traditional grid based, 8-directional a-star pathfinding library. That is, the path is limited to 8 directions in discrete chunks of an arbitary sized (say 32x32 pixel) grid.

I know I could use another existing library for this, but I really like this lib and would like to stick to only one pathfinding solution!

Thanks.

error on update to phaser 3.6

I just updated phaser to 3.6 and got errors from this plugin.

This is related to a change in phaser ScenePlugin class where scene can be set to null.

NavMesh breaks when run on server due to umd target

For a server authoritative game I'm running path finding on the server. When included in a node application, the navmesh library fails due to a window variable in the built navmesh library.

Changing the umd libraryTarget to commonjs2 on this line and rebuilding the library resolved this issue - see this fork.

The tests and examples all run without error, but I'm not expert enough in libraryTargets to know what effect this might have on other applications.

Building mesh from a single polygon

It seems to that if you have a complicated polygon, you have to subdivide it first - am I correct here?
Let's take an example from the readme file:

    +-----+-----+
    |     |     |
    |  1  |  2  |
    |     |     |
    +-----------+
          |     |
          |  3  |
          |     |
          +-----+

If we want to go from (0,0) to (10,20), such path will be produced:
{ x: 0, y: 0 }, { x: 10, y: 10 }, { x: 10, y: 20 }.

Let's modify this case so we have just one complex polygon:

   a             b
    +-----------+
    |           |
    |           |
    |           |
    +-----+     |
   f     e|     |
          | x   |
          |     |
          +-----+
         d       c

Let's say we want to go from (0,0) to x=(12,15).

const polygon = [
    { x: 0,  y: 0  }, //a
    { x: 20, y: 0  }, //b
    { x: 20, y: 20 }, //c
    { x: 10, y: 20 }, //d
    { x: 10, y: 10 }, //e
    { x: 0,  y: 10 }  //f
];
const nm = new NavMesh([polygon]);
const path = nm.findPath({ x: 0, y: 0 }, { x: 12, y: 15 });

When I run this, path seems to contain just two points: {x: 0, y: 0}, {x: 12, y: 15}, so my question is: am I doing something wrong or maybe navmesh can work only on already subdivided polygons? If that's the case - are there any plans to add mechanism to do that to your awesome library?

Cheers

Navmesh plugin is not defined

I have the scenes for my game being dynamically added at runtime when the world is generated, and the following game config:
var config = { type: Phaser.AUTO, width: 1260, height: 800, scene: [Loading], plugins: { global: [ { key: "navmesh", plugin: PhaserNavMeshPlugin, mapping: "navMeshPlugin", start: true, }, ], }, physics: { default: "arcade", arcade: { debug: false, }, }, render: { pixelArt: true, }, };
When I try to create a navmesh, it gives me this error: TypeError: Cannot read properties of undefined (reading 'buildMeshFromTilemap'). Any ideas for why this is happening?

add a shrink param to builMeshFromTilemap

Hi,
First thanks for your plugin.

I just noticed that the buildPolysFromGridMap can take a shrink amount as parameter. However the buildMeshFromTilemaps don't.

It would be very convenient to be able to have this param in buildMeshFromTilemaps

how to get a random point on navmesh

Hi there,

Thanks so much for a great library,

I'm trying to get a random point on the navmesh, is this someting possible? And what would be the best way about it?

I know this is a not issue as such, but I have no idea where to ask this question, and it could be a feature? :)

Can not install navmesh using npm

Hi,

Here is the log...

C:\Users\serku\Documents\012>npm install --save navmesh
npm ERR! code ENOENT
npm ERR! syscall spawn git
npm ERR! path git
npm ERR! errno ENOENT
npm ERR! enoent Error while executing:
npm ERR! enoent undefined ls-remote -h -t ssh://[email protected]/mikewesthad/javascript-astar.git
npm ERR! enoent
npm ERR! enoent
npm ERR! enoent spawn git ENOENT
npm ERR! enoent This is related to npm not being able to find a file.
npm ERR! enoent

npm ERR! A complete log of this run can be found in:
npm ERR! C:\Users\serku\AppData\Roaming\npm-cache_logs\2020-05-08T08_56_36_346Z-debug.log

Unexpected token when trying to use phaser 3 plugin

I am getting the following error when importing the plugin:

ERROR in ./node_modules/phaser-navmesh/dist/phaser-navmesh-plugin.js 1:4581
Module parse failed: Unexpected token (1:4581)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders

Using typescript and webpack. Hope you can help!

Defining Navmesh from List of Unwalkable Areas

Hi there, I'm taking an interest in your library navmesh. It seems really good for pathfinding, but I have a question.

How would you use this to make a NavMesh from only the unwalkable areas?
I have a list of obstacles as polygons but I don't have the walkable areas, only the unwalkable areas.

Could this library create a Navmesh defined from the unwalkable areas instead of the walkable areas?

Any plans on adding tools for dynamic meshes?

I'm making a game where obstacles can be placed during run time. Right now I've added a very simple grid generator that saves tile removals and regenerates the NavMesh from a new array, but it definitely starts to slog pretty quickly as tile count increases. I know I could make improvements by making a smarter tile generator, but just wondering if there are resources that already exist for this kind of thing (or if you have plans around this concept) so I'm not reinventing the wheel.

Path null

the path is always null no matter what points I put in 'findPath' method, now I don't know if my tableGround-map.json(attached, extension modified just to fit here)
tableGround-map.txt

is wrong or is the plugin. here is what shows in the console:

buildNavMesh():
t {game: i.Game, _debugGraphics: null, _meshShrinkAmount: 5, _navPolygons: Array(0), _graph: t}

findPath():
null

Not compatible with Phaser v3

Hi,

I'm not sure if this is a bug or just a poor setup but no matter what I do I cannot get this imported. I have tried using both a plain script tag from HTML as well as an import using webpack but nothing seems to work. I am getting two errors, the first, from using

a simple html script tag
<script src="dist/phaser-navmesh.min.js"></script>

as well as a normal require or import from phaser-navmesh
import { PhaserNavmesh } from "phaser-navmesh";
const phaserNavmesh = require('phaser-navmesh');

all three of these methods of importing produce this error for me in Chrome and Firefox

Uncaught TypeError: Super expression must either be null or a function, not undefined

Perhaps a more telling error comes from trying to import from the raw es6 library, using

import PhaserNavmesh from "phaser-navmesh/src/library";

produces the following error

Uncaught TypeError: Class extends value undefined is not a constructor or null

debugging in chrome for this second error takes me to line 14 of nav-mesh-plugin.js

class NavMeshPlugin extends Phaser.Plugin {

it seems that Phaser.Plugin is undefined and this is what is causing the error to be thrown, this leads me to believe that phaser-navmesh doesn't have access to Phaser for some reason but i'm not sure if this is the case.

Unexpected token; when import phaser-navmesh

Hi, I'm can't import package phaser-navmesh. I not cool expert, more information then error text from console I cannot offer.

ERROR in ./src/js/phaser-navmesh-plugin.js 1:16670
Module parse failed: Unexpected token (1:16670)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders ...FILE CODE...

Trying work with just navmesh, not for phaser, it work correcntly.

Shortest Path

I saw this on the Todo list:

The astar heuristic & cost functions need another pass. They don't always produce the shortest path. Implement incomplete funneling while building the astar path?

besides that it works really great.
Is there any chance this library will see an update which fixes this?

Double boot call for phaser plugin

Phaser calls boot on plugins automatically so the boot call in the phaser-navmesh-plugin causes it to be called twice. this means destroy gets called twice (listener registered twice) and an error is thrown.

Fix here: #28

Tile scale doesn't affect polygon size unless offset is also set

I have an issue with tile scale not affecting polygon size unless I also add an offset. I think it's because this if-condition uses && instead of ||
https://github.com/mikewesthad/navmesh/blob/7606d96bac0b374329a11ff9c071cf7c40cd9755/packages/phaser-navmesh/src/phaser-navmesh-plugin.ts#L158C5-L163C6

As a workaround I now just move my tilemap layer slightly using layer.setPosition(0.01, 0.01) so that it has an offset.

Thanks for an otherwise great library!

Destroying a Phaser game instance with noReturn true causes undefined error in navmesh destroy

If you use noReturn (this.game.destroy(true, true)) to destroy your Phaser 3.20.1 (and earlier), you'll get undefined for this.systems and the m in meshes.forEach( in the destroy method for phaser-navmesh.js (rows 1589 to 1596 in the dist file).

destroy() {
      this.systems.events.off("boot", this.boot, this);
      const meshes = Object.values(this.phaserNavMeshes);
      this.phaserNavMeshes = {};
      meshes.forEach(m => m.destroy());
      this.scene = undefined;
      this.systems = undefined;

}

Thank you for the plugin (and the collision plug as well)!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.