MazeAIEntity.cs 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  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. // Simple A* within known tiles
  153. var openSet = new List<Vector2Int> { start };
  154. var cameFrom = new Dictionary<Vector2Int, Vector2Int>();
  155. var gScore = new Dictionary<Vector2Int, float> { [start] = 0 };
  156. var fScore = new Dictionary<Vector2Int, float> { [start] = Vector2Int.Distance(start, goal) };
  157. while (openSet.Count > 0)
  158. {
  159. // Find node with lowest fScore
  160. Vector2Int current = openSet[0];
  161. float lowestF = fScore[current];
  162. for (int i = 1; i < openSet.Count; i++)
  163. {
  164. if (fScore[openSet[i]] < lowestF)
  165. {
  166. current = openSet[i];
  167. lowestF = fScore[current];
  168. }
  169. }
  170. if (current == goal)
  171. {
  172. return ReconstructPath(cameFrom, current);
  173. }
  174. openSet.Remove(current);
  175. // Check neighbors
  176. foreach (var neighbor in maze.GetAdjacentWalkable(current.x, current.y))
  177. {
  178. if (!knownTiles.Contains(neighbor)) continue; // Only path through known tiles
  179. float tentativeG = gScore[current] + 1; // Assume cost of 1
  180. if (!gScore.ContainsKey(neighbor) || tentativeG < gScore[neighbor])
  181. {
  182. cameFrom[neighbor] = current;
  183. gScore[neighbor] = tentativeG;
  184. fScore[neighbor] = tentativeG + Vector2Int.Distance(neighbor, goal);
  185. if (!openSet.Contains(neighbor))
  186. {
  187. openSet.Add(neighbor);
  188. }
  189. }
  190. }
  191. }
  192. return new List<Vector2Int>(); // No path found
  193. }
  194. /// <summary>
  195. /// Reconstructs the path from A* results
  196. /// </summary>
  197. private List<Vector2Int> ReconstructPath(Dictionary<Vector2Int, Vector2Int> cameFrom, Vector2Int current)
  198. {
  199. var path = new List<Vector2Int> { current };
  200. while (cameFrom.ContainsKey(current))
  201. {
  202. current = cameFrom[current];
  203. path.Insert(0, current);
  204. }
  205. return path;
  206. }
  207. /// <summary>
  208. /// Gets all tiles (fallback when no fog of war)
  209. /// </summary>
  210. private HashSet<Vector2Int> GetAllTiles()
  211. {
  212. HashSet<Vector2Int> allTiles = new();
  213. for (int x = 0; x < maze.Width; x++)
  214. {
  215. for (int y = 0; y < maze.Height; y++)
  216. {
  217. allTiles.Add(new Vector2Int(x, y));
  218. }
  219. }
  220. return allTiles;
  221. }
  222. /// <summary>
  223. /// Moves along the current path
  224. /// </summary>
  225. private void FollowPath()
  226. {
  227. if (currentPath.Count == 0 || currentPathIndex >= currentPath.Count) return;
  228. Vector2Int targetTile = currentPath[currentPathIndex];
  229. Vector3 targetPos = new Vector3(targetTile.x + 0.5f, targetTile.y + 0.5f, 0);
  230. Vector3 currentPos = transform.position;
  231. // Move towards target
  232. Vector3 direction = (targetPos - currentPos).normalized;
  233. transform.position += direction * movementSpeed * Time.deltaTime;
  234. // Check if reached target
  235. if (Vector3.Distance(currentPos, targetPos) < 0.1f)
  236. {
  237. currentPathIndex++;
  238. // If reached end of path, find new path
  239. if (currentPathIndex >= currentPath.Count)
  240. {
  241. currentPath.Clear();
  242. UpdatePathfinding();
  243. }
  244. }
  245. }
  246. /// <summary>
  247. /// Updates the path visualization
  248. /// </summary>
  249. private void UpdatePathVisualization()
  250. {
  251. if (!showPath || pathRenderer == null) return;
  252. pathRenderer.positionCount = currentPath.Count;
  253. for (int i = 0; i < currentPath.Count; i++)
  254. {
  255. Vector2Int tile = currentPath[i];
  256. pathRenderer.SetPosition(i, new Vector3(tile.x + 0.5f, tile.y + 0.5f, -0.1f));
  257. }
  258. }
  259. /// <summary>
  260. /// Converts world position to tile coordinates
  261. /// </summary>
  262. private Vector2Int WorldToTile(Vector3 worldPos)
  263. {
  264. return new Vector2Int(Mathf.FloorToInt(worldPos.x), Mathf.FloorToInt(worldPos.y));
  265. }
  266. void OnDestroy()
  267. {
  268. if (fogOfWar != null)
  269. {
  270. fogOfWar.RemoveEntity(gameObject);
  271. }
  272. }
  273. }