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 } }