| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 |
- using UnityEngine;
- using System.Collections.Generic;
- /// <summary>
- /// Simple AI entity that navigates the maze using fog of war
- /// Only knows about areas it has explored or can currently see
- /// </summary>
- [RequireComponent(typeof(Rigidbody2D))]
- public class MazeAIEntity : MonoBehaviour
- {
- [Header("AI Settings")]
- [SerializeField] private MazeController mazeController;
- [SerializeField] private MazeFogOfWar fogOfWar;
- [SerializeField] private float visionRange = 5f;
- [SerializeField] private float movementSpeed = 2f;
- [SerializeField] private float pathUpdateInterval = 1f;
- [Header("Pathfinding")]
- [SerializeField] private bool showPath = true;
- [SerializeField] private LineRenderer pathRenderer;
- private MazeData maze;
- private MazePathfinder pathfinder;
- private List<Vector2Int> currentPath = new();
- private int currentPathIndex = 0;
- private Vector2Int currentTarget;
- private float lastPathUpdate = 0f;
- void Start()
- {
- maze = mazeController.GetCurrentMaze();
- pathfinder = new MazePathfinder(maze);
- // Start at a random start point
- if (maze.StartPoints.Count > 0)
- {
- int startIndex = Random.Range(0, maze.StartPoints.Count);
- Vector2Int startPos = maze.StartPoints[startIndex];
- transform.position = new Vector3(startPos.x + 0.5f, startPos.y + 0.5f, 0);
- // Mark starting area as explored
- ExploreCurrentArea();
- }
- if (pathRenderer == null)
- {
- pathRenderer = gameObject.AddComponent<LineRenderer>();
- pathRenderer.startWidth = 0.1f;
- pathRenderer.endWidth = 0.1f;
- pathRenderer.material = new Material(Shader.Find("Sprites/Default"));
- pathRenderer.startColor = Color.yellow;
- pathRenderer.endColor = Color.yellow;
- }
- }
- void Update()
- {
- if (maze == null) return;
- // Update vision and exploration
- UpdateVision();
- // Update pathfinding
- if (Time.time - lastPathUpdate > pathUpdateInterval)
- {
- UpdatePathfinding();
- lastPathUpdate = Time.time;
- }
- // Move along path
- FollowPath();
- }
- /// <summary>
- /// Updates the entity's vision and explores new areas
- /// </summary>
- private void UpdateVision()
- {
- Vector2Int currentTile = WorldToTile(transform.position);
- if (fogOfWar != null)
- {
- fogOfWar.UpdateEntityVision(gameObject, currentTile, visionRange);
- }
- else
- {
- // If no fog of war, explore current area
- ExploreCurrentArea();
- }
- }
- /// <summary>
- /// Explores the area around the current position
- /// </summary>
- private void ExploreCurrentArea()
- {
- Vector2Int center = WorldToTile(transform.position);
- HashSet<Vector2Int> explored = new();
- int range = Mathf.CeilToInt(visionRange);
- for (int x = center.x - range; x <= center.x + range; x++)
- {
- for (int y = center.y - range; y <= center.y + range; y++)
- {
- Vector2Int tile = new Vector2Int(x, y);
- if (Vector2Int.Distance(center, tile) <= visionRange && maze.IsInBounds(x, y))
- {
- explored.Add(tile);
- }
- }
- }
- if (fogOfWar != null)
- {
- fogOfWar.ExploreTiles(explored);
- }
- }
- /// <summary>
- /// Updates pathfinding towards an exit
- /// </summary>
- private void UpdatePathfinding()
- {
- if (maze.ExitPoints.Count == 0) return;
- Vector2Int currentPos = WorldToTile(transform.position);
- // Find closest unexplored exit
- Vector2Int targetExit = FindBestExit(currentPos);
- if (targetExit != currentPos)
- {
- // Only pathfind to areas we know about
- HashSet<Vector2Int> knownTiles = fogOfWar != null ?
- fogOfWar.GetExploredTiles() : GetAllTiles();
- // Find path within known areas
- currentPath = FindPathInKnownArea(currentPos, targetExit, knownTiles);
- if (currentPath.Count > 0)
- {
- currentPathIndex = 0;
- UpdatePathVisualization();
- }
- }
- }
- /// <summary>
- /// Finds the best exit to path towards
- /// </summary>
- private Vector2Int FindBestExit(Vector2Int from)
- {
- Vector2Int bestExit = from;
- float bestDistance = float.MaxValue;
- foreach (var exit in maze.ExitPoints)
- {
- // Only consider exits we know about
- if (fogOfWar == null || fogOfWar.IsTileExplored(exit))
- {
- float distance = Vector2Int.Distance(from, exit);
- if (distance < bestDistance)
- {
- bestDistance = distance;
- bestExit = exit;
- }
- }
- }
- return bestExit;
- }
- /// <summary>
- /// Finds a path within explored areas
- /// </summary>
- private List<Vector2Int> FindPathInKnownArea(Vector2Int start, Vector2Int goal, HashSet<Vector2Int> knownTiles)
- {
- // A* within known tiles using min-heap open set (O(n log n) vs O(n²))
- var openSet = new MinHeap<Vector2Int>();
- var cameFrom = new Dictionary<Vector2Int, Vector2Int>();
- var gScore = new Dictionary<Vector2Int, float> { [start] = 0f };
- float h0 = Mathf.Abs(start.x - goal.x) + Mathf.Abs(start.y - goal.y);
- openSet.Enqueue(start, h0);
- while (openSet.Count > 0)
- {
- Vector2Int current = openSet.Dequeue();
- if (current == goal)
- return ReconstructPath(cameFrom, current);
- foreach (var neighbor in maze.GetAdjacentWalkable(current.x, current.y))
- {
- if (!knownTiles.Contains(neighbor)) continue;
- float tentativeG = gScore[current] + 1f;
- if (!gScore.TryGetValue(neighbor, out float existingG) || tentativeG < existingG)
- {
- cameFrom[neighbor] = current;
- gScore[neighbor] = tentativeG;
- float f = tentativeG + Mathf.Abs(neighbor.x - goal.x) + Mathf.Abs(neighbor.y - goal.y);
- if (openSet.Contains(neighbor, out _)) openSet.UpdatePriority(neighbor, f);
- else openSet.Enqueue(neighbor, f);
- }
- }
- }
- return new List<Vector2Int>();
- }
- /// <summary>
- /// Reconstructs the path from A* results using Add+Reverse (O(n)) instead of Insert(0,...) (O(n²))
- /// </summary>
- private static List<Vector2Int> ReconstructPath(Dictionary<Vector2Int, Vector2Int> cameFrom, Vector2Int current)
- {
- var path = new List<Vector2Int>();
- while (cameFrom.TryGetValue(current, out var prev))
- {
- path.Add(current);
- current = prev;
- }
- path.Add(current);
- path.Reverse();
- return path;
- }
- /// <summary>
- /// Gets all tiles (fallback when no fog of war)
- /// </summary>
- private HashSet<Vector2Int> GetAllTiles()
- {
- HashSet<Vector2Int> allTiles = new();
- for (int x = 0; x < maze.Width; x++)
- {
- for (int y = 0; y < maze.Height; y++)
- {
- allTiles.Add(new Vector2Int(x, y));
- }
- }
- return allTiles;
- }
- /// <summary>
- /// Moves along the current path
- /// </summary>
- private void FollowPath()
- {
- if (currentPath.Count == 0 || currentPathIndex >= currentPath.Count) return;
- Vector2Int targetTile = currentPath[currentPathIndex];
- Vector3 targetPos = new Vector3(targetTile.x + 0.5f, targetTile.y + 0.5f, 0);
- Vector3 currentPos = transform.position;
- // Move towards target
- Vector3 direction = (targetPos - currentPos).normalized;
- transform.position += direction * movementSpeed * Time.deltaTime;
- // Check if reached target
- if (Vector3.Distance(currentPos, targetPos) < 0.1f)
- {
- currentPathIndex++;
- // If reached end of path, find new path
- if (currentPathIndex >= currentPath.Count)
- {
- currentPath.Clear();
- UpdatePathfinding();
- }
- }
- }
- /// <summary>
- /// Updates the path visualization
- /// </summary>
- private void UpdatePathVisualization()
- {
- if (!showPath || pathRenderer == null) return;
- pathRenderer.positionCount = currentPath.Count;
- for (int i = 0; i < currentPath.Count; i++)
- {
- Vector2Int tile = currentPath[i];
- pathRenderer.SetPosition(i, new Vector3(tile.x + 0.5f, tile.y + 0.5f, -0.1f));
- }
- }
- /// <summary>
- /// Converts world position to tile coordinates
- /// </summary>
- private Vector2Int WorldToTile(Vector3 worldPos)
- {
- return new Vector2Int(Mathf.FloorToInt(worldPos.x), Mathf.FloorToInt(worldPos.y));
- }
- void OnDestroy()
- {
- if (fogOfWar != null)
- {
- fogOfWar.RemoveEntity(gameObject);
- }
- }
- }
|