Path: blob/main/projects/tank-trouble-2/pathfind.js
1834 views
// Pathfind.js1// Written by Ashley Gullen2// Copyright (c) 2013 Scirra Ltd.34// A* pathfinding javascript implementation5"use strict";67(function() {89var PF_CLEAR = 0;10var PF_OBSTACLE = 32767;1112function node()13{14this.parent = null;1516this.x = 0;17this.y = 0;1819this.f = 0;20this.g = 0;21this.h = 0;22};2324var nodeCache = []; // for recycling nodes2526function allocNode()27{28var ret;2930if (nodeCache.length)31ret = nodeCache.pop();32else33ret = new node();3435ret.parent = null;36ret.x = 0;37ret.y = 0;38ret.f = 0;39ret.g = 0;40ret.h = 0;41return ret;42};4344function freeNode(n)45{46if (nodeCache.length < 64000)47nodeCache.push(n);48};4950function resultNode(x_, y_)51{52this.x = x_ || 0;53this.y = y_ || 0;54};5556var resultNodeCache = []; // for recycling resultNodes5758function allocResultNode()59{60if (resultNodeCache.length)61return resultNodeCache.pop();62else63return new resultNode(0, 0);64};6566function freeResultNode(n)67{68if (resultNodeCache.length < 10000)69resultNodeCache.push(n);70};7172var workersSupported = (typeof Worker !== "undefined");73var isInWebWorker = (typeof document === "undefined"); // no DOM in a worker74var myInstance = null; // single pathfinder instance for worker7576if (isInWebWorker)77{78self.addEventListener("message", function (e)79{80var d = e.data;8182if (!d)83return; // could be empty postMessage() to start worker8485if (d.cmd === "init")86{87if (!myInstance)88myInstance = new pathfinder();8990myInstance.init(d.hcells, d.vcells, d.data, d.diagonals);91}92else if (d.cmd === "find")93{94// receive a pathfind job, process it them dispatch result95myInstance.pathEnd.parent = null;96myInstance.targetX = d.endX;97myInstance.targetY = d.endY;9899if (myInstance.doLongFindPath(d.startX, d.startY))100{101self.postMessage({ cmd: "result", pathList: myInstance.pathList });102}103else104{105self.postMessage({ cmd: "result", pathList: null });106}107}108else if (d.cmd === "region")109{110myInstance.writeCells(d.offx, d.offy, d.lenx, d.leny, d.data);111}112else if (d.cmd === "setdiag")113{114// update diagonalsEnabled flag115if (myInstance)116myInstance.diagonalsEnabled = d.diagonals;117}118119}, false);120}121122function pathfinder()123{124this.hcells = 0;125this.vcells = 0;126127this.pathEnd = allocNode();128129this.cells = null;130this.openList = [];131this.closedList = [];132this.closedCache = {};133this.pathList = [];134135this.currentNode = null;136this.targetX = 0;137this.targetY = 0;138this.diagonalsEnabled = true;139140this.worker = null;141this.workerQueue = []; // jobs awaiting completion from worker in order of requests made142this.workerRecycle = [];143144var self = this;145var i, len;146147if (workersSupported && !isInWebWorker)148{149// Create worker and receive results of pathfind jobs from it150this.worker = new Worker("pathfind.js");151152this.worker.addEventListener("message", function (e) {153154if (!e || !e.data)155return;156157if (e.data.cmd === "result")158{159if (e.data.pathList)160{161for (i = 0, len = self.pathList.length; i < len; i++)162freeResultNode(self.pathList[i]);163164self.pathList = e.data.pathList;165self.workerQueue[0].success();166}167else168self.workerQueue[0].fail();169170self.workerRecycle.push(self.workerQueue.shift());171}172}, false);173174this.worker.addEventListener("error", function (e) {175console.error(e);176}, false);177178this.worker.postMessage(null);179}180};181182pathfinder.prototype.init = function (hcells_, vcells_, data_, diagonals_)183{184this.hcells = hcells_;185this.vcells = vcells_;186this.cells = data_;187this.diagonalsEnabled = diagonals_;188189if (workersSupported && !isInWebWorker)190{191this.worker.postMessage({192cmd: "init",193hcells: hcells_,194vcells: vcells_,195diagonals: diagonals_,196data: data_197});198}199};200201pathfinder.prototype.updateRegion = function (cx1_, cy1_, lenx_, leny_, data_)202{203this.writeCells(cx1_, cy1_, lenx_, leny_, data_);204205if (workersSupported && !isInWebWorker)206{207this.worker.postMessage({208cmd: "region",209offx: cx1_,210offy: cy1_,211lenx: lenx_,212leny: leny_,213data: data_214});215}216};217218pathfinder.prototype.writeCells = function (cx1, cy1, lenx, leny, data_)219{220var x, y;221222for (x = 0; x < lenx; ++x)223{224for (y = 0; y < leny; ++y)225{226this.cells[cx1 + x][cy1 + y] = data_[x][y];227}228}229};230231pathfinder.prototype.unsetReady = function ()232{233// revert to no data state234this.cells = null;235};236237pathfinder.prototype.isReady = function ()238{239return !!this.cells;240};241242pathfinder.prototype.setDiagonals = function (d)243{244if (this.diagonalsEnabled === d)245return;246247this.diagonalsEnabled = d;248249if (workersSupported && !isInWebWorker)250{251this.worker.postMessage({252cmd: "setdiag",253diagonals: d,254});255}256};257258pathfinder.prototype.at = function (x_, y_)259{260if (x_ < 0 || y_ < 0 || x_ >= this.hcells || y_ >= this.vcells)261return PF_OBSTACLE;262263return this.cells[x_][y_];264};265266pathfinder.prototype.findPath = function (startX, startY, endX, endY, successCallback, failCallback)267{268if (!this.cells)269{270// not yet initialised271failCallback();272return;273}274275startX = Math.floor(startX);276startY = Math.floor(startY);277endX = Math.floor(endX);278endY = Math.floor(endY);279280this.targetX = endX;281this.targetY = endY;282this.pathEnd.parent = null;283284// Check the box made by the start and dest cells.285// If the complete box is empty, allow a direct move to target.286var minX = Math.min(startX, endX);287var maxX = Math.max(startX, endX);288var minY = Math.min(startY, endY);289var maxY = Math.max(startY, endY);290291// Path goes out of bounds: no calculable path292if (minX < 0 || minY < 0 || maxX >= this.hcells || maxY >= this.vcells)293{294failCallback();295return;296}297298var x, y, i, len, c, h, n;299300if (this.diagonalsEnabled)301{302var canMoveDirect = true;303304for (x = minX; x <= maxX; x++)305{306for (y = minY; y <= maxY; y++)307{308if (this.cells[x][y] !== 0)309{310canMoveDirect = false;311312// Break both loops313x = maxX + 1;314break;315}316}317}318319// A "direct" path is available (box is empty)320if (canMoveDirect)321{322for (i = 0, len = this.pathList.length; i < len; i++)323freeResultNode(this.pathList[i]);324this.pathList.length = 0;325326this.pathEnd.x = endX;327this.pathEnd.y = endY;328this.pathEnd.parent = null;329n = allocResultNode();330n.x = endX;331n.y = endY;332this.pathList.push(n);333successCallback();334return;335}336}337338if (workersSupported)339{340// recycle objects in the worker queue341if (this.workerRecycle.length)342h = this.workerRecycle.pop();343else344h = {};345346h.success = successCallback;347h.fail = failCallback;348349// dispatch the heavy lifting to the worker thread350this.workerQueue.push(h);351352this.worker.postMessage({353cmd: "find",354startX: startX,355startY: startY,356endX: endX,357endY: endY358});359}360else361{362// no web worker support, just run on main thread363if (this.doLongFindPath(startX, startY))364successCallback();365else366failCallback();367}368};369370pathfinder.prototype.doLongFindPath = function (startX, startY)371{372var i, len, c, n, p, lastDir = 8, curDir = -1, addNode;373for (i = 0, len = this.openList.length; i < len; i++)374freeNode(this.openList[i]);375for (i = 0, len = this.closedList.length; i < len; i++)376freeNode(this.closedList[i]);377for (i = 0, len = this.pathList.length; i < len; i++)378freeResultNode(this.pathList[i]);379this.openList.length = 0;380this.closedList.length = 0;381this.closedCache = {};382this.pathList.length = 0;383384// Add the start node to the open list385var startNode = allocNode();386startNode.x = startX;387startNode.y = startY;388389this.openList.push(startNode);390var obsLeft = false, obsTop = false, obsRight = false, obsBottom = false;391var diagonals = this.diagonalsEnabled;392393// While there are nodes on the open list394while (this.openList.length)395{396// Move the best F value to closed list397c = this.openList.shift();398this.closedList.unshift(c);399this.closedCache[((c.x << 16) + c.y).toString()] = true;400401// Are we there yet?402if (c.x === this.targetX && c.y === this.targetY)403{404this.pathEnd.parent = c.parent;405this.pathEnd.x = c.x;406this.pathEnd.y = c.y;407408// Link up the whole path to an indexable array409p = this.pathEnd;410411while (p)412{413// filter redundant nodes in straight lines414if (this.pathList.length === 0)415{416addNode = true;417418if (p.parent)419{420lastDir = this.nodeDirection(p, p.parent);421curDir = lastDir;422}423}424else if (!p.parent)425addNode = false;426else427{428curDir = this.nodeDirection(p, p.parent);429addNode = (curDir !== lastDir);430}431432if (addNode)433{434n = allocResultNode();435n.x = p.x;436n.y = p.y;437this.pathList.unshift(n);438lastDir = curDir;439}440441p = p.parent;442}443444return true;445}446447// Get current node448this.currentNode = c;449var x = c.x;450var y = c.y;451452var obsLeft = (this.at(x - 1, y) === PF_OBSTACLE);453var obsTop = (this.at(x, y - 1) === PF_OBSTACLE);454var obsRight = (this.at(x + 1, y) === PF_OBSTACLE);455var obsBottom = (this.at(x, y + 1) === PF_OBSTACLE);456457// Check adjacent 8 nodes. No diagonals allowed if either cell being crossed is obstacle.458if (!obsLeft)459this.addCellToOpenList(x - 1, y, 10);460461if (diagonals && !obsLeft && !obsTop)462this.addCellToOpenList(x - 1, y - 1, 14);463464if (!obsTop)465this.addCellToOpenList(x, y - 1, 10);466467if (diagonals && !obsTop && !obsRight)468this.addCellToOpenList(x + 1, y - 1, 14);469470if (!obsRight)471this.addCellToOpenList(x + 1, y, 10);472473if (diagonals && !obsRight && !obsBottom)474this.addCellToOpenList(x + 1, y + 1, 14);475476if (!obsBottom)477this.addCellToOpenList(x, y + 1, 10);478479if (diagonals && !obsBottom && !obsLeft)480this.addCellToOpenList(x - 1, y + 1, 14);481}482483return false;484};485486pathfinder.prototype.insertToOpenList = function (c)487{488var i, len;489490// Needs to go at end491if (c.f >= this.openList[this.openList.length - 1].f)492{493this.openList.push(c);494}495else496{497for (i = 0, len = this.openList.length; i < len; i++)498{499if (c.f < this.openList[i].f)500{501this.openList.splice(i, 0, c);502break;503}504}505}506};507508pathfinder.prototype.addCellToOpenList = function (x_, y_, g_)509{510// Ignore if cell on closed list511if (this.closedCache.hasOwnProperty(((x_ << 16) + y_).toString()))512return;513514var i, len, c;515516// Cell costs can be increased by changing the number in the map517var curCellCost = this.at(x_, y_);518519// Is this cell already on the open list?520for (i = 0, len = this.openList.length; i < len; i++)521{522c = this.openList[i];523524if (x_ === c.x && y_ === c.y)525{526// Is this a better path?527if (this.currentNode.g + g_ + curCellCost < c.g)528{529// Update F, G and H and update parent530c.parent = this.currentNode;531c.g = this.currentNode.g + g_ + curCellCost;532c.h = this.estimateH(c.x, c.y);533c.f = c.g + c.h;534535// This node's F has changed: Delete it then re-insert it in the right place536537if (this.openList.length === 1)538{539// no need to remove then re-insert same node, just leave it there540return;541}542543this.openList.splice(i, 1);544this.insertToOpenList(c);545}546547return;548}549}550551// Not on the open list; add it in the right place552c = allocNode();553c.x = x_;554c.y = y_;555c.h = this.estimateH(x_, y_);556c.g = this.currentNode.g + g_ + curCellCost;557c.f = c.h + c.g;558c.parent = this.currentNode;559560// Insert this node sorted in the open list561// The loop below won't add new largest F values562if (!this.openList.length)563{564this.openList.push(c);565return;566}567568this.insertToOpenList(c);569};570571function quickAbs(x)572{573return x < 0 ? -x : x;574};575576pathfinder.prototype.estimateH = function (x_, y_)577{578var dx = quickAbs(x_ - this.targetX);579var dy = quickAbs(y_ - this.targetY);580581return dx * 10 + dy * 10;582};583584pathfinder.prototype.nodeDirection = function (a, b)585{586var ax = a.x;587var ay = a.y;588var bx = b.x;589var by = b.y;590591if (ax === bx)592{593if (by > ay) return 6;594if (by < ay) return 2;595if (ay == by) return 8;596}597else if (ay === by)598{599if (bx > ax) return 4;600if (by < ax) return 0;601}602else603{604if (bx < ax && by < ay) return 1;605if (bx > ax && by < ay) return 3;606if (bx < ax && by > ay) return 7;607if (bx > ax && by > ay) return 5;608}609return 8;610};611612if (!isInWebWorker)613{614window.PF_CLEAR = PF_CLEAR;615window.PF_OBSTACLE = PF_OBSTACLE;616window.Pathfinder = pathfinder;617618window.ResultNode = resultNode;619window.allocResultNode = allocResultNode;620window.freeResultNode = freeResultNode;621}622623})();624625