├── AINavMeshGenerator.cs ├── LICENSE └── README.md /AINavMeshGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using System.Linq; 5 | using Unity.Jobs; 6 | using Unity.Collections; 7 | using Sirenix.OdinInspector; 8 | using Sirenix.Serialization; 9 | #if UNITY_EDITOR 10 | using UnityEditor; 11 | #endif 12 | 13 | public class Node 14 | { 15 | public bool valid = true; 16 | public Vector2 position; 17 | public readonly Node[] connections; 18 | public float gCost; 19 | public float hCost; 20 | public float fCost { get { return gCost + hCost; } } 21 | public Node parent; 22 | public GameObject associatedObject; 23 | 24 | public Node(Vector2 pos) 25 | { 26 | position = pos; 27 | connections = new Node[8]; 28 | } 29 | 30 | public void Reset() 31 | { 32 | valid = true; 33 | associatedObject = null; 34 | gCost = 0; 35 | hCost = 0; 36 | parent = null; 37 | } 38 | 39 | public bool AnyConnectionsBad() 40 | { 41 | for (int i = 0; i < connections.Length; i++) 42 | { 43 | if(connections[i] == null || !connections[i].valid) 44 | { 45 | return true; 46 | } 47 | } 48 | return false; 49 | } 50 | } 51 | 52 | [ExecuteInEditMode] 53 | public class AINavMeshGenerator : SerializedMonoBehaviour 54 | { 55 | enum Directions { Right, DownRight, Down, DownLeft, Left, UpLeft, Up, UpRight } 56 | 57 | [OdinSerialize] 58 | private float updateInterval = 0.1f; 59 | [OdinSerialize] 60 | private float pointDistributionSize = 0.5f; 61 | [OdinSerialize] 62 | LayerMask destroyNodeMask; 63 | [OdinSerialize] 64 | LayerMask obstacleMask; 65 | 66 | public Rect size; 67 | public static AINavMeshGenerator instance; 68 | 69 | private float updateTimer = 0; 70 | private List grid = null; 71 | private List Grid 72 | { 73 | get 74 | { 75 | if(grid == null) 76 | { 77 | GenerateNewGrid(); 78 | } 79 | return grid; 80 | } 81 | } 82 | private Dictionary positionNodeDictionary = new Dictionary(); 83 | public static Pathfinder pathfinder = null; 84 | 85 | public void GenerateNewGrid() 86 | { 87 | FillOutGrid(); 88 | DestroyBadNodes(); 89 | CheckForBadNodes(); 90 | } 91 | 92 | 93 | public LayerMask GetAvoidanceMasks() 94 | { 95 | return destroyNodeMask | obstacleMask; 96 | } 97 | 98 | private void Awake() 99 | { 100 | instance = this; 101 | pathfinder = new Pathfinder(this); 102 | } 103 | 104 | private void Start() 105 | { 106 | GenerateNewGrid(); 107 | updateTimer = updateInterval; 108 | } 109 | 110 | private void FillOutGrid() 111 | { 112 | grid = new List(); 113 | positionNodeDictionary.Clear(); 114 | Vector2 currentPoint = new Vector2((size.x - size.width / 2) + pointDistributionSize, (size.y + size.height / 2) - pointDistributionSize); 115 | int iteration = 0; 116 | bool alternate = false; 117 | bool cacheIteration = false; 118 | int length = -1; 119 | int yLength = 0; 120 | while (true) 121 | { 122 | iteration++; 123 | Node newNode = new Node(currentPoint); 124 | Grid.Add(newNode); 125 | positionNodeDictionary.Add(currentPoint, newNode); 126 | currentPoint += new Vector2(pointDistributionSize * 2, 0); 127 | if (currentPoint.x > size.x + size.width / 2) 128 | { 129 | if(length != -1) 130 | { 131 | while(iteration < length) 132 | { 133 | Node extraNode = new Node(currentPoint); 134 | Grid.Add(extraNode); 135 | iteration++; 136 | } 137 | } 138 | else 139 | { 140 | Node extraNode = new Node(currentPoint); 141 | Grid.Add(extraNode); 142 | } 143 | currentPoint = new Vector2((size.x - size.width / 2) + (alternate ? pointDistributionSize : 0), currentPoint.y - pointDistributionSize); 144 | alternate = !alternate; 145 | cacheIteration = true; 146 | yLength++; 147 | } 148 | if (currentPoint.y < size.y - size.height / 2) 149 | { 150 | break; 151 | } 152 | if(cacheIteration) 153 | { 154 | if(length == -1) 155 | { 156 | length = iteration + 1; 157 | } 158 | iteration = 0; 159 | cacheIteration = false; 160 | } 161 | } 162 | for (int i = 0; i < Grid.Count; i++) 163 | { 164 | for (int direction = 0; direction < Grid[i].connections.Length; direction++) 165 | { 166 | Grid[i].connections[direction] = GetNodeFromDirection(i, (Directions)direction, length); 167 | } 168 | } 169 | } 170 | 171 | private void DestroyBadNodes() 172 | { 173 | //First check if each node is inside a destroy mask 174 | for (int i = Grid.Count - 1; i >= 0; i--) 175 | { 176 | Collider2D hit = Physics2D.OverlapCircle(Grid[i].position, 0.01f, destroyNodeMask); 177 | if (hit != null) 178 | { 179 | //At this point, we know this node is bad, and we must destroy it. For humanity itself. 180 | for (int j = 0; j < Grid[i].connections.Length; j++) 181 | { 182 | //Go through all the connections to this node 183 | if (Grid[i].connections[j] != null) 184 | { 185 | for (int k = 0; k < Grid[i].connections[j].connections.Length; k++) 186 | { 187 | //Set the nodes connections reference to this node to null, because it no longer exists. 188 | //Is that confusing? It sounds confusing. 189 | if (Grid[i].connections[j].connections[k] != null) 190 | { 191 | if (Grid[i].connections[j].connections[k] == Grid[i]) 192 | { 193 | Grid[i].connections[j].connections[k] = null; 194 | } 195 | } 196 | } 197 | } 198 | } 199 | Grid.RemoveAt(i); 200 | } 201 | } 202 | } 203 | 204 | private void CheckForBadNodes() 205 | { 206 | for (int i = 0; i < Grid.Count; i++) 207 | { 208 | Grid[i].Reset(); 209 | } 210 | 211 | //Make any node with a destroyed outside have an extra layer barrier around it, so that they dont get too close to walls 212 | for (int i = 0; i < Grid.Count; i++) 213 | { 214 | if (Grid[i].valid) 215 | { 216 | for (int j = 0; j < Grid[i].connections.Length; j++) 217 | { 218 | Node connection = Grid[i].connections[j]; 219 | if (connection == null) 220 | { 221 | Grid[i].valid = false; 222 | } 223 | } 224 | } 225 | } 226 | 227 | //Then check if the node is inside a normal mask to disable it. 228 | for (int i = 0; i < Grid.Count; i++) 229 | { 230 | if(Grid[i].valid) 231 | { 232 | Collider2D hit = Physics2D.OverlapCircle(Grid[i].position, 0.05f, obstacleMask); 233 | if (hit != null) 234 | { 235 | Grid[i].valid = false; 236 | Grid[i].associatedObject = hit.transform.gameObject; 237 | } 238 | } 239 | } 240 | } 241 | 242 | Node GetNodeFromDirection(int nodeIndex, Directions direction, int length) 243 | { 244 | int index = -1; 245 | bool isStartOfRow = (nodeIndex + 1) % length == 1; 246 | bool isEndOfRow = (nodeIndex + 1) % length == 0; 247 | bool isOddRow = (((nodeIndex + 1) - Mathf.FloorToInt((nodeIndex) % length)) / length) % 2 == 0; 248 | 249 | switch (direction) 250 | { 251 | case Directions.Right: 252 | if (isEndOfRow) return null; 253 | index = nodeIndex + 1; 254 | break; 255 | case Directions.DownRight: 256 | if (isEndOfRow && isOddRow) return null; 257 | index = nodeIndex + length + (isOddRow ? 1 : 0); 258 | break; 259 | case Directions.Down: 260 | index = nodeIndex + length * 2; 261 | break; 262 | case Directions.DownLeft: 263 | if (isStartOfRow && !isOddRow) return null; 264 | index = nodeIndex + (length - (isOddRow ? 0 : 1)); 265 | break; 266 | case Directions.Left: 267 | if (isStartOfRow) return null; 268 | index = nodeIndex - 1; 269 | break; 270 | case Directions.UpLeft: 271 | if (isStartOfRow && !isOddRow) return null; 272 | index = nodeIndex - (length + (isOddRow ? 0 : 1)); 273 | break; 274 | case Directions.Up: 275 | index = nodeIndex - length * 2; 276 | break; 277 | case Directions.UpRight: 278 | if (isEndOfRow && isOddRow) return null; 279 | index = nodeIndex - (length - (isOddRow ? 1 : 0)); 280 | break; 281 | } 282 | 283 | if (index >= 0 && index < Grid.Count) 284 | { 285 | return Grid[index]; 286 | } 287 | else 288 | { 289 | return null; 290 | } 291 | } 292 | 293 | public Node FindClosestNode(Vector2 position, bool mustBeGood = false, GameObject associatedObject = null) 294 | { 295 | Node closest = null; 296 | float current = float.MaxValue; 297 | for (int i = 0; i < Grid.Count; i++) 298 | { 299 | if(!mustBeGood || Grid[i].valid || associatedObject == Grid[i].associatedObject) 300 | { 301 | float distance = Vector2.Distance(Grid[i].position, position); 302 | if (distance < current) 303 | { 304 | current = distance; 305 | closest = Grid[i]; 306 | } 307 | } 308 | } 309 | 310 | return closest; 311 | } 312 | 313 | public void ClearGrid() 314 | { 315 | Grid.Clear(); 316 | } 317 | 318 | void Update() 319 | { 320 | if(Grid == null) 321 | { 322 | GenerateNewGrid(); 323 | } 324 | 325 | //We update the bad nodes constantly, so as objects or enemies move, the grid automatically adjusts itself. 326 | updateTimer -= Time.deltaTime; 327 | if(updateTimer <= 0) 328 | { 329 | updateTimer = updateInterval; 330 | CheckForBadNodes(); 331 | } 332 | } 333 | 334 | void OnDrawGizmosSelected() 335 | { 336 | if(Grid == null) 337 | { 338 | return; 339 | } 340 | for (int i = 0; i < Grid.Count; i++) 341 | { 342 | for (int j = 0; j < Grid[i].connections.Length; j++) 343 | { 344 | if(Grid[i].connections[j] != null) 345 | { 346 | Gizmos.color = Grid[i].valid && Grid[i].connections[j].valid ? Color.green : Color.red; 347 | Gizmos.DrawLine(Grid[i].position, Grid[i].connections[j].position); 348 | } 349 | } 350 | } 351 | for (int i = 0; i < Grid.Count; i++) 352 | { 353 | Gizmos.color = Grid[i].valid ? Color.blue : Color.red; 354 | Gizmos.DrawCube(Grid[i].position, Vector3.one * 0.2f); 355 | } 356 | } 357 | } 358 | 359 | public class Pathfinder 360 | { 361 | AINavMeshGenerator generator; 362 | 363 | List openSet = new List(); 364 | HashSet closedSet = new HashSet(); 365 | List path = new List(); 366 | public Pathfinder(AINavMeshGenerator gen) 367 | { 368 | generator = gen; 369 | } 370 | 371 | private float Heuristic(Node one, Node two) 372 | { 373 | return Vector2.Distance(one.position, two.position); 374 | } 375 | 376 | private Node[] GetAStar(Vector2 currentPosition, Vector2 destination, GameObject obj = null) 377 | { 378 | Node start = generator.FindClosestNode(currentPosition, false, obj); 379 | Node end = generator.FindClosestNode(destination, true, obj); 380 | if (start == null || end == null) 381 | { 382 | return null; 383 | } 384 | 385 | openSet.Clear(); 386 | closedSet.Clear(); 387 | openSet.Add(start); 388 | while (openSet.Count > 0) 389 | { 390 | Node current = openSet[0]; 391 | 392 | //Evaluate costs 393 | for (int i = 1; i < openSet.Count; i++) 394 | { 395 | if (openSet[i].fCost < current.fCost || openSet[i].fCost == current.fCost) 396 | { 397 | if (openSet[i].hCost < current.hCost) 398 | { 399 | current = openSet[i]; 400 | } 401 | } 402 | } 403 | 404 | openSet.Remove(current); 405 | closedSet.Add(current); 406 | 407 | if (current.Equals(end)) 408 | { 409 | break; 410 | } 411 | 412 | //Go through neighbors 413 | foreach (Node neighbor in current.connections.Where(x => x != null)) 414 | { 415 | //The associated object check is so the enemy ignores pathing through it's own bad sector 416 | if ((!neighbor.valid && neighbor.associatedObject != obj) || closedSet.Contains(neighbor)) 417 | { 418 | continue; 419 | } 420 | 421 | float newCost = current.gCost + Heuristic(current, neighbor); 422 | if (newCost < neighbor.gCost || !openSet.Contains(neighbor)) 423 | { 424 | neighbor.gCost = newCost; 425 | neighbor.hCost = Heuristic(neighbor, end); 426 | neighbor.parent = current; 427 | 428 | if (!openSet.Contains(neighbor)) 429 | { 430 | openSet.Add(neighbor); 431 | } 432 | } 433 | } 434 | } 435 | 436 | if(end.parent == null) 437 | { 438 | return null; 439 | } 440 | 441 | //Calculate path 442 | path.Clear(); 443 | Node currentCheck = end; 444 | while (!path.Contains(currentCheck) && currentCheck != null) 445 | { 446 | path.Add(currentCheck); 447 | currentCheck = currentCheck.parent; 448 | } 449 | path.Reverse(); 450 | if(path[0] != start) 451 | { 452 | return null; 453 | } 454 | return path.ToArray(); 455 | } 456 | 457 | private List ShortenPointsByVisibility(Node[] points) 458 | { 459 | //If we have small amount of points, dont bother with all this. 460 | //if(points.Length < 2) 461 | { 462 | List p = new List(); 463 | for (int i = 0; i < points.Length; i++) 464 | { 465 | p.Add(points[i].position); 466 | } 467 | return p; 468 | } 469 | 470 | List corners = new List(); 471 | corners.Add(points[0].position); 472 | Node start = points[0]; 473 | Node end = points[1]; 474 | //Go through all the points, starting at 1 (since we already set our initial start to the first point) 475 | for (int i = 1; i < points.Length; i++) 476 | { 477 | //Set the end to our current point and check if its in a bad spot to hang out in town. 478 | end = null; 479 | end = points[i]; 480 | bool inBadArea = end.AnyConnectionsBad(); 481 | 482 | if(inBadArea) 483 | { 484 | //If it's a bad boy, we add it to our corners, so we walk this way. 485 | corners.Add(end.position); 486 | //Then start anew. 487 | start = null; 488 | start = end; 489 | } 490 | } 491 | //Add that last rebel into the mix for sure. 492 | corners.Add(points[points.Length - 1].position); 493 | 494 | return corners; 495 | } 496 | 497 | public Vector2[] FindPath(Vector2 currentPosition, Vector2 destination, GameObject associatedObject = null) 498 | { 499 | Node[] points = GetAStar(currentPosition, destination, associatedObject); 500 | if(points == null) 501 | { 502 | return null; 503 | } 504 | 505 | List shortPath = ShortenPointsByVisibility(points); 506 | //shortPath.Insert(0, currentPosition); 507 | return shortPath.ToArray(); 508 | } 509 | } 510 | #if UNITY_EDITOR 511 | [CustomEditor(typeof(AINavMeshGenerator))] 512 | public class AINavMeshGeneratorEditor : Editor 513 | { 514 | void OnSceneGUI() 515 | { 516 | AINavMeshGenerator source = target as AINavMeshGenerator; 517 | 518 | Rect rect = RectUtils.ResizeRect(source.size, 519 | Handles.CubeHandleCap, 520 | Color.green, new Color(1, 1, 1, 0.5f), 521 | HandleUtility.GetHandleSize(Vector3.zero) * 0.1f, 522 | 0.1f); 523 | 524 | source.size = rect; 525 | } 526 | 527 | public override void OnInspectorGUI() 528 | { 529 | AINavMeshGenerator source = target as AINavMeshGenerator; 530 | base.OnInspectorGUI(); 531 | if (GUILayout.Button("Generate grid")) 532 | { 533 | source.GenerateNewGrid(); 534 | } 535 | if(GUILayout.Button("Clear grid")) 536 | { 537 | source.ClearGrid(); 538 | } 539 | } 540 | } 541 | 542 | public class RectUtils 543 | { 544 | public static Rect ResizeRect(Rect rect, Handles.CapFunction capFunc, Color capCol, Color fillCol, float capSize, float snap) 545 | { 546 | Vector2 halfRectSize = new Vector2(rect.size.x * 0.5f, rect.size.y * 0.5f); 547 | 548 | Vector3[] rectangleCorners = 549 | { 550 | new Vector3(rect.position.x - halfRectSize.x, rect.position.y - halfRectSize.y, 0), // Bottom Left 551 | new Vector3(rect.position.x + halfRectSize.x, rect.position.y - halfRectSize.y, 0), // Bottom Right 552 | new Vector3(rect.position.x + halfRectSize.x, rect.position.y + halfRectSize.y, 0), // Top Right 553 | new Vector3(rect.position.x - halfRectSize.x, rect.position.y + halfRectSize.y, 0) // Top Left 554 | }; 555 | 556 | Handles.color = fillCol; 557 | Handles.DrawSolidRectangleWithOutline(rectangleCorners, new Color(fillCol.r, fillCol.g, fillCol.b, 0.25f), capCol); 558 | 559 | Vector3[] handlePoints = 560 | { 561 | new Vector3(rect.position.x - halfRectSize.x, rect.position.y, 0), // Left 562 | new Vector3(rect.position.x + halfRectSize.x, rect.position.y, 0), // Right 563 | new Vector3(rect.position.x, rect.position.y + halfRectSize.y, 0), // Top 564 | new Vector3(rect.position.x, rect.position.y - halfRectSize.y, 0) // Bottom 565 | }; 566 | 567 | Handles.color = capCol; 568 | 569 | var newSize = rect.size; 570 | var newPosition = rect.position; 571 | 572 | var leftHandle = Handles.Slider(handlePoints[0], -Vector3.right, capSize, capFunc, snap).x - handlePoints[0].x; 573 | var rightHandle = Handles.Slider(handlePoints[1], Vector3.right, capSize, capFunc, snap).x - handlePoints[1].x; 574 | var topHandle = Handles.Slider(handlePoints[2], Vector3.up, capSize, capFunc, snap).y - handlePoints[2].y; 575 | var bottomHandle = Handles.Slider(handlePoints[3], -Vector3.up, capSize, capFunc, snap).y - handlePoints[3].y; 576 | 577 | newSize = new Vector2( 578 | Mathf.Max(.1f, newSize.x - leftHandle + rightHandle), 579 | Mathf.Max(.1f, newSize.y + topHandle - bottomHandle)); 580 | 581 | newPosition = new Vector2( 582 | newPosition.x + leftHandle * .5f + rightHandle * .5f, 583 | newPosition.y + topHandle * .5f + bottomHandle * .5f); 584 | 585 | return new Rect(newPosition.x, newPosition.y, newSize.x, newSize.y); 586 | } 587 | } 588 | 589 | #endif 590 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Cory Koseck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unity-Pathfinding 2 | Super messy. Super early - but hey, it exists. 3 | Dynamically updates the grid over a set time period. 4 | --------------------------------------------------------------------------------