using UnityEngine; using System.Collections.Generic; /// /// Generates 3D meshes for maze walls and floors instead of using tilemaps /// Much more efficient for large mazes and allows for better performance /// public class MeshMazeRenderer : MonoBehaviour { [Header("Maze Setup")] [SerializeField] private MazeController mazeController; [Header("Materials")] [SerializeField] private Material floorMaterial; [SerializeField] private Material wallMaterial; [Header("Start/End Markers")] [SerializeField] private GameObject startMarkerPrefab; [SerializeField] private GameObject exitMarkerPrefab; [SerializeField] private Color startMarkerColor = new Color(0f, 1f, 0f, 0.65f); [SerializeField] private Color exitMarkerColor = new Color(1f, 0f, 0f, 0.65f); [SerializeField] private float markerSize = 3f; [SerializeField] private bool showStartEndRoomHighlights = true; [SerializeField] private Color startRoomHighlightColor = new Color(0f, 1f, 0f, 0.25f); [SerializeField] private Color exitRoomHighlightColor = new Color(1f, 0f, 0f, 0.25f); [Header("Fog of War")] [SerializeField] private bool showFogOfWar = true; [SerializeField] private MazeFogOfWar fogOfWar; [SerializeField] private Color fogColor = new Color(0f, 0f, 0f, 0.6f); [Header("Mesh Settings")] [SerializeField] private float wallHeight = 2f; [SerializeField] private float tileSize = 1f; [SerializeField] private bool generateFloor = true; [SerializeField] private bool generateWalls = true; [SerializeField] private bool twoSidedWalls = true; [SerializeField] private bool wallCaps = true; [Header("Optimization")] [SerializeField] private int chunkSize = 32; [SerializeField] private int renderDistance = 2; private Dictionary chunkObjects = new(); private Transform cameraTransform; private GameObject markerContainer; private GameObject fogOfWarContainer; private bool lastFogVisible; void OnEnable() { if (mazeController == null) { mazeController = GetComponent(); } } void Start() { cameraTransform = Camera.main?.transform; lastFogVisible = showFogOfWar; if (mazeController != null && mazeController.GetCurrentMaze() != null) { GenerateMazeMesh(); } } void Update() { if (cameraTransform != null) { UpdateVisibleChunks(); } if (showFogOfWar != lastFogVisible) { UpdateFogVisibility(); } else if (fogOfWarContainer == null && showFogOfWar && fogOfWar != null) { UpdateFogVisibility(); } } /// /// Generates the complete maze mesh /// public void GenerateMazeMesh() { if (mazeController == null) { mazeController = GetComponent(); } if (mazeController == null) { Debug.LogError("MeshMazeRenderer requires a MazeController reference."); return; } if (fogOfWar == null) { fogOfWar = GetComponent() ?? FindFirstObjectByType(); } ClearChunks(); ClearMarkers(); ClearFogOfWar(); var maze = mazeController.GetCurrentMaze(); if (maze == null) { Debug.LogWarning("No maze to render yet. Generate the maze first, then refresh the mesh."); return; } Debug.Log($"MeshMazeRenderer: rendering maze {maze.Width}x{maze.Height} with {maze.Rooms.Count} rooms"); // For very large mazes, use chunked generation if (maze.Width > chunkSize || maze.Height > chunkSize) { GenerateChunkedMesh(maze); } else { GenerateSingleMesh(maze); } RenderStartExitMarkers(maze); if (showFogOfWar && fogOfWar != null) { RenderFogOfWar(maze); } } /// /// Generates a single mesh for smaller mazes /// private void GenerateSingleMesh(MazeData maze) { GameObject chunkObj = new GameObject("MazeMesh"); chunkObj.transform.parent = transform; chunkObj.transform.localPosition = Vector3.zero; MeshFilter meshFilter = chunkObj.AddComponent(); MeshRenderer meshRenderer = chunkObj.AddComponent(); Mesh mesh = CreateMazeMesh(maze, 0, 0, maze.Width, maze.Height); meshFilter.mesh = mesh; // Assign materials Material[] materials = new Material[generateFloor && generateWalls ? 2 : 1]; int matIndex = 0; if (generateFloor) materials[matIndex++] = floorMaterial; if (generateWalls) materials[matIndex++] = wallMaterial; meshRenderer.materials = materials; chunkObjects[new Vector2Int(0, 0)] = chunkObj; } /// /// Generates chunked meshes for large mazes /// private void GenerateChunkedMesh(MazeData maze) { int chunksX = Mathf.CeilToInt((float)maze.Width / chunkSize); int chunksY = Mathf.CeilToInt((float)maze.Height / chunkSize); for (int cx = 0; cx < chunksX; cx++) { for (int cy = 0; cy < chunksY; cy++) { Vector2Int chunkPos = new Vector2Int(cx, cy); CreateChunkMesh(maze, chunkPos); } } } /// /// Creates a mesh for a single chunk /// private void CreateChunkMesh(MazeData maze, Vector2Int chunkPos) { 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); GameObject chunkObj = new GameObject($"Chunk_{chunkPos.x}_{chunkPos.y}"); chunkObj.transform.parent = transform; chunkObj.transform.localPosition = new Vector3(startX * tileSize, 0, startY * tileSize); MeshFilter meshFilter = chunkObj.AddComponent(); MeshRenderer meshRenderer = chunkObj.AddComponent(); Mesh mesh = CreateMazeMesh(maze, startX, startY, endX - startX, endY - startY); meshFilter.mesh = mesh; // Assign materials Material[] materials = new Material[generateFloor && generateWalls ? 2 : 1]; int matIndex = 0; if (generateFloor) materials[matIndex++] = floorMaterial; if (generateWalls) materials[matIndex++] = wallMaterial; meshRenderer.materials = materials; chunkObjects[chunkPos] = chunkObj; } private void RenderStartExitMarkers(MazeData maze) { if (markerContainer != null) { Destroy(markerContainer); } markerContainer = new GameObject("MazeMarkers"); markerContainer.transform.parent = transform; markerContainer.transform.localPosition = Vector3.zero; markerContainer.transform.localRotation = Quaternion.identity; foreach (var start in maze.StartPoints) { CreateMarker(maze, start, startMarkerPrefab, startMarkerColor, "StartMarker", startRoomHighlightColor); } foreach (var exit in maze.ExitPoints) { CreateMarker(maze, exit, exitMarkerPrefab, exitMarkerColor, "ExitMarker", exitRoomHighlightColor); } } private void CreateMarker(MazeData maze, Vector2Int tilePos, GameObject prefab, Color color, string namePrefix, Color roomHighlight) { Vector3 markerPos = new Vector3(tilePos.x * tileSize + tileSize * 0.5f, 0.1f, tilePos.y * tileSize + tileSize * 0.5f); GameObject marker = prefab != null ? Instantiate(prefab, markerContainer.transform) : null; if (marker == null) { marker = GameObject.CreatePrimitive(PrimitiveType.Cylinder); marker.transform.parent = markerContainer.transform; marker.transform.localPosition = markerPos; marker.transform.localScale = new Vector3(markerSize, markerSize * 0.2f, markerSize); DestroyImmediate(marker.GetComponent()); var renderer = marker.GetComponent(); renderer.material = CreateURPUnlitColorMaterial(color, true); renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; renderer.receiveShadows = false; } else { marker.transform.parent = markerContainer.transform; marker.transform.localPosition = markerPos; marker.transform.localRotation = Quaternion.identity; marker.transform.localScale = Vector3.one * markerSize; var renderer = marker.GetComponent(); if (renderer != null) { renderer.material = CreateURPUnlitColorMaterial(color, true); renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; renderer.receiveShadows = false; } } marker.name = namePrefix + "_" + tilePos.x + "_" + tilePos.y; if (showStartEndRoomHighlights) { var room = maze.GetRoomAtTile(tilePos.x, tilePos.y); if (room != null) { CreateRoomHighlight(room, roomHighlight); } } } private void CreateRoomHighlight(MazeRoom room, Color highlightColor) { if (room == null) return; GameObject highlight = new GameObject("RoomHighlight_" + room.Id); highlight.transform.parent = markerContainer.transform; highlight.transform.localPosition = Vector3.zero; var meshFilter = highlight.AddComponent(); var meshRenderer = highlight.AddComponent(); meshRenderer.material = CreateURPUnlitColorMaterial(highlightColor, true); meshRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; meshRenderer.receiveShadows = false; Mesh mesh = new Mesh(); float minX = room.MinX * tileSize; float maxX = (room.MaxX + 1) * tileSize; float minZ = room.MinY * tileSize; float maxZ = (room.MaxY + 1) * tileSize; float y = 0.15f; mesh.vertices = new Vector3[] { new Vector3(minX, y, minZ), new Vector3(maxX, y, minZ), new Vector3(maxX, y, maxZ), new Vector3(minX, y, maxZ) }; mesh.uv = new Vector2[] { new Vector2(0,0), new Vector2(1,0), new Vector2(1,1), new Vector2(0,1) }; mesh.triangles = new int[] { 0, 1, 2, 0, 2, 3 }; mesh.RecalculateNormals(); meshFilter.mesh = mesh; } private void ClearMarkers() { if (markerContainer != null) { Destroy(markerContainer); markerContainer = null; } } private void ClearFogOfWar() { if (fogOfWarContainer != null) { Destroy(fogOfWarContainer); fogOfWarContainer = null; } } /// /// Toggles fog of war visibility /// public void ToggleFogOfWar() { showFogOfWar = !showFogOfWar; UpdateFogVisibility(); } /// /// Sets fog of war visibility /// public void SetFogOfWarVisible(bool visible) { showFogOfWar = visible; UpdateFogVisibility(); } /// /// Refreshes the fog of war display (call this when entity vision changes) /// public void RefreshFogOfWar() { var maze = mazeController?.GetCurrentMaze(); if (maze != null && showFogOfWar && fogOfWar != null) { ClearFogOfWar(); RenderFogOfWar(maze); } } private void UpdateFogVisibility() { if (showFogOfWar && fogOfWar != null) { if (fogOfWarContainer == null) { var maze = mazeController?.GetCurrentMaze(); if (maze != null) { RenderFogOfWar(maze); } } else if (!fogOfWarContainer.activeSelf) { fogOfWarContainer.SetActive(true); } } else if (fogOfWarContainer != null) { fogOfWarContainer.SetActive(false); } lastFogVisible = showFogOfWar; } /// /// Creates a URP-compatible unlit material for color overlays. /// private Material CreateURPUnlitColorMaterial(Color color, bool transparent) { Shader shader = Shader.Find("Universal Render Pipeline/Unlit"); if (shader == null) { shader = Shader.Find("Unlit/Color"); } Material material = new Material(shader); // Always set base color with alpha if (material.HasProperty("_BaseColor")) { material.SetColor("_BaseColor", color); } else if (material.HasProperty("_Color")) { material.SetColor("_Color", color); } else { material.color = color; } if (transparent) { // Enable alpha blending for transparency material.SetFloat("_AlphaClip", 0); if (material.HasProperty("_Surface")) { material.SetFloat("_Surface", 1f); } if (material.HasProperty("_Blend")) { material.SetFloat("_Blend", 0f); } if (material.HasProperty("_Cull")) { material.SetFloat("_Cull", 0f); } material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent; material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); material.SetInt("_ZWrite", 0); material.EnableKeyword("_ALPHABLEND_ON"); } else if (material.HasProperty("_Cull")) { material.SetFloat("_Cull", 0f); } return material; } /// /// Renders the fog of war overlay /// private void RenderFogOfWar(MazeData maze) { if (fogOfWarContainer != null) { Destroy(fogOfWarContainer); } fogOfWarContainer = new GameObject("FogOfWar"); fogOfWarContainer.transform.parent = transform; fogOfWarContainer.transform.localPosition = Vector3.zero; fogOfWarContainer.transform.localRotation = Quaternion.identity; fogOfWarContainer.SetActive(showFogOfWar); var fogMesh = new Mesh(); var vertices = new List(); var uvs = new List(); var triangles = new List(); Material fogMaterial = CreateURPUnlitColorMaterial(fogColor, true); HashSet exploredTiles = fogOfWar.GetExploredTiles(); int vertexIndex = 0; for (int x = 0; x < maze.Width; x++) { for (int y = 0; y < maze.Height; y++) { Vector2Int tilePos = new Vector2Int(x, y); if (!exploredTiles.Contains(tilePos)) { float posX = x * tileSize; float posZ = y * tileSize; vertices.Add(new Vector3(posX, 0.08f, posZ)); vertices.Add(new Vector3(posX + tileSize, 0.08f, posZ)); vertices.Add(new Vector3(posX + tileSize, 0.08f, posZ + tileSize)); vertices.Add(new Vector3(posX, 0.08f, posZ + tileSize)); uvs.Add(new Vector2(0, 0)); uvs.Add(new Vector2(1, 0)); uvs.Add(new Vector2(1, 1)); uvs.Add(new Vector2(0, 1)); triangles.Add(vertexIndex); triangles.Add(vertexIndex + 1); triangles.Add(vertexIndex + 2); triangles.Add(vertexIndex); triangles.Add(vertexIndex + 2); triangles.Add(vertexIndex + 3); vertexIndex += 4; } } } fogMesh.SetVertices(vertices); fogMesh.SetUVs(0, uvs); fogMesh.SetTriangles(triangles, 0); fogMesh.RecalculateNormals(); fogMesh.RecalculateBounds(); GameObject fogOverlay = new GameObject("FogOverlay"); fogOverlay.transform.parent = fogOfWarContainer.transform; fogOverlay.transform.localPosition = Vector3.zero; fogOverlay.transform.localRotation = Quaternion.identity; var fogFilter = fogOverlay.AddComponent(); var fogRenderer = fogOverlay.AddComponent(); fogFilter.mesh = fogMesh; fogRenderer.material = fogMaterial; fogRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; fogRenderer.receiveShadows = false; } /// /// Creates the actual mesh data for a maze section /// private Mesh CreateMazeMesh(MazeData maze, int offsetX, int offsetY, int width, int height) { Mesh mesh = new Mesh(); List vertices = new List(); List uvs = new List(); List floorTriangles = new List(); List wallTriangles = new List(); // Generate floor and walls for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { int worldX = offsetX + x; int worldY = offsetY + y; var tile = maze.GetTile(worldX, worldY); if (tile == null) continue; float posX = x * tileSize; float posZ = y * tileSize; if (generateFloor && tile.IsWalkable()) { // Floor quad int vertStart = vertices.Count; vertices.Add(new Vector3(posX, 0, posZ)); vertices.Add(new Vector3(posX + tileSize, 0, posZ)); vertices.Add(new Vector3(posX + tileSize, 0, posZ + tileSize)); vertices.Add(new Vector3(posX, 0, posZ + tileSize)); // UVs for floor uvs.Add(new Vector2(0, 0)); uvs.Add(new Vector2(1, 0)); uvs.Add(new Vector2(1, 1)); uvs.Add(new Vector2(0, 1)); // Floor triangles floorTriangles.Add(vertStart); floorTriangles.Add(vertStart + 1); floorTriangles.Add(vertStart + 2); floorTriangles.Add(vertStart); floorTriangles.Add(vertStart + 2); floorTriangles.Add(vertStart + 3); } if (generateWalls && tile.Type == MazeTile.TileType.Wall) { // Wall quads for each side that's exposed CreateWallQuads(vertices, uvs, wallTriangles, posX, posZ, tileSize, wallHeight, maze, worldX, worldY); // Add a top cap so walls remain visible in direct top-down view if (wallCaps) { CreateWallTop(vertices, uvs, wallTriangles, posX, posZ, tileSize, wallHeight); } } } } mesh.vertices = vertices.ToArray(); mesh.uv = uvs.ToArray(); // Combine submeshes mesh.subMeshCount = (generateFloor ? 1 : 0) + (generateWalls ? 1 : 0); int submeshIndex = 0; if (generateFloor) { mesh.SetTriangles(floorTriangles.ToArray(), submeshIndex++); } if (generateWalls) { mesh.SetTriangles(wallTriangles.ToArray(), submeshIndex); } mesh.RecalculateNormals(); mesh.RecalculateBounds(); return mesh; } /// /// Creates wall quads for exposed sides /// private void CreateWallQuads(List vertices, List uvs, List triangles, float posX, float posZ, float size, float height, MazeData maze, int worldX, int worldY) { // Check each direction for exposed walls Vector2Int[] directions = { new Vector2Int(0, 1), // North new Vector2Int(1, 0), // East new Vector2Int(0, -1), // South new Vector2Int(-1, 0) // West }; foreach (var dir in directions) { int checkX = worldX + dir.x; int checkY = worldY + dir.y; // If neighbor is not a wall or is out of bounds, create wall face if (!maze.IsInBounds(checkX, checkY) || maze.GetTile(checkX, checkY).Type != MazeTile.TileType.Wall) { CreateWallQuad(vertices, uvs, triangles, posX, posZ, size, height, dir); } } } /// /// Creates a single wall quad /// private void CreateWallQuad(List vertices, List uvs, List triangles, float posX, float posZ, float size, float height, Vector2Int direction) { int vertStart = vertices.Count; // Determine quad vertices based on direction Vector3 v0, v1, v2, v3; if (direction == Vector2Int.up) // North face { v0 = new Vector3(posX, 0, posZ + size); v1 = new Vector3(posX + size, 0, posZ + size); v2 = new Vector3(posX + size, height, posZ + size); v3 = new Vector3(posX, height, posZ + size); } else if (direction == Vector2Int.right) // East face { v0 = new Vector3(posX + size, 0, posZ); v1 = new Vector3(posX + size, 0, posZ + size); v2 = new Vector3(posX + size, height, posZ + size); v3 = new Vector3(posX + size, height, posZ); } else if (direction == Vector2Int.down) // South face { v0 = new Vector3(posX + size, 0, posZ); v1 = new Vector3(posX, 0, posZ); v2 = new Vector3(posX, height, posZ); v3 = new Vector3(posX + size, height, posZ); } else // West face { v0 = new Vector3(posX, 0, posZ + size); v1 = new Vector3(posX, 0, posZ); v2 = new Vector3(posX, height, posZ); v3 = new Vector3(posX, height, posZ + size); } vertices.Add(v0); vertices.Add(v1); vertices.Add(v2); vertices.Add(v3); // UVs uvs.Add(new Vector2(0, 0)); uvs.Add(new Vector2(1, 0)); uvs.Add(new Vector2(1, 1)); uvs.Add(new Vector2(0, 1)); // Front-facing triangles triangles.Add(vertStart); triangles.Add(vertStart + 1); triangles.Add(vertStart + 2); triangles.Add(vertStart); triangles.Add(vertStart + 2); triangles.Add(vertStart + 3); // Optionally duplicate the face for the opposite side if (twoSidedWalls) { triangles.Add(vertStart + 2); triangles.Add(vertStart + 1); triangles.Add(vertStart); triangles.Add(vertStart + 3); triangles.Add(vertStart + 2); triangles.Add(vertStart + 0); } } /// /// Creates a top cap for a wall tile so it remains visible from above /// private void CreateWallTop(List vertices, List uvs, List triangles, float posX, float posZ, float size, float height) { int vertStart = vertices.Count; vertices.Add(new Vector3(posX, height, posZ)); vertices.Add(new Vector3(posX + size, height, posZ)); vertices.Add(new Vector3(posX + size, height, posZ + size)); vertices.Add(new Vector3(posX, height, posZ + size)); uvs.Add(new Vector2(0, 0)); uvs.Add(new Vector2(1, 0)); uvs.Add(new Vector2(1, 1)); uvs.Add(new Vector2(0, 1)); triangles.Add(vertStart); triangles.Add(vertStart + 1); triangles.Add(vertStart + 2); triangles.Add(vertStart); triangles.Add(vertStart + 2); triangles.Add(vertStart + 3); if (twoSidedWalls) { triangles.Add(vertStart + 2); triangles.Add(vertStart + 1); triangles.Add(vertStart); triangles.Add(vertStart + 3); triangles.Add(vertStart + 2); triangles.Add(vertStart + 0); } } /// /// Updates which chunks are visible /// private void UpdateVisibleChunks() { if (chunkObjects.Count <= 1) return; // No chunking used Vector2Int cameraChunk = WorldToChunk(cameraTransform.position); foreach (var chunk in chunkObjects) { Vector2Int chunkPos = chunk.Key; bool isVisible = Mathf.Abs(chunkPos.x - cameraChunk.x) <= renderDistance && Mathf.Abs(chunkPos.y - cameraChunk.y) <= renderDistance; chunk.Value.SetActive(isVisible); } } /// /// Converts world position to chunk coordinates /// private Vector2Int WorldToChunk(Vector3 worldPos) { return new Vector2Int( Mathf.FloorToInt(worldPos.x / (chunkSize * tileSize)), Mathf.FloorToInt(worldPos.z / (chunkSize * tileSize)) ); } /// /// Clears all chunk objects /// private void ClearChunks() { foreach (var chunk in chunkObjects.Values) { Destroy(chunk); } chunkObjects.Clear(); } /// /// Regenerates the mesh /// public void RefreshMesh() { GenerateMazeMesh(); } }