MazeAIEntity.cs 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. using UnityEngine;
  2. using System.Collections.Generic;
  3. /// <summary>
  4. /// Simple AI entity that navigates the maze using fog of war
  5. /// Only knows about areas it has explored or can currently see
  6. /// </summary>
  7. [RequireComponent(typeof(Rigidbody2D))]
  8. public class MazeAIEntity : MonoBehaviour
  9. {
  10. [Header("AI Settings")]
  11. [SerializeField] private MazeController mazeController;
  12. [SerializeField] private MazeFogOfWar fogOfWar;
  13. [SerializeField] private float visionRange = 5f;
  14. [SerializeField] private float movementSpeed = 2f;
  15. [SerializeField] private float pathUpdateInterval = 1f;
  16. [Header("Pathfinding")]
  17. [SerializeField] private bool showPath = true;
  18. [SerializeField] private LineRenderer pathRenderer;
  19. private MazeData maze;
  20. private MazePathfinder pathfinder;
  21. private List<Vector2Int> currentPath = new();
  22. private int currentPathIndex = 0;
  23. private Vector2Int currentTarget;
  24. private float lastPathUpdate = 0f;
  25. void Start()
  26. {
  27. maze = mazeController.GetCurrentMaze();
  28. pathfinder = new MazePathfinder(maze);
  29. // Start at a random start point
  30. if (maze.StartPoints.Count > 0)
  31. {
  32. int startIndex = Random.Range(0, maze.StartPoints.Count);
  33. Vector2Int startPos = maze.StartPoints[startIndex];
  34. transform.position = new Vector3(startPos.x + 0.5f, startPos.y + 0.5f, 0);
  35. // Mark starting area as explored
  36. ExploreCurrentArea();
  37. }
  38. if (pathRenderer == null)
  39. {
  40. pathRenderer = gameObject.AddComponent<LineRenderer>();
  41. pathRenderer.startWidth = 0.1f;
  42. pathRenderer.endWidth = 0.1f;
  43. pathRenderer.material = new Material(Shader.Find("Sprites/Default"));
  44. pathRenderer.startColor = Color.yellow;
  45. pathRenderer.endColor = Color.yellow;
  46. }
  47. }
  48. void Update()
  49. {
  50. if (maze == null) return;
  51. // Update vision and exploration
  52. UpdateVision();
  53. // Update pathfinding
  54. if (Time.time - lastPathUpdate > pathUpdateInterval)
  55. {
  56. UpdatePathfinding();
  57. lastPathUpdate = Time.time;
  58. }
  59. // Move along path
  60. FollowPath();
  61. }
  62. /// <summary>
  63. /// Updates the entity's vision and explores new areas
  64. /// </summary>
  65. private void UpdateVision()
  66. {
  67. Vector2Int currentTile = WorldToTile(transform.position);
  68. if (fogOfWar != null)
  69. {
  70. fogOfWar.UpdateEntityVision(gameObject, currentTile, visionRange);
  71. }
  72. else
  73. {
  74. // If no fog of war, explore current area
  75. ExploreCurrentArea();
  76. }
  77. }
  78. /// <summary>
  79. /// Explores the area around the current position
  80. /// </summary>
  81. private void ExploreCurrentArea()
  82. {
  83. Vector2Int center = WorldToTile(transform.position);
  84. HashSet<Vector2Int> explored = new();
  85. int range = Mathf.CeilToInt(visionRange);
  86. for (int x = center.x - range; x <= center.x + range; x++)
  87. {
  88. for (int y = center.y - range; y <= center.y + range; y++)
  89. {
  90. Vector2Int tile = new Vector2Int(x, y);
  91. if (Vector2Int.Distance(center, tile) <= visionRange && maze.IsInBounds(x, y))
  92. {
  93. explored.Add(tile);
  94. }
  95. }
  96. }
  97. if (fogOfWar != null)
  98. {
  99. fogOfWar.ExploreTiles(explored);
  100. }
  101. }
  102. /// <summary>
  103. /// Updates pathfinding towards an exit
  104. /// </summary>
  105. private void UpdatePathfinding()
  106. {
  107. if (maze.ExitPoints.Count == 0) return;
  108. Vector2Int currentPos = WorldToTile(transform.position);
  109. // Find closest unexplored exit
  110. Vector2Int targetExit = FindBestExit(currentPos);
  111. if (targetExit != currentPos)
  112. {
  113. // Only pathfind to areas we know about
  114. HashSet<Vector2Int> knownTiles = fogOfWar != null ?
  115. fogOfWar.GetExploredTiles() : GetAllTiles();
  116. // Find path within known areas
  117. currentPath = FindPathInKnownArea(currentPos, targetExit, knownTiles);
  118. if (currentPath.Count > 0)
  119. {
  120. currentPathIndex = 0;
  121. UpdatePathVisualization();
  122. }
  123. }
  124. }
  125. /// <summary>
  126. /// Finds the best exit to path towards
  127. /// </summary>
  128. private Vector2Int FindBestExit(Vector2Int from)
  129. {
  130. Vector2Int bestExit = from;
  131. float bestDistance = float.MaxValue;
  132. foreach (var exit in maze.ExitPoints)
  133. {
  134. // Only consider exits we know about
  135. if (fogOfWar == null || fogOfWar.IsTileExplored(exit))
  136. {
  137. float distance = Vector2Int.Distance(from, exit);
  138. if (distance < bestDistance)
  139. {
  140. bestDistance = distance;
  141. bestExit = exit;
  142. }
  143. }
  144. }
  145. return bestExit;
  146. }
  147. /// <summary>
  148. /// Finds a path within explored areas
  149. /// </summary>
  150. private List<Vector2Int> FindPathInKnownArea(Vector2Int start, Vector2Int goal, HashSet<Vector2Int> knownTiles)
  151. {
  152. // A* within known tiles using min-heap open set (O(n log n) vs O(n²))
  153. var openSet = new MinHeap<Vector2Int>();
  154. var cameFrom = new Dictionary<Vector2Int, Vector2Int>();
  155. var gScore = new Dictionary<Vector2Int, float> { [start] = 0f };
  156. float h0 = Mathf.Abs(start.x - goal.x) + Mathf.Abs(start.y - goal.y);
  157. openSet.Enqueue(start, h0);
  158. while (openSet.Count > 0)
  159. {
  160. Vector2Int current = openSet.Dequeue();
  161. if (current == goal)
  162. return ReconstructPath(cameFrom, current);
  163. foreach (var neighbor in maze.GetAdjacentWalkable(current.x, current.y))
  164. {
  165. if (!knownTiles.Contains(neighbor)) continue;
  166. float tentativeG = gScore[current] + 1f;
  167. if (!gScore.TryGetValue(neighbor, out float existingG) || tentativeG < existingG)
  168. {
  169. cameFrom[neighbor] = current;
  170. gScore[neighbor] = tentativeG;
  171. float f = tentativeG + Mathf.Abs(neighbor.x - goal.x) + Mathf.Abs(neighbor.y - goal.y);
  172. if (openSet.Contains(neighbor, out _)) openSet.UpdatePriority(neighbor, f);
  173. else openSet.Enqueue(neighbor, f);
  174. }
  175. }
  176. }
  177. return new List<Vector2Int>();
  178. }
  179. /// <summary>
  180. /// Reconstructs the path from A* results using Add+Reverse (O(n)) instead of Insert(0,...) (O(n²))
  181. /// </summary>
  182. private static List<Vector2Int> ReconstructPath(Dictionary<Vector2Int, Vector2Int> cameFrom, Vector2Int current)
  183. {
  184. var path = new List<Vector2Int>();
  185. while (cameFrom.TryGetValue(current, out var prev))
  186. {
  187. path.Add(current);
  188. current = prev;
  189. }
  190. path.Add(current);
  191. path.Reverse();
  192. return path;
  193. }
  194. /// <summary>
  195. /// Gets all tiles (fallback when no fog of war)
  196. /// </summary>
  197. private HashSet<Vector2Int> GetAllTiles()
  198. {
  199. HashSet<Vector2Int> allTiles = new();
  200. for (int x = 0; x < maze.Width; x++)
  201. {
  202. for (int y = 0; y < maze.Height; y++)
  203. {
  204. allTiles.Add(new Vector2Int(x, y));
  205. }
  206. }
  207. return allTiles;
  208. }
  209. /// <summary>
  210. /// Moves along the current path
  211. /// </summary>
  212. private void FollowPath()
  213. {
  214. if (currentPath.Count == 0 || currentPathIndex >= currentPath.Count) return;
  215. Vector2Int targetTile = currentPath[currentPathIndex];
  216. Vector3 targetPos = new Vector3(targetTile.x + 0.5f, targetTile.y + 0.5f, 0);
  217. Vector3 currentPos = transform.position;
  218. // Move towards target
  219. Vector3 direction = (targetPos - currentPos).normalized;
  220. transform.position += direction * movementSpeed * Time.deltaTime;
  221. // Check if reached target
  222. if (Vector3.Distance(currentPos, targetPos) < 0.1f)
  223. {
  224. currentPathIndex++;
  225. // If reached end of path, find new path
  226. if (currentPathIndex >= currentPath.Count)
  227. {
  228. currentPath.Clear();
  229. UpdatePathfinding();
  230. }
  231. }
  232. }
  233. /// <summary>
  234. /// Updates the path visualization
  235. /// </summary>
  236. private void UpdatePathVisualization()
  237. {
  238. if (!showPath || pathRenderer == null) return;
  239. pathRenderer.positionCount = currentPath.Count;
  240. for (int i = 0; i < currentPath.Count; i++)
  241. {
  242. Vector2Int tile = currentPath[i];
  243. pathRenderer.SetPosition(i, new Vector3(tile.x + 0.5f, tile.y + 0.5f, -0.1f));
  244. }
  245. }
  246. /// <summary>
  247. /// Converts world position to tile coordinates
  248. /// </summary>
  249. private Vector2Int WorldToTile(Vector3 worldPos)
  250. {
  251. return new Vector2Int(Mathf.FloorToInt(worldPos.x), Mathf.FloorToInt(worldPos.y));
  252. }
  253. void OnDestroy()
  254. {
  255. if (fogOfWar != null)
  256. {
  257. fogOfWar.RemoveEntity(gameObject);
  258. }
  259. }
  260. }