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() ?? FindAnyObjectByType();
}
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();
}
}