using UnityEngine;
using UnityEngine.Tilemaps;
using System.Collections.Generic;
///
/// Renders large mazes using chunked tilemaps to prevent performance issues
/// Only renders chunks that are visible or near the camera
///
[RequireComponent(typeof(Grid))]
public class ChunkedMazeRenderer : MonoBehaviour
{
[Header("Maze Setup")]
[SerializeField] private MazeController mazeController;
[Header("Chunk Settings")]
[SerializeField] private int chunkSize = 32;
[SerializeField] private int renderDistance = 2; // chunks around camera
[SerializeField] private Transform cameraTransform;
[SerializeField] private bool useXZPlane = false;
[Header("Tile Assets")]
[SerializeField] private TileBase floorTile;
[SerializeField] private TileBase wallTile;
[SerializeField] private TileBase swampTile;
[SerializeField] private TileBase stoneTile;
[Header("Markers")]
[SerializeField] private Sprite startMarkerSprite;
[SerializeField] private Sprite exitMarkerSprite;
[SerializeField] private bool showStartEndMarkers = true;
[Header("Debug")]
[SerializeField] private bool showChunkBounds = false;
private Dictionary activeChunks = new();
private Dictionary terrainTiles;
private Vector2Int lastCameraChunk;
private GameObject markerContainer;
void Start()
{
SetupTerrainDictionary();
if (cameraTransform == null)
{
cameraTransform = Camera.main?.transform;
}
EnsurePlaceholderTiles();
}
void Update()
{
if (cameraTransform != null)
{
UpdateVisibleChunks();
}
}
void SetupTerrainDictionary()
{
terrainTiles = new Dictionary
{
{ MazeTile.TerrainType.Normal, floorTile },
{ MazeTile.TerrainType.Swamp, swampTile ?? floorTile },
{ MazeTile.TerrainType.Stone, stoneTile ?? floorTile },
};
}
///
/// Updates which chunks are visible and renders them
///
private void UpdateVisibleChunks()
{
var maze = mazeController.GetCurrentMaze();
if (maze == null) return;
Vector2Int cameraChunk = WorldToChunk(cameraTransform.position);
// Only update if camera moved to a different chunk
if (cameraChunk != lastCameraChunk)
{
lastCameraChunk = cameraChunk;
RenderVisibleChunks(maze, cameraChunk);
}
}
///
/// Converts world position to chunk coordinates
///
private Vector2Int WorldToChunk(Vector3 worldPos)
{
if (useXZPlane)
{
return new Vector2Int(
Mathf.FloorToInt(worldPos.x / chunkSize),
Mathf.FloorToInt(worldPos.z / chunkSize)
);
}
return new Vector2Int(
Mathf.FloorToInt(worldPos.x / chunkSize),
Mathf.FloorToInt(worldPos.y / chunkSize)
);
}
///
/// Renders chunks within render distance of camera
///
private void RenderVisibleChunks(MazeData maze, Vector2Int centerChunk)
{
HashSet chunksToKeep = new();
// Determine which chunks should be visible
for (int x = centerChunk.x - renderDistance; x <= centerChunk.x + renderDistance; x++)
{
for (int y = centerChunk.y - renderDistance; y <= centerChunk.y + renderDistance; y++)
{
Vector2Int chunkPos = new Vector2Int(x, y);
chunksToKeep.Add(chunkPos);
if (!activeChunks.ContainsKey(chunkPos))
{
CreateAndRenderChunk(maze, chunkPos);
}
}
}
// Remove chunks that are no longer visible
List chunksToRemove = new();
foreach (var chunk in activeChunks.Keys)
{
if (!chunksToKeep.Contains(chunk))
{
chunksToRemove.Add(chunk);
}
}
foreach (var chunk in chunksToRemove)
{
Destroy(activeChunks[chunk].gameObject);
activeChunks.Remove(chunk);
}
UpdateMarkers();
}
///
/// Creates and renders a single chunk
///
private void CreateAndRenderChunk(MazeData maze, Vector2Int chunkPos)
{
GameObject chunkObj = new GameObject($"Chunk_{chunkPos.x}_{chunkPos.y}");
chunkObj.transform.parent = transform;
chunkObj.transform.localPosition = GetChunkWorldPosition(chunkPos);
chunkObj.transform.localRotation = useXZPlane ? Quaternion.Euler(90, 0, 0) : Quaternion.identity;
Tilemap tilemap = chunkObj.AddComponent();
TilemapRenderer renderer = chunkObj.AddComponent();
renderer.sortingOrder = 0;
if (showChunkBounds)
{
AddChunkBounds(chunkObj, chunkSize);
}
// Render tiles in this chunk
int startX = chunkPos.x * chunkSize;
int startY = chunkPos.y * chunkSize;
int endX = Mathf.Min(startX + chunkSize, maze.Width);
int endY = Mathf.Min(startY + chunkSize, maze.Height);
for (int x = startX; x < endX; x++)
{
for (int y = startY; y < endY; y++)
{
var tile = maze.GetTile(x, y);
if (tile != null)
{
Vector3Int localPos = new Vector3Int(x - startX, y - startY, 0);
TileBase tileToSet = GetTileForMazeTile(tile);
if (tileToSet != null)
{
tilemap.SetTile(localPos, tileToSet);
}
}
}
}
activeChunks[chunkPos] = tilemap;
}
private void UpdateMarkers()
{
if (!showStartEndMarkers) return;
if (markerContainer != null)
{
Destroy(markerContainer);
}
markerContainer = new GameObject("MazeMarkers");
markerContainer.transform.parent = transform;
markerContainer.transform.localPosition = Vector3.zero;
var maze = mazeController?.GetCurrentMaze();
if (maze == null) return;
foreach (var start in maze.StartPoints)
{
CreateMarker(start, startMarkerSprite, Color.green, "StartPoint_");
}
foreach (var exit in maze.ExitPoints)
{
CreateMarker(exit, exitMarkerSprite, Color.red, "ExitPoint_");
}
}
private void CreateMarker(Vector2Int position, Sprite markerSprite, Color color, string prefix)
{
var go = new GameObject(prefix + position.x + "_" + position.y);
go.transform.parent = markerContainer.transform;
go.transform.position = GetMarkerWorldPosition(position);
go.transform.localScale = Vector3.one * 1.5f;
var spriteRenderer = go.AddComponent();
spriteRenderer.sprite = markerSprite ?? TileGenerator.CreateColoredSprite(color, prefix.TrimEnd('_'));
spriteRenderer.color = new Color(color.r, color.g, color.b, 0.9f);
spriteRenderer.sortingOrder = 100;
spriteRenderer.material = new Material(Shader.Find("Sprites/Default"));
}
///
/// Gets the appropriate tile for a maze tile
///
private TileBase GetTileForMazeTile(MazeTile mazeTile)
{
if (mazeTile.Type == MazeTile.TileType.Wall)
{
return wallTile;
}
else if (mazeTile.Type == MazeTile.TileType.Floor || mazeTile.Type == MazeTile.TileType.Terrain)
{
if (terrainTiles.TryGetValue(mazeTile.Terrain, out var terrainTile))
{
return terrainTile;
}
return floorTile;
}
return null;
}
private void EnsurePlaceholderTiles()
{
if (floorTile == null)
{
floorTile = TileGenerator.CreateTile(TileGenerator.CreateColoredSprite(new Color(0.8f, 0.8f, 0.8f), "Floor"));
}
if (wallTile == null)
{
wallTile = TileGenerator.CreateTile(TileGenerator.CreateColoredSprite(new Color(0.3f, 0.3f, 0.3f), "Wall"));
}
if (swampTile == null)
{
swampTile = TileGenerator.CreateTile(TileGenerator.CreateCheckerSprite(new Color(0.5f, 0.8f, 0.3f), new Color(0.4f, 0.6f, 0.2f), "Swamp"));
}
if (stoneTile == null)
{
stoneTile = TileGenerator.CreateTile(TileGenerator.CreateCheckerSprite(new Color(0.7f, 0.7f, 0.7f), new Color(0.6f, 0.6f, 0.6f), "Stone"));
}
SetupTerrainDictionary();
}
private Vector3 GetChunkWorldPosition(Vector2Int chunkPos)
{
if (useXZPlane)
{
return new Vector3(chunkPos.x * chunkSize, 0, chunkPos.y * chunkSize);
}
return new Vector3(chunkPos.x * chunkSize, chunkPos.y * chunkSize, 0);
}
private Vector3 GetMarkerWorldPosition(Vector2Int position)
{
if (useXZPlane)
{
return new Vector3(position.x + 0.5f, 0.5f, position.y + 0.5f);
}
return new Vector3(position.x + 0.5f, position.y + 0.5f, -0.5f);
}
///
/// Adds visual bounds for debugging chunks
///
private void AddChunkBounds(GameObject chunkObj, int size)
{
GameObject bounds = new GameObject("Bounds");
bounds.transform.parent = chunkObj.transform;
bounds.transform.localPosition = Vector3.zero;
LineRenderer lineRenderer = bounds.AddComponent();
lineRenderer.positionCount = 5;
lineRenderer.SetPositions(new Vector3[]
{
new Vector3(0, 0, 0),
new Vector3(size, 0, 0),
new Vector3(size, size, 0),
new Vector3(0, size, 0),
new Vector3(0, 0, 0)
});
lineRenderer.startWidth = 0.1f;
lineRenderer.endWidth = 0.1f;
lineRenderer.material = new Material(Shader.Find("Sprites/Default"));
lineRenderer.startColor = Color.red;
lineRenderer.endColor = Color.red;
}
///
/// Forces a re-render of all visible chunks
///
public void RefreshVisibleChunks()
{
foreach (var chunk in activeChunks.Values)
{
Destroy(chunk.gameObject);
}
activeChunks.Clear();
lastCameraChunk = new Vector2Int(int.MinValue, int.MinValue); // Force update
}
}