/** GDevelop - Pathfinding Behavior Extension Copyright (c) 2010-2016 Florian Rival (Florian.Rival@gmail.com) This project is released under the MIT License. */ #include "PathfindingBehavior.h" #include #include #include #include #include #include #include "GDCore/Tools/Localization.h" #include "GDCpp/Extensions/Builtin/MathematicalTools.h" #include "GDCpp/Runtime/CommonTools.h" #include "GDCpp/Runtime/Project/Layout.h" #include "GDCpp/Runtime/RuntimeObject.h" #include "GDCpp/Runtime/RuntimeScene.h" #include "GDCpp/Runtime/Serialization/SerializerElement.h" #include "PathfindingObstacleBehavior.h" #include "ScenePathfindingObstaclesManager.h" #if defined(GD_IDE_ONLY) #include #include "GDCore/IDE/Dialogs/PropertyDescriptor.h" #endif /** * \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; }; std::ostream& operator<<(std::ostream& stream, const NodePosition& nodePos) { stream << nodePos.x << ";" << nodePos.y; return stream; } bool operator==(const NodePosition& a, const NodePosition& b) { return ((a.x == b.x) && (a.y == b.y)); } namespace std { /** * \brief Tool function used to store a NodePosition as key in * std::unordered_set. */ template <> struct hash { std::size_t operator()(NodePosition const& n) const { return (std::hash()(n.x)) ^ (std::hash()(n.y) << 1); } }; } // namespace std namespace { /** * \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 std::size_t iterationCount = 0; std::size_t 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; } 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) / (float)cellWidth); int topLeftCellY = floor((obj->GetDrawableY() - bottomBorder) / (float)cellHeight); int bottomRightCellX = ceil((obj->GetDrawableX() + obj->GetWidth() + leftBorder) / (float)cellWidth); int bottomRightCellY = ceil((obj->GetDrawableY() + obj->GetHeight() + topBorder) / (float)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: { // remove it as its estimate cost will be updated. auto it = openNodes.find(&neighbor); if (it != openNodes.end()) // /!\ ALWAYS use an iterator with multiset::erase openNodes.erase(it); // otherwise, other nodes which are equivalent // get removed too. } 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); } } std::unordered_map 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. std::size_t maxComplexityFactor; float cellWidth; float cellHeight; float leftBorder; float rightBorder; float topBorder; float bottomBorder; static const float sqrt2; }; const float SearchContext::sqrt2 = 1.414213562; } // namespace PathfindingBehavior::PathfindingBehavior() : parentScene(NULL), sceneManager(NULL), 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 PathfindingBehavior::MoveTo(RuntimeScene& scene, float x, float y) { if (parentScene != &scene) // Parent scene has changed { parentScene = &scene; sceneManager = parentScene ? &ScenePathfindingObstaclesManager::managers[&scene] : NULL; } 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 PathfindingBehavior::EnterSegment(std::size_t 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 PathfindingBehavior::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(object->GetElapsedTime(scene)) / 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 PathfindingBehavior::DoStepPostEvents(RuntimeScene& scene) { if (parentScene != &scene) // Parent scene has changed { parentScene = &scene; sceneManager = parentScene ? &ScenePathfindingObstaclesManager::managers[&scene] : NULL; } } float PathfindingBehavior::GetNodeX(std::size_t index) const { if (index < path.size()) return path[index].x; return 0; } float PathfindingBehavior::GetNodeY(std::size_t index) const { if (index < path.size()) return path[index].y; return 0; } std::size_t PathfindingBehavior::GetNextNodeIndex() const { if (currentSegment + 1 < path.size()) return currentSegment + 1; else return path.size() - 1; } float PathfindingBehavior::GetNextNodeX() const { if (path.empty()) return 0; if (currentSegment + 1 < path.size()) return path[currentSegment + 1].x; else return path.back().x; } float PathfindingBehavior::GetNextNodeY() const { if (path.empty()) return 0; if (currentSegment + 1 < path.size()) return path[currentSegment + 1].y; else return path.back().y; } float PathfindingBehavior::GetLastNodeX() const { if (path.size() < 2) return 0; if (currentSegment < path.size() - 1) return path[currentSegment].x; else return path[path.size() - 1].x; } float PathfindingBehavior::GetLastNodeY() const { if (path.size() < 2) return 0; if (currentSegment < path.size() - 1) return path[currentSegment].y; else return path[path.size() - 1].y; } float PathfindingBehavior::GetDestinationX() const { if (path.empty()) return 0; return path.back().x; } float PathfindingBehavior::GetDestinationY() const { if (path.empty()) return 0; return path.back().y; } void PathfindingBehavior::UnserializeFrom( const gd::SerializerElement& element) { allowDiagonals = element.GetBoolAttribute("allowDiagonals"); acceleration = element.GetDoubleAttribute("acceleration"); maxSpeed = element.GetDoubleAttribute("maxSpeed"); angularMaxSpeed = element.GetDoubleAttribute("angularMaxSpeed"); rotateObject = element.GetBoolAttribute("rotateObject"); angleOffset = element.GetDoubleAttribute("angleOffset"); extraBorder = element.GetDoubleAttribute("extraBorder"); { int value = element.GetIntAttribute("cellWidth", 0); if (value > 0) cellWidth = value; } { int value = element.GetIntAttribute("cellHeight", 0); if (value > 0) cellHeight = value; } } #if defined(GD_IDE_ONLY) void PathfindingBehavior::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 PathfindingBehavior::GetProperties( gd::Project& project) const { std::map properties; properties[_("Allows diagonals")] .SetValue(allowDiagonals ? "true" : "false") .SetType("Boolean"); properties[_("Acceleration")].SetValue(gd::String::From(acceleration)); properties[_("Max. speed")].SetValue(gd::String::From(maxSpeed)); properties[_("Rotate speed")].SetValue(gd::String::From(angularMaxSpeed)); properties[_("Rotate object")] .SetValue(rotateObject ? "true" : "false") .SetType("Boolean"); properties[_("Angle offset")].SetValue(gd::String::From(angleOffset)); properties[_("Virtual cell width")].SetValue(gd::String::From(cellWidth)); properties[_("Virtual cell height")].SetValue(gd::String::From(cellHeight)); properties[_("Extra border size")].SetValue(gd::String::From(extraBorder)); return properties; } bool PathfindingBehavior::UpdateProperty(const gd::String& name, const gd::String& value, gd::Project& project) { if (name == _("Allows diagonals")) { allowDiagonals = (value != "0"); return true; } if (name == _("Rotate object")) { rotateObject = (value != "0"); return true; } if (name == _("Extra border size")) { extraBorder = value.To(); return true; } if (value.To() < 0) return false; if (name == _("Acceleration")) acceleration = value.To(); else if (name == _("Max. speed")) maxSpeed = value.To(); else if (name == _("Rotate speed")) angularMaxSpeed = value.To(); else if (name == _("Angle offset")) angleOffset = value.To(); else if (name == _("Virtual cell width")) cellWidth = value.To(); else if (name == _("Virtual cell height")) cellHeight = value.To(); else return false; return true; } #endif