/** Game Develop - Pathfinding Automatism Extension Copyright (c) 2010-2014 Florian Rival (Florian.Rival@gmail.com) This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. */ #include #include #include #include #include "PathfindingAutomatism.h" #include "PathfindingObstacleAutomatism.h" #include "ScenePathfindingObstaclesManager.h" #include "GDCpp/BuiltinExtensions/MathematicalTools.h" #include "GDCpp/Scene.h" #include "GDCpp/Serialization/SerializerElement.h" #include "GDCpp/XmlMacros.h" #include "GDCpp/RuntimeScene.h" #include "GDCpp/RuntimeObject.h" #include "GDCpp/CommonTools.h" #include "GDCpp/XmlMacros.h" #include #include #include #if defined(GD_IDE_ONLY) #include #include "GDCore/Tools/Localization.h" #include "GDCore/IDE/Dialogs/PropertyDescriptor.h" #endif namespace { /** * \brief Internal tool class representing the position of a node when looking for a path. */ class NodePosition { public: NodePosition(int x_, int y_) : x(x_), y(y_) {}; int x; int y; /** * \brief Tool function used to store a NodePosition as key in boost::unordered_set. */ struct NodePositionHash : std::unary_function { std::size_t operator()(NodePosition const & n) const { std::size_t seed = 0; boost::hash_combine(seed, n.x); boost::hash_combine(seed, n.y); return seed; } }; }; std::ostream& operator<<(std::ostream& stream, const NodePosition & nodePos) { stream << nodePos.x << ";" << nodePos.y; return stream; } /** * \brief Internal tool class representing a node when looking for a path */ class Node { public: Node() : pos(0, 0), cost(0), smallestCost(-1), estimateCost(-1), parent(NULL), open(true) {}; Node(int x, int y) : pos(x, y), cost(0), smallestCost(-1), estimateCost(-1), parent(NULL), open(true) {}; Node(const NodePosition & pos_) : pos(pos_), cost(0), smallestCost(-1), estimateCost(-1), parent(NULL), open(true) {}; NodePosition pos; float cost; ///< The cost for traveling on this node float smallestCost; ///< the cost to go to this node (when considering the shortest path). float estimateCost; ///< the estimate cost total to go to the destination through this node (when considering the shortest path). const Node * parent; ///< The previous node to be visited to go to this node (when considering the shortest path). bool open; ///< true if the node is "open" (must be explored), false if "close" (already explored) /** * \brief Tool function used to store a Node in a priority_queue. */ class NodeComparator { public: bool operator()(const Node * n1, const Node * n2) { return n1->estimateCost < n2->estimateCost; } }; }; bool operator==(Node const& n1, Node const& n2) { return n1.pos.x == n2.pos.x && n1.pos.y == n2.pos.y; }; typedef float (*DistanceFunPtr)(const NodePosition & , const NodePosition & ); /** * \brief Internal tool class containing the structures used by A* and members functions related * to them. */ class SearchContext { public: SearchContext(ScenePathfindingObstaclesManager & obstacles_, bool allowsDiagonal_ = true) : obstacles(obstacles_), finalNode(NULL), destination(0, 0), startX(0), startY(0), allowsDiagonal(allowsDiagonal_), maxComplexityFactor(50), cellWidth(20), cellHeight(20), leftBorder(0), rightBorder(0), topBorder(0), bottomBorder(0) { distanceFunction = allowsDiagonal ? &SearchContext::EuclideanDistance : &SearchContext::ManhattanDistance; } /** * \brief Set the start position. * \param x The coordinate on X axis of the start position, in "world" coordinates. * \param y The coordinate on Y axis of the start position, in "world" coordinates. */ SearchContext & SetStartPosition(float x, float y) { startX = x; startY = y; return *this; } /** * \brief Set the size to be considered for the object for which the path will be planned. */ SearchContext & SetObjectSize(float leftBorder_, float topBorder_, float rightBorder_, float bottomBorder_) { leftBorder = leftBorder_; rightBorder = rightBorder_; topBorder = topBorder_; bottomBorder = bottomBorder_; return *this; } /** * \brief Change the size of a virtual cell, in pixels. */ SearchContext & SetCellSize(unsigned int cellWidth_, unsigned int cellHeight_) { cellWidth = cellWidth_; cellHeight = cellHeight_; return *this; } /** * \brief Compute a path to the specified position, considering the obstacles * and the start position passed in the constructor. * \return true if computation found a path, in which case you can call GetFinalNode method * to construct the path. * \param x The coordinate on X axis of the target position, in "world" coordinates. * \param y The coordinate on Y axis of the target position, in "world" coordinates. */ bool ComputePathTo(float targetX, float targetY) { destination = NodePosition(GDRound(targetX/cellWidth), GDRound(targetY/cellHeight)); NodePosition start(GDRound(startX/cellWidth), GDRound(startY/cellHeight)); //Initialize the algorithm allNodes.clear(); Node & startNode = GetNode(start); startNode.smallestCost = 0; startNode.estimateCost = 0 + distanceFunction(start, destination); openNodes.clear(); openNodes.insert(&startNode); //A* algorithm main loop unsigned int iterationCount = 0; unsigned int maxIterationCount = startNode.estimateCost*maxComplexityFactor; while (!openNodes.empty()) { if (iterationCount++ > maxIterationCount) return false; //Make sure we do not search forever. Node * n = *openNodes.begin(); //Get the most promising node... n->open = false; //...and flag it as explored openNodes.erase(openNodes.begin()); //Be sure to remove ONLY the first element! //Check if we reached destination? if ( n->pos.x == destination.x && n->pos.y == destination.y ) { finalNode = n; return true; } //No, so add neighbors to the nodes to explore. InsertNeighbors(*n); } return false; } /** * @return The final node of the computed path. * Iterate on the parent member to create the path. Beware, the coordinates of the node * must be multiplied by the cell size to get the "world" coordinates of the path. */ Node * GetFinalNode() const { return finalNode; } #if 0 static void UnitTest() { { ScenePathfindingObstaclesManager obstacles; SearchContext ctx(obstacles, 0, 0); ctx.ComputePathTo(0,0); } std::cout << "---" << std::endl; { ScenePathfindingObstaclesManager obstacles; SearchContext ctx(obstacles, 0, 0); ctx.ComputePathTo(1,0); } std::cout << "---" << std::endl; { ScenePathfindingObstaclesManager obstacles; SearchContext ctx(obstacles, 0, 0); ctx.ComputePathTo(0,1); } std::cout << "---" << std::endl; { ScenePathfindingObstaclesManager obstacles; SearchContext ctx(obstacles, 0, 0); ctx.ComputePathTo(1,1); } std::cout << "---" << std::endl; { ScenePathfindingObstaclesManager obstacles; SearchContext ctx(obstacles, 3, 4); ctx.ComputePathTo(12,9); } std::cout << "---" << std::endl; std::cout << "End unit tests"; } #endif private: /** * 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). */ void InsertNeighbors(const Node & currentNode) { AddOrUpdateNode(NodePosition(currentNode.pos.x+1, currentNode.pos.y), currentNode, 1); AddOrUpdateNode(NodePosition(currentNode.pos.x-1, currentNode.pos.y), currentNode, 1); AddOrUpdateNode(NodePosition(currentNode.pos.x, currentNode.pos.y+1), currentNode, 1); AddOrUpdateNode(NodePosition(currentNode.pos.x, currentNode.pos.y-1), currentNode, 1); if ( allowsDiagonal ) { AddOrUpdateNode(NodePosition(currentNode.pos.x+1, currentNode.pos.y+1), currentNode, sqrt2); AddOrUpdateNode(NodePosition(currentNode.pos.x+1, currentNode.pos.y-1), currentNode, sqrt2); AddOrUpdateNode(NodePosition(currentNode.pos.x-1, currentNode.pos.y-1), currentNode, sqrt2); AddOrUpdateNode(NodePosition(currentNode.pos.x-1, currentNode.pos.y+1), currentNode, sqrt2); } } /** * \brief 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. */ Node & GetNode(const NodePosition & pos) { if (allNodes.find(pos) != allNodes.end()) return allNodes.find(pos)->second; Node newNode(pos); bool objectsOnCell = false; const std::set & allObstacles = obstacles.GetAllObstacles(); for (std::set::const_iterator it = allObstacles.begin(); it != allObstacles.end(); ++it) { RuntimeObject * obj = (*it)->GetObject(); int topLeftCellX = floor((obj->GetDrawableX()-rightBorder)/cellWidth); int topLeftCellY = floor((obj->GetDrawableY()-bottomBorder)/cellHeight); int bottomRightCellX = ceil((obj->GetDrawableX()+obj->GetWidth()+leftBorder)/cellWidth); int bottomRightCellY = ceil((obj->GetDrawableY()+obj->GetHeight()+topBorder)/cellHeight); if ( topLeftCellX <= pos.x && pos.x <= bottomRightCellX && topLeftCellY <= pos.y && pos.y <= bottomRightCellY) { objectsOnCell = true; if ( (*it)->IsImpassable() ) { newNode.cost = -1; break; //The cell is impassable, stop here. } else //Superimpose obstacles newNode.cost += (*it)->GetCost(); } } if (!objectsOnCell) newNode.cost = 1; //Default cost when no objects put on the cell. allNodes[pos] = newNode; return allNodes[pos]; } /** * Compute the euclidean distance between two positions. */ static float EuclideanDistance(const NodePosition & a, const NodePosition & b) { return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)); } /** * Compute the taxi distance between two positions. */ static float ManhattanDistance(const NodePosition & a, const NodePosition & b) { return abs(a.x-b.x)+abs(a.y-b.y); } /** * Add a node to the openNodes (only if the cost to reach it is less than the existing cost, if any). */ void AddOrUpdateNode(const NodePosition & newNodePosition, const Node & currentNode, float factor) { Node & neighbor = GetNode(newNodePosition); 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: openNodes.erase(&neighbor); //remove it as its estimate cost will be updated. neighbor.smallestCost = currentNode.smallestCost + (currentNode.cost+neighbor.cost)/2.0*factor; neighbor.parent = ¤tNode; neighbor.estimateCost = neighbor.smallestCost + distanceFunction(neighbor.pos, destination); openNodes.insert(&neighbor); } } boost::unordered_map< NodePosition, Node, NodePosition::NodePositionHash > allNodes; ///< All the nodes std::multiset openNodes; ///< Only the open nodes (Such that Node::open == true) const ScenePathfindingObstaclesManager & obstacles; ///< A reference to all the obstacles of the scene Node * finalNode; //If computation succeeded, the final node is stored here. NodePosition destination; int startX; ///< The start X position, in "world" coordinates (not in "node" coordinates!). int startY; ///< The start Y position, in "world" coordinates (not in "node" coordinates!). DistanceFunPtr distanceFunction; bool allowsDiagonal; ///< True to allow diagonals when planning the path. unsigned int maxComplexityFactor; float cellWidth; float cellHeight; float leftBorder; float rightBorder; float topBorder; float bottomBorder; static const float sqrt2; }; const float SearchContext::sqrt2 = 1.414213562; } PathfindingAutomatism::PathfindingAutomatism() : pathFound(false), allowDiagonals(true), acceleration(400), maxSpeed(200), angularMaxSpeed(180), rotateObject(true), angleOffset(0), cellWidth(20), cellHeight(20), extraBorder(0), speed(0), angularSpeed(0), timeOnSegment(0), totalSegmentTime(0), currentSegment(0), reachedEnd(false) { } void PathfindingAutomatism::MoveTo(RuntimeScene & scene, float x, float y) { if ( !sceneManager ) return; path.clear(); //First be sure that there is a path to compute. int targetCellX = GDRound(x/(float)cellWidth); int targetCellY = GDRound(y/(float)cellHeight); int startCellX = GDRound(object->GetX()/(float)cellWidth); int startCellY = GDRound(object->GetY()/(float)cellHeight); if ( startCellX == targetCellX && startCellY == targetCellY ) { path.push_back(sf::Vector2f(object->GetX(), object->GetY())); path.push_back(sf::Vector2f(x, y)); EnterSegment(0); pathFound = true; return; } //Start searching for a path //TODO: Customizable heuristic. ::SearchContext ctx(*sceneManager, allowDiagonals); ctx.SetCellSize(cellWidth, cellHeight).SetStartPosition(object->GetX(), object->GetY()); ctx.SetObjectSize(object->GetX()-object->GetDrawableX()+extraBorder, object->GetY()-object->GetDrawableY()+extraBorder, object->GetWidth()-(object->GetX()-object->GetDrawableX())+extraBorder, object->GetHeight()-(object->GetY()-object->GetDrawableY())+extraBorder); if (ctx.ComputePathTo(x, y)) { //Path found: memorize it const ::Node * node = ctx.GetFinalNode(); while (node) { path.push_back(sf::Vector2f(node->pos.x*(float)cellWidth, node->pos.y*(float)cellHeight)); node = node->parent; } std::reverse(path.begin(), path.end()); path[0] = sf::Vector2f(object->GetX(), object->GetY()); EnterSegment(0); pathFound = true; return; } //Not path found pathFound = false; } void PathfindingAutomatism::EnterSegment(unsigned int segmentNumber) { if ( path.empty() ) return; currentSegment = segmentNumber; if (currentSegment < path.size()-1) { sf::Vector2f newPath = (path[currentSegment + 1] - path[currentSegment]); totalSegmentTime = sqrtf(newPath.x*newPath.x+newPath.y*newPath.y); timeOnSegment = 0; reachedEnd = false; } else { reachedEnd = true; speed = 0; } } void PathfindingAutomatism::DoStepPreEvents(RuntimeScene & scene) { if ( parentScene != &scene ) //Parent scene has changed { parentScene = &scene; sceneManager = parentScene ? &ScenePathfindingObstaclesManager::managers[&scene] : NULL; } if ( !sceneManager ) return; if (path.empty() || reachedEnd) return; //Update the speed of the object float timeDelta = static_cast(scene.GetElapsedTime())/1000000.0; speed += acceleration*timeDelta; if ( speed > maxSpeed ) speed = maxSpeed; angularSpeed = angularMaxSpeed; //No acceleration for angular speed for now //Update the time on the segment and change segment if needed timeOnSegment += speed*timeDelta; if (timeOnSegment >= totalSegmentTime && currentSegment < path.size()) EnterSegment(currentSegment + 1); //Position object on the segment and update its angle sf::Vector2f newPos; float pathAngle = object->GetAngle(); if ( currentSegment < path.size()-1 ) { newPos = path[currentSegment] + (path[currentSegment + 1] - path[currentSegment]) * (timeOnSegment / totalSegmentTime); pathAngle = atan2(path[currentSegment+1].y - path[currentSegment].y, path[currentSegment+1].x - path[currentSegment].x)*180/3.14159+angleOffset; } else newPos = path.back(); object->SetX(newPos.x); object->SetY(newPos.y); //Also update angle if needed if ( rotateObject ) object->RotateTowardAngle(pathAngle, angularSpeed, scene); } void PathfindingAutomatism::DoStepPostEvents(RuntimeScene & scene) { if ( parentScene != &scene ) //Parent scene has changed { parentScene = &scene; sceneManager = parentScene ? &ScenePathfindingObstaclesManager::managers[&scene] : NULL; } } float PathfindingAutomatism::GetNodeX(unsigned int index) const { if (index 0) cellWidth = value; } { int value = element.GetIntAttribute("cellHeight", 0); if (value > 0) cellHeight = value; } } #if defined(GD_IDE_ONLY) void PathfindingAutomatism::SerializeTo(gd::SerializerElement & element) const { element.SetAttribute("allowDiagonals", allowDiagonals); element.SetAttribute("acceleration", acceleration); element.SetAttribute("maxSpeed", maxSpeed); element.SetAttribute("angularMaxSpeed", angularMaxSpeed); element.SetAttribute("rotateObject", rotateObject); element.SetAttribute("angleOffset", angleOffset); element.SetAttribute("cellWidth", (int)cellWidth); element.SetAttribute("cellHeight", (int)cellHeight); element.SetAttribute("extraBorder", extraBorder); } std::map PathfindingAutomatism::GetProperties(gd::Project & project) const { std::map properties; properties[ToString(_("Allows diagonals"))].SetValue(allowDiagonals ? "true" : "false").SetType("Boolean"); properties[ToString(_("Acceleration"))].SetValue(ToString(acceleration)); properties[ToString(_("Max. speed"))].SetValue(ToString(maxSpeed)); properties[ToString(_("Rotate speed"))].SetValue(ToString(angularMaxSpeed)); properties[ToString(_("Rotate object"))].SetValue(rotateObject ? "true" : "false").SetType("Boolean"); properties[ToString(_("Angle offset"))].SetValue(ToString(angleOffset)); properties[ToString(_("Virtual cell width"))].SetValue(ToString(cellWidth)); properties[ToString(_("Virtual cell height"))].SetValue(ToString(cellHeight)); properties[ToString(_("Extra border size"))].SetValue(ToString(extraBorder)); return properties; } bool PathfindingAutomatism::UpdateProperty(const std::string & name, const std::string & value, gd::Project & project) { if ( name == ToString(_("Allows diagonals")) ) { allowDiagonals = (value != "0"); return true; } if ( name == ToString(_("Rotate object")) ) { rotateObject = (value != "0"); return true; } if ( name == ToString(_("Extra border size")) ) { extraBorder = ToDouble(value); return true; } if ( ToDouble(value) < 0 ) return false; if ( name == ToString(_("Acceleration")) ) acceleration = ToDouble(value); else if ( name == ToString(_("Max. speed")) ) maxSpeed = ToDouble(value); else if ( name == ToString(_("Rotate speed")) ) angularMaxSpeed = ToDouble(value); else if ( name == ToString(_("Angle offset")) ) angleOffset = ToDouble(value); else if ( name == ToString(_("Virtual cell width")) ) cellWidth = ToInt(value); else if ( name == ToString(_("Virtual cell height")) ) cellHeight = ToInt(value); else return false; return true; } #endif