using UnityEngine; using System.Collections.Generic; /// /// Simple AI entity that navigates the maze using fog of war /// Only knows about areas it has explored or can currently see /// [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 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(); 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(); } /// /// Updates the entity's vision and explores new areas /// 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(); } } /// /// Explores the area around the current position /// private void ExploreCurrentArea() { Vector2Int center = WorldToTile(transform.position); HashSet 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); } } /// /// Updates pathfinding towards an exit /// 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 knownTiles = fogOfWar != null ? fogOfWar.GetExploredTiles() : GetAllTiles(); // Find path within known areas currentPath = FindPathInKnownArea(currentPos, targetExit, knownTiles); if (currentPath.Count > 0) { currentPathIndex = 0; UpdatePathVisualization(); } } } /// /// Finds the best exit to path towards /// 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; } /// /// Finds a path within explored areas /// private List FindPathInKnownArea(Vector2Int start, Vector2Int goal, HashSet knownTiles) { // Simple A* within known tiles var openSet = new List { start }; var cameFrom = new Dictionary(); var gScore = new Dictionary { [start] = 0 }; var fScore = new Dictionary { [start] = Vector2Int.Distance(start, goal) }; while (openSet.Count > 0) { // Find node with lowest fScore Vector2Int current = openSet[0]; float lowestF = fScore[current]; for (int i = 1; i < openSet.Count; i++) { if (fScore[openSet[i]] < lowestF) { current = openSet[i]; lowestF = fScore[current]; } } if (current == goal) { return ReconstructPath(cameFrom, current); } openSet.Remove(current); // Check neighbors foreach (var neighbor in maze.GetAdjacentWalkable(current.x, current.y)) { if (!knownTiles.Contains(neighbor)) continue; // Only path through known tiles float tentativeG = gScore[current] + 1; // Assume cost of 1 if (!gScore.ContainsKey(neighbor) || tentativeG < gScore[neighbor]) { cameFrom[neighbor] = current; gScore[neighbor] = tentativeG; fScore[neighbor] = tentativeG + Vector2Int.Distance(neighbor, goal); if (!openSet.Contains(neighbor)) { openSet.Add(neighbor); } } } } return new List(); // No path found } /// /// Reconstructs the path from A* results /// private List ReconstructPath(Dictionary cameFrom, Vector2Int current) { var path = new List { current }; while (cameFrom.ContainsKey(current)) { current = cameFrom[current]; path.Insert(0, current); } return path; } /// /// Gets all tiles (fallback when no fog of war) /// private HashSet GetAllTiles() { HashSet 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; } /// /// Moves along the current path /// 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(); } } } /// /// Updates the path visualization /// 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)); } } /// /// Converts world position to tile coordinates /// private Vector2Int WorldToTile(Vector3 worldPos) { return new Vector2Int(Mathf.FloorToInt(worldPos.x), Mathf.FloorToInt(worldPos.y)); } void OnDestroy() { if (fogOfWar != null) { fogOfWar.RemoveEntity(gameObject); } } }