/* ------------------- Code Monkey ------------------- Thank you for downloading this package I hope you find it useful in your projects If you have any questions let me know Cheers! unitycodemonkey.com -------------------------------------------------- */ using UnityEngine; using System.Collections; using System.Collections.Generic; using System; namespace GridPathfindingSystem { public class GridPathfinding { public static GridPathfinding instance; public const int WALL_WEIGHT = 56000; public enum UnitMovementCallbackType { Simple, } //private List openList; private BinaryTree binaryTree; private int openListCount; private PathNode[][] mapNodes; private int widthMax, heightMax; private float nodeSize; private Vector3 worldOrigin; private bool foundTarget; private int movementCost = 10; private float timer; public delegate void OnPathCallback(List path, MapPos finalPos); public delegate void OnVoidDelegate(); public OnVoidDelegate callbacks; public GridPathfinding(Vector3 worldLowerLeft, Vector3 worldUpperRight, float nodeSize) { instance = this; worldOrigin = worldLowerLeft; this.nodeSize = nodeSize; float worldWidth = worldUpperRight.x - worldLowerLeft.x; float worldHeight = worldUpperRight.y - worldLowerLeft.y; int mapWidth = Mathf.RoundToInt(worldWidth / nodeSize); int mapHeight = Mathf.RoundToInt(worldHeight / nodeSize); mapNodes = new PathNode[mapWidth][]; for (int i = 0; i < mapWidth; i++) { mapNodes[i] = new PathNode[mapHeight]; } widthMax = mapWidth; heightMax = mapHeight; Initialize(mapWidth, mapHeight); } public GridPathfinding(int mapWidth, int mapHeight, float nodeSize, Vector3 worldOrigin) {//, Texture2D map) { this.nodeSize = nodeSize; this.worldOrigin = worldOrigin; mapNodes = new PathNode[mapWidth][]; for (int i = 0; i < mapWidth; i++) { mapNodes[i] = new PathNode[mapHeight]; } widthMax = mapWidth; heightMax = mapHeight; Initialize(mapWidth, mapHeight); } public void RaycastWalkable() { for (int i = 0; i < widthMax; i++) { for (int j = 0; j < heightMax; j++) { Vector3 nodeWorldPosition = mapNodes[i][j].GetWorldVector(worldOrigin, nodeSize); RaycastHit2D raycastHit = Physics2D.Raycast(nodeWorldPosition, Vector2.zero, 0f); if (raycastHit.collider != null) { mapNodes[i][j].SetWalkable(false); } } } } public void ModifySize(int modifyX, int modifyY, int newPathNodeWeight) { if (modifyX == 0 && modifyY == 0) { return; } int newWidth = widthMax + modifyX; int newHeight = heightMax + modifyY; PathNode[][] newMapNodes = new PathNode[newWidth][]; for (int i = 0; i < newWidth; i++) { newMapNodes[i] = new PathNode[newHeight]; for (int j = 0; j < newHeight; j++) { if (i < mapNodes.Length && j < mapNodes[0].Length) { newMapNodes[i][j] = mapNodes[i][j]; } else { newMapNodes[i][j] = new PathNode(i, j); newMapNodes[i][j].SetWeight(newPathNodeWeight); } } } widthMax = newWidth; heightMax = newHeight; mapNodes = newMapNodes; UpdateNodeConnections(); //if (OnSizeModified != null) OnSizeModified(this, EventArgs.Empty); } public Vector3 GetWorldOffset() { return worldOrigin; } public float GetNodeSize() { return nodeSize; } public void SetWalkable(int x, int y, bool walkable) { mapNodes[x][y].SetWalkable(walkable); } public void SetAllWalkable(bool walkable) { for (int x = 0; x < mapNodes.Length; x++) { for (int y = 0; y < mapNodes[x].Length; y++) { mapNodes[x][y].SetWalkable(walkable); } } } public void SetWeight(int x, int y, int weight) { mapNodes[x][y].SetWeight(weight); } public void SetAllWeight(int weight) { for (int x = 0; x < mapNodes.Length; x++) { for (int y = 0; y < mapNodes[x].Length; y++) { mapNodes[x][y].SetWeight(weight); } } } public int GetMapWidth() { return widthMax; } public int GetMapHeight() { return heightMax; } public void Initialize(int mapWidth, int mapHeight) { // Creates PathNodes for (int x = 0; x < mapNodes.Length; x++) { for (int y = 0; y < mapNodes[x].Length; y++) { mapNodes[x][y] = new PathNode(x, y); } } UpdateNodeConnections(); } private void UpdateNodeConnections() { for (int x = 0; x < mapNodes.Length; x++) { for (int y = 0; y < mapNodes[x].Length; y++) { if (y < mapNodes[x].Length - 1) mapNodes[x][y].north = mapNodes[x][y + 1]; if (y > 0) mapNodes[x][y].south = mapNodes[x][y - 1]; if (x < mapNodes.Length - 1) mapNodes[x][y].east = mapNodes[x + 1][y]; if (x > 0) mapNodes[x][y].west = mapNodes[x - 1][y]; } } } public void PrintMap(Transform prefabWalkable, Transform prefabUnwalkable) { for (int x = 0; x < mapNodes.Length; x++) { for (int y = 0; y < mapNodes[x].Length; y++) { PathNode pathNode = mapNodes[x][y]; UnityEngine.Object.Instantiate(pathNode.IsWalkable() ? prefabWalkable : prefabUnwalkable, new Vector3(x * nodeSize, y * nodeSize), Quaternion.identity); } } } public void PrintMap(Action printNode) { for (int x = 0; x < mapNodes.Length; x++) { for (int y = 0; y < mapNodes[x].Length; y++) { printNode(x, y); } } } public void PrintMap(Action printNode) { for (int x = 0; x < mapNodes.Length; x++) { for (int y = 0; y < mapNodes[x].Length; y++) { printNode(x, y, worldOrigin + new Vector3(x * nodeSize, y * nodeSize)); } } } public void PrintMap(Action printWalkable, Action printUnwalkable) { for (int x = 0; x < mapNodes.Length; x++) { for (int y = 0; y < mapNodes[x].Length; y++) { PathNode pathNode = mapNodes[x][y]; if (pathNode.IsWalkable()) { printWalkable(x, y); } else { printUnwalkable(x, y); } } } } public void PrintMap(Action createSprite) { PrintMap( (int x, int y) => { createSprite(worldOrigin + new Vector3(x * nodeSize, y * nodeSize), new Vector3(2, 2), Color.green); //MyUtils.World_Sprite.Create(worldOrigin + new Vector3(x * nodeSize, y * nodeSize), new Vector3(2, 2), SpriteHolder.instance.s_White, Color.green); }, (int x, int y) => { createSprite(worldOrigin + new Vector3(x * nodeSize, y * nodeSize), new Vector3(2, 2), Color.red); //MyUtils.World_Sprite.Create(worldOrigin + new Vector3(x * nodeSize, y * nodeSize), new Vector3(2, 2), SpriteHolder.instance.s_White, Color.red); } ); } /*public void PrintMapUpdateable() { }*/ private bool IsValidShortcut(int startX, int startY, int endX, int endY) { //Debug.Log("Testing Shortcut: " + startX + ", " + startY + " -> " + endX + ", " + endY); int shortcutWeight = mapNodes[startX][startY].weight; Vector3 dir = (new Vector3(endX, endY) - new Vector3(startX, startY)).normalized; Vector3 test = new Vector3(startX, startY) + dir; int testX = Mathf.RoundToInt(test.x); int testY = Mathf.RoundToInt(test.y); // Check if shortcut is walkable //Debug.Log("Testing: "+testX+","+testY); while (!(testX == endX && testY == endY)) { if (!IsWalkable(testX, testY) || mapNodes[testX][testY].weight != shortcutWeight) { // Not walkable //Debug.Log("Shortcut invalid!"); return false; } else { test += dir; testX = Mathf.RoundToInt(test.x); testY = Mathf.RoundToInt(test.y); //Debug.Log("Testing: "+testX+","+testY); } } // Shortcut walkable //Debug.Log("Shortcut valid!"); return true; } public List GetFindPath(MapPos startPos, MapPos finalPos) { int width = widthMax; int height = heightMax; startPos = GetClosestValidPos(startPos.x, startPos.y); if (startPos.x < 0 || startPos.y < 0 || finalPos.x < 0 || finalPos.y < 0 || startPos.x >= width || finalPos.x >= width || startPos.y >= height || finalPos.y >= height) { return null; //Out of bounds! } if (mapNodes[finalPos.x][finalPos.y].weight == WALL_WEIGHT || mapNodes[startPos.x][startPos.y].weight == WALL_WEIGHT) return null; //Wall return findPath(startPos.x, startPos.y, finalPos.x, finalPos.y); } public List GetFindPathClosest(MapPos startPos, List allFinalPos) { List closest = null; for (int i = 0; i < allFinalPos.Count; i++) { List path = GetFindPath(startPos, allFinalPos[i]); if (path != null) { if (closest == null) closest = path; else { if (path.Count < closest.Count) { closest = path; } } } } return closest; } public bool FindPath(int startX, int startY, MapPos finalPos, OnPathCallback callback) { return FindPath(startX, startY, new List() { finalPos }, callback); } public bool FindPath(int startX, int startY, List finalPositions, OnPathCallback callback) { int width = widthMax; int height = heightMax; MapPos start = GetClosestValidPos(startX, startY); startX = start.x; startY = start.y; List paths = new List(); foreach (MapPos finalPos in finalPositions) { if (startX < 0 || startY < 0 || finalPos.x < 0 || finalPos.y < 0 || startX >= width || finalPos.x >= width || startY >= height || finalPos.y >= height) { continue; // Out of bounds! } if (mapNodes[finalPos.x][finalPos.y].weight == WALL_WEIGHT || mapNodes[startX][startY].weight == WALL_WEIGHT) { // Find close non-wall start/end continue; // Wall } List currentPath = findPath(startX, startY, finalPos.x, finalPos.y); if (currentPath.Count <= 0 && (startX != finalPos.x || startY != finalPos.y)) { // Don't add path if there's no path } else { if (!finalPos.straightToOffset) { // Don't go straight to offset, add dummy currentPath.Add(currentPath[currentPath.Count - 1]); } paths.Add(new PathRoute(currentPath, worldOrigin, nodeSize, finalPos)); } } int smallest = 0; for (int i = 1; i < paths.Count; i++) { if (paths[i].pathNodeList.Count < paths[smallest].pathNodeList.Count) smallest = i; } if (paths.Count <= 0 || (paths.Count > 0 && paths[smallest].pathNodeList.Count <= 0)) { // No path return false; } else { callback(paths[smallest].pathNodeList, paths[smallest].finalPos); } return true; } public Vector3 GetClosestValidPosition(Vector3 position) { int mapX, mapY; ConvertVectorPositionValidate(position, out mapX, out mapY); MapPos closestValidMapPos = GetClosestValidPos(mapX, mapY); PathNode pathNode = mapNodes[closestValidMapPos.x][closestValidMapPos.y]; return pathNode.GetWorldVector(worldOrigin, nodeSize); } private MapPos GetClosestValidPos(int mapX, int mapY) { int width = widthMax; int height = heightMax; // Inside bounds while (mapX < 0) mapX++; while (mapY < 0) mapY++; while (mapX >= width) mapX--; while (mapY >= height) mapY--; // Check inside walls if (mapNodes[mapX][mapY].weight == WALL_WEIGHT) { int radius = 1; MapPos valid = null; do { valid = GetValidPosRadius(mapX, mapY, radius); radius++; } while (valid == null && radius < 100); if (radius == 100) return new MapPos(0, 0); return valid; } return new MapPos(mapX, mapY); } private MapPos GetValidPosRadius(int mapX, int mapY, int radius) { int width = widthMax; int height = heightMax; int endX = mapX + radius; for (int i = mapX - radius; i <= endX; i++) { int j = mapY + radius; if (i < 0 || i >= width || j < 0 || j >= height) { //Out of bounds } else { if (mapNodes[i][j].weight != WALL_WEIGHT) return new MapPos(i, j); } j = mapY - radius; if (i < 0 || i >= width || j < 0 || j >= height) { //Out of bounds } else { if (mapNodes[i][j].weight != WALL_WEIGHT) return new MapPos(i, j); } } int endY = mapY + radius; for (int j = mapY - radius + 1; j < endY; j++) { int i = mapX - radius; if (i < 0 || i >= width || j < 0 || j >= height) { //Out of bounds } else { if (mapNodes[i][j].weight != WALL_WEIGHT) return new MapPos(i, j); } i = mapX + radius; if (i < 0 || i >= width || j < 0 || j >= height) { //Out of bounds } else { if (mapNodes[i][j].weight != WALL_WEIGHT) return new MapPos(i, j); } } return null; } public void ApplyShortcuts(ref List pathNodeList) { if (pathNodeList.Count > 1) { int testStartNodeIndex = 1; while (testStartNodeIndex < pathNodeList.Count - 2) { // Only test untils there's 3 nodes left PathNode testStartNode = pathNodeList[testStartNodeIndex]; int testEndNodeIndex = testStartNodeIndex + 2; // Test start node with node 2 indexes in front PathNode testEndNode = pathNodeList[testEndNodeIndex]; while (IsValidShortcut(testStartNode.xPos, testStartNode.yPos, testEndNode.xPos, testEndNode.yPos)) { // Valid shortcut // Remove in between node pathNodeList.RemoveAt(testStartNodeIndex + 1); if (testEndNodeIndex >= pathNodeList.Count - 1) { // No more nodes break; } else { // Test next node testEndNode = pathNodeList[testEndNodeIndex]; } } // Start next shortcut test from this end node testStartNodeIndex = testEndNodeIndex; } } } public PathRoute GetPathRouteWithShortcuts(Vector3 start, Vector3 end) { List pathNodeList = GetPath(start, end); ApplyShortcuts(ref pathNodeList); return new PathRoute(pathNodeList, worldOrigin, nodeSize, null); } public PathRoute GetPathRoute(Vector3 start, Vector3 end) { List pathNodeList = GetPath(start, end); return new PathRoute(pathNodeList, worldOrigin, nodeSize, null); } public List GetPath(Vector3 start, Vector3 end) { start = start - worldOrigin; end = end - worldOrigin; start = start / nodeSize; end = end / nodeSize; MapPos startMapPos = GetClosestValidPos(Mathf.RoundToInt(start.x), Mathf.RoundToInt(start.y)); MapPos endMapPos = GetClosestValidPos(Mathf.RoundToInt(end.x), Mathf.RoundToInt(end.y)); return findPath(startMapPos.x, startMapPos.y, endMapPos.x, endMapPos.y); } public List findPath(int startX, int startY, int endX, int endY) { List ret = new List(); // Calculate H for all nodes CalculateAllHeuristics(endX, endY); // Start finding target foundTarget = false; binaryTree = new BinaryTree(); openListCount = 1; PathNode currentNode = mapNodes[startX][startY]; PathNode targetNode = mapNodes[endX][endY]; if (currentNode == targetNode) { return new List { currentNode }; } int iterations = 0; do { iterations++; currentNode = FindTarget(currentNode, targetNode); } while (!foundTarget && openListCount > 0 && iterations < 60000); if (iterations >= 60000) UnityEngine.Debug.Log("iteration overload"); if (foundTarget) { // Get path currentNode = targetNode; ret.Add(currentNode); while (currentNode.parent != null && currentNode.parent != currentNode) { ret.Add(currentNode.parent); currentNode = currentNode.parent; } if (currentNode.parent == currentNode) UnityEngine.Debug.Log("parent == child"); } else { // No path possible } ret.Reverse(); return ret; } private PathNode FindTarget(PathNode currentNode, PathNode targetNode) { // Check the north node if (currentNode.moveNorth) DetermineNodeValues(currentNode, currentNode.north, targetNode); // Check the east node if (currentNode.moveEast) DetermineNodeValues(currentNode, currentNode.east, targetNode); // Check the south node if (currentNode.moveSouth) DetermineNodeValues(currentNode, currentNode.south, targetNode); // Check the west node if (currentNode.moveWest) DetermineNodeValues(currentNode, currentNode.west, targetNode); if (!foundTarget) { // Once done checking add to the closed list and remove from the open list AddToClosedList(currentNode); RemoveFromOpenList(currentNode); // Get the next node with the smallest F value return GetSmallestFValueNode(); } else { return null; } } private void DetermineNodeValues(PathNode currentNode, PathNode testing, PathNode targetNode) { // Dont work on null nodes if (testing == null) return; // Check to see if the node is the target if (testing == targetNode) { targetNode.parent = currentNode; foundTarget = true; return; } // Ignore Walls if (currentNode.weight == WALL_WEIGHT || testing.weight == WALL_WEIGHT) return; // While the node has not already been tested if (!testing.isOnClosedList) { // Check to see if the node is already on the open list if (testing.isOnOpenList) { // Get a Gcost to move from this node to the testing node int newGcost = currentNode.gValue + currentNode.weight + movementCost; // If the G cost is better then change the nodes parent and update its costs. if (newGcost < testing.gValue) { testing.parent = currentNode; testing.gValue = newGcost; binaryTree.RemoveNode(testing); testing.CalculateFValue(); binaryTree.AddNode(testing); } } else { // Set the testing nodes parent to the current location, calculate its costs, and add it to the open list testing.parent = currentNode; testing.gValue = currentNode.gValue + currentNode.weight + movementCost; testing.CalculateFValue(); AddToOpenList(testing); } } } private void AddToOpenList(PathNode node) { binaryTree.AddNode(node); openListCount++; node.isOnOpenList = true; } private void AddToClosedList(PathNode currentNode) { currentNode.isOnClosedList = true; } private void RemoveFromOpenList(PathNode currentNode) { binaryTree.RemoveNode(currentNode); openListCount--; currentNode.isOnOpenList = false; } private PathNode GetSmallestFValueNode() { return binaryTree.GetSmallest(); } private void CalculateManhattanDistance(PathNode currentNode, int currX, int currY, int targetX, int targetY) { currentNode.parent = null; currentNode.hValue = (Mathf.Abs(currX - targetX) + Mathf.Abs(currY - targetY)); currentNode.isOnOpenList = false; currentNode.isOnClosedList = false; } private void CalculateAllHeuristics(int endX, int endY) { int rows = heightMax; int cols = widthMax; for (int x = 0; x < cols; x++) { for (int y = 0; y < rows; y++) { CalculateManhattanDistance(mapNodes[x][y], x, y, endX, endY); } } } public void ResetRestrictions() { for (int i = 0; i < GetMapWidth(); i++) { for (int j = 0; j < GetMapHeight(); j++) { mapNodes[i][j].ResetRestrictions(); } } } public void RefreshAllHitboxes() { for (int x = 0; x < widthMax; x++) { for (int y = 0; y < heightMax; y++) { mapNodes[x][y].TestHitbox(); } } //Event_Speaker.Broadcast(Event_Trigger.Pathfinding_Refresh); } public void RefreshHitbox(MapPos mapPos) { mapNodes[mapPos.x][mapPos.y].TestHitbox(); //Event_Speaker.Broadcast(Event_Trigger.Pathfinding_Refresh); } public bool IsWalkable(MapPos mapPos) { return mapNodes[mapPos.x][mapPos.y].weight != WALL_WEIGHT; } public bool IsWalkable(int x, int y) { return mapNodes[x][y].weight != WALL_WEIGHT; } public bool IsWall(int x, int y) { return mapNodes[x][y].weight == WALL_WEIGHT; } public bool HasWeight(int x, int y) { return mapNodes[x][y].weight > 0; } private void ConvertVectorPosition(Vector3 position, out int x, out int y) { x = (int)((position.x - worldOrigin.x) / nodeSize); y = (int)((position.y - worldOrigin.y) / nodeSize); } private void ConvertVectorPositionValidate(Vector3 position, out int x, out int y) { ConvertVectorPosition(position, out x, out y); if (x < 0) x = 0; if (y < 0) y = 0; if (x >= widthMax) x = widthMax - 1; if (y >= heightMax) y = heightMax - 1; } } }