ChunkedMazeRenderer.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. using UnityEngine;
  2. using UnityEngine.Tilemaps;
  3. using System.Collections.Generic;
  4. /// <summary>
  5. /// Renders large mazes using chunked tilemaps to prevent performance issues
  6. /// Only renders chunks that are visible or near the camera
  7. /// </summary>
  8. [RequireComponent(typeof(Grid))]
  9. public class ChunkedMazeRenderer : MonoBehaviour
  10. {
  11. [Header("Maze Setup")]
  12. [SerializeField] private MazeController mazeController;
  13. [Header("Chunk Settings")]
  14. [SerializeField] private int chunkSize = 32;
  15. [SerializeField] private int renderDistance = 2; // chunks around camera
  16. [SerializeField] private Transform cameraTransform;
  17. [SerializeField] private bool useXZPlane = false;
  18. [Header("Tile Assets")]
  19. [SerializeField] private TileBase floorTile;
  20. [SerializeField] private TileBase wallTile;
  21. [SerializeField] private TileBase swampTile;
  22. [SerializeField] private TileBase stoneTile;
  23. [Header("Markers")]
  24. [SerializeField] private Sprite startMarkerSprite;
  25. [SerializeField] private Sprite exitMarkerSprite;
  26. [SerializeField] private bool showStartEndMarkers = true;
  27. [Header("Debug")]
  28. [SerializeField] private bool showChunkBounds = false;
  29. private Dictionary<Vector2Int, Tilemap> activeChunks = new();
  30. private Dictionary<MazeTile.TerrainType, TileBase> terrainTiles;
  31. private Vector2Int lastCameraChunk;
  32. private GameObject markerContainer;
  33. void Start()
  34. {
  35. SetupTerrainDictionary();
  36. if (cameraTransform == null)
  37. {
  38. cameraTransform = Camera.main?.transform;
  39. }
  40. EnsurePlaceholderTiles();
  41. }
  42. void Update()
  43. {
  44. if (cameraTransform != null)
  45. {
  46. UpdateVisibleChunks();
  47. }
  48. }
  49. void SetupTerrainDictionary()
  50. {
  51. terrainTiles = new Dictionary<MazeTile.TerrainType, TileBase>
  52. {
  53. { MazeTile.TerrainType.Normal, floorTile },
  54. { MazeTile.TerrainType.Swamp, swampTile ?? floorTile },
  55. { MazeTile.TerrainType.Stone, stoneTile ?? floorTile },
  56. };
  57. }
  58. /// <summary>
  59. /// Updates which chunks are visible and renders them
  60. /// </summary>
  61. private void UpdateVisibleChunks()
  62. {
  63. var maze = mazeController.GetCurrentMaze();
  64. if (maze == null) return;
  65. Vector2Int cameraChunk = WorldToChunk(cameraTransform.position);
  66. // Only update if camera moved to a different chunk
  67. if (cameraChunk != lastCameraChunk)
  68. {
  69. lastCameraChunk = cameraChunk;
  70. RenderVisibleChunks(maze, cameraChunk);
  71. }
  72. }
  73. /// <summary>
  74. /// Converts world position to chunk coordinates
  75. /// </summary>
  76. private Vector2Int WorldToChunk(Vector3 worldPos)
  77. {
  78. if (useXZPlane)
  79. {
  80. return new Vector2Int(
  81. Mathf.FloorToInt(worldPos.x / chunkSize),
  82. Mathf.FloorToInt(worldPos.z / chunkSize)
  83. );
  84. }
  85. return new Vector2Int(
  86. Mathf.FloorToInt(worldPos.x / chunkSize),
  87. Mathf.FloorToInt(worldPos.y / chunkSize)
  88. );
  89. }
  90. /// <summary>
  91. /// Renders chunks within render distance of camera
  92. /// </summary>
  93. private void RenderVisibleChunks(MazeData maze, Vector2Int centerChunk)
  94. {
  95. HashSet<Vector2Int> chunksToKeep = new();
  96. // Determine which chunks should be visible
  97. for (int x = centerChunk.x - renderDistance; x <= centerChunk.x + renderDistance; x++)
  98. {
  99. for (int y = centerChunk.y - renderDistance; y <= centerChunk.y + renderDistance; y++)
  100. {
  101. Vector2Int chunkPos = new Vector2Int(x, y);
  102. chunksToKeep.Add(chunkPos);
  103. if (!activeChunks.ContainsKey(chunkPos))
  104. {
  105. CreateAndRenderChunk(maze, chunkPos);
  106. }
  107. }
  108. }
  109. // Remove chunks that are no longer visible
  110. List<Vector2Int> chunksToRemove = new();
  111. foreach (var chunk in activeChunks.Keys)
  112. {
  113. if (!chunksToKeep.Contains(chunk))
  114. {
  115. chunksToRemove.Add(chunk);
  116. }
  117. }
  118. foreach (var chunk in chunksToRemove)
  119. {
  120. Destroy(activeChunks[chunk].gameObject);
  121. activeChunks.Remove(chunk);
  122. }
  123. UpdateMarkers();
  124. }
  125. /// <summary>
  126. /// Creates and renders a single chunk
  127. /// </summary>
  128. private void CreateAndRenderChunk(MazeData maze, Vector2Int chunkPos)
  129. {
  130. GameObject chunkObj = new GameObject($"Chunk_{chunkPos.x}_{chunkPos.y}");
  131. chunkObj.transform.parent = transform;
  132. chunkObj.transform.localPosition = GetChunkWorldPosition(chunkPos);
  133. chunkObj.transform.localRotation = useXZPlane ? Quaternion.Euler(90, 0, 0) : Quaternion.identity;
  134. Tilemap tilemap = chunkObj.AddComponent<Tilemap>();
  135. TilemapRenderer renderer = chunkObj.AddComponent<TilemapRenderer>();
  136. renderer.sortingOrder = 0;
  137. if (showChunkBounds)
  138. {
  139. AddChunkBounds(chunkObj, chunkSize);
  140. }
  141. // Render tiles in this chunk
  142. int startX = chunkPos.x * chunkSize;
  143. int startY = chunkPos.y * chunkSize;
  144. int endX = Mathf.Min(startX + chunkSize, maze.Width);
  145. int endY = Mathf.Min(startY + chunkSize, maze.Height);
  146. for (int x = startX; x < endX; x++)
  147. {
  148. for (int y = startY; y < endY; y++)
  149. {
  150. var tile = maze.GetTile(x, y);
  151. if (tile != null)
  152. {
  153. Vector3Int localPos = new Vector3Int(x - startX, y - startY, 0);
  154. TileBase tileToSet = GetTileForMazeTile(tile);
  155. if (tileToSet != null)
  156. {
  157. tilemap.SetTile(localPos, tileToSet);
  158. }
  159. }
  160. }
  161. }
  162. activeChunks[chunkPos] = tilemap;
  163. }
  164. private void UpdateMarkers()
  165. {
  166. if (!showStartEndMarkers) return;
  167. if (markerContainer != null)
  168. {
  169. Destroy(markerContainer);
  170. }
  171. markerContainer = new GameObject("MazeMarkers");
  172. markerContainer.transform.parent = transform;
  173. markerContainer.transform.localPosition = Vector3.zero;
  174. var maze = mazeController?.GetCurrentMaze();
  175. if (maze == null) return;
  176. foreach (var start in maze.StartPoints)
  177. {
  178. CreateMarker(start, startMarkerSprite, Color.green, "StartPoint_");
  179. }
  180. foreach (var exit in maze.ExitPoints)
  181. {
  182. CreateMarker(exit, exitMarkerSprite, Color.red, "ExitPoint_");
  183. }
  184. }
  185. private void CreateMarker(Vector2Int position, Sprite markerSprite, Color color, string prefix)
  186. {
  187. var go = new GameObject(prefix + position.x + "_" + position.y);
  188. go.transform.parent = markerContainer.transform;
  189. go.transform.position = GetMarkerWorldPosition(position);
  190. go.transform.localScale = Vector3.one * 1.5f;
  191. var spriteRenderer = go.AddComponent<SpriteRenderer>();
  192. spriteRenderer.sprite = markerSprite ?? TileGenerator.CreateColoredSprite(color, prefix.TrimEnd('_'));
  193. spriteRenderer.color = new Color(color.r, color.g, color.b, 0.9f);
  194. spriteRenderer.sortingOrder = 100;
  195. spriteRenderer.material = new Material(Shader.Find("Sprites/Default"));
  196. }
  197. /// <summary>
  198. /// Gets the appropriate tile for a maze tile
  199. /// </summary>
  200. private TileBase GetTileForMazeTile(MazeTile mazeTile)
  201. {
  202. if (mazeTile.Type == MazeTile.TileType.Wall)
  203. {
  204. return wallTile;
  205. }
  206. else if (mazeTile.Type == MazeTile.TileType.Floor || mazeTile.Type == MazeTile.TileType.Terrain)
  207. {
  208. if (terrainTiles.TryGetValue(mazeTile.Terrain, out var terrainTile))
  209. {
  210. return terrainTile;
  211. }
  212. return floorTile;
  213. }
  214. return null;
  215. }
  216. private void EnsurePlaceholderTiles()
  217. {
  218. if (floorTile == null)
  219. {
  220. floorTile = TileGenerator.CreateTile(TileGenerator.CreateColoredSprite(new Color(0.8f, 0.8f, 0.8f), "Floor"));
  221. }
  222. if (wallTile == null)
  223. {
  224. wallTile = TileGenerator.CreateTile(TileGenerator.CreateColoredSprite(new Color(0.3f, 0.3f, 0.3f), "Wall"));
  225. }
  226. if (swampTile == null)
  227. {
  228. swampTile = TileGenerator.CreateTile(TileGenerator.CreateCheckerSprite(new Color(0.5f, 0.8f, 0.3f), new Color(0.4f, 0.6f, 0.2f), "Swamp"));
  229. }
  230. if (stoneTile == null)
  231. {
  232. stoneTile = TileGenerator.CreateTile(TileGenerator.CreateCheckerSprite(new Color(0.7f, 0.7f, 0.7f), new Color(0.6f, 0.6f, 0.6f), "Stone"));
  233. }
  234. SetupTerrainDictionary();
  235. }
  236. private Vector3 GetChunkWorldPosition(Vector2Int chunkPos)
  237. {
  238. if (useXZPlane)
  239. {
  240. return new Vector3(chunkPos.x * chunkSize, 0, chunkPos.y * chunkSize);
  241. }
  242. return new Vector3(chunkPos.x * chunkSize, chunkPos.y * chunkSize, 0);
  243. }
  244. private Vector3 GetMarkerWorldPosition(Vector2Int position)
  245. {
  246. if (useXZPlane)
  247. {
  248. return new Vector3(position.x + 0.5f, 0.5f, position.y + 0.5f);
  249. }
  250. return new Vector3(position.x + 0.5f, position.y + 0.5f, -0.5f);
  251. }
  252. /// <summary>
  253. /// Adds visual bounds for debugging chunks
  254. /// </summary>
  255. private void AddChunkBounds(GameObject chunkObj, int size)
  256. {
  257. GameObject bounds = new GameObject("Bounds");
  258. bounds.transform.parent = chunkObj.transform;
  259. bounds.transform.localPosition = Vector3.zero;
  260. LineRenderer lineRenderer = bounds.AddComponent<LineRenderer>();
  261. lineRenderer.positionCount = 5;
  262. lineRenderer.SetPositions(new Vector3[]
  263. {
  264. new Vector3(0, 0, 0),
  265. new Vector3(size, 0, 0),
  266. new Vector3(size, size, 0),
  267. new Vector3(0, size, 0),
  268. new Vector3(0, 0, 0)
  269. });
  270. lineRenderer.startWidth = 0.1f;
  271. lineRenderer.endWidth = 0.1f;
  272. lineRenderer.material = new Material(Shader.Find("Sprites/Default"));
  273. lineRenderer.startColor = Color.red;
  274. lineRenderer.endColor = Color.red;
  275. }
  276. /// <summary>
  277. /// Forces a re-render of all visible chunks
  278. /// </summary>
  279. public void RefreshVisibleChunks()
  280. {
  281. foreach (var chunk in activeChunks.Values)
  282. {
  283. Destroy(chunk.gameObject);
  284. }
  285. activeChunks.Clear();
  286. lastCameraChunk = new Vector2Int(int.MinValue, int.MinValue); // Force update
  287. }
  288. }