├── .eslintrc.js ├── .gitignore ├── .prettierrc.json ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── babel.config.json ├── config ├── jest.config.json └── tsconfig.base.json ├── doc-source ├── after.png ├── before.png ├── combined.png ├── demo.png ├── multiple-following-agents.png ├── single-following-agent.gif └── tiled-guide │ ├── tiled-final.png │ ├── tiled-navmesh-layer.gif │ ├── tiled-object-layer.png │ ├── tiled-preferences-2.png │ ├── tiled-preferences.png │ └── tiled-snapping.png ├── lerna.json ├── package.json ├── packages ├── examples-node │ ├── package.json │ ├── src │ │ ├── index.js │ │ └── typed-demo.ts │ └── tsconfig.json ├── examples-phaser2 │ ├── .gitignore │ ├── .prettierignore │ ├── package.json │ ├── src │ │ ├── demo │ │ │ ├── images │ │ │ │ ├── follower.ai │ │ │ │ └── follower.png │ │ │ ├── index.html │ │ │ ├── js │ │ │ │ ├── game-objects │ │ │ │ │ └── follower.js │ │ │ │ ├── main.js │ │ │ │ └── states │ │ │ │ │ ├── load.js │ │ │ │ │ └── start.js │ │ │ └── tilemaps │ │ │ │ ├── map.json │ │ │ │ ├── map.tmx │ │ │ │ ├── tiles.png │ │ │ │ └── tiles.psd │ │ └── index.html │ └── webpack.config.js ├── examples-phaser3 │ ├── .gitignore │ ├── .prettierignore │ ├── package.json │ ├── src │ │ ├── demo │ │ │ ├── images │ │ │ │ ├── follower.ai │ │ │ │ └── follower.png │ │ │ ├── index.html │ │ │ ├── js │ │ │ │ ├── game-objects │ │ │ │ │ └── follower.js │ │ │ │ ├── main.js │ │ │ │ └── scenes │ │ │ │ │ ├── load.js │ │ │ │ │ ├── many-paths.js │ │ │ │ │ └── start.js │ │ │ └── tilemaps │ │ │ │ ├── map.json │ │ │ │ ├── map.tmx │ │ │ │ ├── tiles.png │ │ │ │ └── tiles.psd │ │ ├── index.html │ │ ├── performance │ │ │ ├── index.html │ │ │ ├── js │ │ │ │ ├── main.js │ │ │ │ ├── plugins │ │ │ │ │ ├── easy-star-plugin.js │ │ │ │ │ └── phaser-astar.js │ │ │ │ └── scenes │ │ │ │ │ ├── load.js │ │ │ │ │ └── start.js │ │ │ └── tilemaps │ │ │ │ ├── map.json │ │ │ │ ├── map.tmx │ │ │ │ ├── tiles.png │ │ │ │ └── tiles.psd │ │ └── test │ │ │ ├── images │ │ │ ├── follower.ai │ │ │ └── follower.png │ │ │ ├── index.html │ │ │ ├── js │ │ │ ├── game-objects │ │ │ │ └── follower.js │ │ │ ├── main.js │ │ │ └── scenes │ │ │ │ ├── load.js │ │ │ │ └── start.js │ │ │ └── tilemaps │ │ │ ├── map.json │ │ │ ├── map.tmx │ │ │ ├── tiles.png │ │ │ └── tiles.psd │ └── webpack.config.js ├── navmesh │ ├── .gitignore │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── channel.ts │ │ ├── common-types.ts │ │ ├── index.ts │ │ ├── javascript-astar.d.ts │ │ ├── map-parsers │ │ │ ├── build-polys-from-grid-map.test.ts │ │ │ ├── build-polys-from-grid-map.ts │ │ │ ├── grid-map.ts │ │ │ ├── index.ts │ │ │ ├── point-queue.ts │ │ │ └── rectangle-hull.ts │ │ ├── math │ │ │ ├── line.ts │ │ │ ├── polygon.ts │ │ │ └── vector-2.ts │ │ ├── nav-mesh.test.ts │ │ ├── navgraph.ts │ │ ├── navmesh.ts │ │ ├── navpoly.ts │ │ ├── utils.test.ts │ │ └── utils.ts │ ├── tsconfig.json │ └── webpack.config.js ├── phaser-navmesh │ ├── .gitignore │ ├── README.md │ ├── cypress.json │ ├── cypress │ │ ├── fixtures │ │ │ └── example.json │ │ ├── integration │ │ │ └── navmesh.spec.js │ │ ├── plugins │ │ │ └── index.js │ │ └── support │ │ │ ├── commands.js │ │ │ └── index.js │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── phaser-navmesh-plugin.ts │ │ ├── phaser-navmesh.ts │ │ └── triangulate.ts │ ├── tsconfig.json │ └── webpack.config.js └── phaser2-navmesh │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ ├── index.ts │ ├── phaser2-navmesh-plugin.ts │ └── phaser2-navmesh.ts │ ├── tsconfig.json │ └── webpack.config.js ├── public ├── demo │ ├── images │ │ ├── follower.ai │ │ └── follower.png │ ├── index.html │ ├── js │ │ ├── main.js │ │ └── main.js.map │ └── tilemaps │ │ ├── map.json │ │ ├── map.tmx │ │ ├── tiles.png │ │ └── tiles.psd ├── index.html └── performance │ ├── index.html │ ├── js │ ├── main.js │ └── main.js.map │ └── tilemaps │ ├── map.json │ ├── map.tmx │ ├── tiles.png │ └── tiles.psd ├── tiled-navmesh-guide.md ├── typedoc.tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["eslint:recommended", "prettier"], 3 | plugins: ["prettier"], 4 | rules: { 5 | "no-var": 1, 6 | "brace-style": ["warn", "1tbs"], 7 | "no-unused-vars": ["error", { args: "after-used" }], 8 | indent: ["warn", 2, { SwitchCase: 1 }], 9 | "max-len": ["warn", 100, { ignoreUrls: true, ignoreTemplateLiterals: true }], 10 | "no-console": "off" 11 | }, 12 | env: { 13 | browser: true, 14 | commonjs: true, 15 | es6: true 16 | }, 17 | parserOptions: { 18 | sourceType: "module", 19 | ecmaVersion: 2020 20 | }, 21 | globals: { 22 | Phaser: true 23 | }, 24 | overrides: [ 25 | { 26 | files: ["**/*test.js"], 27 | env: { 28 | jest: true 29 | } 30 | } 31 | ] 32 | }; 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | docs 2 | types 3 | node_modules 4 | *.log 5 | 6 | # Ignore files used for publishing to gh-pages 7 | .publish -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "semi": true, 4 | "singleQuote": false, 5 | "quoteProps": "as-needed", 6 | "arrowParens": "always", 7 | "endOfLine": "auto", 8 | "trailingComma": "es5", 9 | "tabWidth": 2 10 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "search.exclude": { 4 | "**/node_modules": true, 5 | "**/bower_components": true, 6 | "public": true 7 | } 8 | , 9 | "vsicons.presets.angular": false 10 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Michael Hadley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": "> 0.25%, not dead, not ie 11", 7 | "useBuiltIns": "usage", 8 | "corejs": 3 9 | } 10 | ] 11 | ], 12 | "env": { 13 | "test": { 14 | "presets": [ 15 | [ 16 | "@babel/preset-env", 17 | { 18 | "modules": "commonjs", 19 | "targets": "> 0.25%, not dead, not ie 11", 20 | "useBuiltIns": "usage", 21 | "corejs": 3 22 | } 23 | ] 24 | ] 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /config/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": [ 3 | "packages/navmesh" 4 | ] 5 | } -------------------------------------------------------------------------------- /config/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "compilerOptions": { 4 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 5 | "resolveJsonModule": true, 6 | 7 | /* Basic Options */ 8 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 9 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 10 | "allowJs": false, /* Allow javascript files to be compiled. */ 11 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | "incremental": true, /* Enable incremental compilation */ 14 | // "lib": [], /* Specify library files to be included in the compilation. */ 15 | // "checkJs": true, /* Report errors in .js files. */ 16 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 17 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 18 | // "outFile": "./", /* Concatenate and emit output to single file. */ 19 | // "outDir": "./out", /* Redirect output structure to the directory. */ 20 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 21 | // "composite": true, /* Enable project compilation */ 22 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 23 | // "removeComments": true, /* Do not emit comments to output. */ 24 | // "noEmit": true, /* Do not emit outputs. */ 25 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 26 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 27 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 28 | 29 | /* Strict Type-Checking Options */ 30 | "strict": true, /* Enable all strict type-checking options. */ 31 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 32 | // "strictNullChecks": true, /* Enable strict null checks. */ 33 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 34 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 35 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 36 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 37 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 38 | 39 | /* Additional Checks */ 40 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 41 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 42 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 43 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 44 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 45 | 46 | /* Module Resolution Options */ 47 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 48 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 49 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 50 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 51 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 52 | // "typeRoots": [], /* List of folders to include type definitions from. */ 53 | // "types": [], /* Type declaration files to be included in compilation. */ 54 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 56 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 57 | 58 | /* Source Map Options */ 59 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 62 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 63 | 64 | /* Experimental Options */ 65 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 66 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 67 | 68 | /* Advanced Options */ 69 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 70 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /doc-source/after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/doc-source/after.png -------------------------------------------------------------------------------- /doc-source/before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/doc-source/before.png -------------------------------------------------------------------------------- /doc-source/combined.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/doc-source/combined.png -------------------------------------------------------------------------------- /doc-source/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/doc-source/demo.png -------------------------------------------------------------------------------- /doc-source/multiple-following-agents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/doc-source/multiple-following-agents.png -------------------------------------------------------------------------------- /doc-source/single-following-agent.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/doc-source/single-following-agent.gif -------------------------------------------------------------------------------- /doc-source/tiled-guide/tiled-final.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/doc-source/tiled-guide/tiled-final.png -------------------------------------------------------------------------------- /doc-source/tiled-guide/tiled-navmesh-layer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/doc-source/tiled-guide/tiled-navmesh-layer.gif -------------------------------------------------------------------------------- /doc-source/tiled-guide/tiled-object-layer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/doc-source/tiled-guide/tiled-object-layer.png -------------------------------------------------------------------------------- /doc-source/tiled-guide/tiled-preferences-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/doc-source/tiled-guide/tiled-preferences-2.png -------------------------------------------------------------------------------- /doc-source/tiled-guide/tiled-preferences.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/doc-source/tiled-guide/tiled-preferences.png -------------------------------------------------------------------------------- /doc-source/tiled-guide/tiled-snapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/doc-source/tiled-guide/tiled-snapping.png -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.11.0", 3 | "npmClient": "yarn", 4 | "useWorkspaces": true, 5 | "packages": ["packages/*"], 6 | "version": "independent" 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "packages/*" 5 | ], 6 | "scripts": { 7 | "bootstrap": "lerna bootstrap", 8 | "build:libs": "lerna run --parallel build --scope '{navmesh,phaser-navmesh,phaser2-navmesh}'", 9 | "test": "jest --config ./config/jest.config.json", 10 | "watch": "lerna run --parallel watch", 11 | "watch:libs": "lerna run --parallel watch --scope '{navmesh,phaser-navmesh,phaser2-navmesh}'", 12 | "serve:examples": "lerna run --parallel serve --scope '{examples-phaser3,examples-phaser2,examples-node}'", 13 | "dev": "concurrently \"yarn watch:libs\" \"yarn serve:examples\"", 14 | "dev:phaser2": "concurrently \"lerna run watch --scope '{navmesh,phaser2-navmesh}'\" \"lerna run serve --scope examples-phaser2\"", 15 | "dev:phaser3": "concurrently \"lerna run watch --scope '{navmesh,phaser-navmesh}'\" \"lerna run serve --scope examples-phaser3\"", 16 | "dev:node": "concurrently \"lerna run watch --scope navmesh\" \"lerna run serve --scope examples-node\"", 17 | "doc": "typedoc --tsconfig ./typedoc.tsconfig.json", 18 | "postdoc": "cp -r doc-source/ docs/", 19 | "deploy:examples": "gh-pages --branch gh-pages --dist packages/examples/public --add", 20 | "predeploy:examples": "cd packages/examples && npm run build", 21 | "deploy:doc": "gh-pages --branch gh-pages --dist docs --dest docs", 22 | "predeploy:doc": "npm run doc", 23 | "publish": "lerna publish", 24 | "prettier": "yarn workspaces run prettier --config ../../.prettierrc.json --write \"src/**/*.{js,ts,json}\"" 25 | }, 26 | "devDependencies": { 27 | "babel-jest": "^27.0.2", 28 | "concurrently": "^6.2.0", 29 | "cypress": "^7.5.0", 30 | "eslint": "^7.29.0", 31 | "eslint-config-prettier": "^8.3.0", 32 | "eslint-plugin-prettier": "^3.4.0", 33 | "gh-pages": "^3.2.3", 34 | "jest": "^27.0.4", 35 | "lerna": "^4.0.0", 36 | "prettier": "^2.3.1", 37 | "typedoc": "^0.21.0", 38 | "yarn": "^1.22.10" 39 | }, 40 | "prettier": { 41 | "printWidth": 100 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/examples-node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples-node", 3 | "private": true, 4 | "version": "1.0.7", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "", 9 | "build": "", 10 | "watch": "nodemon src/index.js", 11 | "serve": "npm run watch", 12 | "watch:ts": "nodemon --watch \"src/**\" --ext \"ts,json\" --exec \"ts-node src/typed-demo.ts\"", 13 | "serve:ts": "npm run watch" 14 | }, 15 | "keywords": [], 16 | "author": "", 17 | "license": "ISC", 18 | "dependencies": { 19 | "express": "^4.17.1", 20 | "navmesh": "^2.3.1", 21 | "nodemon": "^2.0.7", 22 | "yargs": "^17.0.1" 23 | }, 24 | "devDependencies": { 25 | "@types/express": "^4.17.12", 26 | "@types/node": "^15.12.4", 27 | "@types/yargs": "^17.0.0", 28 | "ts-node": "^10.0.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/examples-node/src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | // Simple usage of navmesh in Node to find a path within geometry 4 | 5 | const { NavMesh } = require("navmesh"); 6 | const express = require("express"); 7 | const yargs = require("yargs/yargs"); 8 | 9 | const argv = yargs(process.argv.slice(2)).argv; 10 | const port = argv.port ?? 8082; 11 | const app = express(); 12 | 13 | /* 14 | Imaging your game world has three walkable rooms, like this: 15 | 16 | +-----+-----+ 17 | | | | 18 | | 1 | 2 | 19 | | | | 20 | +-----------+ 21 | | | 22 | | 3 | 23 | | | 24 | +-----+ 25 | */ 26 | 27 | // The mesh is represented as an array where each element contains the points for an individual 28 | // polygon within the mesh. 29 | const meshPolygonPoints = [ 30 | // Polygon 1 31 | [ 32 | { x: 0, y: 0 }, 33 | { x: 10, y: 0 }, 34 | { x: 10, y: 10 }, 35 | { x: 0, y: 10 }, 36 | ], 37 | // Polygon 2 38 | [ 39 | { x: 10, y: 0 }, 40 | { x: 20, y: 0 }, 41 | { x: 20, y: 10 }, 42 | { x: 10, y: 10 }, 43 | ], 44 | // Polygon 3 45 | [ 46 | { x: 10, y: 10 }, 47 | { x: 20, y: 10 }, 48 | { x: 20, y: 20 }, 49 | { x: 10, y: 20 }, 50 | ], 51 | ]; 52 | 53 | const navMesh = new NavMesh(meshPolygonPoints); 54 | 55 | // Construct an SVG representation of the mesh. 56 | const initX = meshPolygonPoints[0][0].x; 57 | const initY = meshPolygonPoints[0][0].y; 58 | const bounds = { left: initX, right: initX, top: initY, bottom: initY }; 59 | meshPolygonPoints.forEach((polyPoints) => { 60 | polyPoints.forEach((point) => { 61 | if (point.x < bounds.left) bounds.left = point.x; 62 | if (point.x > bounds.right) bounds.right = point.x; 63 | if (point.y < bounds.top) bounds.top = point.y; 64 | if (point.y > bounds.bottom) bounds.bottom = point.y; 65 | }); 66 | }); 67 | const navWidth = bounds.right - bounds.left; 68 | const navHeight = bounds.bottom - bounds.top; 69 | const svgScale = 10; 70 | const viewbox = `-10 -10 ${navWidth * svgScale + 20} ${navHeight * svgScale + 20}`; 71 | 72 | app.get("/", (req, res) => { 73 | // Find a path from the top left of room 1 to the bottom left of room 3 74 | const path = navMesh.findPath({ x: 0, y: 0 }, { x: 10, y: 20 }); 75 | // ⮡ [{ x: 0, y: 0 }, { x: 10, y: 10 }, { x: 10, y: 20 }] 76 | 77 | const formattedPath = 78 | path !== null 79 | ? path.map((p) => `
  • (${p.x}, ${p.y})
  • `).join("\n") 80 | : "
  • No path found!
  • "; 81 | 82 | // Create an SVG visualization. 83 | const svgPolys = meshPolygonPoints.map((polyPoints) => { 84 | const pointString = polyPoints 85 | .map((point) => `${point.x * svgScale},${point.y * svgScale}`) 86 | .join(" "); 87 | return ``; 88 | }); 89 | let svgPathString = ""; 90 | if (path) { 91 | svgPathString += `M ${path[0].x * svgScale} ${path[0].y * svgScale} `; 92 | svgPathString += path.map((p) => `L ${p.x * svgScale} ${p.y * svgScale}`).join(" "); 93 | } 94 | const svgPath = ``; 95 | const svg = ` 96 | ${svgPolys} 97 | ${svgPath} 98 | `; 99 | 100 | res.send( 101 | ` 102 | 103 | 104 | 117 | 118 | 119 |

    NavMesh in Node

    120 |

    Visual Representation

    121 | ${svg} 122 |

    Path

    123 |

    Path from (0, 0) => (10, 20)

    124 | 127 | 128 | 129 | ` 130 | ); 131 | }); 132 | 133 | app.listen(port, () => console.log(`Node app listening on port ${port}!`)); 134 | -------------------------------------------------------------------------------- /packages/examples-node/src/typed-demo.ts: -------------------------------------------------------------------------------- 1 | // Simple usage of navmesh in Node to find a path within geometry 2 | 3 | import { NavMesh } from "navmesh"; 4 | import express from "express"; 5 | import yargs from "yargs"; 6 | import { hideBin } from "yargs/helpers"; 7 | 8 | const argv = yargs(hideBin(process.argv)).options({ port: { type: "number", default: 8082 } }).argv; 9 | // yargs returns argv as obj or a promise, so force it to be a plain object. 10 | const options = argv as { port: number }; 11 | const port = options.port ?? 8082; 12 | 13 | const app = express(); 14 | 15 | /* 16 | Imaging your game world has three walkable rooms, like this: 17 | 18 | +-----+-----+ 19 | | | | 20 | | 1 | 2 | 21 | | | | 22 | +-----------+ 23 | | | 24 | | 3 | 25 | | | 26 | +-----+ 27 | */ 28 | 29 | // The mesh is represented as an array where each element contains the points for an individual 30 | // polygon within the mesh. 31 | const meshPolygonPoints = [ 32 | // Polygon 1 33 | [ 34 | { x: 0, y: 0 }, 35 | { x: 10, y: 0 }, 36 | { x: 10, y: 10 }, 37 | { x: 0, y: 10 }, 38 | ], 39 | // Polygon 2 40 | [ 41 | { x: 10, y: 0 }, 42 | { x: 20, y: 0 }, 43 | { x: 20, y: 10 }, 44 | { x: 10, y: 10 }, 45 | ], 46 | // Polygon 3 47 | [ 48 | { x: 10, y: 10 }, 49 | { x: 20, y: 10 }, 50 | { x: 20, y: 20 }, 51 | { x: 10, y: 20 }, 52 | ], 53 | ]; 54 | const navMesh = new NavMesh(meshPolygonPoints); 55 | 56 | app.get("/", (req, res) => { 57 | // Find a path from the top left of room 1 to the bottom left of room 3 58 | const path = navMesh.findPath({ x: 0, y: 0 }, { x: 10, y: 20 }); 59 | // ⮡ [{ x: 0, y: 0 }, { x: 10, y: 10 }, { x: 10, y: 20 }] 60 | 61 | const formattedPath = 62 | path !== null 63 | ? path.map((p) => `
  • (${p.x}, ${p.y})
  • `).join("\n") 64 | : "
  • No path found!
  • "; 65 | 66 | res.send( 67 | ` 68 | 69 | 70 |

    Path from (0, 0) => (10, 20)

    71 | 74 | 75 | 76 | ` 77 | ); 78 | }); 79 | 80 | app.listen(port, () => console.log(`Node app listening on port ${port}!`)); 81 | -------------------------------------------------------------------------------- /packages/examples-node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../config/tsconfig.base.json", 3 | "include": ["src/**/*.ts"], 4 | "exclude": ["node_modules/**"], 5 | "compilerOptions": { 6 | "target": "ESNext" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/examples-phaser2/.gitignore: -------------------------------------------------------------------------------- 1 | public -------------------------------------------------------------------------------- /packages/examples-phaser2/.prettierignore: -------------------------------------------------------------------------------- 1 | src/demo/tilemaps -------------------------------------------------------------------------------- /packages/examples-phaser2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples-phaser2", 3 | "private": true, 4 | "version": "1.0.10", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "", 9 | "build": "webpack --mode production", 10 | "watch": "webpack --mode development --watch", 11 | "serve": "webpack serve --open --mode development --port 8081" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "core-js": "3", 18 | "easystarjs": "^0.4.4", 19 | "fontfaceobserver": "^2.1.0", 20 | "phaser-ce": "^2.18.0", 21 | "phaser2-navmesh": "^2.3.1" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.14.6", 25 | "@babel/preset-env": "^7.14.5", 26 | "babel-loader": "^8.2.2", 27 | "copy-webpack-plugin": "^9.0.0", 28 | "expose-loader": "^3.0.0", 29 | "webpack": "^5.40.0", 30 | "webpack-cli": "^4.7.2", 31 | "webpack-dev-server": "^3.11.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/examples-phaser2/src/demo/images/follower.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/packages/examples-phaser2/src/demo/images/follower.ai -------------------------------------------------------------------------------- /packages/examples-phaser2/src/demo/images/follower.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/packages/examples-phaser2/src/demo/images/follower.png -------------------------------------------------------------------------------- /packages/examples-phaser2/src/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Navmesh Example 7 | 8 | 30 | 31 | 32 | 33 |
    34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /packages/examples-phaser2/src/demo/js/game-objects/follower.js: -------------------------------------------------------------------------------- 1 | import Phaser from "phaser"; 2 | 3 | export default class FollowerSprite extends Phaser.Sprite { 4 | constructor(game, x, y, navMesh) { 5 | super(game, x, y, "follower"); 6 | this.anchor.set(0.5); 7 | this.game.add.existing(this); 8 | this.navMesh = navMesh; 9 | this.path = null; 10 | this.currentTarget = null; 11 | 12 | // Enable arcade physics for moving with velocity 13 | this.game.physics.arcade.enable(this); 14 | } 15 | 16 | goTo(targetPoint) { 17 | // Find a path to the target 18 | this.path = this.navMesh.findPath(this.position, targetPoint); 19 | 20 | // If there is a valid path, grab the first point from the path and set it as the target 21 | if (this.path && this.path.length > 0) this.currentTarget = this.path.shift(); 22 | else this.currentTarget = null; 23 | } 24 | 25 | update() { 26 | // Stop any previous movement 27 | this.body.velocity.set(0); 28 | 29 | // If we currently have a valid target location 30 | if (this.currentTarget) { 31 | // Move towards the target 32 | this._moveTowards(this.currentTarget); 33 | 34 | // Check if we have reached the current target (within a fudge factor) 35 | const d = this.position.distance(this.currentTarget); 36 | if (d < 5) { 37 | // If there is path left, grab the next point. Otherwise, null the target. 38 | if (this.path.length > 0) this.currentTarget = this.path.shift(); 39 | else this.currentTarget = null; 40 | } 41 | } 42 | } 43 | 44 | _moveTowards(position, maxSpeed = 200) { 45 | const angle = this.position.angle(position); 46 | 47 | // Move towards target 48 | const distance = this.position.distance(position); 49 | const targetSpeed = distance / (this.game.time.elapsedMS / 1000); 50 | const magnitude = Math.min(maxSpeed, targetSpeed); 51 | this.body.velocity.x = magnitude * Math.cos(angle); 52 | this.body.velocity.y = magnitude * Math.sin(angle); 53 | 54 | // Rotate towards target 55 | this.rotation = angle; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/examples-phaser2/src/demo/js/main.js: -------------------------------------------------------------------------------- 1 | // Phaser 2 wasn't intended to be used with npm modules. See webpack config for how these libs are 2 | // exposed. These need to be imported here so that all other files have access to them from the 3 | // global scope. 4 | import "pixi"; 5 | import "p2"; 6 | import Phaser from "phaser"; 7 | 8 | import Load from "./states/load"; 9 | import Start from "./states/start"; 10 | 11 | const game = new Phaser.Game({ 12 | width: 750, 13 | height: 750, 14 | renderer: Phaser.WEBGL, 15 | parent: "game-container", 16 | }); 17 | 18 | game.state.add("load", Load); 19 | game.state.add("start", Start); 20 | 21 | game.state.start("load"); 22 | -------------------------------------------------------------------------------- /packages/examples-phaser2/src/demo/js/states/load.js: -------------------------------------------------------------------------------- 1 | import Phaser from "phaser"; 2 | 3 | export default class LoadState extends Phaser.State { 4 | preload() { 5 | this.load.tilemap("map", "tilemaps/map.json", null, Phaser.Tilemap.TILED_JSON); 6 | this.load.image("tiles", "tilemaps/tiles.png"); 7 | this.load.image("follower", "images/follower.png"); 8 | } 9 | 10 | create() { 11 | this.game.state.start("start"); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/examples-phaser2/src/demo/js/states/start.js: -------------------------------------------------------------------------------- 1 | import Phaser from "phaser"; 2 | import { Phaser2NavMeshPlugin } from "phaser2-navmesh"; 3 | import FollowerSprite from "../game-objects/follower"; 4 | 5 | class StartState extends Phaser.State { 6 | create() { 7 | // -- Tilemap Setup -- 8 | 9 | // Load the map from the Phaser cache 10 | const tilemap = this.game.add.tilemap("map"); 11 | 12 | // Set up the tilesets - first parameter is name of tileset in Tiled and second paramter is 13 | // name of tileset image in Phaser's cache 14 | const wallTileset = tilemap.addTilesetImage("tiles", "tiles"); 15 | 16 | // Load the named layers - first parameter corresponds to layer name in Tiled 17 | tilemap.createLayer("bg", this.game.width, this.game.height); 18 | const wallLayer = tilemap.createLayer("walls", this.game.width, this.game.height); 19 | 20 | // Set all tiles in the wall layer to be colliding 21 | tilemap.setCollisionBetween( 22 | wallTileset.firstgid, 23 | wallTileset.firstgid + wallTileset.total, 24 | true, 25 | wallLayer 26 | ); 27 | 28 | // -- NavMesh Setup -- 29 | 30 | // Register the plugin with Phaser 31 | this.navMeshPlugin = this.game.plugins.add(Phaser2NavMeshPlugin); 32 | 33 | // Load the navMesh from the tilemap object layer "navmesh". The navMesh was created with 34 | // 12.5 pixels of space around obstacles. 35 | const navMesh = this.navMeshPlugin.buildMeshFromTiled( 36 | "mesh1", 37 | tilemap.objects["navmesh"], 38 | 12.5 39 | ); 40 | 41 | // Now you could find a path via navMesh.findPath(startPoint, endPoint) 42 | 43 | // -- Instructions -- 44 | 45 | const style = { 46 | font: "22px Arial", 47 | fill: "#ff0044", 48 | align: "left", 49 | backgroundColor: "#fff", 50 | }; 51 | const pathInfoText = this.game.add.text(10, 5, "Click to find a path!", style); 52 | this.game.add.text(10, 35, "Press 'm' to see navmesh.", style); 53 | 54 | // -- Click to Find Path -- 55 | 56 | // Graphics overlay for visualizing path 57 | const graphics = this.game.add.graphics(0, 0); 58 | graphics.alpha = 0.5; 59 | navMesh.enableDebug(graphics); 60 | 61 | // Game object that can follow a path (inherits from Phaser.Sprite) 62 | const follower = new FollowerSprite(this.game, 50, 200, navMesh); 63 | 64 | // On click 65 | this.game.input.onDown.add(() => { 66 | // Get the location of the mouse 67 | const target = this.game.input.activePointer.position.clone(); 68 | 69 | // Tell the follower sprite to find its path to the target 70 | follower.goTo(target); 71 | 72 | // For demo purposes, let's recalculate the path here and draw it on the screen 73 | const startTime = performance.now(); 74 | const path = navMesh.findPath(follower.position, target); 75 | // -> path is now an array of points, or null if no valid path found 76 | const pathTime = performance.now() - startTime; 77 | 78 | navMesh.debugDrawClear(); 79 | navMesh.debugDrawPath(path, 0xffd900); 80 | 81 | // Display the path, if it exists 82 | pathInfoText.setText( 83 | path 84 | ? `Path found in: ${pathTime.toFixed(2)}ms` 85 | : `No path found (${pathTime.toFixed(2)}ms)` 86 | ); 87 | }); 88 | 89 | // Toggle the navmesh visibility on/off 90 | this.game.input.keyboard.addKey(Phaser.KeyCode.M).onDown.add(() => { 91 | navMesh.debugDrawClear(); 92 | navMesh.debugDrawMesh({ 93 | drawCentroid: true, 94 | drawBounds: false, 95 | drawNeighbors: false, 96 | drawPortals: true, 97 | }); 98 | }); 99 | } 100 | 101 | shutdown() { 102 | // Clean up references and destroy navmeshes 103 | this.game.plugins.remove(this.navMeshPlugin, true); 104 | } 105 | } 106 | 107 | export default StartState; 108 | -------------------------------------------------------------------------------- /packages/examples-phaser2/src/demo/tilemaps/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/packages/examples-phaser2/src/demo/tilemaps/tiles.png -------------------------------------------------------------------------------- /packages/examples-phaser2/src/demo/tilemaps/tiles.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/packages/examples-phaser2/src/demo/tilemaps/tiles.psd -------------------------------------------------------------------------------- /packages/examples-phaser2/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/examples-phaser2/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 4 | const path = require("path"); 5 | const webpack = require("webpack"); 6 | const root = __dirname; 7 | 8 | module.exports = function (env, argv) { 9 | const isDev = argv.mode === "development"; 10 | 11 | return { 12 | mode: isDev ? "development" : "production", 13 | context: path.join(root, "src"), 14 | entry: { 15 | "demo/js/main": "./demo/js/main.js", 16 | }, 17 | output: { 18 | filename: "[name].js", 19 | path: path.resolve(root, "public"), 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.js$/, 25 | // Exclude phaser2-navmesh here when in a monorepo, because the resolved path isn't 26 | // through node_modules. In a project outside of the monorepo, you shouldn't need this. 27 | exclude: /node_modules|phaser2-navmesh/, 28 | // Configure babel to look for the root babel.config.json with rootMode. 29 | use: { loader: "babel-loader", options: { rootMode: "upward" } }, 30 | }, 31 | { 32 | test: require.resolve("phaser-ce/build/custom/pixi"), 33 | loader: "expose-loader", 34 | options: { 35 | exposes: ["PIXI"], 36 | }, 37 | }, 38 | { 39 | test: require.resolve("phaser-ce/build/custom/p2"), 40 | loader: "expose-loader", 41 | options: { 42 | exposes: ["p2"], 43 | }, 44 | }, 45 | { 46 | test: require.resolve("phaser-ce/build/custom/phaser-split"), 47 | loader: "expose-loader", 48 | options: { 49 | exposes: ["Phaser"], 50 | }, 51 | }, 52 | ], 53 | }, 54 | resolve: { 55 | alias: { 56 | pixi: require.resolve("phaser-ce/build/custom/pixi"), 57 | p2: require.resolve("phaser-ce/build/custom/p2"), 58 | phaser: require.resolve("phaser-ce/build/custom/phaser-split"), 59 | }, 60 | }, 61 | plugins: [ 62 | new CopyWebpackPlugin({ 63 | patterns: [{ from: "**/*", globOptions: { ignore: ["**/js/**/*"] } }], 64 | }), 65 | new webpack.DefinePlugin({ PRODUCTION: !isDev }), 66 | ], 67 | devtool: isDev ? "eval-source-map" : "source-map", 68 | }; 69 | }; 70 | -------------------------------------------------------------------------------- /packages/examples-phaser3/.gitignore: -------------------------------------------------------------------------------- 1 | public -------------------------------------------------------------------------------- /packages/examples-phaser3/.prettierignore: -------------------------------------------------------------------------------- 1 | src/performance/js/plugins 2 | src/performance/tilemaps 3 | src/demo/tilemaps -------------------------------------------------------------------------------- /packages/examples-phaser3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples-phaser3", 3 | "private": true, 4 | "version": "1.0.11", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "", 9 | "build": "webpack --mode production", 10 | "watch": "webpack --mode development --watch", 11 | "serve": "webpack serve --open --mode development --port 8080" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "core-js": "3", 18 | "easystarjs": "^0.4.4", 19 | "fontfaceobserver": "^2.1.0", 20 | "phaser": "^3.55.2", 21 | "phaser-navmesh": "^2.3.1" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.14.6", 25 | "@babel/preset-env": "^7.14.5", 26 | "babel-loader": "^8.2.2", 27 | "copy-webpack-plugin": "^9.0.0", 28 | "webpack": "^5.40.0", 29 | "webpack-cli": "^4.7.2", 30 | "webpack-dev-server": "^3.11.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/examples-phaser3/src/demo/images/follower.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/packages/examples-phaser3/src/demo/images/follower.ai -------------------------------------------------------------------------------- /packages/examples-phaser3/src/demo/images/follower.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/packages/examples-phaser3/src/demo/images/follower.png -------------------------------------------------------------------------------- /packages/examples-phaser3/src/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Navmesh Example 7 | 8 | 30 | 31 | 32 | 33 |
    34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /packages/examples-phaser3/src/demo/js/game-objects/follower.js: -------------------------------------------------------------------------------- 1 | import Phaser from "phaser"; 2 | 3 | const map = (value, min, max, newMin, newMax) => { 4 | return ((value - min) / (max - min)) * (newMax - newMin) + newMin; 5 | }; 6 | 7 | class FollowerSprite extends Phaser.GameObjects.Sprite { 8 | /** 9 | * @param {Phaser.Scene} scene 10 | * @param {*} x 11 | * @param {*} y 12 | * @param {*} navMesh 13 | * @param {*} wallLayer 14 | * @memberof FollowerSprite 15 | */ 16 | constructor(scene, x, y, navMesh) { 17 | super(scene, x, y, "follower"); 18 | 19 | this.navMesh = navMesh; 20 | this.path = null; 21 | this.currentTarget = null; 22 | this.scene = scene; 23 | 24 | // Enable arcade physics for moving with velocity 25 | scene.physics.world.enable(this); 26 | 27 | scene.add.existing(this); 28 | scene.events.on("update", this.update, this); 29 | scene.events.once("shutdown", this.destroy, this); 30 | } 31 | 32 | goTo(targetPoint) { 33 | // Find a path to the target 34 | this.path = this.navMesh.findPath(new Phaser.Math.Vector2(this.x, this.y), targetPoint); 35 | 36 | // If there is a valid path, grab the first point from the path and set it as the target 37 | if (this.path && this.path.length > 0) this.currentTarget = this.path.shift(); 38 | else this.currentTarget = null; 39 | } 40 | 41 | update(time, deltaTime) { 42 | // Bugfix: Phaser's event emitter caches listeners, so it's possible to get updated once after 43 | // being destroyed 44 | if (!this.body) return; 45 | 46 | // Stop any previous movement 47 | this.body.velocity.set(0); 48 | 49 | if (this.currentTarget) { 50 | // Check if we have reached the current target (within a fudge factor) 51 | const { x, y } = this.currentTarget; 52 | const distance = Phaser.Math.Distance.Between(this.x, this.y, x, y); 53 | 54 | if (distance < 5) { 55 | // If there is path left, grab the next point. Otherwise, null the target. 56 | if (this.path.length > 0) this.currentTarget = this.path.shift(); 57 | else this.currentTarget = null; 58 | } 59 | 60 | // Slow down as we approach final point in the path. This helps prevent issues with the 61 | // physics body overshooting the goal and leaving the mesh. 62 | let speed = 400; 63 | if (this.path.length === 0 && distance < 50) { 64 | speed = map(distance, 50, 0, 400, 50); 65 | } 66 | 67 | // Still got a valid target? 68 | if (this.currentTarget) this.moveTowards(this.currentTarget, speed, deltaTime / 1000); 69 | } 70 | } 71 | 72 | moveTowards(targetPosition, maxSpeed = 200, elapsedSeconds) { 73 | const { x, y } = targetPosition; 74 | const angle = Phaser.Math.Angle.Between(this.x, this.y, x, y); 75 | const distance = Phaser.Math.Distance.Between(this.x, this.y, x, y); 76 | const targetSpeed = distance / elapsedSeconds; 77 | const magnitude = Math.min(maxSpeed, targetSpeed); 78 | 79 | this.scene.physics.velocityFromRotation(angle, magnitude, this.body.velocity); 80 | this.rotation = angle; 81 | } 82 | 83 | destroy() { 84 | if (this.scene) this.scene.events.off("update", this.update, this); 85 | super.destroy(); 86 | } 87 | } 88 | 89 | export default FollowerSprite; 90 | -------------------------------------------------------------------------------- /packages/examples-phaser3/src/demo/js/main.js: -------------------------------------------------------------------------------- 1 | import Phaser from "phaser"; 2 | import Load from "./scenes/load"; 3 | import Start from "./scenes/start"; 4 | import ManyPaths from "./scenes/many-paths"; 5 | import { PhaserNavMeshPlugin } from "phaser-navmesh"; 6 | 7 | const game = new Phaser.Game({ 8 | type: Phaser.AUTO, 9 | parent: "game-container", 10 | width: 750, 11 | height: 750, 12 | backgroundColor: "#fff", 13 | pixelArt: false, 14 | plugins: { 15 | scene: [ 16 | { 17 | key: "NavMeshPlugin", // Key to store the plugin class under in cache 18 | plugin: PhaserNavMeshPlugin, // Class that constructs plugins 19 | mapping: "navMeshPlugin", // Property mapping to use for the scene, e.g. this.navMeshPlugin 20 | start: true, 21 | }, 22 | ], 23 | }, 24 | physics: { 25 | default: "arcade", 26 | arcade: { 27 | gravity: 0, 28 | }, 29 | }, 30 | }); 31 | 32 | game.scene.add("load", Load); 33 | game.scene.add("start", Start); 34 | game.scene.add("many-paths", ManyPaths); 35 | game.scene.start("load"); 36 | -------------------------------------------------------------------------------- /packages/examples-phaser3/src/demo/js/scenes/load.js: -------------------------------------------------------------------------------- 1 | import Phaser from "phaser"; 2 | import FontFaceObserver from "fontfaceobserver"; 3 | 4 | export default class Load extends Phaser.Scene { 5 | preload() { 6 | const loadingBar = this.add.graphics(); 7 | const { width, height } = this.sys.game.config; 8 | this.load.on("progress", (value) => { 9 | loadingBar.clear(); 10 | loadingBar.fillStyle(0xffffff, 1); 11 | loadingBar.fillRect(0, height / 2 - 25, width * value, 50); 12 | }); 13 | this.load.on("complete", () => loadingBar.destroy()); 14 | 15 | this.load.tilemapTiledJSON("map", "tilemaps/map.json"); 16 | this.load.image("tiles", "tilemaps/tiles.png"); 17 | this.load.image("follower", "images/follower.png"); 18 | 19 | this.fontLoaded = false; 20 | this.fontErrored = false; 21 | new FontFaceObserver("Josefin Sans") 22 | .load() 23 | .then(() => (this.fontLoaded = true)) 24 | .catch(() => (this.fontErrored = true)); 25 | } 26 | 27 | update() { 28 | if (this.fontLoaded || this.fontErrored) this.scene.start("start"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/examples-phaser3/src/demo/js/scenes/many-paths.js: -------------------------------------------------------------------------------- 1 | import FollowerSprite from "../game-objects/follower"; 2 | 3 | export default class ManyPaths extends Phaser.Scene { 4 | create() { 5 | // -- Tilemap Setup -- 6 | 7 | // Load the map from the Phaser cache 8 | const tilemap = this.add.tilemap("map"); 9 | this.tilemap = tilemap; 10 | 11 | // Set up the tilesets - first parameter is name of tileset in Tiled and second paramter is 12 | // name of tileset image in Phaser's cache 13 | const wallTileset = tilemap.addTilesetImage("tiles", "tiles"); 14 | 15 | // Load the named layers - first parameter corresponds to layer name in Tiled 16 | tilemap.createLayer("bg", wallTileset); 17 | 18 | const wallLayer = tilemap.createLayer("walls", wallTileset); 19 | wallLayer.setCollisionByProperty({ collides: true }); 20 | this.wallLayer = wallLayer; 21 | 22 | // -- NavMesh Setup -- 23 | 24 | // Load the navMesh from the tilemap object layer "navmesh". The navMesh was created with 25 | // 12.5 pixels of space around obstacles. 26 | const objectLayer = tilemap.getObjectLayer("navmesh"); 27 | this.navMesh = this.navMeshPlugin.buildMeshFromTiled("mesh1", objectLayer, 12.5); 28 | 29 | // Now you could find a path via navMesh.findPath(startPoint, endPoint) 30 | 31 | // -- Instructions -- 32 | const style = { 33 | font: "22px Josefin Sans", 34 | fill: "#ff0044", 35 | padding: { x: 20, y: 10 }, 36 | backgroundColor: "#fff", 37 | }; 38 | this.uiTextLines = [ 39 | "Click to add followers", 40 | "Press '1' to go to scene 1.", 41 | "Followers: ", 42 | "FPS: ", 43 | ]; 44 | this.uiText = this.add.text(10, 5, this.uiTextLines, style).setAlpha(0.9).setDepth(1000000); 45 | 46 | // -- Click to Add Follower -- 47 | 48 | // Game object that can follow a path (inherits from Phaser.Sprite) 49 | this.followers = []; 50 | this.addFollowers(200, 200, 25); 51 | 52 | // On click 53 | this.input.on("pointerdown", (pointer) => { 54 | const worldPoint = pointer.positionToCamera(this.cameras.main); 55 | if (!wallLayer.hasTileAtWorldXY(worldPoint.x, worldPoint.y)) { 56 | this.addFollowers(pointer.x, pointer.y, 25); 57 | } 58 | }); 59 | 60 | // -- Scene Changer -- 61 | 62 | this.input.keyboard.on("keydown-ONE", () => { 63 | this.scene.start("start"); 64 | }); 65 | } 66 | 67 | update(time, delta) { 68 | this.uiTextLines[2] = `Followers: ${this.followers.length}`; 69 | this.uiTextLines[3] = `FPS: ${(1000 / delta).toFixed(2)}`; 70 | this.uiText.setText(this.uiTextLines); 71 | 72 | this.followers.forEach((follower) => { 73 | if (!follower.currentTarget) { 74 | const randomTarget = this.getRandomEmptyPoint(); 75 | follower.goTo(randomTarget); 76 | } 77 | }); 78 | } 79 | 80 | addFollowers(x, y, num) { 81 | for (let i = 0; i < num; i++) { 82 | const follower = new FollowerSprite(this, x, y, this.navMesh); 83 | this.followers.push(follower); 84 | const randomTarget = this.getRandomEmptyPoint(); 85 | follower.goTo(randomTarget); 86 | } 87 | } 88 | 89 | getRandomEmptyPoint() { 90 | // Find random tile that is empty 91 | let tileX, tileY, tile; 92 | do { 93 | tileX = Phaser.Math.Between(0, this.tilemap.width); 94 | tileY = Phaser.Math.Between(0, this.tilemap.height); 95 | tile = this.wallLayer.hasTileAt(tileX, tileY); 96 | } while (tile && tile.collides); 97 | // Convert from tile location to pixel location (at center of tile) 98 | return { 99 | x: tileX * this.tilemap.tileWidth + this.tilemap.tileWidth / 2, 100 | y: tileY * this.tilemap.tileHeight + this.tilemap.tileHeight / 2, 101 | }; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /packages/examples-phaser3/src/demo/js/scenes/start.js: -------------------------------------------------------------------------------- 1 | import Phaser from "phaser"; 2 | import FollowerSprite from "../game-objects/follower"; 3 | 4 | export default class Start extends Phaser.Scene { 5 | create() { 6 | // -- Tilemap Setup -- 7 | 8 | // Load the map from the Phaser cache 9 | const tilemap = this.add.tilemap("map"); 10 | 11 | // Set up the tilesets - first parameter is name of tileset in Tiled and second paramter is 12 | // name of tileset image in Phaser's cache 13 | const wallTileset = tilemap.addTilesetImage("tiles", "tiles"); 14 | 15 | // Load the named layers - first parameter corresponds to layer name in Tiled 16 | tilemap.createLayer("bg", wallTileset); 17 | 18 | const wallLayer = tilemap.createLayer("walls", wallTileset); 19 | wallLayer.setCollisionByProperty({ collides: true }); 20 | 21 | // -- NavMesh Setup -- 22 | 23 | // You can load a navmesh created by hand in Tiled: 24 | // Load the navMesh from the tilemap object layer "navmesh". The navMesh was created with 25 | // 12.5 pixels of space around obstacles. 26 | // const objectLayer = tilemap.getObjectLayer("navmesh"); 27 | // const navMesh = this.navMeshPlugin.buildMeshFromTiled("mesh1", objectLayer, 12.5); 28 | 29 | // Or, you can build one from your tilemap automatically: 30 | const navMesh = this.navMeshPlugin.buildMeshFromTilemap("mesh1", tilemap, [wallLayer]); 31 | 32 | // Now you could find a path via navMesh.findPath(startPoint, endPoint) 33 | 34 | // -- Click to Find Path -- 35 | 36 | // Graphics overlay for visualizing path 37 | const graphics = this.add.graphics(0, 0).setAlpha(0.5); 38 | navMesh.enableDebug(graphics); 39 | 40 | // Game object that can follow a path (inherits from Phaser.Sprite) 41 | const follower = new FollowerSprite(this, 50, 200, navMesh); 42 | 43 | // On click 44 | this.input.on("pointerdown", (pointer) => { 45 | const start = new Phaser.Math.Vector2(follower.x, follower.y); 46 | const end = new Phaser.Math.Vector2(pointer.x, pointer.y); 47 | 48 | // Tell the follower sprite to find its path to the target 49 | follower.goTo(end); 50 | 51 | // For demo purposes, let's recalculate the path here and draw it on the screen 52 | const startTime = performance.now(); 53 | const path = navMesh.findPath(start, end); 54 | // -> path is now an array of points, or null if no valid path found 55 | const pathTime = performance.now() - startTime; 56 | 57 | navMesh.debugDrawClear(); 58 | navMesh.debugDrawPath(path, 0xffd900); 59 | 60 | const formattedTime = pathTime.toFixed(3); 61 | uiTextLines[0] = path 62 | ? `Path found in: ${formattedTime}ms` 63 | : `No path found (${formattedTime}ms)`; 64 | uiText.setText(uiTextLines); 65 | }); 66 | 67 | // Display whether the mouse is currently over a valid point in the navmesh 68 | this.input.on(Phaser.Input.Events.POINTER_MOVE, (pointer) => { 69 | const isInMesh = navMesh.isPointInMesh(pointer); 70 | uiTextLines[1] = `Is mouse inside navmesh: ${isInMesh ? "yes" : "no "}`; 71 | uiText.setText(uiTextLines); 72 | }); 73 | 74 | // Toggle the navmesh visibility on/off 75 | this.input.keyboard.on("keydown-M", () => { 76 | navMesh.debugDrawClear(); 77 | navMesh.debugDrawMesh({ 78 | drawCentroid: true, 79 | drawBounds: false, 80 | drawNeighbors: false, 81 | drawPortals: true, 82 | }); 83 | }); 84 | 85 | // -- Instructions -- 86 | 87 | const style = { 88 | font: "22px Josefin Sans", 89 | fill: "#ff0044", 90 | padding: { x: 20, y: 10 }, 91 | backgroundColor: "#fff", 92 | }; 93 | const uiTextLines = [ 94 | "Click to find a path!", 95 | "Is mouse inside navmesh: false", 96 | "Press 'm' to see navmesh.", 97 | "Press '2' to go to scene 2.", 98 | ]; 99 | const uiText = this.add.text(10, 5, uiTextLines, style).setAlpha(0.9); 100 | 101 | // -- Scene Changer -- 102 | this.input.keyboard.on("keydown-TWO", () => { 103 | this.scene.start("many-paths"); 104 | }); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /packages/examples-phaser3/src/demo/tilemaps/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/packages/examples-phaser3/src/demo/tilemaps/tiles.png -------------------------------------------------------------------------------- /packages/examples-phaser3/src/demo/tilemaps/tiles.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/packages/examples-phaser3/src/demo/tilemaps/tiles.psd -------------------------------------------------------------------------------- /packages/examples-phaser3/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/examples-phaser3/src/performance/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Navmesh Example 6 | 7 | 29 | 30 | 31 | 32 |
    33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /packages/examples-phaser3/src/performance/js/main.js: -------------------------------------------------------------------------------- 1 | import Phaser from "phaser"; 2 | import Load from "./scenes/load"; 3 | import Start from "./scenes/start"; 4 | import NavMeshPlugin from "phaser-navmesh"; 5 | 6 | const game = new Phaser.Game({ 7 | type: Phaser.AUTO, 8 | parent: "game-container", 9 | width: 750, 10 | height: 750, 11 | backgroundColor: "#fff", 12 | pixelArt: false, 13 | plugins: { 14 | scene: [ 15 | { 16 | key: "NavMeshPlugin", // Key to store the plugin class under in cache 17 | plugin: NavMeshPlugin, // Class that constructs plugins 18 | mapping: "navMeshPlugin", // Property mapping to use for the scene, e.g. this.navMeshPlugin 19 | start: true, 20 | }, 21 | ], 22 | }, 23 | }); 24 | 25 | game.scene.add("load", Load); 26 | game.scene.add("start", Start); 27 | game.scene.start("load"); 28 | -------------------------------------------------------------------------------- /packages/examples-phaser3/src/performance/js/plugins/easy-star-plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Modified from PathFinderPlugin: https://github.com/appsbu-de/phaser_plugin_pathfinding 3 | */ 4 | 5 | import EasyStar from "easystarjs"; 6 | import Phaser from "phaser"; 7 | 8 | /** 9 | * A plugin that is powered by easystarjs. Easystar is forced to be synchronous in order to make 10 | * it easy to use. The plugin needs to be set up via EasyStarPlugin#setGrid before paths can be 11 | * calculated. 12 | * 13 | * Inspired by: https://github.com/appsbu-de/phaser_plugin_pathfinding 14 | * 15 | * @class EasyStarPlugin 16 | * @extends {Phaser.Plugin} 17 | */ 18 | class EasyStarPlugin extends Phaser.Plugin { 19 | /** 20 | * Creates an instance of EasyStarPlugin. 21 | * @param {Phaser.Game} game 22 | * @param {Phaser.PluginManager} manager 23 | * 24 | * @memberOf EasyStarPlugin 25 | */ 26 | constructor(game, manager) { 27 | super(game, manager); 28 | this.game = game; 29 | this._pluginManager = manager; 30 | 31 | this._easyStar = new EasyStar.js(); 32 | this._easyStar.enableSync(); // Make easy star synchronous - for testing! 33 | } 34 | 35 | /** 36 | * Set the grid for pathfinding. 37 | * 38 | * @param {Phaser.TilemapLayer} tilemapLayer The tilemap layer to use for the pathfinding 39 | * @param {number[]} walkableTiles An array of tile indices that are walkable in the layer. 40 | * Everything else is not walkable. 41 | * 42 | * @returns {this} 43 | * 44 | * @memberOf EasyStarPlugin 45 | */ 46 | setGrid(tilemapLayer, walkableTiles) { 47 | this._tilemap = tilemapLayer.map; 48 | this._tilemapLayer = tilemapLayer; 49 | this._tileWidth = this._tilemap.width; 50 | this._tileHeight = this._tilemap.height; 51 | 52 | // Extract the tile indices from the layer and store them in a 2D grid 53 | this._grid = []; 54 | const data = tilemapLayer.layer.data; 55 | for (const row of data) { 56 | const gridRow = []; 57 | for (const tile of row) { 58 | gridRow.push(tile.index); 59 | } 60 | this._grid.push(gridRow); 61 | } 62 | 63 | this._walkables = walkableTiles; 64 | this._easyStar.setGrid(this._grid); 65 | this._easyStar.setAcceptableTiles(this._walkables); 66 | this._easyStar.enableDiagonals(); 67 | return this; 68 | } 69 | 70 | setTileCost(tile, cost) { 71 | this._easyStar.setTileCost(tile, cost); 72 | } 73 | 74 | setTileCosts(tileCosts) { 75 | for (const tileCost of tileCosts) this.setTileCost(tileCost.tile, tileCost.cost); 76 | } 77 | 78 | /** 79 | * Calculate the easystar path between two tile locations in the current grid. 80 | * 81 | * @param {Phaser.Point} tileStart A point representing the starting location in tile coords 82 | * @param {Phaser.Point} tileDest A point representing the destination location in tile coords 83 | * @returns {object[]|null} Returns array of points in the form {x, y} or null if no path found 84 | * 85 | * @memberOf EasyStarPlugin 86 | */ 87 | getTilePath(tileStart, tileDest) { 88 | let path = null; 89 | this._easyStar.findPath(tileStart.x, tileStart.y, tileDest.x, tileDest.y, (p) => path = p); 90 | this._easyStar.calculate(); 91 | return path; 92 | } 93 | 94 | /** 95 | * Calculate the easystar path between two world locations in the current grid. The returned 96 | * path's points are in world coordinates - each point is the (world coord) center of a tile 97 | * along the path. 98 | * 99 | * @param {Phaser.Point} worldStart A point representing the starting location in world coords 100 | * @param {Phaser.Point} worldDest A point representing the destination location in world coords 101 | * @returns {object[]|null} Returns array of points in the form {x, y} or null if no path found 102 | * 103 | * @memberOf EasyStarPlugin 104 | */ 105 | getWorldPath(worldStart, worldDest) { 106 | const tileStart = this._tilemapLayer.getTileXY(worldStart.x, worldStart.y, {}); 107 | const tileDest = this._tilemapLayer.getTileXY(worldDest.x, worldDest.y, {}); 108 | // Keep the start and destination on the tilemap 109 | tileStart.x = Phaser.Math.clamp(tileStart.x, 0, this._tilemap.width - 1); 110 | tileStart.y = Phaser.Math.clamp(tileStart.y, 0, this._tilemap.height - 1); 111 | tileDest.x = Phaser.Math.clamp(tileDest.x, 0, this._tilemap.width - 1); 112 | tileDest.y = Phaser.Math.clamp(tileDest.y, 0, this._tilemap.height - 1); 113 | const path = this.getTilePath(tileStart, tileDest); 114 | if (path) { 115 | const tw = this._tilemap.tileWidth; 116 | const th = this._tilemap.tileHeight; 117 | for (let i = 0; i < path.length; i++) { 118 | path[i].x = (path[i].x * tw) + (tw / 2); 119 | path[i].y = (path[i].y * th) + (th / 2); 120 | } 121 | } 122 | return path; 123 | } 124 | } 125 | 126 | export default EasyStarPlugin; -------------------------------------------------------------------------------- /packages/examples-phaser3/src/performance/js/scenes/load.js: -------------------------------------------------------------------------------- 1 | import Phaser from "phaser"; 2 | import FontFaceObserver from "fontfaceobserver"; 3 | 4 | export default class Load extends Phaser.Scene { 5 | preload() { 6 | const loadingBar = this.add.graphics(); 7 | const { width, height } = this.sys.game.config; 8 | this.load.on("progress", (value) => { 9 | loadingBar.clear(); 10 | loadingBar.fillStyle(0xffffff, 1); 11 | loadingBar.fillRect(0, height / 2 - 25, width * value, 50); 12 | }); 13 | this.load.on("complete", () => loadingBar.destroy()); 14 | 15 | this.load.tilemapTiledJSON("map", "tilemaps/map.json"); 16 | this.load.image("tiles", "tilemaps/tiles.png"); 17 | 18 | this.fontLoaded = false; 19 | this.fontErrored = false; 20 | new FontFaceObserver("Josefin Sans") 21 | .load() 22 | .then(() => (this.fontLoaded = true)) 23 | .catch(() => (this.fontErrored = true)); 24 | } 25 | 26 | update() { 27 | if (this.fontLoaded || this.fontErrored) this.scene.start("start"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/examples-phaser3/src/performance/js/scenes/start.js: -------------------------------------------------------------------------------- 1 | import Phaser from "phaser"; 2 | // import "../plugins/phaser-astar"; 3 | // import EasyStarPlugin from "../plugins/easy-star-plugin"; 4 | 5 | export default class StartState extends Phaser.Scene { 6 | create() { 7 | // -- Setup -- 8 | 9 | const tilemap = this.add.tilemap("map"); 10 | this.tilemap = tilemap; 11 | const wallTileset = tilemap.addTilesetImage("tiles", "tiles"); 12 | tilemap.createLayer("bg", wallTileset); 13 | const wallLayer = tilemap.createLayer("walls", wallTileset); 14 | wallLayer.setCollisionByProperty({ collides: true }); 15 | this.wallLayer = wallLayer; 16 | 17 | // -- Plugins -- 18 | 19 | const objectLayer = tilemap.getObjectLayer("navmesh"); 20 | const navMesh = this.navMeshPlugin.buildMeshFromTiled("mesh1", objectLayer, 12.5); 21 | 22 | // // Configure Phaser A* Plugin 23 | // this.phaserAStar = this.game.plugins.add(Phaser.Plugin.AStar); 24 | // this.phaserAStar.setAStarMap(tilemap, "walls", "tiles"); 25 | 26 | // // Configure Easy Star Plugin 27 | // this.easyStar = this.game.plugins.add(EasyStarPlugin); 28 | // this.easyStar.setGrid(wallLayer, [-1]); 29 | 30 | // -- Compare Performance --- 31 | 32 | const iterations = 1000; 33 | const shortPaths = []; 34 | const longPaths = []; 35 | for (let i = 0; i < iterations; i += 1) { 36 | shortPaths.push(this.getRandomPointsWithDistance(50, 150)); 37 | longPaths.push(this.getRandomPointsWithDistance(600)); 38 | } 39 | 40 | let start; 41 | const perf = { shortPaths: {}, longPaths: {} }; 42 | 43 | // start = performance.now(); 44 | // for (const path of shortPaths) this._phaserAStar(path[0], path[1]); 45 | // perf.shortPaths.phaserAStar = (performance.now() - start) / iterations; 46 | 47 | // start = performance.now(); 48 | // for (const path of shortPaths) this.easyStar.getWorldPath(path[0], path[1]); 49 | // perf.shortPaths.easyStar = (performance.now() - start) / iterations; 50 | 51 | start = performance.now(); 52 | for (const path of shortPaths) navMesh.findPath(path[0], path[1]); 53 | perf.shortPaths.navMesh = (performance.now() - start) / iterations; 54 | 55 | // start = performance.now(); 56 | // for (const path of longPaths) this._phaserAStar(path[0], path[1]); 57 | // perf.longPaths.phaserAStar = (performance.now() - start) / iterations; 58 | 59 | // start = performance.now(); 60 | // for (const path of longPaths) this.easyStar.getWorldPath(path[0], path[1]); 61 | // perf.longPaths.easyStar = (performance.now() - start) / iterations; 62 | 63 | start = performance.now(); 64 | for (const path of longPaths) navMesh.findPath(path[0], path[1]); 65 | perf.longPaths.navMesh = (performance.now() - start) / iterations; 66 | 67 | let message = `Performance Comparison, ${iterations} iterations, 30x30 tilemap`; 68 | message += "\n\nShort paths (150 - 500 pixel length)\n"; 69 | // message += `\n\tAStart Plugin: ${perf.shortPaths.phaserAStar.toFixed(5)}ms`; 70 | // message += `\n\tEasyStar Plugin: ${perf.shortPaths.easyStar.toFixed(5)}ms`; 71 | message += `\n\tNavMesh Plugin: ${perf.shortPaths.navMesh.toFixed(5)}ms`; 72 | // const shortNavVsEasy = perf.shortPaths.easyStar / perf.shortPaths.navMesh; 73 | // const shortNavVsPhaser = perf.shortPaths.phaserAStar / perf.shortPaths.navMesh; 74 | // message += `\n\tNavMesh is ${shortNavVsPhaser.toFixed(2)}x faster than Phaser AStar`; 75 | // message += `\n\tNavMesh is ${shortNavVsEasy.toFixed(2)}x faster than EasyStar`; 76 | message += "\n\nLong paths (600 pixels and greater length)\n"; 77 | // message += `\n\tAStart Plugin: ${perf.longPaths.phaserAStar.toFixed(5)}ms`; 78 | // message += `\n\tEasyStar Plugin: ${perf.longPaths.easyStar.toFixed(5)}ms`; 79 | message += `\n\tNavMesh Plugin: ${perf.longPaths.navMesh.toFixed(5)}ms`; 80 | // const longNavVsEasy = perf.longPaths.easyStar / perf.longPaths.navMesh; 81 | // const longNavVsPhaser = perf.longPaths.phaserAStar / perf.longPaths.navMesh; 82 | // message += `\n\tNavMesh is ${longNavVsPhaser.toFixed(2)}x faster than Phaser AStar`; 83 | // message += `\n\tNavMesh is ${longNavVsEasy.toFixed(2)}x faster than EasyStar`; 84 | 85 | console.log(message); 86 | } 87 | 88 | _phaserAStar(start, end) { 89 | const startTile = this.wallLayer.getTileXY(start.x, start.y, {}); 90 | const endTile = this.wallLayer.getTileXY(end.x, end.y, {}); 91 | return this.phaserAStar.findPath(startTile, endTile); 92 | } 93 | 94 | _drawPath(graphic, path) { 95 | graphic.lineStyle(5, 0xffd900); 96 | const p = new Phaser.Polygon(...path); 97 | p.closed = false; 98 | graphic.drawShape(p); 99 | graphic.beginFill(0xffd900); 100 | graphic.drawEllipse(path[0].x, path[0].y, 10, 10); 101 | const lastPoint = path[path.length - 1]; 102 | graphic.drawEllipse(lastPoint.x, lastPoint.y, 10, 10); 103 | graphic.endFill(); 104 | } 105 | 106 | getRandomEmptyPoint() { 107 | // Find random tile that is empty 108 | let tileX, tileY, tile; 109 | do { 110 | tileX = Phaser.Math.Between(0, this.tilemap.width); 111 | tileY = Phaser.Math.Between(0, this.tilemap.height); 112 | tile = this.wallLayer.hasTileAt(tileX, tileY); 113 | } while (tile && tile.collides); 114 | // Convert from tile location to pixel location (at center of tile) 115 | return new Phaser.Math.Vector2( 116 | tileX * this.tilemap.tileWidth + this.tilemap.tileWidth / 2, 117 | tileY * this.tilemap.tileHeight + this.tilemap.tileHeight / 2 118 | ); 119 | } 120 | 121 | getRandomPointsWithDistance(minDistance = 0, maxDistance = Number.MAX_VALUE) { 122 | let p1 = this.getRandomEmptyPoint(); 123 | let p2 = this.getRandomEmptyPoint(); 124 | let d = p1.distance(p2); 125 | while (d < minDistance || d > maxDistance) { 126 | p1 = this.getRandomEmptyPoint(); 127 | p2 = this.getRandomEmptyPoint(); 128 | d = p1.distance(p2); 129 | } 130 | return [p1, p2]; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /packages/examples-phaser3/src/performance/tilemaps/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/packages/examples-phaser3/src/performance/tilemaps/tiles.png -------------------------------------------------------------------------------- /packages/examples-phaser3/src/performance/tilemaps/tiles.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/packages/examples-phaser3/src/performance/tilemaps/tiles.psd -------------------------------------------------------------------------------- /packages/examples-phaser3/src/test/images/follower.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/packages/examples-phaser3/src/test/images/follower.ai -------------------------------------------------------------------------------- /packages/examples-phaser3/src/test/images/follower.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/packages/examples-phaser3/src/test/images/follower.png -------------------------------------------------------------------------------- /packages/examples-phaser3/src/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Navmesh Example 6 | 7 | 29 | 30 | 31 | 32 |
    33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /packages/examples-phaser3/src/test/js/game-objects/follower.js: -------------------------------------------------------------------------------- 1 | import Phaser from "phaser"; 2 | 3 | const map = (value, min, max, newMin, newMax) => { 4 | return ((value - min) / (max - min)) * (newMax - newMin) + newMin; 5 | }; 6 | 7 | class FollowerSprite extends Phaser.GameObjects.Sprite { 8 | /** 9 | * @param {Phaser.Scene} scene 10 | * @param {*} x 11 | * @param {*} y 12 | * @param {*} navMesh 13 | * @param {*} wallLayer 14 | * @memberof FollowerSprite 15 | */ 16 | constructor(scene, x, y, navMesh) { 17 | super(scene, x, y, "follower"); 18 | 19 | this.navMesh = navMesh; 20 | this.path = null; 21 | this.currentTarget = null; 22 | this.scene = scene; 23 | 24 | // Enable arcade physics for moving with velocity 25 | scene.physics.world.enable(this); 26 | 27 | scene.add.existing(this); 28 | scene.events.on("update", this.update, this); 29 | scene.events.once("shutdown", this.destroy, this); 30 | } 31 | 32 | goTo(targetPoint) { 33 | // Find a path to the target 34 | this.path = this.navMesh.findPath(new Phaser.Math.Vector2(this.x, this.y), targetPoint); 35 | 36 | // If there is a valid path, grab the first point from the path and set it as the target 37 | if (this.path && this.path.length > 0) this.currentTarget = this.path.shift(); 38 | else this.currentTarget = null; 39 | } 40 | 41 | update(time, deltaTime) { 42 | // Bugfix: Phaser's event emitter caches listeners, so it's possible to get updated once after 43 | // being destroyed 44 | if (!this.body) return; 45 | 46 | // Stop any previous movement 47 | this.body.velocity.set(0); 48 | 49 | if (this.currentTarget) { 50 | // Check if we have reached the current target (within a fudge factor) 51 | const { x, y } = this.currentTarget; 52 | const distance = Phaser.Math.Distance.Between(this.x, this.y, x, y); 53 | 54 | if (distance < 5) { 55 | // If there is path left, grab the next point. Otherwise, null the target. 56 | if (this.path.length > 0) this.currentTarget = this.path.shift(); 57 | else this.currentTarget = null; 58 | } 59 | 60 | // Slow down as we approach final point in the path. This helps prevent issues with the 61 | // physics body overshooting the goal and leaving the mesh. 62 | let speed = 400; 63 | if (this.path.length === 0 && distance < 50) { 64 | speed = map(distance, 50, 0, 400, 50); 65 | } 66 | 67 | // Still got a valid target? 68 | if (this.currentTarget) this.moveTowards(this.currentTarget, speed, deltaTime / 1000); 69 | } 70 | } 71 | 72 | moveTowards(targetPosition, maxSpeed = 200, elapsedSeconds) { 73 | const { x, y } = targetPosition; 74 | const angle = Phaser.Math.Angle.Between(this.x, this.y, x, y); 75 | const distance = Phaser.Math.Distance.Between(this.x, this.y, x, y); 76 | const targetSpeed = distance / elapsedSeconds; 77 | const magnitude = Math.min(maxSpeed, targetSpeed); 78 | 79 | this.scene.physics.velocityFromRotation(angle, magnitude, this.body.velocity); 80 | this.rotation = angle; 81 | } 82 | 83 | destroy() { 84 | if (this.scene) this.scene.events.off("update", this.update, this); 85 | super.destroy(); 86 | } 87 | } 88 | 89 | export default FollowerSprite; 90 | -------------------------------------------------------------------------------- /packages/examples-phaser3/src/test/js/main.js: -------------------------------------------------------------------------------- 1 | import Phaser from "phaser"; 2 | import Load from "./scenes/load"; 3 | import Start from "./scenes/start"; 4 | import NavMeshPlugin from "phaser-navmesh"; 5 | 6 | const game = new Phaser.Game({ 7 | type: Phaser.AUTO, 8 | parent: "game-container", 9 | width: 750, 10 | height: 750, 11 | backgroundColor: "#fff", 12 | pixelArt: false, 13 | plugins: { 14 | scene: [ 15 | { 16 | key: "NavMeshPlugin", // Key to store the plugin class under in cache 17 | plugin: NavMeshPlugin, // Class that constructs plugins 18 | mapping: "navMeshPlugin", // Property mapping to use for the scene, e.g. this.navMeshPlugin 19 | start: true, 20 | }, 21 | ], 22 | }, 23 | physics: { 24 | default: "arcade", 25 | arcade: { 26 | gravity: 0, 27 | }, 28 | }, 29 | }); 30 | 31 | game.scene.add("load", Load); 32 | game.scene.add("start", Start); 33 | game.scene.start("load"); 34 | -------------------------------------------------------------------------------- /packages/examples-phaser3/src/test/js/scenes/load.js: -------------------------------------------------------------------------------- 1 | import Phaser from "phaser"; 2 | import FontFaceObserver from "fontfaceobserver"; 3 | 4 | export default class Load extends Phaser.Scene { 5 | preload() { 6 | const loadingBar = this.add.graphics(); 7 | const { width, height } = this.sys.game.config; 8 | this.load.on("progress", (value) => { 9 | loadingBar.clear(); 10 | loadingBar.fillStyle(0xffffff, 1); 11 | loadingBar.fillRect(0, height / 2 - 25, width * value, 50); 12 | }); 13 | this.load.on("complete", () => loadingBar.destroy()); 14 | 15 | this.load.tilemapTiledJSON("map", "tilemaps/map.json"); 16 | this.load.image("tiles", "tilemaps/tiles.png"); 17 | this.load.image("follower", "images/follower.png"); 18 | 19 | this.fontLoaded = false; 20 | this.fontErrored = false; 21 | new FontFaceObserver("Josefin Sans") 22 | .load() 23 | .then(() => (this.fontLoaded = true)) 24 | .catch(() => (this.fontErrored = true)); 25 | } 26 | 27 | update() { 28 | if (this.fontLoaded || this.fontErrored) this.scene.start("start"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/examples-phaser3/src/test/js/scenes/start.js: -------------------------------------------------------------------------------- 1 | import Phaser from "phaser"; 2 | import FollowerSprite from "../game-objects/follower"; 3 | 4 | const timeIt = (fn) => { 5 | const startTime = performance.now(); 6 | const fnReturn = fn(); 7 | const elaspedMs = performance.now() - startTime; 8 | return [fnReturn, elaspedMs]; 9 | }; 10 | 11 | export default class Start extends Phaser.Scene { 12 | create() { 13 | const tilemap = this.add.tilemap("map"); 14 | const wallTileset = tilemap.addTilesetImage("tiles", "tiles"); 15 | tilemap.createLayer("bg", wallTileset); 16 | const wallLayer1 = tilemap.createLayer("wall-configurations", wallTileset); 17 | const wallLayer2 = tilemap.createLayer("random-walls", wallTileset); 18 | 19 | const layers = [wallLayer1, wallLayer2]; 20 | const [navMesh, meshMs] = timeIt(() => 21 | this.navMeshPlugin.buildMeshFromTilemap("mesh1", tilemap, layers, (t) => t.index === -1, 0) 22 | ); 23 | console.log(`It took ${meshMs.toFixed(2)}ms to build the mesh.`); 24 | 25 | // Graphics overlay for visualizing path 26 | const graphics = this.add.graphics(0, 0).setAlpha(0.5); 27 | navMesh.enableDebug(graphics); 28 | const drawDebug = () => { 29 | navMesh.debugDrawClear(); 30 | navMesh.debugDrawMesh({ 31 | drawCentroid: true, 32 | drawBounds: false, 33 | drawNeighbors: false, 34 | drawPortals: false, 35 | }); 36 | }; 37 | drawDebug(); 38 | this.input.keyboard.on("keydown-M", drawDebug); 39 | 40 | // Game object that can follow a path (inherits from Phaser.Sprite) 41 | const follower = new FollowerSprite(this, 30, 410, navMesh); 42 | 43 | // On click 44 | this.input.on("pointerdown", (pointer) => { 45 | const start = new Phaser.Math.Vector2(follower.x, follower.y); 46 | const end = new Phaser.Math.Vector2(pointer.x, pointer.y); 47 | 48 | // Tell the follower sprite to find its path to the target 49 | follower.goTo(end); 50 | 51 | const [path, pathTime] = timeIt(() => navMesh.findPath(start, end)); 52 | 53 | graphics.clear(); 54 | navMesh.debugDrawPath(path, 0xffd900); 55 | 56 | const formattedTime = pathTime.toFixed(3); 57 | uiTextLines[0] = path 58 | ? `Path found in: ${formattedTime}ms` 59 | : `No path found (${formattedTime}ms)`; 60 | uiText.setText(uiTextLines); 61 | }); 62 | 63 | // Display whether the mouse is currently over a valid point in the navmesh 64 | this.input.on(Phaser.Input.Events.POINTER_MOVE, (pointer) => { 65 | const isInMesh = navMesh.isPointInMesh(pointer); 66 | uiTextLines[1] = `Mouse: (${pointer.x}, ${pointer.y})`; 67 | uiTextLines[2] = `Is mouse inside navmesh: ${isInMesh ? "yes" : "no "}`; 68 | uiText.setText(uiTextLines); 69 | }); 70 | 71 | // -- Instructions -- 72 | 73 | const style = { 74 | font: "22px Josefin Sans", 75 | fill: "#ff0044", 76 | padding: { x: 20, y: 10 }, 77 | backgroundColor: "#fff", 78 | }; 79 | const uiTextLines = [ 80 | "Click to find a path!", 81 | "Mouse: (0, 0)", 82 | "Is mouse inside navmesh: false", 83 | "Arrow keys & q/e to move the camera.", 84 | "Note: debug drawing is slow!", 85 | ]; 86 | const uiText = this.add.text(10, 5, uiTextLines, style).setAlpha(0.9); 87 | 88 | const cursors = this.input.keyboard.createCursorKeys(); 89 | const controlConfig = { 90 | camera: this.cameras.main, 91 | left: cursors.left, 92 | right: cursors.right, 93 | up: cursors.up, 94 | down: cursors.down, 95 | zoomIn: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q), 96 | zoomOut: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E), 97 | acceleration: 0.06, 98 | drag: 0.0005, 99 | maxSpeed: 1.0, 100 | }; 101 | this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig); 102 | } 103 | update(time, delta) { 104 | this.controls.update(delta); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /packages/examples-phaser3/src/test/tilemaps/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/packages/examples-phaser3/src/test/tilemaps/tiles.png -------------------------------------------------------------------------------- /packages/examples-phaser3/src/test/tilemaps/tiles.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/packages/examples-phaser3/src/test/tilemaps/tiles.psd -------------------------------------------------------------------------------- /packages/examples-phaser3/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 4 | const path = require("path"); 5 | const webpack = require("webpack"); 6 | const root = __dirname; 7 | 8 | module.exports = function (env, argv) { 9 | const isDev = argv.mode === "development"; 10 | 11 | return { 12 | mode: isDev ? "development" : "production", 13 | context: path.join(root, "src"), 14 | entry: { 15 | "demo/js/main": "./demo/js/main.js", 16 | "test/js/main": "./test/js/main.js", 17 | "performance/js/main": "./performance/js/main.js", 18 | }, 19 | output: { 20 | filename: "[name].js", 21 | path: path.resolve(root, "public"), 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.js$/, 27 | // Exclude phaser-navmesh here when in a monorepo, because the resolved path isn't through 28 | // node_modules. In a project outside of the monorepo, you shouldn't need this. 29 | exclude: /node_modules|phaser-navmesh/, 30 | // Configure babel to look for the root babel.config.json with rootMode. 31 | use: { loader: "babel-loader", options: { rootMode: "upward" } }, 32 | }, 33 | ], 34 | }, 35 | plugins: [ 36 | new CopyWebpackPlugin({ 37 | patterns: [{ from: "**/*", globOptions: { ignore: ["**/js/**/*"] } }], 38 | }), 39 | 40 | new webpack.DefinePlugin({ 41 | "typeof CANVAS_RENDERER": JSON.stringify(true), 42 | "typeof WEBGL_RENDERER": JSON.stringify(true), 43 | PRODUCTION: !isDev, 44 | }), 45 | ], 46 | devtool: isDev ? "eval-source-map" : "source-map", 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /packages/navmesh/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /packages/navmesh/README.md: -------------------------------------------------------------------------------- 1 | # NavMesh 2 | 3 | A JS plugin for fast pathfinding using [navigation meshes](https://en.wikipedia.org/wiki/Navigation_mesh). 4 | 5 | For usage information, see: [mikewesthad/navmesh](https://github.com/mikewesthad/navmesh). 6 | 7 | ## Changelog 8 | 9 | Version 2.3.1 10 | 11 | - Documentation fixes. 12 | 13 | Version 2.3.0 14 | 15 | - Fix: webpack misconfiguration that caused [issue 37](https://github.com/mikewesthad/navmesh/issues/37). The build was only picking up the "default" export, but now it properly picks up all library exports. Thanks to[@Wenish](https://github.com/Wenish). 16 | 17 | Version 2.2.1 18 | 19 | - Update all dependencies to the latest versions. 20 | 21 | Version 2.2.0 22 | 23 | - Feature: `NavMesh#isPointInMesh` allows you to check if a point is inside of the navmesh. 24 | - Feature: `NavMesh#findClosestMeshPoint` allows you to find the nearest point on the mesh to a given location. 25 | - Feature: `buildPolysFromGridMap` allows you to build polygons for a navmesh using a 2D array of a grid-based level. 26 | 27 | Version 2.1.0 28 | 29 | - Converted library to TypeScript. 30 | 31 | Version 2.0.4 32 | 33 | - Internal optimization [#26](https://github.com/mikewesthad/navmesh/pull/26): thanks to [@herohan](https://github.com/herohan) and seowoo. 34 | 35 | Version 2.0.3 36 | 37 | - Bug: fixed webpack config so that it applied babel transform and so that it worked under node environments, thanks to [@will-hart](https://github.com/will-hart) 38 | 39 | Version 2.0.2 40 | 41 | - Initial publish of changelog 42 | -------------------------------------------------------------------------------- /packages/navmesh/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | transform: { 5 | "^.+\\.(ts|tsx)?$": "ts-jest", 6 | "^.+\\.(js|jsx)$": "babel-jest", 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/navmesh/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "navmesh", 3 | "version": "2.3.1", 4 | "description": "A library for fast pathfinding using navigation meshes in JS", 5 | "main": "dist/navmesh.js", 6 | "types": "dist/index.d.ts", 7 | "files": [ 8 | "src", 9 | "dist" 10 | ], 11 | "scripts": { 12 | "test": "jest", 13 | "tsc": "tsc", 14 | "build": "webpack --mode production", 15 | "watch": "webpack --mode development --watch", 16 | "prepublishOnly": "npm run build" 17 | }, 18 | "dependencies": { 19 | "core-js": "3", 20 | "javascript-astar": "github:mikewesthad/javascript-astar" 21 | }, 22 | "devDependencies": { 23 | "@babel/core": "^7.14.6", 24 | "@babel/preset-env": "^7.14.5", 25 | "@types/jest": "^26.0.23", 26 | "babel-jest": "^27.0.2", 27 | "babel-loader": "^8.2.2", 28 | "clean-webpack-plugin": "^3.0.0", 29 | "eslint": "^7.29.0", 30 | "jest": "^27.0.4", 31 | "ts-jest": "^27.0.3", 32 | "ts-loader": "^9.2.3", 33 | "typescript": "^4.3.4", 34 | "webpack": "^5.40.0", 35 | "webpack-cli": "^4.7.2" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/mikewesthad/phaser-navmesh-plugin.git" 40 | }, 41 | "keywords": [ 42 | "path finding", 43 | "navigation mesh", 44 | "a*" 45 | ], 46 | "author": "Michael Hadley", 47 | "contributors": [ 48 | "Rex Twedt" 49 | ], 50 | "license": "MIT", 51 | "bugs": { 52 | "url": "https://github.com/mikewesthad/phaser-navmesh-plugin/issues" 53 | }, 54 | "homepage": "https://github.com/mikewesthad/phaser-navmesh-plugin#readme" 55 | } 56 | -------------------------------------------------------------------------------- /packages/navmesh/src/channel.ts: -------------------------------------------------------------------------------- 1 | // Mostly sourced from PatrolJS at the moment. TODO: come back and reimplement this as an incomplete 2 | // funnel algorithm so astar checks can be more accurate. 3 | 4 | import Vector2 from "./math/vector-2"; 5 | import { triarea2 } from "./utils"; 6 | 7 | export interface Portal { 8 | left: Vector2; 9 | right: Vector2; 10 | } 11 | 12 | /** 13 | * @private 14 | */ 15 | export default class Channel { 16 | public path: Vector2[]; 17 | private portals: Portal[]; 18 | 19 | constructor() { 20 | this.portals = []; 21 | this.path = []; 22 | } 23 | 24 | push(p1: Vector2, p2?: Vector2) { 25 | if (p2 === undefined) p2 = p1; 26 | this.portals.push({ 27 | left: p1, 28 | right: p2, 29 | }); 30 | } 31 | 32 | stringPull() { 33 | const portals = this.portals; 34 | const pts: Vector2[] = []; 35 | // Init scan state 36 | let apexIndex = 0; 37 | let leftIndex = 0; 38 | let rightIndex = 0; 39 | let portalApex = portals[0].left; 40 | let portalLeft = portals[0].left; 41 | let portalRight = portals[0].right; 42 | 43 | // Add start point. 44 | pts.push(portalApex); 45 | 46 | for (var i = 1; i < portals.length; i++) { 47 | // Find the next portal vertices 48 | const left = portals[i].left; 49 | const right = portals[i].right; 50 | 51 | // Update right vertex. 52 | if (triarea2(portalApex, portalRight, right) <= 0.0) { 53 | if (portalApex.equals(portalRight) || triarea2(portalApex, portalLeft, right) > 0.0) { 54 | // Tighten the funnel. 55 | portalRight = right; 56 | rightIndex = i; 57 | } else { 58 | // Right vertex just crossed over the left vertex, so the left vertex should 59 | // now be part of the path. 60 | pts.push(portalLeft); 61 | 62 | // Restart scan from portal left point. 63 | 64 | // Make current left the new apex. 65 | portalApex = portalLeft; 66 | apexIndex = leftIndex; 67 | // Reset portal 68 | portalLeft = portalApex; 69 | portalRight = portalApex; 70 | leftIndex = apexIndex; 71 | rightIndex = apexIndex; 72 | // Restart scan 73 | i = apexIndex; 74 | continue; 75 | } 76 | } 77 | 78 | // Update left vertex. 79 | if (triarea2(portalApex, portalLeft, left) >= 0.0) { 80 | if (portalApex.equals(portalLeft) || triarea2(portalApex, portalRight, left) < 0.0) { 81 | // Tighten the funnel. 82 | portalLeft = left; 83 | leftIndex = i; 84 | } else { 85 | // Left vertex just crossed over the right vertex, so the right vertex should 86 | // now be part of the path 87 | pts.push(portalRight); 88 | 89 | // Restart scan from portal right point. 90 | 91 | // Make current right the new apex. 92 | portalApex = portalRight; 93 | apexIndex = rightIndex; 94 | // Reset portal 95 | portalLeft = portalApex; 96 | portalRight = portalApex; 97 | leftIndex = apexIndex; 98 | rightIndex = apexIndex; 99 | // Restart scan 100 | i = apexIndex; 101 | continue; 102 | } 103 | } 104 | } 105 | 106 | if (pts.length === 0 || !pts[pts.length - 1].equals(portals[portals.length - 1].left)) { 107 | // Append last point to path. 108 | pts.push(portals[portals.length - 1].left); 109 | } 110 | 111 | this.path = pts; 112 | return pts; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /packages/navmesh/src/common-types.ts: -------------------------------------------------------------------------------- 1 | export interface Point { 2 | x: number; 3 | y: number; 4 | } 5 | 6 | /** Lightweight representation of a Polygon as a series of points. */ 7 | export type PolyPoints = Point[]; 8 | -------------------------------------------------------------------------------- /packages/navmesh/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * `navmesh` is the core logic package. It is game-engine agnostic, usable outside of Phaser. 3 | * @packageDocumentation 4 | * @module navmesh 5 | */ 6 | 7 | import NavMesh from "./navmesh"; 8 | 9 | export { NavMesh }; 10 | export * from "./common-types"; 11 | export * from "./map-parsers"; 12 | export default NavMesh; 13 | -------------------------------------------------------------------------------- /packages/navmesh/src/javascript-astar.d.ts: -------------------------------------------------------------------------------- 1 | // Modified DefinitelyTyped definitions for javascript-astar. The original ones did not play nicely 2 | // with importing it as a module. 3 | 4 | declare module "javascript-astar" { 5 | interface GridNode { 6 | x: number; 7 | y: number; 8 | weight: number; 9 | toString(): string; 10 | getCost(node: GridNode): number; 11 | isWall(): boolean; 12 | } 13 | 14 | interface PointLike { 15 | x: number; 16 | y: number; 17 | } 18 | 19 | interface Heuristic { 20 | (pos0: any, pos1: any): number; 21 | } 22 | 23 | export class Graph { 24 | grid: Array>; 25 | constructor(grid: Array>, options?: { diagonal?: boolean }); 26 | 27 | init(): void; 28 | cleanDirty(): void; 29 | markDirty(): void; 30 | neighbors(node: NodeType): NodeType[]; 31 | toString(): string; 32 | } 33 | 34 | export namespace astar { 35 | function search( 36 | graph: Graph, 37 | start: { x: number; y: number }, 38 | end: { x: number; y: number }, 39 | options?: { 40 | closest?: boolean; 41 | heuristic?: Heuristic; 42 | } 43 | ): Array; 44 | 45 | var heuristics: { 46 | manhattan: Heuristic; 47 | diagonal: Heuristic; 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/navmesh/src/map-parsers/build-polys-from-grid-map.test.ts: -------------------------------------------------------------------------------- 1 | import buildPolysFromGridMap from "./build-polys-from-grid-map"; 2 | 3 | describe("buildPolysFromGridMap", () => { 4 | it("should return an empty array when passed in no walkable tiles", () => { 5 | const polys = buildPolysFromGridMap([ 6 | [0, 0, 0], 7 | [0, 0, 0], 8 | [0, 0, 0], 9 | ]); 10 | expect(polys).toEqual([]); 11 | }); 12 | 13 | it("should return one polygon for a grid of all walkable tiles", () => { 14 | const polys = buildPolysFromGridMap([ 15 | [1, 1, 1], 16 | [1, 1, 1], 17 | [1, 1, 1], 18 | ]); 19 | expect(polys.length).toBe(1); 20 | }); 21 | 22 | it("should return two polygons for a grid of two simple rectangular islands", () => { 23 | const polys = buildPolysFromGridMap([ 24 | [0, 0, 0, 0, 1, 1], 25 | [1, 1, 1, 0, 1, 1], 26 | [1, 1, 1, 0, 1, 1], 27 | [1, 1, 1, 0, 1, 1], 28 | [0, 0, 0, 0, 1, 1], 29 | ]); 30 | expect(polys.length).toBe(2); 31 | }); 32 | 33 | it("should return 15 polygons for a grid with 15 1x1 islands", () => { 34 | const polys = buildPolysFromGridMap([ 35 | [1, 0, 1, 0, 1, 0, 1, 0, 1, 0], 36 | [0, 1, 0, 1, 0, 1, 0, 1, 0, 1], 37 | [1, 0, 1, 0, 1, 0, 1, 0, 1, 0], 38 | ]); 39 | expect(polys.length).toBe(15); 40 | }); 41 | 42 | it("should return scale polygons based on given tile width and height", () => { 43 | const tileWidth = 20; 44 | const tileHeight = 40; 45 | const grid = [ 46 | [1, 1], 47 | [1, 1], 48 | ]; 49 | const polys = buildPolysFromGridMap(grid, tileWidth, tileHeight); 50 | const topLeft = { x: 0, y: 0 }; 51 | const bottomRight = { x: tileWidth * 2, y: tileHeight * 2 }; 52 | expect(polys[0]).toContainEqual(topLeft); 53 | expect(polys[0]).toContainEqual(bottomRight); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/navmesh/src/map-parsers/build-polys-from-grid-map.ts: -------------------------------------------------------------------------------- 1 | import { Point, PolyPoints } from "../common-types"; 2 | import { isTruthy } from "../utils"; 3 | import { GridMap, TileWalkableTest } from "./grid-map"; 4 | import { PointQueue } from "./point-queue"; 5 | import { RectangleHull } from "./rectangle-hull"; 6 | 7 | type CardinalDirection = "top" | "bottom" | "left" | "right"; 8 | 9 | /** 10 | * This parses a world that is a uniform grid into convex polygons (specifically rectangles) that 11 | * can be used for building a navmesh. This is designed mainly for parsing tilemaps into polygons. 12 | * The functions takes a 2D array that indicates which tiles are walkable and which aren't. The 13 | * function returns PolyPoint[] that can be used to construct a NavMesh. 14 | * 15 | * Notes: 16 | * - This algorithm traverses the walkable tiles in a depth-first search, combining neighbors into 17 | * rectangular polygons. This may not produce the best navmesh, but it doesn't require any manual 18 | * work! 19 | * - This assumes the world is a uniform grid. It should work for any tile size, provided that all 20 | * tiles are the same width and height. 21 | * 22 | * @param map 2D array of any type. 23 | * @param tileWidth The width of each tile in the grid. 24 | * @param tileHeight The height of each tile in the grid. 25 | * @param isWalkable Function that is used to test if a specific location in the map is walkable. 26 | * Defaults to assuming "truthy" means walkable. 27 | * @param shrinkAmount Amount to "shrink" the mesh away from the tiles. This adds more polygons 28 | * to the generated mesh, but can be helpful for preventing agents from getting caught on edges. 29 | * This supports values between 0 and tileWidth/tileHeight (whichever dimension is smaller). 30 | */ 31 | export default function buildPolysFromGridMap( 32 | map: TileType[][], 33 | tileWidth: number = 1, 34 | tileHeight: number = 1, 35 | isWalkable: TileWalkableTest = isTruthy, 36 | shrinkAmount: number = 0 37 | ): PolyPoints[] { 38 | const gridMap = new GridMap(map, isWalkable, tileWidth, tileHeight); 39 | 40 | if (shrinkAmount >= tileWidth || shrinkAmount >= tileHeight) { 41 | throw new Error( 42 | `navmesh: Unsupported shrink amount ${shrinkAmount}. Must be less than tile width and height.` 43 | ); 44 | } 45 | 46 | let hulls: RectangleHull[] = buildInitialHulls(gridMap); 47 | 48 | if (shrinkAmount > 0) { 49 | hulls = shrinkHulls(hulls, gridMap, shrinkAmount); 50 | } 51 | 52 | return hulls.map((hull) => hull.toPoints()); 53 | } 54 | 55 | /** 56 | * Build up rectangular hulls from the walkable areas of a GridMap. This starts with a walkable tile 57 | * and attempts to "grow" each of its edges to engulf its neighbors. This process repeats until the 58 | * current hull can't engulf any neighbors. 59 | * @param gridMap 60 | */ 61 | function buildInitialHulls(gridMap: GridMap) { 62 | const walkableQueue = new PointQueue(); 63 | const { tileWidth, tileHeight } = gridMap; 64 | const hulls: RectangleHull[] = []; 65 | let currentHull; 66 | 67 | gridMap.forEach((x, y) => { 68 | if (gridMap.isWalkable(x, y)) walkableQueue.add({ x, y }); 69 | }); 70 | 71 | const getExtensionPoints = (hull: RectangleHull, dir: CardinalDirection) => { 72 | const { top, left, right, bottom } = hull; 73 | let points: Point[] = []; 74 | if (dir === "top") { 75 | for (let x = left; x <= right - 1; x++) points.push({ x, y: top }); 76 | } else if (dir === "bottom") { 77 | for (let x = left; x <= right - 1; x++) points.push({ x, y: bottom }); 78 | } else if (dir === "left") { 79 | for (let y = top; y <= bottom - 1; y++) points.push({ x: left, y }); 80 | } else if (dir === "right") { 81 | for (let y = top; y <= bottom - 1; y++) points.push({ x: right, y }); 82 | } else { 83 | throw new Error(`Invalid dir "${dir}" for extend`); 84 | } 85 | return points; 86 | }; 87 | 88 | const extendHullInDirection = (hull: RectangleHull, dir: CardinalDirection) => { 89 | if (dir === "top") hull.y -= 1; 90 | else if (dir === "bottom") hull.bottom += 1; 91 | else if (dir === "left") hull.x -= 1; 92 | else if (dir === "right") hull.right += 1; 93 | else throw new Error(`Invalid dir "${dir}" for extend`); 94 | }; 95 | 96 | const attemptExtension = (hull: RectangleHull, dir: CardinalDirection) => { 97 | const neighborPoints = getExtensionPoints(hull, dir); 98 | const canExtend = walkableQueue.containsAllPoints(neighborPoints); 99 | if (canExtend) { 100 | extendHullInDirection(hull, dir); 101 | walkableQueue.removePoints(neighborPoints); 102 | } 103 | return canExtend; 104 | }; 105 | 106 | while (!walkableQueue.isEmpty()) { 107 | // Find next colliding tile to start the algorithm. 108 | const tile = walkableQueue.shift(); 109 | if (tile === undefined) break; 110 | 111 | // Use tile dimensions (i.e. 1 tile wide, 1 tile tall) to simplify the checks. 112 | currentHull = new RectangleHull(tile.x, tile.y, 1, 1); 113 | 114 | // Check edges of bounding box to see if they can be extended. 115 | let needsExtensionCheck = true; 116 | while (needsExtensionCheck) { 117 | const extendedTop = attemptExtension(currentHull, "top"); 118 | const extendedRight = attemptExtension(currentHull, "right"); 119 | const extendedLeft = attemptExtension(currentHull, "left"); 120 | const extendedBottom = attemptExtension(currentHull, "bottom"); 121 | needsExtensionCheck = extendedTop || extendedBottom || extendedLeft || extendedRight; 122 | } 123 | 124 | // Scale the hull up from grid dimensions to real world dimensions. 125 | currentHull.setPosition(currentHull.x * tileWidth, currentHull.y * tileHeight); 126 | currentHull.setSize(currentHull.width * tileWidth, currentHull.height * tileHeight); 127 | hulls.push(currentHull); 128 | } 129 | 130 | return hulls; 131 | } 132 | // TODO: check larger than tile size. Assumes shrink <= 1 tile. 133 | function shrinkHull( 134 | hull: RectangleHull, 135 | gridMap: GridMap, 136 | shrinkAmount: number, 137 | tileWidth: number, 138 | tileHeight: number 139 | ) { 140 | const s = shrinkAmount; 141 | const halfWidth = tileWidth / 2; 142 | const halfHeight = tileHeight / 2; 143 | const { left, top, right, bottom } = hull; 144 | 145 | const info = { 146 | left: false, 147 | right: false, 148 | top: false, 149 | bottom: false, 150 | topLeft: gridMap.isBlockedAtWorld(left - s, top - s), 151 | topRight: gridMap.isBlockedAtWorld(right + s, top - s), 152 | bottomLeft: gridMap.isBlockedAtWorld(left - s, bottom + s), 153 | bottomRight: gridMap.isBlockedAtWorld(right + s, bottom + s), 154 | }; 155 | 156 | for (let y = top + halfHeight; y < bottom; y += halfHeight) { 157 | if (gridMap.isBlockedAtWorld(left - s, y)) { 158 | info.left = true; 159 | break; 160 | } 161 | } 162 | for (let y = top + halfHeight; y < bottom; y += halfHeight) { 163 | if (gridMap.isBlockedAtWorld(right + s, y)) { 164 | info.right = true; 165 | break; 166 | } 167 | } 168 | for (let x = left + halfWidth; x < right; x += halfWidth) { 169 | if (gridMap.isBlockedAtWorld(x, top - shrinkAmount)) { 170 | info.top = true; 171 | break; 172 | } 173 | } 174 | for (let x = left + halfWidth; x < right; x += halfWidth) { 175 | if (gridMap.isBlockedAtWorld(x, bottom + shrinkAmount)) { 176 | info.bottom = true; 177 | break; 178 | } 179 | } 180 | 181 | const shrink = { 182 | left: info.left, 183 | right: info.right, 184 | top: info.top, 185 | bottom: info.bottom, 186 | }; 187 | 188 | if (info.topLeft && !info.left && !info.top) { 189 | if (hull.width > hull.height) shrink.left = true; 190 | else shrink.top = true; 191 | } 192 | if (info.topRight && !info.right && !info.top) { 193 | if (hull.width > hull.height) shrink.right = true; 194 | else shrink.top = true; 195 | } 196 | if (info.bottomLeft && !info.bottom && !info.left) { 197 | if (hull.width > hull.height) shrink.left = true; 198 | else shrink.bottom = true; 199 | } 200 | if (info.bottomRight && !info.bottom && !info.right) { 201 | if (hull.width > hull.height) shrink.right = true; 202 | else shrink.bottom = true; 203 | } 204 | 205 | if (shrink.left) { 206 | hull.x += shrinkAmount; 207 | hull.width -= shrinkAmount; 208 | } 209 | if (shrink.top) { 210 | hull.y += shrinkAmount; 211 | hull.height -= shrinkAmount; 212 | } 213 | if (shrink.right) { 214 | hull.width -= shrinkAmount; 215 | } 216 | if (shrink.bottom) { 217 | hull.height -= shrinkAmount; 218 | } 219 | 220 | return shrink; 221 | } 222 | 223 | function shrinkHulls( 224 | hulls: RectangleHull[], 225 | gridMap: GridMap, 226 | shrinkAmount: number 227 | ) { 228 | const { tileHeight, tileWidth } = gridMap; 229 | const newHulls: RectangleHull[] = []; 230 | const finalHulls: RectangleHull[] = []; 231 | 232 | hulls.forEach((hull, hullIndex) => { 233 | const th = tileHeight; 234 | const tw = tileWidth; 235 | const tLeft = gridMap.getGridX(hull.x); 236 | const tTop = gridMap.getGridY(hull.y); 237 | const tBottom = gridMap.getGridY(hull.bottom); 238 | const tRight = gridMap.getGridX(hull.right); 239 | const shrink = shrinkHull(hull, gridMap, shrinkAmount, tileWidth, tileHeight); 240 | 241 | if (hull.left >= hull.right || hull.top >= hull.bottom) return; 242 | 243 | finalHulls.push(hull); 244 | 245 | const newVerticalHulls: RectangleHull[] = []; 246 | const newHorizontalHulls: RectangleHull[] = []; 247 | const addHull = (x: number, y: number, w: number, h: number) => { 248 | const hull = new RectangleHull(x, y, w, h); 249 | if (w > h) newHorizontalHulls.push(hull); 250 | else newVerticalHulls.push(hull); 251 | }; 252 | 253 | if (shrink.left) { 254 | const x = hull.left - shrinkAmount; 255 | let startY = tTop; 256 | let endY = startY - 1; 257 | for (let y = tTop; y < tBottom; y++) { 258 | if (gridMap.isBlocked(tLeft - 1, y)) { 259 | if (startY <= endY) { 260 | addHull(x, startY * th, shrinkAmount, (endY - startY + 1) * th); 261 | } 262 | startY = y + 1; 263 | } else { 264 | endY = y; 265 | } 266 | } 267 | if (startY <= endY) { 268 | addHull(x, startY * th, shrinkAmount, (endY - startY + 1) * th); 269 | } 270 | } 271 | 272 | if (shrink.right) { 273 | const x = hull.right; 274 | let startY = tTop; 275 | let endY = startY - 1; 276 | for (let y = tTop; y < tBottom; y++) { 277 | if (gridMap.isBlocked(tRight, y)) { 278 | if (startY <= endY) { 279 | addHull(x, startY * th, shrinkAmount, (endY - startY + 1) * th); 280 | } 281 | startY = y + 1; 282 | } else { 283 | endY = y; 284 | } 285 | } 286 | if (startY <= endY) { 287 | addHull(x, startY * th, shrinkAmount, (endY - startY + 1) * th); 288 | } 289 | } 290 | 291 | if (shrink.top) { 292 | const y = hull.top - shrinkAmount; 293 | let startX = tLeft; 294 | let endX = startX - 1; 295 | for (let x = tLeft; x < tRight; x++) { 296 | if (gridMap.isBlocked(x, tTop - 1)) { 297 | if (startX <= endX) { 298 | addHull(startX * tw, y, (endX - startX + 1) * th, shrinkAmount); 299 | } 300 | startX = x + 1; 301 | } else { 302 | endX = x; 303 | } 304 | } 305 | if (startX <= endX) { 306 | addHull(startX * tw, y, (endX - startX + 1) * th, shrinkAmount); 307 | } 308 | } 309 | 310 | if (shrink.bottom) { 311 | const y = hull.bottom; 312 | let startX = tLeft; 313 | let endX = startX - 1; 314 | for (let x = tLeft; x < tRight; x++) { 315 | if (gridMap.isBlocked(x, tBottom)) { 316 | if (startX <= endX) { 317 | addHull(startX * tw, y, (endX - startX + 1) * th, shrinkAmount); 318 | } 319 | startX = x + 1; 320 | } else { 321 | endX = x; 322 | } 323 | } 324 | if (startX <= endX) { 325 | addHull(startX * tw, y, (endX - startX + 1) * th, shrinkAmount); 326 | } 327 | } 328 | 329 | // Shrunk at corners when the new hulls overlap. 330 | newHorizontalHulls.forEach((hh) => { 331 | newVerticalHulls.forEach((vh) => { 332 | if (hh.doesOverlap(vh)) { 333 | const isBottomSide = hh.y > vh.y; 334 | if (isBottomSide) vh.height -= shrinkAmount; 335 | else vh.top += shrinkAmount; 336 | } 337 | }); 338 | }); 339 | 340 | [...newHorizontalHulls, ...newVerticalHulls].forEach((hull) => { 341 | shrinkHull(hull, gridMap, shrinkAmount, tileWidth, tileHeight); 342 | if (hull.left >= hull.right || hull.top >= hull.bottom) return; 343 | newHulls.push(hull); 344 | }); 345 | }); 346 | 347 | // Attempt to merge new hulls into existing hulls if possible. 348 | for (let i = 0; i < newHulls.length; i++) { 349 | let wasMerged = false; 350 | // Attempt to merge into the main (shrunken) hulls first. 351 | for (const mainHull of hulls) { 352 | wasMerged = mainHull.attemptMergeIn(newHulls[i]); 353 | if (wasMerged) break; 354 | } 355 | if (wasMerged) continue; 356 | // Then check to see if we can merge into a later hull in newHulls. 357 | for (let j = i + 1; j < newHulls.length; j++) { 358 | wasMerged = newHulls[j].attemptMergeIn(newHulls[i]); 359 | if (wasMerged) break; 360 | } 361 | if (!wasMerged) finalHulls.push(newHulls[i]); 362 | } 363 | 364 | return finalHulls; 365 | } 366 | -------------------------------------------------------------------------------- /packages/navmesh/src/map-parsers/grid-map.ts: -------------------------------------------------------------------------------- 1 | export type TileWalkableTest = (tile: TileType, x: number, y: number) => boolean; 2 | 3 | export class GridMap { 4 | public readonly width; 5 | public readonly height; 6 | public readonly tileWidth: number; 7 | public readonly tileHeight: number; 8 | private map: TileType[][]; 9 | private isWalkableTest: TileWalkableTest; 10 | 11 | public constructor( 12 | map: TileType[][], 13 | isWalkable: TileWalkableTest, 14 | tileWidth: number, 15 | tileHeight: number 16 | ) { 17 | this.map = map; 18 | this.isWalkableTest = isWalkable; 19 | this.height = map.length; 20 | this.width = map[0].length; 21 | this.tileWidth = tileWidth; 22 | this.tileHeight = tileHeight; 23 | } 24 | 25 | public forEach(fn: (x: number, y: number, tile: TileType) => void) { 26 | this.map.forEach((row, y) => { 27 | row.forEach((col, x) => { 28 | fn(x, y, this.map[y][x]); 29 | }); 30 | }); 31 | } 32 | 33 | public isInGrid(x: number, y: number) { 34 | return x >= 0 && x < this.width && y >= 0 && y < this.height; 35 | } 36 | 37 | public isWalkable(x: number, y: number) { 38 | return this.isInGrid(x, y) && this.isWalkableTest(this.map[y][x], x, y); 39 | } 40 | 41 | public isBlocked(x: number, y: number) { 42 | return this.isInGrid(x, y) && !this.isWalkableTest(this.map[y][x], x, y); 43 | } 44 | 45 | public isBlockedAtWorld(worldX: number, worldY: number) { 46 | return this.isBlocked(this.getGridX(worldX), this.getGridY(worldY)); 47 | } 48 | 49 | public getGridX(worldX: number) { 50 | return Math.floor(worldX / this.tileWidth); 51 | } 52 | 53 | public getGridY(worldY: number) { 54 | return Math.floor(worldY / this.tileHeight); 55 | } 56 | 57 | public getGridXY(worldX: number, worldY: number) { 58 | return { x: this.getGridX(worldX), y: this.getGridY(worldY) }; 59 | } 60 | 61 | public getWorldX(gridX: number) { 62 | return gridX * this.tileWidth; 63 | } 64 | 65 | public getWorldY(gridY: number) { 66 | return gridY * this.tileHeight; 67 | } 68 | 69 | public getWorldXY(gridX: number, gridY: number) { 70 | return { x: this.getWorldX(gridX), y: this.getWorldY(gridY) }; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/navmesh/src/map-parsers/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Currently there is only one map parser (for grid tile maps), but the plan is to extend this to 3 | * multiple parsers. 4 | */ 5 | 6 | import buildPolysFromGridMap from "./build-polys-from-grid-map"; 7 | 8 | export { buildPolysFromGridMap }; 9 | export * from "./build-polys-from-grid-map"; 10 | export * from "./point-queue"; 11 | export * from "./rectangle-hull"; 12 | -------------------------------------------------------------------------------- /packages/navmesh/src/map-parsers/point-queue.ts: -------------------------------------------------------------------------------- 1 | import { Point } from "../common-types"; 2 | 3 | /** 4 | * Internal helper class to manage a queue of points when parsing a square map. 5 | */ 6 | export class PointQueue { 7 | private data: Point[] = []; 8 | 9 | add(point: Point) { 10 | this.data.push(point); 11 | } 12 | 13 | shift() { 14 | return this.data.shift(); 15 | } 16 | 17 | isEmpty() { 18 | return this.data.length === 0; 19 | } 20 | 21 | containsPoint(point: Point) { 22 | return this.data.find((p) => p.x === point.x && p.y === point.y) !== undefined ? true : false; 23 | } 24 | 25 | containsAllPoints(points: Point[]) { 26 | return points.every((p) => this.containsPoint(p)); 27 | } 28 | 29 | getIndexOfPoint(point: Point) { 30 | return this.data.findIndex((p) => p.x == point.x && p.y == point.y); 31 | } 32 | 33 | removePoint(point: Point) { 34 | const index = this.getIndexOfPoint(point); 35 | if (index !== -1) this.data.splice(index, 1); 36 | } 37 | 38 | removePoints(points: Point[]) { 39 | points.forEach((p) => this.removePoint(p)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/navmesh/src/map-parsers/rectangle-hull.ts: -------------------------------------------------------------------------------- 1 | import { Point } from "../common-types"; 2 | 3 | /** 4 | * Class for managing hulls created by combining square tiles. 5 | */ 6 | export class RectangleHull { 7 | public x: number; 8 | public y: number; 9 | public width: number; 10 | public height: number; 11 | 12 | public constructor(x: number, y: number, width: number, height: number) { 13 | this.x = x; 14 | this.y = y; 15 | this.width = width; 16 | this.height = height; 17 | } 18 | 19 | public setPosition(x: number, y: number) { 20 | this.x = x; 21 | this.y = y; 22 | } 23 | 24 | public setSize(width: number, height: number) { 25 | this.width = width; 26 | this.height = height; 27 | } 28 | 29 | public set(left: number, top: number, width: number, height: number) { 30 | this.setPosition(left, top); 31 | this.setSize(width, height); 32 | } 33 | 34 | public get left() { 35 | return this.x; 36 | } 37 | 38 | public set left(val) { 39 | this.x = val; 40 | } 41 | 42 | public get top() { 43 | return this.y; 44 | } 45 | 46 | public set top(val) { 47 | this.y = val; 48 | } 49 | 50 | // TODO: make consistent. Either left/right should both resize or they should both just reposition 51 | public get right() { 52 | return this.x + this.width; 53 | } 54 | 55 | public set right(val) { 56 | this.width = val - this.x; 57 | } 58 | 59 | public get bottom() { 60 | return this.y + this.height; 61 | } 62 | 63 | public set bottom(val) { 64 | this.height = val - this.top; 65 | } 66 | 67 | public get center(): Point { 68 | return { x: (this.x + this.right) / 2, y: (this.y + this.bottom) / 2 }; 69 | } 70 | 71 | public doesOverlap(otherHull: RectangleHull) { 72 | return !( 73 | this.right < otherHull.x || 74 | this.x > otherHull.right || 75 | this.y > otherHull.bottom || 76 | this.bottom < otherHull.y 77 | ); 78 | } 79 | 80 | /** 81 | * Attempt to merge another hull into this one. If they share an edge, `this` will be extended to 82 | * contain `otherHull`. 83 | * @param otherHull 84 | */ 85 | public attemptMergeIn(otherHull: RectangleHull): boolean { 86 | const horizontalMatch = this.x === otherHull.x && this.width === otherHull.width; 87 | const verticalMatch = this.y === otherHull.y && this.height === otherHull.height; 88 | if (horizontalMatch && this.top === otherHull.bottom) { 89 | this.height += otherHull.height; 90 | this.y = otherHull.y; 91 | return true; 92 | } 93 | if (horizontalMatch && this.bottom === otherHull.top) { 94 | this.bottom = otherHull.bottom; 95 | return true; 96 | } 97 | if (verticalMatch && this.left === otherHull.right) { 98 | this.width += otherHull.width; 99 | this.x = otherHull.x; 100 | return true; 101 | } 102 | if (verticalMatch && this.right === otherHull.left) { 103 | this.right = otherHull.right; 104 | return true; 105 | } 106 | return false; 107 | } 108 | 109 | public toPoints() { 110 | const { left, right, top, bottom } = this; 111 | return [ 112 | { x: left, y: top }, 113 | { x: right, y: top }, 114 | { x: right, y: bottom }, 115 | { x: left, y: bottom }, 116 | ]; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /packages/navmesh/src/math/line.ts: -------------------------------------------------------------------------------- 1 | import Vector2 from "./vector-2"; 2 | 3 | /** 4 | * Stripped down version of Phaser's Line with just the functionality needed for navmeshes. 5 | * 6 | * @export 7 | * @class Line 8 | */ 9 | export default class Line { 10 | public start: Vector2; 11 | public end: Vector2; 12 | public left: number; 13 | public right: number; 14 | public top: number; 15 | public bottom: number; 16 | 17 | constructor(x1: number, y1: number, x2: number, y2: number) { 18 | this.start = new Vector2(x1, y1); 19 | this.end = new Vector2(x2, y2); 20 | 21 | this.left = Math.min(x1, x2); 22 | this.right = Math.max(x1, x2); 23 | this.top = Math.min(y1, y2); 24 | this.bottom = Math.max(y1, y2); 25 | } 26 | 27 | public pointOnSegment(x: number, y: number) { 28 | return ( 29 | x >= this.left && 30 | x <= this.right && 31 | y >= this.top && 32 | y <= this.bottom && 33 | this.pointOnLine(x, y) 34 | ); 35 | } 36 | 37 | pointOnLine(x: number, y: number) { 38 | // Compare slope of line start -> xy to line start -> line end 39 | return (x - this.left) * (this.bottom - this.top) === (this.right - this.left) * (y - this.top); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/navmesh/src/math/polygon.ts: -------------------------------------------------------------------------------- 1 | import { Point } from "../common-types"; 2 | import Line from "./line"; 3 | 4 | /** 5 | * Stripped down version of Phaser's Polygon with just the functionality needed for navmeshes. 6 | * 7 | * @export 8 | * @class Polygon 9 | */ 10 | export default class Polygon { 11 | public edges: Line[]; 12 | public points: Point[]; 13 | private isClosed: boolean; 14 | 15 | constructor(points: Point[], closed = true) { 16 | this.isClosed = closed; 17 | this.points = points; 18 | this.edges = []; 19 | 20 | for (let i = 1; i < points.length; i++) { 21 | const p1 = points[i - 1]; 22 | const p2 = points[i]; 23 | this.edges.push(new Line(p1.x, p1.y, p2.x, p2.y)); 24 | } 25 | 26 | if (this.isClosed) { 27 | const first = points[0]; 28 | const last = points[points.length - 1]; 29 | this.edges.push(new Line(first.x, first.y, last.x, last.y)); 30 | } 31 | } 32 | 33 | public contains(x: number, y: number) { 34 | let inside = false; 35 | 36 | for (let i = -1, j = this.points.length - 1; ++i < this.points.length; j = i) { 37 | const ix = this.points[i].x; 38 | const iy = this.points[i].y; 39 | 40 | const jx = this.points[j].x; 41 | const jy = this.points[j].y; 42 | 43 | if ( 44 | ((iy <= y && y < jy) || (jy <= y && y < iy)) && 45 | x < ((jx - ix) * (y - iy)) / (jy - iy) + ix 46 | ) { 47 | inside = !inside; 48 | } 49 | } 50 | 51 | return inside; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/navmesh/src/math/vector-2.ts: -------------------------------------------------------------------------------- 1 | import { Point } from "../common-types"; 2 | 3 | type PointLike = Vector2 | Point; 4 | 5 | /** 6 | * Stripped down version of Phaser's Vector2 with just the functionality needed for navmeshes. 7 | * 8 | * @export 9 | * @class Vector2 10 | */ 11 | export default class Vector2 { 12 | public x: number; 13 | public y: number; 14 | 15 | constructor(x: number = 0, y: number = 0) { 16 | this.x = x; 17 | this.y = y; 18 | } 19 | 20 | public equals(v: PointLike) { 21 | return this.x === v.x && this.y === v.y; 22 | } 23 | 24 | public angle(v: PointLike) { 25 | return Math.atan2(v.y - this.y, v.x - this.x); 26 | } 27 | 28 | public distance(v: PointLike) { 29 | const dx = v.x - this.x; 30 | const dy = v.y - this.y; 31 | return Math.sqrt(dx * dx + dy * dy); 32 | } 33 | 34 | public add(v: PointLike) { 35 | this.x += v.x; 36 | this.y += v.y; 37 | } 38 | 39 | public subtract(v: PointLike) { 40 | this.x -= v.x; 41 | this.y -= v.y; 42 | } 43 | 44 | public clone() { 45 | return new Vector2(this.x, this.y); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/navmesh/src/nav-mesh.test.ts: -------------------------------------------------------------------------------- 1 | import NavMesh from "./navmesh"; 2 | import Vector2 from "./math/vector-2"; 3 | 4 | const v2 = (x: number, y: number) => new Vector2(x, y); 5 | 6 | describe("An empty NavMesh instance", () => { 7 | let emptyNavMesh: NavMesh; 8 | beforeAll(() => (emptyNavMesh = new NavMesh([]))); 9 | 10 | it("should not throw an error on construction", () => { 11 | expect(() => emptyNavMesh).not.toThrow(); 12 | }); 13 | 14 | it("should always return null when queried for a path", () => { 15 | const path = emptyNavMesh.findPath(v2(10, 20), v2(30, 50)); 16 | expect(path).toBeNull(); 17 | }); 18 | }); 19 | 20 | describe("A simple, fully connected NavMesh instance", () => { 21 | let navMesh: NavMesh; 22 | /* 23 | - - - - - 24 | - 1 - 2 - 25 | - - - - - 26 | */ 27 | // prettier-ignore 28 | const polygons = [ 29 | [v2(0,0), v2(10,0), v2(10,10), v2(0,10)], // 1 30 | [v2(10,0), v2(20,0), v2(20,10), v2(10,10)] // 2 31 | ]; 32 | beforeAll(() => (navMesh = new NavMesh(polygons))); 33 | 34 | it("should return a direct path when points are in the same polygon", () => { 35 | const path = navMesh.findPath(v2(0, 0), v2(5, 5)); 36 | expect(path).toEqual([v2(0, 0), v2(5, 5)]); 37 | }); 38 | 39 | it("should return null when a point is outside all polygon", () => { 40 | const path = navMesh.findPath(v2(-10, 0), v2(5, 5)); 41 | expect(path).toBeNull(); 42 | }); 43 | 44 | it("should return a path when points are in neighboring polygons", () => { 45 | const path = navMesh.findPath(v2(5, 5), v2(15, 5)); 46 | expect(path).toEqual([v2(5, 5), v2(15, 5)]); 47 | }); 48 | 49 | it("should return a path when points are on the edges of the polygons", () => { 50 | const path = navMesh.findPath(v2(0, 0), v2(20, 10)); 51 | expect(path).toEqual([v2(0, 0), v2(20, 10)]); 52 | }); 53 | }); 54 | 55 | describe("A NavMesh instance with two islands", () => { 56 | let navMesh: NavMesh; 57 | /* 58 | - - - - - - 59 | - 1 - - 2 - 60 | - - - - - - 61 | */ 62 | // prettier-ignore 63 | const polygons = [ 64 | [v2(0,0), v2(10,0), v2(10,10), v2(0,10)], // 1 65 | [v2(40,0), v2(50,0), v2(50,10), v2(40,10)], // 2 66 | ]; 67 | beforeAll(() => (navMesh = new NavMesh(polygons))); 68 | 69 | it("should return null when queried for a path between islands", () => { 70 | const path = navMesh.findPath(v2(0, 0), v2(40, 0)); 71 | expect(path).toBeNull(); 72 | }); 73 | }); 74 | 75 | describe("A NavMesh instance with a corner", () => { 76 | let navMesh: NavMesh; 77 | /* 78 | - - - - - 79 | - 1 - 2 - 80 | - - - - - 81 | - 3 - 82 | - - - 83 | */ 84 | // prettier-ignore 85 | const polygons = [ 86 | [v2(0,0), v2(10,0), v2(10,10), v2(0,10)], // 1 87 | [v2(10,0), v2(20,0), v2(20,10), v2(10,10)], // 2 88 | [v2(10,10), v2(20,10), v2(20,20), v2(10,20)] // 3 89 | ]; 90 | beforeAll(() => (navMesh = new NavMesh(polygons))); 91 | 92 | it("should return a path that hugs the corner", () => { 93 | const path = navMesh.findPath(v2(0, 0), v2(10, 20)); 94 | expect(path).toEqual([v2(0, 0), v2(10, 10), v2(10, 20)]); 95 | }); 96 | }); 97 | 98 | describe("isPointInMesh", () => { 99 | let navMesh: NavMesh; 100 | /* 101 | - - - - - - - - 102 | - 1 - 2 - - 4 - 103 | - - - - - - - - 104 | - 3 - 105 | - - - 106 | */ 107 | // prettier-ignore 108 | const polygons = [ 109 | [v2(0,0), v2(10,0), v2(10,10), v2(0,10)], // 1 110 | [v2(10,0), v2(20,0), v2(20,10), v2(10,10)], // 2 111 | [v2(10,10), v2(20,10), v2(20,20), v2(10,20)], // 3 112 | [v2(30,0), v2(40,0), v2(40,10), v2(30,10)] // 4 113 | ]; 114 | beforeAll(() => (navMesh = new NavMesh(polygons))); 115 | 116 | it("should return true if point is on the edge of a polygon", () => { 117 | expect(navMesh.isPointInMesh(v2(0, 0))).toEqual(true); 118 | expect(navMesh.isPointInMesh(v2(10, 0))).toEqual(true); 119 | expect(navMesh.isPointInMesh(v2(10, 10))).toEqual(true); 120 | expect(navMesh.isPointInMesh(v2(40, 10))).toEqual(true); 121 | }); 122 | 123 | it("should return true if point is in a polygon in the mesh", () => { 124 | expect(navMesh.isPointInMesh(v2(5, 5))).toEqual(true); 125 | expect(navMesh.isPointInMesh(v2(10, 5))).toEqual(true); 126 | expect(navMesh.isPointInMesh(v2(32, 2))).toEqual(true); 127 | }); 128 | 129 | it("should return false for a point outside the mesh", () => { 130 | expect(navMesh.isPointInMesh(v2(-10, -20))).toEqual(false); 131 | expect(navMesh.isPointInMesh(v2(25, 0))).toEqual(false); 132 | expect(navMesh.isPointInMesh(v2(300, 100))).toEqual(false); 133 | }); 134 | }); 135 | 136 | describe("findClosestMeshPoint", () => { 137 | let navMesh: NavMesh; 138 | /* 139 | - - - - - - - - 140 | - 1 - 2 - - 4 - 141 | - - - - - - - - 142 | - 3 - 143 | - - - 144 | */ 145 | // prettier-ignore 146 | const polygons = [ 147 | [v2(0,0), v2(10,0), v2(10,10), v2(0,10)], // 1 148 | [v2(10,0), v2(20,0), v2(20,10), v2(10,10)], // 2 149 | [v2(10,10), v2(20,10), v2(20,20), v2(10,20)], // 3 150 | [v2(30,0), v2(40,0), v2(40,10), v2(30,10)] // 4 151 | ]; 152 | beforeAll(() => (navMesh = new NavMesh(polygons))); 153 | 154 | it("should return null for points outside of the max distance", () => { 155 | expect(navMesh.findClosestMeshPoint(v2(-100, 0), 10).polygon).toBeNull(); 156 | }); 157 | 158 | it("should return poly 1 for point inside poly 1", () => { 159 | const result = navMesh.findClosestMeshPoint(v2(5, 5)); 160 | expect(result?.polygon?.id).toBe(0); 161 | expect(result?.point).toEqual({ x: 5, y: 5 }); 162 | }); 163 | 164 | it("should return poly 1 or 2 for point on shared edge between poly 1 and 2", () => { 165 | const result = navMesh.findClosestMeshPoint(v2(10, 5)); 166 | expect(result?.polygon?.id).toBeGreaterThanOrEqual(0); 167 | expect(result?.polygon?.id).toBeLessThanOrEqual(1); 168 | }); 169 | 170 | it("should return top left corner of poly 1 for a point just outside of top left corner", () => { 171 | const result = navMesh.findClosestMeshPoint(v2(-10, -10)); 172 | expect(result?.point).toEqual({ x: 0, y: 0 }); 173 | }); 174 | 175 | it("should return top middle of poly 1 for a point just outside of top middle", () => { 176 | const result = navMesh.findClosestMeshPoint(v2(-10, 5)); 177 | expect(result?.point).toEqual({ x: 0, y: 5 }); 178 | }); 179 | 180 | it("should return poly 2 or 4 for a point equidistant from them", () => { 181 | const result = navMesh.findClosestMeshPoint(v2(25, 5)); 182 | const isPoly2or4 = result?.polygon?.id === 1 || result?.polygon?.id === 3; 183 | expect(isPoly2or4).toBe(true); 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /packages/navmesh/src/navgraph.ts: -------------------------------------------------------------------------------- 1 | import jsastar from "javascript-astar"; 2 | import NavPoly from "./navpoly"; 3 | 4 | /** 5 | * Graph for javascript-astar. It implements the functionality for astar. See GPS test from astar 6 | * repo for structure: https://github.com/bgrins/javascript-astar/blob/master/test/tests.js 7 | * 8 | * @class NavGraph 9 | * @private 10 | */ 11 | class NavGraph implements jsastar.Graph { 12 | public nodes: NavPoly[]; 13 | 14 | public grid = []; 15 | 16 | constructor(navPolygons: NavPoly[]) { 17 | this.nodes = navPolygons; 18 | this.init(); 19 | } 20 | 21 | neighbors(navPolygon: NavPoly) { 22 | return navPolygon.neighbors; 23 | } 24 | 25 | navHeuristic(navPolygon1: NavPoly, navPolygon2: NavPoly) { 26 | return navPolygon1.centroidDistance(navPolygon2); 27 | } 28 | 29 | destroy() { 30 | this.cleanDirty(); 31 | this.nodes = []; 32 | } 33 | 34 | public init = jsastar.Graph.prototype.init.bind(this); 35 | public cleanDirty = jsastar.Graph.prototype.cleanDirty.bind(this); 36 | public markDirty = jsastar.Graph.prototype.markDirty.bind(this); 37 | public toString = jsastar.Graph.prototype.toString.bind(this); 38 | } 39 | 40 | export default NavGraph; 41 | -------------------------------------------------------------------------------- /packages/navmesh/src/navpoly.ts: -------------------------------------------------------------------------------- 1 | import Line from "./math/line"; 2 | import Polygon from "./math/polygon"; 3 | import Vector2 from "./math/vector-2"; 4 | import { Portal } from "./channel"; 5 | import { Point } from "./common-types"; 6 | import jsastar from "javascript-astar"; 7 | 8 | /** 9 | * A class that represents a navigable polygon with a navmesh. It is built on top of a 10 | * {@link Polygon}. It implements the properties and fields that javascript-astar needs - weight, 11 | * toString, isWall and getCost. See GPS test from astar repo for structure: 12 | * https://github.com/bgrins/javascript-astar/blob/master/test/tests.js 13 | */ 14 | export default class NavPoly implements jsastar.GridNode { 15 | public id: number; 16 | public polygon: Polygon; 17 | public edges: Line[]; 18 | public neighbors: NavPoly[]; 19 | public portals: Line[]; 20 | public centroid: Vector2; 21 | public boundingRadius: number; 22 | 23 | // jsastar property: 24 | public weight = 1; 25 | public x: number = 0; 26 | public y: number = 0; 27 | 28 | /** 29 | * Creates an instance of NavPoly. 30 | */ 31 | constructor(id: number, polygon: Polygon) { 32 | this.id = id; 33 | this.polygon = polygon; 34 | this.edges = polygon.edges; 35 | this.neighbors = []; 36 | this.portals = []; 37 | this.centroid = this.calculateCentroid(); 38 | this.boundingRadius = this.calculateRadius(); 39 | } 40 | 41 | /** 42 | * Returns an array of points that form the polygon. 43 | */ 44 | public getPoints() { 45 | return this.polygon.points; 46 | } 47 | 48 | /** 49 | * Check if the given point-like object is within the polygon. 50 | */ 51 | public contains(point: Point) { 52 | // Phaser's polygon check doesn't handle when a point is on one of the edges of the line. Note: 53 | // check numerical stability here. It would also be good to optimize this for different shapes. 54 | return this.polygon.contains(point.x, point.y) || this.isPointOnEdge(point); 55 | } 56 | 57 | /** 58 | * Only rectangles are supported, so this calculation works, but this is not actually the centroid 59 | * calculation for a polygon. This is just the average of the vertices - proper centroid of a 60 | * polygon factors in the area. 61 | */ 62 | public calculateCentroid() { 63 | const centroid = new Vector2(0, 0); 64 | const length = this.polygon.points.length; 65 | this.polygon.points.forEach((p) => centroid.add(p)); 66 | centroid.x /= length; 67 | centroid.y /= length; 68 | return centroid; 69 | } 70 | 71 | /** 72 | * Calculate the radius of a circle that circumscribes the polygon. 73 | */ 74 | public calculateRadius() { 75 | let boundingRadius = 0; 76 | for (const point of this.polygon.points) { 77 | const d = this.centroid.distance(point); 78 | if (d > boundingRadius) boundingRadius = d; 79 | } 80 | return boundingRadius; 81 | } 82 | 83 | /** 84 | * Check if the given point-like object is on one of the edges of the polygon. 85 | */ 86 | public isPointOnEdge({ x, y }: Point) { 87 | for (const edge of this.edges) { 88 | if (edge.pointOnSegment(x, y)) return true; 89 | } 90 | return false; 91 | } 92 | 93 | public destroy() { 94 | this.neighbors = []; 95 | this.portals = []; 96 | } 97 | 98 | // === jsastar methods === 99 | 100 | public toString() { 101 | return `NavPoly(id: ${this.id} at: ${this.centroid})`; 102 | } 103 | 104 | public isWall() { 105 | return this.weight === 0; 106 | } 107 | 108 | public centroidDistance(navPolygon: NavPoly) { 109 | return this.centroid.distance(navPolygon.centroid); 110 | } 111 | 112 | public getCost(navPolygon: NavPoly) { 113 | return this.centroidDistance(navPolygon); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /packages/navmesh/src/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { almostEqual, angleDifference, areCollinear } from "./utils"; 2 | import Line from "./math/line"; 3 | 4 | const line = (x1: number, y1: number, x2: number, y2: number) => new Line(x1, y1, x2, y2); 5 | 6 | describe("almostEqual", () => { 7 | test("should be false for numbers with a significant difference", () => { 8 | expect(almostEqual(1, 2)).toEqual(false); 9 | expect(almostEqual(-1, -0.95)).toEqual(false); 10 | }); 11 | test("should be true for numbers that are within floating point error margin", () => { 12 | expect(almostEqual(1, 1.00001)).toEqual(true); 13 | expect(almostEqual(1 / 3, 0.33333)).toEqual(true); 14 | }); 15 | }); 16 | 17 | describe("areCollinear", () => { 18 | test("should return false for non-collinear lines", () => { 19 | expect(areCollinear(line(-5, 0, 5, 0), line(-5, 10, 5, 10))).toEqual(false); // Parallel 20 | expect(areCollinear(line(0, 0, 10, 10), line(0, 0, 0, 10))).toEqual(false); // Intersecting 21 | }); 22 | test("should return true for collinear lines", () => { 23 | expect(areCollinear(line(10, 10, 0, 0), line(10, 10, 0, 0))).toEqual(true); // Same 24 | expect(areCollinear(line(0, 0, 10, 10), line(10, 10, 0, 0))).toEqual(true); // Reversed 25 | }); 26 | }); 27 | 28 | describe("angleDifference", () => { 29 | test("should return the difference between angles in radians", () => { 30 | expect(angleDifference(Math.PI, Math.PI / 2)).toEqual(Math.PI / 2); 31 | expect(angleDifference(1, -2)).toEqual(3); 32 | }); 33 | test("should wrap angles to calculate minimum angular distance", () => { 34 | expect(angleDifference(4 * Math.PI, Math.PI / 2)).toEqual(-Math.PI / 2); 35 | expect(angleDifference(-3 * Math.PI, 0)).toEqual(-Math.PI); 36 | }); 37 | }); 38 | 39 | // TODO: triarea2 40 | -------------------------------------------------------------------------------- /packages/navmesh/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Point } from "./common-types"; 2 | import Line from "./math/line"; 3 | import Vector2 from "./math/vector-2"; 4 | 5 | /** 6 | * Calculate the distance squared between two points. This is an optimization to a square root when 7 | * you just need to compare relative distances without needing to know the specific distance. 8 | * @param a 9 | * @param b 10 | */ 11 | export function distanceSquared(a: Point, b: Point) { 12 | const dx = b.x - a.x; 13 | const dy = b.y - a.y; 14 | return dx * dx + dy * dy; 15 | } 16 | 17 | /** 18 | * Project a point onto a line segment. 19 | * JS Source: http://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment 20 | * @param point 21 | * @param line 22 | */ 23 | export function projectPointToEdge(point: Point, line: Line) { 24 | const a = line.start; 25 | const b = line.end; 26 | // Consider the parametric equation for the edge's line, p = a + t (b - a). We want to find 27 | // where our point lies on the line by solving for t: 28 | // t = [(p-a) . (b-a)] / |b-a|^2 29 | const l2 = distanceSquared(a, b); 30 | let t = ((point.x - a.x) * (b.x - a.x) + (point.y - a.y) * (b.y - a.y)) / l2; 31 | // We clamp t from [0,1] to handle points outside the segment vw. 32 | t = clamp(t, 0, 1); 33 | // Project onto the segment 34 | const p = new Vector2(a.x + t * (b.x - a.x), a.y + t * (b.y - a.y)); 35 | return p; 36 | } 37 | 38 | /** 39 | * Twice the area of the triangle formed by a, b and c. 40 | */ 41 | export function triarea2(a: Point, b: Point, c: Point) { 42 | const ax = b.x - a.x; 43 | const ay = b.y - a.y; 44 | const bx = c.x - a.x; 45 | const by = c.y - a.y; 46 | return bx * ay - ax * by; 47 | } 48 | 49 | /** 50 | * Clamp the given value between min and max. 51 | */ 52 | export function clamp(value: number, min: number, max: number) { 53 | if (value < min) value = min; 54 | if (value > max) value = max; 55 | return value; 56 | } 57 | 58 | /** 59 | * Check if two values are within a small margin of one another. 60 | */ 61 | export function almostEqual(value1: number, value2: number, errorMargin = 0.0001) { 62 | if (Math.abs(value1 - value2) <= errorMargin) return true; 63 | else return false; 64 | } 65 | 66 | /** 67 | * Find the smallest angle difference between two angles 68 | * https://gist.github.com/Aaronduino/4068b058f8dbc34b4d3a9eedc8b2cbe0 69 | */ 70 | export function angleDifference(x: number, y: number) { 71 | let a = x - y; 72 | const i = a + Math.PI; 73 | const j = Math.PI * 2; 74 | a = i - Math.floor(i / j) * j; // (a+180) % 360; this ensures the correct sign 75 | a -= Math.PI; 76 | return a; 77 | } 78 | 79 | /** 80 | * Check if two lines are collinear (within a small error margin). 81 | */ 82 | export function areCollinear(line1: Line, line2: Line, errorMargin = 0.0001) { 83 | // Figure out if the two lines are equal by looking at the area of the triangle formed 84 | // by their points 85 | const area1 = triarea2(line1.start, line1.end, line2.start); 86 | const area2 = triarea2(line1.start, line1.end, line2.end); 87 | if (almostEqual(area1, 0, errorMargin) && almostEqual(area2, 0, errorMargin)) { 88 | return true; 89 | } else return false; 90 | } 91 | 92 | export function isTruthy(input: InputType) { 93 | return Boolean(input); 94 | } 95 | -------------------------------------------------------------------------------- /packages/navmesh/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../config/tsconfig.base.json", 3 | "include": ["src/**/*.ts"], 4 | "exclude": ["node_modules/**", "src/**/*.test.ts"], 5 | "compilerOptions": { 6 | "module": "ESNext", 7 | "target": "ESNext", 8 | "outDir": "./dist", 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/navmesh/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | const path = require("path"); 4 | const root = __dirname; 5 | const { CleanWebpackPlugin } = require("clean-webpack-plugin"); 6 | 7 | module.exports = function (env, argv) { 8 | const isDev = argv.mode === "development"; 9 | 10 | return { 11 | context: path.join(root, "src"), 12 | entry: "./index.ts", 13 | output: { 14 | library: { 15 | name: "NavMesh", 16 | type: "umd", 17 | }, 18 | filename: "navmesh.js", 19 | path: path.resolve(root, "dist"), 20 | globalObject: '(typeof self !== "undefined" ? self : this)', 21 | }, 22 | plugins: [new CleanWebpackPlugin()], 23 | resolve: { 24 | extensions: [".ts", ".js"], 25 | }, 26 | module: { 27 | rules: [ 28 | { 29 | test: /\.ts$/, 30 | use: "ts-loader", 31 | exclude: /node_modules/, 32 | }, 33 | { 34 | test: /\.js$/, 35 | exclude: /node_modules/, 36 | // Configure babel to look for the root babel.config.json with rootMode. 37 | use: { loader: "babel-loader", options: { rootMode: "upward" } }, 38 | }, 39 | ], 40 | }, 41 | devtool: isDev ? "eval-source-map" : "source-map", 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /packages/phaser-navmesh/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /packages/phaser-navmesh/README.md: -------------------------------------------------------------------------------- 1 | # Phaser NavMesh 2 | 3 | A Phaser 3 plugin for fast pathfinding using [navigation meshes](https://en.wikipedia.org/wiki/Navigation_mesh). 4 | 5 | For usage information, see: [mikewesthad/navmesh](https://github.com/mikewesthad/navmesh). 6 | 7 | ## Changelog 8 | 9 | 10 | Version 2.3.1 11 | 12 | - Fix [issue 40](https://github.com/mikewesthad/navmesh/issues/40) where shrink parameter was not used in buildMeshFromTilemap. Thanks [@SlvainBreton](https://github.com/SylvainBreton). 13 | - Documentation fixes. 14 | 15 | Version 2.3.0 16 | 17 | - Fix: webpack misconfiguration that caused [issue 37](https://github.com/mikewesthad/navmesh/issues/37). The build was only picking up the "default" export, but now it properly picks up all library exports. Thanks to[@Wenish](https://github.com/Wenish). 18 | 19 | Version 2.2.2 20 | 21 | - Update all dependencies to the latest versions. 22 | - Fixed TS bug where constructor was missing `pluginKey` which is required in latest version of Phaser. 23 | 24 | Version 2.2.1 25 | 26 | - Fix: `PhaserNavMeshPlugin#buildMeshFromTilemap` mesh output is no longer incorrectly rotated (rows & cols were flipped). 27 | 28 | Version 2.2.0 29 | 30 | - Feature: `PhaserNavMeshPlugin#buildMeshFromTilemap` allows you to automatically generate a navmesh from a Tilemap. 31 | - Feature: `PhaserNavMesh#isPointInMesh` allows you to check if a point is inside of the navmesh. 32 | 33 | Version 2.1.0 34 | 35 | - Added `public removeAllMeshes()` to `PhaserNavMeshPlugin`. 36 | - Bug: fixed double subscription to boot, thanks [@malahaas](https://github.com/malahaas)! 37 | - Converted library to TypeScript. 38 | 39 | Version 2.0.5 40 | 41 | - Bug: fixed webpack config so that it applied babel transform and so that it worked under node environments, thanks to [@will-hart](https://github.com/will-hart) 42 | 43 | Version 2.0.4 44 | 45 | - Bug: fixed a bug when destroying navmeshes, thanks to [@GGAlanSmithee](https://github.com/GGAlanSmithee) for pointing it out 46 | -------------------------------------------------------------------------------- /packages/phaser-navmesh/cypress.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /packages/phaser-navmesh/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /packages/phaser-navmesh/cypress/integration/navmesh.spec.js: -------------------------------------------------------------------------------- 1 | import Phaser from "phaser"; 2 | import NavMeshPlugin from "phaser-navmesh"; 3 | 4 | describe("NavMeshPlugin", () => { 5 | it("is installed properly as a scene plugin", async () => { 6 | const game = new Phaser.Game({ 7 | plugins: { 8 | scene: [ 9 | { 10 | key: "NavMeshPlugin", 11 | plugin: NavMeshPlugin, 12 | mapping: "navMeshPlugin", 13 | start: true, 14 | }, 15 | ], 16 | }, 17 | }); 18 | const scene = { 19 | create() { 20 | expect(this.navMeshPlugin).to.be.instanceOf(NavMeshPlugin); 21 | return; 22 | }, 23 | }; 24 | game.scene.add("main", scene, true); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/phaser-navmesh/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | module.exports = (on, config) => { 19 | // `on` is used to hook into various events Cypress emits 20 | // `config` is the resolved Cypress config 21 | } 22 | -------------------------------------------------------------------------------- /packages/phaser-navmesh/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /packages/phaser-navmesh/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /packages/phaser-navmesh/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phaser-navmesh", 3 | "version": "2.3.1", 4 | "description": "A plugin for Phaser (v3) for fast pathfinding using navigation meshes", 5 | "main": "dist/phaser-navmesh-plugin.js", 6 | "types": "dist/index.d.ts", 7 | "files": [ 8 | "src", 9 | "dist" 10 | ], 11 | "scripts": { 12 | "test": "", 13 | "tsc": "tsc", 14 | "build": "webpack --mode production", 15 | "watch": "webpack --mode development --watch", 16 | "prepublishOnly": "npm run build" 17 | }, 18 | "dependencies": { 19 | "core-js": "3", 20 | "navmesh": "^2.3.1" 21 | }, 22 | "peerDependencies": { 23 | "phaser": "^3.55.2" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.14.6", 27 | "@babel/preset-env": "^7.14.5", 28 | "babel-loader": "^8.2.2", 29 | "clean-webpack-plugin": "^3.0.0", 30 | "ts-loader": "^9.2.3", 31 | "typescript": "^4.3.4", 32 | "webpack": "^5.40.0", 33 | "webpack-cli": "^4.7.2" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "git+https://github.com/mikewesthad/phaser-navmesh-plugin.git" 38 | }, 39 | "keywords": [ 40 | "path finding", 41 | "navigation mesh", 42 | "phaser", 43 | "a*" 44 | ], 45 | "author": "Michael Hadley", 46 | "contributors": [ 47 | "Rex Twedt" 48 | ], 49 | "license": "MIT", 50 | "bugs": { 51 | "url": "https://github.com/mikewesthad/phaser-navmesh-plugin/issues" 52 | }, 53 | "homepage": "https://github.com/mikewesthad/phaser-navmesh-plugin#readme" 54 | } 55 | -------------------------------------------------------------------------------- /packages/phaser-navmesh/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * `phaser-navmesh` is a Phaser v3 wrapper around `navmesh` that creates a Phaser 3 Scene plugin. 3 | * Phaser 3 is expected to be a dependency in your project. 4 | * @packageDocumentation 5 | * @module phaser-navmesh 6 | */ 7 | 8 | import PhaserNavMeshPlugin from "./phaser-navmesh-plugin"; 9 | import PhaserNavMesh from "./phaser-navmesh"; 10 | 11 | export { PhaserNavMeshPlugin, PhaserNavMesh }; 12 | export default PhaserNavMeshPlugin; 13 | -------------------------------------------------------------------------------- /packages/phaser-navmesh/src/phaser-navmesh-plugin.ts: -------------------------------------------------------------------------------- 1 | import Phaser from "phaser"; 2 | import PhaserNavMesh from "./phaser-navmesh"; 3 | import { buildPolysFromGridMap } from "navmesh/src/map-parsers"; 4 | 5 | /** 6 | * This class can create navigation meshes for use in Phaser 3. The navmeshes can be constructed 7 | * from convex polygons embedded in a Tiled map. The class that conforms to Phaser 3's plugin 8 | * structure. 9 | * 10 | * @export 11 | * @class PhaserNavMeshPlugin 12 | */ 13 | export default class PhaserNavMeshPlugin extends Phaser.Plugins.ScenePlugin { 14 | private phaserNavMeshes: Record = {}; 15 | 16 | public constructor( 17 | scene: Phaser.Scene, 18 | pluginManager: Phaser.Plugins.PluginManager, 19 | pluginKey: string 20 | ) { 21 | super(scene, pluginManager, pluginKey); 22 | } 23 | 24 | /** Phaser.Scene lifecycle event */ 25 | public boot() { 26 | const emitter = this.systems.events; 27 | emitter.once("destroy", this.destroy, this); 28 | } 29 | 30 | /** Phaser.Scene lifecycle event - noop in this plugin, but still required. */ 31 | public init() {} 32 | 33 | /** Phaser.Scene lifecycle event - noop in this plugin, but still required.*/ 34 | public start() {} 35 | 36 | /** Phaser.Scene lifecycle event - will destroy all navmeshes created. */ 37 | public destroy() { 38 | this.systems.events.off("boot", this.boot, this); 39 | this.removeAllMeshes(); 40 | } 41 | 42 | /** 43 | * Remove all the meshes from the navmesh. 44 | */ 45 | public removeAllMeshes() { 46 | const meshes = Object.values(this.phaserNavMeshes); 47 | this.phaserNavMeshes = {}; 48 | meshes.forEach((m) => m.destroy()); 49 | } 50 | 51 | /** 52 | * Remove the navmesh stored under the given key from the plugin. This does not destroy the 53 | * navmesh. 54 | * @param key 55 | */ 56 | public removeMesh(key: string) { 57 | if (this.phaserNavMeshes[key]) delete this.phaserNavMeshes[key]; 58 | } 59 | 60 | /** 61 | * This method attempts to automatically build a navmesh based on the give tilemap and tilemap 62 | * layer(s). It attempts to respect the x/y position and scale of the layer(s). Important note: it 63 | * doesn't support rotation/flip or multiple layers that have different positions/scales. This 64 | * method is a bit experimental. It will generate a valid mesh, but it won't necessarily be 65 | * optimal, so you may end up sometimes getting non-shortest paths. 66 | * 67 | * @param key Key to use when storing this navmesh within the plugin. 68 | * @param tilemap The tilemap to use for building the navmesh. 69 | * @param tilemapLayers An optional array of tilemap layers to use for building the mesh. 70 | * @param isWalkable An optional function to use to test if a tile is walkable. Defaults to 71 | * assuming non-colliding tiles are walkable. 72 | * @param shrinkAmount Amount to "shrink" the mesh away from the tiles. This adds more 73 | * polygons to the generated mesh, but can be helpful for preventing agents from getting caught on 74 | * edges. This supports values between 0 and tileWidth/tileHeight (whichever dimension is 75 | * smaller). 76 | */ 77 | public buildMeshFromTilemap( 78 | key: string, 79 | tilemap: Phaser.Tilemaps.Tilemap, 80 | tilemapLayers?: Phaser.Tilemaps.TilemapLayer[], 81 | isWalkable?: (tile: Phaser.Tilemaps.Tile) => boolean, 82 | shrinkAmount = 0 83 | ) { 84 | // Use all layers in map, or just the specified ones. 85 | const dataLayers = tilemapLayers ? tilemapLayers.map((tl) => tl.layer) : tilemap.layers; 86 | if (!isWalkable) isWalkable = (tile: Phaser.Tilemaps.Tile) => !tile.collides; 87 | 88 | let offsetX = 0; 89 | let offsetY = 0; 90 | let scaleX = 1; 91 | let scaleY = 1; 92 | 93 | // Attempt to set position offset and scale from the 1st tilemap layer given. 94 | if (tilemapLayers) { 95 | const layer = tilemapLayers[0]; 96 | offsetX = layer.tileToWorldX(0); 97 | offsetY = layer.tileToWorldY(0); 98 | scaleX = layer.scaleX; 99 | scaleY = layer.scaleY; 100 | 101 | // Check and warn for layer settings that will throw off the calculation. 102 | for (const layer of tilemapLayers) { 103 | if ( 104 | offsetX !== layer.tileToWorldX(0) || 105 | offsetY !== layer.tileToWorldY(0) || 106 | scaleX !== layer.scaleX || 107 | scaleY !== layer.scaleY 108 | ) { 109 | console.warn( 110 | `PhaserNavMeshPlugin: buildMeshFromTilemap reads position & scale from the 1st TilemapLayer. Layer index ${layer.layerIndex} has a different position & scale from the 1st TilemapLayer.` 111 | ); 112 | } 113 | if (layer.rotation !== 0) { 114 | console.warn( 115 | `PhaserNavMeshPlugin: buildMeshFromTilemap doesn't support TilemapLayer with rotation. Layer index ${layer.layerIndex} is rotated.` 116 | ); 117 | } 118 | } 119 | } 120 | 121 | // Check and warn about DataLayer that have x/y position from Tiled. In the future, these could 122 | // be supported if A) only one colliding layer is offset, or B) the offset is a multiple of the 123 | // tile size. 124 | dataLayers.forEach((layer) => { 125 | if (layer.x !== 0 || layer.y !== 0) { 126 | console.warn( 127 | `PhaserNavMeshPlugin: buildMeshFromTilemap doesn't support layers with x/y positions from Tiled.` 128 | ); 129 | } 130 | }); 131 | 132 | // Build 2D array of walkable tiles across all given layers. 133 | const walkableAreas: boolean[][] = []; 134 | for (let ty = 0; ty < tilemap.height; ty += 1) { 135 | const row: boolean[] = []; 136 | for (let tx = 0; tx < tilemap.width; tx += 1) { 137 | let walkable = true; 138 | for (const layer of dataLayers) { 139 | const tile = layer.data[ty][tx]; 140 | if (tile && !isWalkable(tile)) { 141 | walkable = false; 142 | break; 143 | } 144 | } 145 | row.push(walkable); 146 | } 147 | walkableAreas.push(row); 148 | } 149 | 150 | let polygons = buildPolysFromGridMap( 151 | walkableAreas, 152 | tilemap.tileWidth, 153 | tilemap.tileHeight, 154 | (bool) => bool, 155 | shrinkAmount 156 | ); 157 | 158 | // Offset and scale each polygon if necessary. 159 | if (scaleX !== 1 && scaleY !== 1 && offsetX !== 0 && offsetY !== 0) { 160 | polygons = polygons.map((poly) => 161 | poly.map((p) => ({ x: p.x * scaleX + offsetX, y: p.y * scaleY + offsetY })) 162 | ); 163 | } 164 | 165 | const mesh = new PhaserNavMesh(this, this.scene, key, polygons, 0); 166 | this.phaserNavMeshes[key] = mesh; 167 | 168 | return mesh; 169 | } 170 | 171 | /** 172 | * Load a navmesh from Tiled. Currently assumes that the polygons are squares! Does not support 173 | * tilemap layer scaling, rotation or position. 174 | * @param key Key to use when storing this navmesh within the plugin. 175 | * @param objectLayer The ObjectLayer from a tilemap that contains the polygons that make up the 176 | * navmesh. 177 | * @param meshShrinkAmount The amount (in pixels) that the navmesh has been shrunk around 178 | * obstacles (a.k.a the amount obstacles have been expanded) 179 | */ 180 | public buildMeshFromTiled( 181 | key: string, 182 | objectLayer: Phaser.Tilemaps.ObjectLayer, 183 | meshShrinkAmount = 0 184 | ) { 185 | if (this.phaserNavMeshes[key]) { 186 | console.warn(`NavMeshPlugin: a navmesh already exists with the given key: ${key}`); 187 | return this.phaserNavMeshes[key]; 188 | } 189 | 190 | if (!objectLayer || objectLayer.objects.length === 0) { 191 | console.warn( 192 | `NavMeshPlugin: The given tilemap object layer is empty or undefined: ${objectLayer}` 193 | ); 194 | } 195 | 196 | const objects = objectLayer.objects ?? []; 197 | 198 | // Loop over the objects and construct a polygon - assumes a rectangle for now! 199 | // TODO: support layer position, scale, rotation 200 | const polygons = objects.map((obj) => { 201 | const h = obj.height ?? 0; 202 | const w = obj.width ?? 0; 203 | const left = obj.x ?? 0; 204 | const top = obj.y ?? 0; 205 | const bottom = top + h; 206 | const right = left + w; 207 | return [ 208 | { x: left, y: top }, 209 | { x: left, y: bottom }, 210 | { x: right, y: bottom }, 211 | { x: right, y: top }, 212 | ]; 213 | }); 214 | 215 | const mesh = new PhaserNavMesh(this, this.scene, key, polygons, meshShrinkAmount); 216 | 217 | this.phaserNavMeshes[key] = mesh; 218 | 219 | return mesh; 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /packages/phaser-navmesh/src/phaser-navmesh.ts: -------------------------------------------------------------------------------- 1 | // Directly importing the TS version - no need to double bundle. 2 | /// 3 | import NavMesh, { Point, PolyPoints } from "navmesh/src"; 4 | 5 | import Phaser from "phaser"; 6 | import PhaserNavMeshPlugin from "./phaser-navmesh-plugin"; 7 | 8 | /** 9 | * A wrapper around {@link NavMesh} for Phaser 3. Create instances of this class from 10 | * {@link PhaserNavMeshPlugin}. This is the workhorse that represents a navigation mesh built from a 11 | * series of polygons. Once built, the mesh can be asked for a path from one point to another point. 12 | * 13 | * Compared to {@link NavMesh}, this adds visual debugging capabilities and converts paths to 14 | * Phaser-compatible point instances. 15 | * 16 | * @export 17 | * @class PhaserNavMesh 18 | */ 19 | export default class PhaserNavMesh { 20 | private key: string; 21 | private plugin: PhaserNavMeshPlugin; 22 | private scene: Phaser.Scene; 23 | private debugGraphics: Phaser.GameObjects.Graphics | null; 24 | private navMesh: NavMesh; 25 | 26 | /** 27 | * Creates an instance of PhaserNavMesh. 28 | * @param plugin The plugin that owns this mesh. 29 | * @param scene 30 | * @param key The key the mesh is stored under within the plugin. 31 | * @param meshPolygonPoints Array where each element is an array of point-like objects that 32 | * defines a polygon. 33 | * @param meshShrinkAmount The amount (in pixels) that the navmesh has been shrunk around 34 | * obstacles (a.k.a the amount obstacles have been expanded) 35 | */ 36 | public constructor( 37 | plugin: PhaserNavMeshPlugin, 38 | scene: Phaser.Scene, 39 | key: string, 40 | meshPolygonPoints: PolyPoints[], 41 | meshShrinkAmount = 0 42 | ) { 43 | this.key = key; 44 | this.plugin = plugin; 45 | this.scene = scene; 46 | this.debugGraphics = null; 47 | this.navMesh = new NavMesh(meshPolygonPoints, meshShrinkAmount); 48 | } 49 | 50 | /** 51 | * Find if the given point is within any of the polygons in the mesh. 52 | * @param point 53 | */ 54 | public isPointInMesh(point: Point) { 55 | return this.navMesh.isPointInMesh(point); 56 | } 57 | 58 | /** 59 | * See {@link NavMesh#findPath}. This implements the same functionality, except that the returned 60 | * path is converted to Phaser-compatible points. 61 | * @param startPoint A point-like object 62 | * @param endPoint A point-like object 63 | * @param PointClass The class used to represent points in the final path 64 | * @returns An array of points if a path is found, or null if no path 65 | */ 66 | public findPath(startPoint: Point, endPoint: Point, PointClass = Phaser.Geom.Point) { 67 | const path = this.navMesh.findPath(startPoint, endPoint); 68 | return path ? path.map(({ x, y }) => new PointClass(x, y)) : path; 69 | } 70 | 71 | /** 72 | * Enable the debug drawing graphics. If no graphics object is provided, a new instance will be 73 | * created. 74 | * @param graphics An optional graphics object for the mesh to use for debug drawing. Note, the 75 | * mesh will destroy this graphics object when the mesh is destroyed. 76 | * @returns The graphics object this mesh uses. 77 | */ 78 | public enableDebug(graphics: Phaser.GameObjects.Graphics) { 79 | if (!graphics && !this.debugGraphics) { 80 | this.debugGraphics = this.scene.add.graphics(); 81 | } else if (graphics) { 82 | if (this.debugGraphics) this.debugGraphics.destroy(); 83 | this.debugGraphics = graphics; 84 | } 85 | 86 | this.debugGraphics!.visible = true; 87 | 88 | return this.debugGraphics; 89 | } 90 | 91 | /** Hide the debug graphics, but don't destroy it. */ 92 | public disableDebug() { 93 | if (this.debugGraphics) this.debugGraphics.visible = false; 94 | } 95 | 96 | /** Returns true if the debug graphics object is enabled and visible. */ 97 | public isDebugEnabled() { 98 | return this.debugGraphics && this.debugGraphics.visible; 99 | } 100 | 101 | /** Clear the debug graphics. */ 102 | public debugDrawClear() { 103 | if (this.debugGraphics) this.debugGraphics.clear(); 104 | } 105 | 106 | /** 107 | * Visualize the polygons in the navmesh by drawing them to the debug graphics. 108 | * @param options 109 | * @param [options.drawCentroid=true] For each polygon, show the approx centroid 110 | * @param [options.drawBounds=false] For each polygon, show the bounding radius 111 | * @param [options.drawNeighbors=true] For each polygon, show the connections to neighbors 112 | * @param [options.drawPortals=true] For each polygon, show the portal edges 113 | * @param [options.palette=[0x00a0b0, 0x6a4a3c, 0xcc333f, 0xeb6841, 0xedc951]] An array of 114 | * Phaser-compatible format colors to use when drawing the individual polygons. The first poly 115 | * uses the first color, the second poly uses the second color, etc. 116 | */ 117 | public debugDrawMesh({ 118 | drawCentroid = true, 119 | drawBounds = false, 120 | drawNeighbors = true, 121 | drawPortals = true, 122 | palette = [0x00a0b0, 0x6a4a3c, 0xcc333f, 0xeb6841, 0xedc951], 123 | } = {}) { 124 | if (!this.debugGraphics) return; 125 | 126 | const graphics = this.debugGraphics; 127 | const navPolys = this.navMesh.getPolygons(); 128 | 129 | navPolys.forEach((poly) => { 130 | const color = palette[poly.id % palette.length]; 131 | graphics.fillStyle(color); 132 | graphics.fillPoints(poly.getPoints(), true); 133 | 134 | if (drawCentroid) { 135 | graphics.fillStyle(0x000000); 136 | graphics.fillCircle(poly.centroid.x, poly.centroid.y, 4); 137 | } 138 | 139 | if (drawBounds) { 140 | graphics.lineStyle(1, 0xffffff); 141 | graphics.strokeCircle(poly.centroid.x, poly.centroid.y, poly.boundingRadius); 142 | } 143 | 144 | if (drawNeighbors) { 145 | graphics.lineStyle(2, 0x000000); 146 | poly.neighbors.forEach((n) => { 147 | graphics.lineBetween(poly.centroid.x, poly.centroid.y, n.centroid.x, n.centroid.y); 148 | }); 149 | } 150 | 151 | if (drawPortals) { 152 | graphics.lineStyle(10, 0x000000); 153 | poly.portals.forEach((portal) => 154 | graphics.lineBetween(portal.start.x, portal.start.y, portal.end.x, portal.end.y) 155 | ); 156 | } 157 | }); 158 | } 159 | 160 | /** 161 | * Visualize a path (array of points) on the debug graphics. 162 | * @param path Array of point-like objects in the form {x, y} 163 | * @param color 164 | * @param thickness 165 | * @param alpha 166 | */ 167 | public debugDrawPath(path: Point[], color = 0x00ff00, thickness = 10, alpha = 1) { 168 | if (!this.debugGraphics) return; 169 | 170 | if (path && path.length) { 171 | // Draw line for path 172 | this.debugGraphics.lineStyle(thickness, color, alpha); 173 | this.debugGraphics.strokePoints(path); 174 | 175 | // Draw circle at start and end of path 176 | this.debugGraphics.fillStyle(color, alpha); 177 | const d = 1.2 * thickness; 178 | this.debugGraphics.fillCircle(path[0].x, path[0].y, d); 179 | 180 | if (path.length > 1) { 181 | const lastPoint = path[path.length - 1]; 182 | this.debugGraphics.fillCircle(lastPoint.x, lastPoint.y, d); 183 | } 184 | } 185 | } 186 | 187 | /** Destroy the mesh, kill the debug graphic and unregister itself with the plugin. */ 188 | public destroy() { 189 | if (this.navMesh) this.navMesh.destroy(); 190 | if (this.debugGraphics) this.debugGraphics.destroy(); 191 | this.plugin.removeMesh(this.key); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /packages/phaser-navmesh/src/triangulate.ts: -------------------------------------------------------------------------------- 1 | // TODO: integrate old code that automatically decomposed maps via triangulation 2 | // // const triangulate = require("./triangulate"); 3 | // // /** 4 | // // * Build a navmesh from an array of convex polygons. This currently tesselates the polygons into 5 | // // * triangles. They aren't as efficient or as well designed as ones made by hand in Tiled. 6 | // // * 7 | // // * @param {string} levelName The key to use to store the navmesh in the plugin 8 | // // * @param {[]} hulls An array of convex polygons describing the obstacles in the 9 | // // * level. See lighting-plugin/hull-from-tiles. 10 | // // * 11 | // // * @memberof NavMeshPlugin 12 | // // */ 13 | // // buildMesh(levelName, hulls) { 14 | // // const contours = this._buildContours(hulls); 15 | // // // Get an array of triangulated vertices 16 | // // const triangles = triangulate(contours, false); // Counter-clockwise ordering! 17 | // // const polygons = []; 18 | // // for (let i = 0; i < triangles.length; i += 6) { 19 | // // const poly = new Phaser.Polygon( 20 | // // // These should be in counter-clockwise order from triangulate 21 | // // triangles[i + 0], triangles[i + 1], 22 | // // triangles[i + 2], triangles[i + 3], 23 | // // triangles[i + 4], triangles[i + 5] 24 | // // ); 25 | // // polygons.push(poly); 26 | // // } 27 | // // const navMesh = new NavMesh(this.game, polygons); 28 | // // this._navMeshes[levelName] = navMesh; 29 | // // this._currentNavMesh = navMesh; 30 | // // } 31 | 32 | // // /** 33 | // // * @param {[]} hulls 34 | // // * @returns 35 | // // * 36 | // // * @memberof NavMeshPlugin 37 | // // */ 38 | // // _buildContours(hulls) { 39 | // // const w = this.game.width; 40 | // // const h = this.game.height; 41 | // // // Start the contours 42 | // // const contours = [ 43 | // // // Full screen - counter clockwise 44 | // // Float32Array.of(0,0, 0,h, w,h, w,0) 45 | // // ]; 46 | // // // For each convex hull add the contour 47 | // // for (const hull of hulls) { 48 | // // const contour = []; 49 | // // for (const lineInfo of hull) { 50 | // // contour.push(lineInfo.line.start.x, lineInfo.line.start.y); 51 | // // } 52 | // // contours.push(Float32Array.from(contour)); 53 | // // } 54 | // // return contours; 55 | // // } 56 | // } 57 | -------------------------------------------------------------------------------- /packages/phaser-navmesh/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../config/tsconfig.base.json", 3 | "include": ["src/**/*.ts"], 4 | "exclude": ["node_modules/**"], 5 | "compilerOptions": { 6 | "module": "ESNext", 7 | "target": "ESNext", 8 | "outDir": "./dist", 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/phaser-navmesh/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | const path = require("path"); 4 | const root = __dirname; 5 | const { CleanWebpackPlugin } = require("clean-webpack-plugin"); 6 | 7 | module.exports = function (env, argv) { 8 | const isDev = argv.mode === "development"; 9 | 10 | return { 11 | context: path.join(root, "src"), 12 | entry: "./index.ts", 13 | output: { 14 | library: { 15 | name: "PhaserNavMeshPlugin", 16 | type: "umd", 17 | }, 18 | filename: "phaser-navmesh-plugin.js", 19 | path: path.resolve(root, "dist"), 20 | }, 21 | plugins: [new CleanWebpackPlugin()], 22 | externals: { 23 | phaser: { 24 | root: "Phaser", 25 | commonjs: "phaser", 26 | commonjs2: "phaser", 27 | amd: "phaser", 28 | }, 29 | }, 30 | resolve: { 31 | extensions: [".ts", ".js"], 32 | }, 33 | module: { 34 | rules: [ 35 | { 36 | test: /\.ts$/, 37 | use: "ts-loader", 38 | exclude: /node_modules/, 39 | }, 40 | { 41 | test: /\.js$/, 42 | exclude: /node_modules/, 43 | // Configure babel to look for the root babel.config.json with rootMode. 44 | use: { loader: "babel-loader", options: { rootMode: "upward" } }, 45 | }, 46 | ], 47 | }, 48 | devtool: isDev ? "eval-source-map" : "source-map", 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /packages/phaser2-navmesh/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /packages/phaser2-navmesh/README.md: -------------------------------------------------------------------------------- 1 | # Phaser 2 NavMesh 2 | 3 | A Phaser 2 plugin for fast pathfinding using [navigation meshes](https://en.wikipedia.org/wiki/Navigation_mesh). 4 | 5 | For usage information, see: [mikewesthad/navmesh](https://github.com/mikewesthad/navmesh). 6 | 7 | ## Changelog 8 | 9 | Version 2.3.1 10 | 11 | - Documentation fixes. 12 | 13 | Version 2.3.0 14 | 15 | - Fix: webpack misconfiguration that caused [issue 37](https://github.com/mikewesthad/navmesh/issues/37). The build was only picking up the "default" export, but now it properly picks up all library exports. Thanks to[@Wenish](https://github.com/Wenish). 16 | 17 | Version 2.2.1 18 | 19 | - Update all dependencies to the latest versions. 20 | 21 | Version 2.2.0 22 | 23 | - Feature: `PhaserNavMesh2#isPointInMesh` allows you to check if a point is inside of the navmesh. 24 | 25 | Version 2.1.0 26 | 27 | - Added `public removeAllMeshes()` to `Phaser2NavMeshPlugin`. 28 | - Converted library to TypeScript. 29 | 30 | Version 2.0.5 31 | 32 | - Bug: fixed webpack config so that it applied babel transform and so that it worked under node environments, thanks to [@will-hart](https://github.com/will-hart) 33 | 34 | Version 2.0.4 35 | 36 | - Bug: fixed a bug when destroying navmeshes, thanks to [@GGAlanSmithee](https://github.com/GGAlanSmithee) for pointing it out 37 | -------------------------------------------------------------------------------- /packages/phaser2-navmesh/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phaser2-navmesh", 3 | "version": "2.3.1", 4 | "description": "A plugin for Phaser (v2) for fast pathfinding using navigation meshes", 5 | "main": "dist/phaser2-navmesh-plugin.js", 6 | "types": "dist/index.d.ts", 7 | "files": [ 8 | "src", 9 | "dist" 10 | ], 11 | "scripts": { 12 | "test": "", 13 | "tsc": "tsc", 14 | "build": "webpack --mode production", 15 | "watch": "webpack --mode development --watch", 16 | "prepublishOnly": "npm run build" 17 | }, 18 | "dependencies": { 19 | "core-js": "3", 20 | "navmesh": "^2.3.1" 21 | }, 22 | "peerDependencies": { 23 | "phaser-ce": "^2.18.0" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.14.6", 27 | "@babel/preset-env": "^7.14.5", 28 | "babel-loader": "^8.2.2", 29 | "clean-webpack-plugin": "^3.0.0", 30 | "ts-loader": "^9.2.3", 31 | "typescript": "^4.3.4", 32 | "webpack": "^5.40.0", 33 | "webpack-cli": "^4.7.2" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "git+https://github.com/mikewesthad/phaser-navmesh-plugin.git" 38 | }, 39 | "keywords": [ 40 | "path finding", 41 | "navigation mesh", 42 | "phaser", 43 | "a*" 44 | ], 45 | "author": "Michael Hadley", 46 | "contributors": [ 47 | "Rex Twedt" 48 | ], 49 | "license": "MIT", 50 | "bugs": { 51 | "url": "https://github.com/mikewesthad/phaser-navmesh-plugin/issues" 52 | }, 53 | "homepage": "https://github.com/mikewesthad/phaser-navmesh-plugin#readme" 54 | } 55 | -------------------------------------------------------------------------------- /packages/phaser2-navmesh/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * `phaser2-navmesh` is a Phaser v2 wrapper around `navmesh` that creates a Phaser 2 game plugin. 3 | * Phaser 2 or Phaser-ce is expected to be in the global scope. 4 | * @packageDocumentation 5 | * @module phaser2-navmesh 6 | */ 7 | 8 | /// 9 | 10 | import Phaser2NavMeshPlugin from "./phaser2-navmesh-plugin"; 11 | import Phaser2NavMesh from "./phaser2-navmesh"; 12 | 13 | export { Phaser2NavMeshPlugin, Phaser2NavMesh }; 14 | export default Phaser2NavMeshPlugin; 15 | -------------------------------------------------------------------------------- /packages/phaser2-navmesh/src/phaser2-navmesh-plugin.ts: -------------------------------------------------------------------------------- 1 | import Phaser2NavMesh from "./phaser2-navmesh"; 2 | 3 | /** 4 | * This class can create navigation meshes for use in Phaser 2 / Phaser CE. (For Phaser 3, see 5 | * {@link PhaserNavMeshPlugin}.) The navmeshes can be constructed from convex polygons embedded in a 6 | * Tiled map. The class that conforms to Phaser 2's plugin structure. 7 | */ 8 | export default class Phaser2NavMeshPlugin extends Phaser.Plugin { 9 | private phaserNavMeshes: Record = {}; 10 | 11 | constructor(game: Phaser.Game, pluginManager: Phaser.PluginManager) { 12 | super(game, pluginManager); 13 | } 14 | 15 | /** Destroy all navmeshes created and the plugin itself. */ 16 | destroy() { 17 | this.removeAllMeshes(); 18 | } 19 | 20 | /** 21 | * Remove all the meshes from the navmesh. 22 | */ 23 | public removeAllMeshes() { 24 | const meshes = Object.values(this.phaserNavMeshes); 25 | this.phaserNavMeshes = {}; 26 | meshes.forEach((m) => m.destroy()); 27 | } 28 | 29 | /** 30 | * Remove the navmesh stored under the given key from the plugin. This does not destroy the 31 | * navmesh. 32 | * 33 | * @param key 34 | */ 35 | removeMesh(key: string) { 36 | if (this.phaserNavMeshes[key]) delete this.phaserNavMeshes[key]; 37 | } 38 | 39 | /** 40 | * Load a navmesh from Tiled. Currently assumes that the polygons are squares! Does not support 41 | * tilemap layer scaling, rotation or position. 42 | * 43 | * @param key Key to use when storing this navmesh within the plugin. 44 | * @param objectLayer The ObjectLayer from a tilemap that contains 45 | * the polygons that make up the navmesh. 46 | * @param The amount (in pixels) that the navmesh has been shrunk 47 | * around obstacles (a.k.a the amount obstacles have been expanded) 48 | */ 49 | buildMeshFromTiled(key: string, objectLayer: any[], meshShrinkAmount = 0) { 50 | if (this.phaserNavMeshes[key]) { 51 | console.warn(`NavMeshPlugin: a navmesh already exists with the given key: ${key}`); 52 | return this.phaserNavMeshes[key]; 53 | } 54 | 55 | if (!objectLayer || objectLayer.length === 0) { 56 | console.warn( 57 | `NavMeshPlugin: The given tilemap object layer is empty or undefined: ${objectLayer}` 58 | ); 59 | } 60 | 61 | // Load up the object layer 62 | const objects = objectLayer || []; 63 | 64 | // Loop over the objects and construct a polygon - assumes a rectangle for now! 65 | // TODO: support layer position, scale, rotation 66 | const polygons = objects.map((obj) => { 67 | const top = obj.y; 68 | const bottom = obj.y + obj.height; 69 | const left = obj.x; 70 | const right = obj.x + obj.width; 71 | return [ 72 | { x: left, y: top }, 73 | { x: left, y: bottom }, 74 | { x: right, y: bottom }, 75 | { x: right, y: top }, 76 | ]; 77 | }); 78 | 79 | const mesh = new Phaser2NavMesh(this, key, polygons, meshShrinkAmount); 80 | 81 | this.phaserNavMeshes[key] = mesh; 82 | 83 | return mesh; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/phaser2-navmesh/src/phaser2-navmesh.ts: -------------------------------------------------------------------------------- 1 | // Directly importing the TS version - no need to double bundle. 2 | /// 3 | import NavMesh, { Point, PolyPoints } from "navmesh/src"; 4 | 5 | import Phaser2NavMeshPlugin from "./phaser2-navmesh-plugin"; 6 | 7 | /** 8 | * A wrapper around {@link NavMesh} for Phaser 2 / Phaser CE. Create instances of this class from 9 | * {@link Phaser2NavMeshPlugin}. This is the workhorse that represents a navigation mesh built from 10 | * a series of polygons. Once built, the mesh can be asked for a path from one point to another 11 | * point. 12 | * 13 | * Compared to {@link NavMesh}, this adds visual debugging capabilities and converts paths to 14 | * Phaser-compatible point instances. 15 | */ 16 | export default class Phaser2NavMesh { 17 | private key: string; 18 | private plugin: Phaser2NavMeshPlugin; 19 | private game: Phaser.Game; 20 | private debugGraphics: Phaser.Graphics | null; 21 | private navMesh: NavMesh; 22 | 23 | /** 24 | * Creates an instance of Phaser2NavMesh. 25 | * @param {Phaser2NavMeshPlugin} plugin The plugin that owns this mesh. 26 | * @param {string} key The key the mesh is stored under within the plugin. 27 | * @param {object[][]} meshPolygonPoints Array where each element is an array of point-like 28 | * objects that defines a polygon. 29 | * @param {number} [meshShrinkAmount=0] The amount (in pixels) that the navmesh has been shrunk 30 | * around obstacles (a.k.a the amount obstacles have been expanded) 31 | * @memberof Phaser2NavMesh 32 | */ 33 | constructor( 34 | plugin: Phaser2NavMeshPlugin, 35 | key: string, 36 | meshPolygonPoints: PolyPoints[], 37 | meshShrinkAmount = 0 38 | ) { 39 | this.key = key; 40 | this.plugin = plugin; 41 | this.game = plugin.game; 42 | this.debugGraphics = null; 43 | this.navMesh = new NavMesh(meshPolygonPoints, meshShrinkAmount); 44 | } 45 | 46 | /** 47 | * Find if the given point is within any of the polygons in the mesh. 48 | * @param point 49 | */ 50 | public isPointInMesh(point: Point) { 51 | return this.navMesh.isPointInMesh(point); 52 | } 53 | 54 | /** 55 | * See {@link NavMesh#findPath}. This implements the same functionality, except that the returned 56 | * path is converted to Phaser-compatible points. 57 | * 58 | * @param startPoint A point-like object in the form {x, y} 59 | * @param endPoint A point-like object in the form {x, y} 60 | * @param PointClass The point class to use to represent points in the path. 61 | * @returns An array of points if a path is found, or null if no path 62 | */ 63 | findPath(startPoint: Point, endPoint: Point, PointClass = Phaser.Point) { 64 | const path = this.navMesh.findPath(startPoint, endPoint); 65 | return path ? path.map(({ x, y }) => new PointClass(x, y)) : path; 66 | } 67 | 68 | /** 69 | * Enable the debug drawing graphics. If no graphics object is provided, a new instance will be 70 | * created. 71 | * 72 | * @param graphics An optional graphics object for the mesh to use for debug 73 | * drawing. Note, the mesh will destroy this graphics object when the mesh is destroyed. 74 | * @returns The graphics object this mesh uses. 75 | */ 76 | enableDebug(graphics: Phaser.Graphics) { 77 | if (!graphics && !this.debugGraphics) { 78 | this.debugGraphics = this.game.add.graphics(); 79 | } else if (graphics) { 80 | if (this.debugGraphics) this.debugGraphics.destroy(); 81 | this.debugGraphics = graphics; 82 | } 83 | 84 | if (this.debugGraphics) this.debugGraphics.visible = true; 85 | 86 | return this.debugGraphics; 87 | } 88 | 89 | /** Hide the debug graphics, but don't destroy it. */ 90 | disableDebug() { 91 | if (this.debugGraphics) this.debugGraphics.visible = false; 92 | } 93 | 94 | /** Returns true if the debug graphics object is enabled and visible. */ 95 | isDebugEnabled() { 96 | return this.debugGraphics && this.debugGraphics.visible; 97 | } 98 | 99 | /** Clear the debug graphics. */ 100 | debugDrawClear() { 101 | if (this.debugGraphics) this.debugGraphics.clear(); 102 | } 103 | 104 | /** 105 | * Visualize the polygons in the navmesh by drawing them to the debug graphics. 106 | * 107 | * @param options 108 | * @param [options.drawCentroid=true] For each polygon, show the approx centroid 109 | * @param [options.drawBounds=false] For each polygon, show the bounding radius 110 | * @param [options.drawNeighbors=true] For each polygon, show the connections to neighbors 111 | * @param [options.drawPortals=true] For each polygon, show the portal edges 112 | * @param [options.palette=[0x00a0b0, 0x6a4a3c, 0xcc333f, 0xeb6841, 0xedc951]] An array of 113 | * Phaser-compatible format colors to use when drawing the individual polygons. The first poly 114 | * uses the first color, the second poly uses the second color, etc. 115 | */ 116 | debugDrawMesh({ 117 | drawCentroid = true, 118 | drawBounds = false, 119 | drawNeighbors = true, 120 | drawPortals = true, 121 | palette = [0x00a0b0, 0x6a4a3c, 0xcc333f, 0xeb6841, 0xedc951], 122 | } = {}) { 123 | if (!this.debugGraphics) return; 124 | 125 | const graphics = this.debugGraphics; 126 | const navPolys = this.navMesh.getPolygons(); 127 | 128 | navPolys.forEach((poly) => { 129 | const color = palette[poly.id % palette.length]; 130 | graphics.lineWidth = 0; 131 | graphics.beginFill(color); 132 | graphics.drawPolygon(new Phaser.Polygon(...(poly.getPoints() as Phaser.Point[]))); 133 | graphics.endFill(); 134 | 135 | if (drawCentroid) { 136 | graphics.beginFill(0x000000); 137 | graphics.drawEllipse(poly.centroid.x, poly.centroid.y, 4, 4); 138 | graphics.endFill(); 139 | } 140 | 141 | if (drawBounds) { 142 | graphics.lineStyle(1, 0xffffff); 143 | const r = poly.boundingRadius; 144 | graphics.drawEllipse(poly.centroid.x, poly.centroid.y, r, r); 145 | } 146 | 147 | if (drawNeighbors) { 148 | graphics.lineStyle(2, 0x000000); 149 | poly.neighbors.forEach((n) => { 150 | graphics.moveTo(poly.centroid.x, poly.centroid.y); 151 | graphics.lineTo(n.centroid.x, n.centroid.y); 152 | }); 153 | } 154 | 155 | if (drawPortals) { 156 | graphics.lineStyle(10, 0x000000); 157 | poly.portals.forEach((portal) => { 158 | graphics.moveTo(portal.start.x, portal.start.y); 159 | graphics.lineTo(portal.end.x, portal.end.y); 160 | }); 161 | } 162 | }); 163 | } 164 | 165 | /** 166 | * Visualize a path (array of points) on the debug graphics. 167 | * 168 | * @param path Array of point-like objects in the form {x, y} 169 | * @param color 170 | * @param thickness 171 | * @param alpha 172 | */ 173 | debugDrawPath(path: Point[], color = 0x00ff00, thickness = 10, alpha = 1) { 174 | if (!this.debugGraphics) return; 175 | 176 | if (path && path.length) { 177 | // Draw line for path 178 | this.debugGraphics.lineStyle(thickness, color, alpha); 179 | this.debugGraphics.drawShape(new Phaser.Polygon(...(path as Phaser.Point[]))); 180 | 181 | // Draw circle at start and end of path 182 | this.debugGraphics.beginFill(color, alpha); 183 | const d = 0.5 * thickness; 184 | this.debugGraphics.drawEllipse(path[0].x, path[0].y, d, d); 185 | if (path.length > 1) { 186 | const lastPoint = path[path.length - 1]; 187 | this.debugGraphics.drawEllipse(lastPoint.x, lastPoint.y, d, d); 188 | } 189 | this.debugGraphics.endFill(); 190 | } 191 | } 192 | 193 | /** Destroy the mesh, kill the debug graphic and unregister itself with the plugin. */ 194 | destroy() { 195 | if (this.navMesh) this.navMesh.destroy(); 196 | if (this.debugGraphics) this.debugGraphics.destroy(); 197 | this.plugin.removeMesh(this.key); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /packages/phaser2-navmesh/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../config/tsconfig.base.json", 3 | "include": ["src/**/*.ts"], 4 | "exclude": ["node_modules/**"], 5 | "compilerOptions": { 6 | "module": "ESNext", 7 | "target": "ESNext", 8 | "outDir": "./dist", 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/phaser2-navmesh/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | const path = require("path"); 4 | const root = __dirname; 5 | const { CleanWebpackPlugin } = require("clean-webpack-plugin"); 6 | 7 | module.exports = function (env, argv) { 8 | const isDev = argv.mode === "development"; 9 | 10 | return { 11 | context: path.join(root, "src"), 12 | entry: "./index.ts", 13 | output: { 14 | library: { 15 | name: "Phaser2NavMeshPlugin", 16 | type: "umd", 17 | }, 18 | filename: "phaser2-navmesh-plugin.js", 19 | path: path.resolve(root, "dist"), 20 | }, 21 | plugins: [new CleanWebpackPlugin()], 22 | externals: { 23 | phaser: { 24 | commonjs: "phaser", 25 | commonjs2: "phaser", 26 | amd: "phaser", 27 | root: "Phaser", 28 | }, 29 | }, 30 | resolve: { 31 | extensions: [".ts", ".js"], 32 | }, 33 | module: { 34 | rules: [ 35 | { 36 | test: /\.ts$/, 37 | use: "ts-loader", 38 | exclude: /node_modules/, 39 | }, 40 | { 41 | test: /\.js$/, 42 | exclude: /node_modules/, 43 | // Configure babel to look for the root babel.config.json with rootMode. 44 | use: { loader: "babel-loader", options: { rootMode: "upward" } }, 45 | }, 46 | ], 47 | }, 48 | devtool: isDev ? "eval-source-map" : "source-map", 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /public/demo/images/follower.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/public/demo/images/follower.ai -------------------------------------------------------------------------------- /public/demo/images/follower.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/public/demo/images/follower.png -------------------------------------------------------------------------------- /public/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Navmesh Example 7 | 29 | 30 | 31 | 32 |
    33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /public/demo/tilemaps/map.tmx: -------------------------------------------------------------------------------- 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 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 122 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 123 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 124 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 125 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 126 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 127 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 128 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 129 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 130 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 131 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 132 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 133 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 134 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 135 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 136 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 137 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 138 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 139 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 140 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 141 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 142 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 143 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 144 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 145 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 146 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 147 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 148 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 149 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 150 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25 151 | 152 | 153 | 154 | 155 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 156 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 157 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 158 | 0,0,0,0,0,0,0,9,6,6,6,10,0,0,0,0,0,0,9,6,6,6,10,0,0,0,0,0,0,0, 159 | 0,0,0,0,0,0,0,8,6,6,6,11,0,0,0,0,0,0,8,6,6,6,11,0,0,0,0,0,0,0, 160 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 161 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 162 | 0,0,0,9,10,0,0,0,0,0,0,0,0,0,9,10,0,0,0,0,0,0,0,0,0,9,10,0,0,0, 163 | 0,0,0,7,7,0,0,0,0,0,0,0,0,0,7,7,0,0,0,0,0,0,0,0,0,7,7,0,0,0, 164 | 0,0,0,7,7,0,0,0,0,0,0,0,0,0,7,7,0,0,0,0,0,0,0,0,0,7,7,0,0,0, 165 | 0,0,0,7,7,0,0,0,0,0,0,0,0,0,7,7,0,0,0,0,0,0,0,0,0,7,7,0,0,0, 166 | 0,0,0,8,11,0,0,0,0,0,0,0,0,0,8,11,0,0,0,0,0,0,0,0,0,8,11,0,0,0, 167 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 168 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 169 | 0,0,0,0,0,0,0,9,6,6,6,10,0,0,0,0,0,0,9,6,6,6,10,0,0,0,0,0,0,0, 170 | 0,0,0,0,0,0,0,8,6,6,6,11,0,0,0,0,0,0,8,6,6,6,11,0,0,0,0,0,0,0, 171 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 172 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 173 | 0,0,0,9,10,0,0,0,0,0,0,0,0,0,9,10,0,0,0,0,0,0,0,0,0,9,10,0,0,0, 174 | 0,0,0,7,7,0,0,0,0,0,0,0,0,0,7,7,0,0,0,0,0,0,0,0,0,7,7,0,0,0, 175 | 0,0,0,7,7,0,0,0,0,0,0,0,0,0,7,7,0,0,0,0,0,0,0,0,0,7,7,0,0,0, 176 | 0,0,0,7,7,0,0,0,0,0,0,0,0,0,7,7,0,0,0,0,0,0,0,0,0,7,7,0,0,0, 177 | 0,0,0,8,11,0,0,0,0,0,0,0,0,0,8,11,0,0,0,0,0,0,0,0,0,8,11,0,0,0, 178 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 179 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 180 | 0,0,0,0,0,0,0,9,6,6,6,10,0,0,0,0,0,0,9,6,6,6,10,0,0,0,0,0,0,0, 181 | 0,0,0,0,0,0,0,8,6,6,6,11,0,0,0,0,0,0,8,6,6,6,11,0,0,0,0,0,0,0, 182 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 183 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 184 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 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 | -------------------------------------------------------------------------------- /public/demo/tilemaps/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/public/demo/tilemaps/tiles.png -------------------------------------------------------------------------------- /public/demo/tilemaps/tiles.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/public/demo/tilemaps/tiles.psd -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 | 12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/performance/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Navmesh Example 7 | 29 | 30 | 31 | 32 |
    33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /public/performance/tilemaps/map.tmx: -------------------------------------------------------------------------------- 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 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 122 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 123 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 124 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 125 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 126 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 127 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 128 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 129 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 130 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 131 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 132 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 133 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 134 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 135 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 136 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 137 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 138 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 139 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 140 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 141 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 142 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 143 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 144 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 145 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 146 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 147 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 148 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 149 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25, 150 | 25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25 151 | 152 | 153 | 154 | 155 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 156 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 157 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 158 | 0,0,0,0,0,0,0,9,6,6,6,10,0,0,0,0,0,0,9,6,6,6,10,0,0,0,0,0,0,0, 159 | 0,0,0,0,0,0,0,8,6,6,6,11,0,0,0,0,0,0,8,6,6,6,11,0,0,0,0,0,0,0, 160 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 161 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 162 | 0,0,0,9,10,0,0,0,0,0,0,0,0,0,9,10,0,0,0,0,0,0,0,0,0,9,10,0,0,0, 163 | 0,0,0,7,7,0,0,0,0,0,0,0,0,0,7,7,0,0,0,0,0,0,0,0,0,7,7,0,0,0, 164 | 0,0,0,7,7,0,0,0,0,0,0,0,0,0,7,7,0,0,0,0,0,0,0,0,0,7,7,0,0,0, 165 | 0,0,0,7,7,0,0,0,0,0,0,0,0,0,7,7,0,0,0,0,0,0,0,0,0,7,7,0,0,0, 166 | 0,0,0,8,11,0,0,0,0,0,0,0,0,0,8,11,0,0,0,0,0,0,0,0,0,8,11,0,0,0, 167 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 168 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 169 | 0,0,0,0,0,0,0,9,6,6,6,10,0,0,0,0,0,0,9,6,6,6,10,0,0,0,0,0,0,0, 170 | 0,0,0,0,0,0,0,8,6,6,6,11,0,0,0,0,0,0,8,6,6,6,11,0,0,0,0,0,0,0, 171 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 172 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 173 | 0,0,0,9,10,0,0,0,0,0,0,0,0,0,9,10,0,0,0,0,0,0,0,0,0,9,10,0,0,0, 174 | 0,0,0,7,7,0,0,0,0,0,0,0,0,0,7,7,0,0,0,0,0,0,0,0,0,7,7,0,0,0, 175 | 0,0,0,7,7,0,0,0,0,0,0,0,0,0,7,7,0,0,0,0,0,0,0,0,0,7,7,0,0,0, 176 | 0,0,0,7,7,0,0,0,0,0,0,0,0,0,7,7,0,0,0,0,0,0,0,0,0,7,7,0,0,0, 177 | 0,0,0,8,11,0,0,0,0,0,0,0,0,0,8,11,0,0,0,0,0,0,0,0,0,8,11,0,0,0, 178 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 179 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 180 | 0,0,0,0,0,0,0,9,6,6,6,10,0,0,0,0,0,0,9,6,6,6,10,0,0,0,0,0,0,0, 181 | 0,0,0,0,0,0,0,8,6,6,6,11,0,0,0,0,0,0,8,6,6,6,11,0,0,0,0,0,0,0, 182 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 183 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 184 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 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 | -------------------------------------------------------------------------------- /public/performance/tilemaps/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/public/performance/tilemaps/tiles.png -------------------------------------------------------------------------------- /public/performance/tilemaps/tiles.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewesthad/navmesh/7606d96bac0b374329a11ff9c071cf7c40cd9755/public/performance/tilemaps/tiles.psd -------------------------------------------------------------------------------- /tiled-navmesh-guide.md: -------------------------------------------------------------------------------- 1 | # Creating a NavMesh in Tiled 2 | 3 | [Tiled](http://www.mapeditor.org/) is an open source tile map editor. If you are already using it to design your levels, then you can also use it to create your nav meshes. 4 | 5 | Tiled allows you to design a map by creating [layers](http://doc.mapeditor.org/manual/layers/) of tiles & objects. A tile layer allows you to place your graphics (images from a tileset) into the world. An object layer allows you to store other kinds of information. Phaser can read both tile layers and object layers, so we can use the object layer to represent a nav mesh. In the image below, there is a layer of tiles (called "walls") and a layer of gray rectangles that represents the nav mesh (called "navmesh"): 6 | 7 | 8 | 9 | This guide assumes you have some familiarity with Tiled. If you don't know Tiled, check out gamefromscratch's video [tutorial series](http://www.gamefromscratch.com/post/2015/10/14/Tiled-Map-Editor-Tutorial-Series.aspx). 10 | 11 | ## Goal 12 | 13 | The idea is that we want to create an object layer in Tiled that represents where an "agent" (the player, an enemy, an npc, etc.) can move. We will describe those walkable area by placing individual shapes (mainly rectangles). An agent can then "walk" from one shape to another as long as they are connected (i.e. their edges overlap). 14 | 15 | Note: We will also want to take the agent's size into account when building the shapes. If you have an agent that is 20px wide, then it shouldn't be allowed to get within 10px of a wall. 16 | 17 | ## Snapping Setup 18 | 19 | In order to place shapes in the nav mesh accurately and ensure that neighboring shapes are "connected," you'll want to enable snapping. Open up your map or create a new one, and go to preferences (`Edit ⟶ Preferences`): 20 | 21 | 22 | 23 | Set the fine grid divisions. This allows you to snap objects "in-between" the grid. E.g. on a 25px x 25px tile map with 5 grid divisions, the fine grid would be every 5px. 24 | 25 | 26 | 27 | Enable snapping (`View ⟶ Snapping`): 28 | 29 | 30 | 31 | ## Creating the Mesh 32 | 33 | Create a new object layer and name it "navmesh." Then start adding in rectangles to define your nav mesh. (Note: rectangles are the only Tiled shape currently supported by this plugin.) 34 | 35 | 36 | 37 | See the Tiled manual for more information on [objects](http://doc.mapeditor.org/manual/objects/#working-with-objects). 38 | 39 | ## Agent Size (Gaps) 40 | 41 | Notice the 10px space left around the walls? That gap is because the agent is 20px wide circle. It would get stuck on corners of walls without that gap. Make sure the gaps you leave are a consistent size - you'll need to pass in the size as the third parameter to `plugin.buildMeshFromTiled`. 42 | 43 | If you wanted, you _could_ leave that gap out and write more complicated path following logic for your agents that avoids getting stuck. "Baking" the agent size into nav mesh with these gaps makes the path following logic pretty simple. 44 | -------------------------------------------------------------------------------- /typedoc.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./config/tsconfig.base.json", 3 | "typedocOptions": { 4 | "out": "docs", 5 | "name": "NavMesh Packages", 6 | "readme": "./README.md", 7 | "entryPoints": [ 8 | "./packages/navmesh/src/index.ts", 9 | "./packages/phaser2-navmesh/src/index.ts", 10 | "./packages/phaser-navmesh/src/index.ts" 11 | ] 12 | }, 13 | "include": [ 14 | "./packages/navmesh/src/index.ts", 15 | "./packages/phaser2-navmesh/src/index.ts", 16 | "./packages/phaser-navmesh/src/index.ts" 17 | ], 18 | "compilerOptions": { 19 | "target": "ESNext" 20 | } 21 | } --------------------------------------------------------------------------------