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)
{
// A* within known tiles using min-heap open set (O(n log n) vs O(n²))
var openSet = new MinHeap();
var cameFrom = new Dictionary();
var gScore = new Dictionary { [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();
}
///
/// Reconstructs the path from A* results using Add+Reverse (O(n)) instead of Insert(0,...) (O(n²))
///
private static List ReconstructPath(Dictionary cameFrom, Vector2Int current)
{
var path = new List();
while (cameFrom.TryGetValue(current, out var prev))
{
path.Add(current);
current = prev;
}
path.Add(current);
path.Reverse();
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);
}
}
}