/** GDevelop - Pathfinding Behavior Extension Copyright (c) 2010-2016 Florian Rival (Florian.Rival@gmail.com) */ /** * PathfindingRuntimeBehavior represents a behavior allowing objects to * follow a path computed to avoid obstacles. * * @class PathfindingRuntimeBehavior * @memberof gdjs */ gdjs.PathfindingRuntimeBehavior = function(runtimeScene, behaviorData, owner) { gdjs.RuntimeBehavior.call(this, runtimeScene, behaviorData, owner); if (this._path === undefined) this._path = []; //The path computed and followed by the object (Array of arrays containing x and y position) else this._path.length = 0; //Behavior configuration: this._allowDiagonals = behaviorData.allowDiagonals; this._acceleration = behaviorData.acceleration; this._maxSpeed = behaviorData.maxSpeed; this._angularMaxSpeed = behaviorData.angularMaxSpeed; this._rotateObject = behaviorData.rotateObject; this._angleOffset = behaviorData.angleOffset; this._cellWidth = behaviorData.cellWidth; this._cellHeight = behaviorData.cellHeight; this._extraBorder = behaviorData.extraBorder; //Attributes used for traveling on the path: this._pathFound = false; this._speed = 0; this._angularSpeed = 0; this._timeOnSegment = 0; this._totalSegmentTime = 0; this._currentSegment = 0; this._reachedEnd = false; this._manager = gdjs.PathfindingObstaclesManager.getManager(runtimeScene); if ( this._searchContext === undefined ) this._searchContext = new gdjs.PathfindingRuntimeBehavior.SearchContext(); }; gdjs.PathfindingRuntimeBehavior.prototype = Object.create( gdjs.RuntimeBehavior.prototype ); gdjs.registerBehavior("PathfindingBehavior::PathfindingBehavior", gdjs.PathfindingRuntimeBehavior); gdjs.PathfindingRuntimeBehavior.prototype.updateFromBehaviorData = function(oldBehaviorData, newBehaviorData) { if (oldBehaviorData.allowDiagonals !== newBehaviorData.allowDiagonals) { this.allowDiagonals(newBehaviorData.allowDiagonals); } if (oldBehaviorData.acceleration !== newBehaviorData.acceleration) { this.setAcceleration(newBehaviorData.acceleration); } if (oldBehaviorData.maxSpeed !== newBehaviorData.maxSpeed) { this.setMaxSpeed(newBehaviorData.maxSpeed); } if (oldBehaviorData.angularMaxSpeed !== newBehaviorData.angularMaxSpeed) { this.setAngularMaxSpeed(newBehaviorData.angularMaxSpeed); } if (oldBehaviorData.rotateObject !== newBehaviorData.rotateObject) { this.setRotateObject(newBehaviorData.rotateObject); } if (oldBehaviorData.angleOffset !== newBehaviorData.angleOffset) { this.setAngleOffset(newBehaviorData.angleOffset); } if (oldBehaviorData.cellWidth !== newBehaviorData.cellWidth) { this.setCellWidth(newBehaviorData.cellWidth); } if (oldBehaviorData.cellHeight !== newBehaviorData.cellHeight) { this.setCellHeight(newBehaviorData.cellHeight); } if (oldBehaviorData.extraBorder !== newBehaviorData.extraBorder) { this.setExtraBorder(newBehaviorData.extraBorder); } return true; } gdjs.PathfindingRuntimeBehavior.prototype.setCellWidth = function(width) { this._cellWidth = width; }; gdjs.PathfindingRuntimeBehavior.prototype.getCellWidth = function() { return this._cellWidth; }; gdjs.PathfindingRuntimeBehavior.prototype.setCellHeight = function(height) { this._cellHeight = height; }; gdjs.PathfindingRuntimeBehavior.prototype.getCellHeight = function() { return this._cellHeight; }; gdjs.PathfindingRuntimeBehavior.prototype.setAcceleration = function(acceleration) { this._acceleration = acceleration; }; gdjs.PathfindingRuntimeBehavior.prototype.getAcceleration = function() { return this._acceleration; }; gdjs.PathfindingRuntimeBehavior.prototype.setMaxSpeed = function(maxSpeed) { this._maxSpeed = maxSpeed; }; gdjs.PathfindingRuntimeBehavior.prototype.getMaxSpeed = function() { return this._maxSpeed; }; gdjs.PathfindingRuntimeBehavior.prototype.setSpeed = function(speed) { this._speed = speed; }; gdjs.PathfindingRuntimeBehavior.prototype.getSpeed = function() { return this._speed; }; gdjs.PathfindingRuntimeBehavior.prototype.setAngularMaxSpeed = function(angularMaxSpeed) { this._angularMaxSpeed = angularMaxSpeed; }; gdjs.PathfindingRuntimeBehavior.prototype.getAngularMaxSpeed = function() { return this._angularMaxSpeed; }; gdjs.PathfindingRuntimeBehavior.prototype.setAngleOffset = function(angleOffset) { this._angleOffset = angleOffset; }; gdjs.PathfindingRuntimeBehavior.prototype.getAngleOffset = function() { return this._angleOffset; }; gdjs.PathfindingRuntimeBehavior.prototype.setExtraBorder = function(extraBorder) { this._extraBorder = extraBorder; }; gdjs.PathfindingRuntimeBehavior.prototype.getExtraBorder = function() { return this._extraBorder; }; gdjs.PathfindingRuntimeBehavior.prototype.allowDiagonals = function(allow) { this._allowDiagonals = allow; }; gdjs.PathfindingRuntimeBehavior.prototype.diagonalsAllowed = function() { return this._allowDiagonals; }; gdjs.PathfindingRuntimeBehavior.prototype.setRotateObject = function(allow) { this._rotateObject = allow; }; gdjs.PathfindingRuntimeBehavior.prototype.isObjectRotated = function() { return this._rotateObject; }; gdjs.PathfindingRuntimeBehavior.prototype.getNodeX = function(index) { if (index this._maxSpeed ) this._speed = this._maxSpeed; this._angularSpeed = this._angularMaxSpeed; //Update the time on the segment and change segment if needed this._timeOnSegment += this._speed*timeDelta; if (this._timeOnSegment >= this._totalSegmentTime && this._currentSegment < this._path.length) this._enterSegment(this._currentSegment + 1); //Position object on the segment and update its angle var newPos = [0, 0]; var pathAngle = this.owner.getAngle(); if ( this._currentSegment < this._path.length-1 ) { newPos[0] = gdjs.evtTools.common.lerp(this._path[this._currentSegment][0], this._path[this._currentSegment + 1][0], this._timeOnSegment / this._totalSegmentTime); newPos[1] = gdjs.evtTools.common.lerp(this._path[this._currentSegment][1], this._path[this._currentSegment + 1][1], this._timeOnSegment / this._totalSegmentTime); pathAngle = gdjs.toDegrees(Math.atan2(this._path[this._currentSegment+1][1] - this._path[this._currentSegment][1], this._path[this._currentSegment+1][0] - this._path[this._currentSegment][0]))+this._angleOffset; } else newPos = this._path[this._path.length-1]; this.owner.setX(newPos[0]); this.owner.setY(newPos[1]); if ( this._rotateObject ) { this.owner.rotateTowardAngle(pathAngle, this._angularSpeed, runtimeScene); } }; gdjs.PathfindingRuntimeBehavior.prototype.doStepPostEvents = function(runtimeScene) { //Scene change is not supported /* if ( parentScene != &scene ) //Parent scene has changed { parentScene = &scene; sceneManager = parentScene ? &ScenePathfindingObstaclesManager::managers[&scene] : null; floorPlatform = null; } */ }; /** * Internal tool class representing a node when looking for a path * @private * @memberof gdjs.PathfindingRuntimeBehavior * @class Node */ gdjs.PathfindingRuntimeBehavior.Node = function(xPos, yPos) { if ( this.pos === undefined) this.pos = [xPos, yPos]; else { this.pos[0] = xPos; this.pos[1] = yPos; } this.cost = 0; this.smallestCost = -1; this.estimateCost = -1; this.parent = null; this.open = true; }; /** * Compute the euclidean distance between two positions. * @private * @memberof gdjs.PathfindingRuntimeBehavior */ gdjs.PathfindingRuntimeBehavior.euclideanDistance = function(a, b) { return Math.sqrt((a[0]-b[0])*(a[0]-b[0])+(a[1]-b[1])*(a[1]-b[1])); }; /** * Compute the taxi distance between two positions. * @private * @memberof gdjs.PathfindingRuntimeBehavior */ gdjs.PathfindingRuntimeBehavior.manhattanDistance = function(a, b) { return Math.abs(a[0]-b[0])+Math.abs(a[1]-b[1]); }; /** * Internal tool class containing the structures used by A* and members functions related * to them. */ gdjs.PathfindingRuntimeBehavior.SearchContext = function() { this._obstacles = null; this._finalNode = null; this._destination = [0,0]; this._start = [0,0]; this._startX = 0; this._startY = 0; this._allowDiagonals = true; this._maxComplexityFactor = 50; this._cellWidth = 20; this._cellHeight = 20; this._leftBorder = 0; this._rightBorder = 0; this._topBorder = 0; this._bottomBorder = 0; this._distanceFunction = gdjs.PathfindingRuntimeBehavior.euclideanDistance; this._allNodes = []; //An array of array. Nodes are indexed by their x position, and then by their y position. this._openNodes = []; //An array of nodes sorted by their estimate cost (First node = Lower estimate cost). this._closeObstacles = []; //Used by getNodes to temporarily store obstacles near a position. this._nodeCache = []; //Old nodes constructed in a previous search are stored here to avoid temporary objects (see _freeAllNodes method). }; gdjs.PathfindingRuntimeBehavior.SearchContext.prototype.setObstacles = function(obstacles) { this._obstacles = obstacles; return this; }; gdjs.PathfindingRuntimeBehavior.SearchContext.prototype.getFinalNode = function() { return this._finalNode; }; gdjs.PathfindingRuntimeBehavior.SearchContext.prototype.allowDiagonals = function(allowDiagonals) { this._allowDiagonals = allowDiagonals; this._distanceFunction = allowDiagonals ? gdjs.PathfindingRuntimeBehavior.euclideanDistance : gdjs.PathfindingRuntimeBehavior.manhattanDistance; return this; }; gdjs.PathfindingRuntimeBehavior.SearchContext.prototype.setStartPosition = function(x, y) { this._startX = x; this._startY = y; return this; }; gdjs.PathfindingRuntimeBehavior.SearchContext.prototype.setObjectSize = function(leftBorder, topBorder, rightBorder, bottomBorder) { this._leftBorder = leftBorder; this._rightBorder = rightBorder; this._topBorder = topBorder; this._bottomBorder = bottomBorder; return this; }; gdjs.PathfindingRuntimeBehavior.SearchContext.prototype.setCellSize = function(cellWidth, cellHeight) { this._cellWidth = cellWidth; this._cellHeight = cellHeight; return this; }; gdjs.PathfindingRuntimeBehavior.SearchContext.prototype.computePathTo = function(targetX, targetY) { if (this._obstacles === null) { console.log("You tried to compute a path without specifying the obstacles"); return; } this._destination[0] = Math.round(targetX/this._cellWidth); this._destination[1] = Math.round(targetY/this._cellHeight); this._start[0] = Math.round(this._startX/this._cellWidth); this._start[1] = Math.round(this._startY/this._cellHeight); //Initialize the algorithm this._freeAllNodes(); var startNode = this._getNode(this._start[0], this._start[1]); startNode.smallestCost = 0; startNode.estimateCost = 0 + this._distanceFunction(this._start, this._destination); this._openNodes.length = 0; this._openNodes.push(startNode); //A* algorithm main loop var iterationCount = 0; var maxIterationCount = startNode.estimateCost*this._maxComplexityFactor; while (this._openNodes.length !== 0) { if (iterationCount++ > maxIterationCount) return false; //Make sure we do not search forever. var n = this._openNodes.shift(); //Get the most promising node... n.open = false; //...and flag it as explored //Check if we reached destination? if ( n.pos[0] == this._destination[0] && n.pos[1] == this._destination[1] ) { this._finalNode = n; return true; } //No, so add neighbors to the nodes to explore. this._insertNeighbors(n); } return false; }; gdjs.PathfindingRuntimeBehavior.SearchContext.prototype._freeAllNodes = function() { if ( this._nodeCache.length <= 32000 ) { for( var i in this._allNodes ) { if ( this._allNodes.hasOwnProperty(i) ) { var nodeArray = this._allNodes[i]; for( var j in nodeArray ) { if ( nodeArray.hasOwnProperty(j) ) { this._nodeCache.push(nodeArray[j]); } } } } } this._allNodes = {}; }; /** * Insert the neighbors of the current node in the open list * (Only if they are not closed, and if the cost is better than the already existing smallest cost). */ gdjs.PathfindingRuntimeBehavior.SearchContext.prototype._insertNeighbors = function(currentNode) { this._addOrUpdateNode(currentNode.pos[0]+1, currentNode.pos[1], currentNode, 1); this._addOrUpdateNode(currentNode.pos[0]-1, currentNode.pos[1], currentNode, 1); this._addOrUpdateNode(currentNode.pos[0], currentNode.pos[1]+1, currentNode, 1); this._addOrUpdateNode(currentNode.pos[0], currentNode.pos[1]-1, currentNode, 1); if ( this._allowDiagonals ) { this._addOrUpdateNode(currentNode.pos[0]+1, currentNode.pos[1]+1, currentNode, 1.414213562); this._addOrUpdateNode(currentNode.pos[0]+1, currentNode.pos[1]-1, currentNode, 1.414213562); this._addOrUpdateNode(currentNode.pos[0]-1, currentNode.pos[1]-1, currentNode, 1.414213562); this._addOrUpdateNode(currentNode.pos[0]-1, currentNode.pos[1]+1, currentNode, 1.414213562); } }; /** * Get (or dynamically construct) a node. * * *All* nodes should be created using this method: The cost of the node is computed thanks * to the objects flagged as obstacles. */ gdjs.PathfindingRuntimeBehavior.SearchContext.prototype._getNode = function(xPos, yPos) { //First check if their is a node a the specified position. if (this._allNodes.hasOwnProperty(xPos)) { if ( this._allNodes[xPos].hasOwnProperty(yPos) ) return this._allNodes[xPos][yPos]; } else this._allNodes[xPos] = []; //No so construct a new node (or get it from the cache)... var newNode = null; if ( this._nodeCache.length !== 0) { newNode = this._nodeCache.shift(); gdjs.PathfindingRuntimeBehavior.Node.call(newNode, xPos, yPos); } else newNode = new gdjs.PathfindingRuntimeBehavior.Node(xPos, yPos); //...and update its cost according to obstacles var objectsOnCell = false; var radius = this._cellHeight > this._cellWidth ? this._cellHeight*2 : this._cellWidth*2; this._obstacles.getAllObstaclesAround(xPos*this._cellWidth, yPos*this._cellHeight, radius, this._closeObstacles); for(var k = 0; k < this._closeObstacles.length; ++k ) { var obj = this._closeObstacles[k].owner; var topLeftCellX = Math.floor((obj.getDrawableX()-this._rightBorder)/this._cellWidth); var topLeftCellY = Math.floor((obj.getDrawableY()-this._bottomBorder)/this._cellHeight); var bottomRightCellX = Math.ceil((obj.getDrawableX()+obj.getWidth()+this._leftBorder)/this._cellWidth); var bottomRightCellY = Math.ceil((obj.getDrawableY()+obj.getHeight()+this._topBorder)/this._cellHeight); if ( topLeftCellX < xPos && xPos < bottomRightCellX && topLeftCellY < yPos && yPos < bottomRightCellY) { objectsOnCell = true; if ( this._closeObstacles[k].isImpassable() ) { newNode.cost = -1; break; //The cell is impassable, stop here. } else //Superimpose obstacles newNode.cost += this._closeObstacles[k].getCost(); } } if (!objectsOnCell) newNode.cost = 1; //Default cost when no objects put on the cell. this._allNodes[xPos][yPos] = newNode; return newNode; }; /** * Add a node to the openNodes (only if the cost to reach it is less than the existing cost, if any). */ gdjs.PathfindingRuntimeBehavior.SearchContext.prototype._addOrUpdateNode = function(newNodeX, newNodeY, currentNode, factor) { var neighbor = this._getNode(newNodeX, newNodeY); if (!neighbor.open || neighbor.cost < 0 ) //cost < 0 means impassable obstacle return; //Update the node costs and parent if the path coming from currentNode is better: if (neighbor.smallestCost === -1 || neighbor.smallestCost > currentNode.smallestCost + (currentNode.cost+neighbor.cost)/2.0*factor) { if (neighbor.smallestCost != -1) { //The node is already in the open list.. for (var i =0;i= neighbor.estimateCost) { this._openNodes.splice(i, 0, neighbor); break; } } } } };