using System.Collections; using System.Collections.Generic; using Sirenix.OdinInspector; using UnityEngine; namespace Hallucinate.GameSetup.Maze { /// /// Responsible for the visual representation of the maze. /// Handles spawning, pooling, and animations with safety checks. /// public class MazeRenderer : MonoBehaviour { [BoxGroup("Visuals")] [Required] [InlineEditor] [SerializeField] private MazeVisualProfile visualProfile; [BoxGroup("Visuals")] [MinValue(0.001f)] public float floorHeight = 3.5f; public float Scale => visualProfile != null ? visualProfile.scale : 1f; public enum CellAnimationType { None, ScaleUp, DropDown, SpinIn } [HideInInspector] public CellAnimationType currentAnimationType = CellAnimationType.ScaleUp; [ShowInInspector] [ReadOnly] [BoxGroup("Runtime")] private int SpawnedCellCount => _spawnedCells.Count; [ShowInInspector] [ReadOnly] [BoxGroup("Runtime")] private int RenderedFloorCount => _grids.Count; private readonly Dictionary _spawnedCells = new Dictionary(); private Transform _container; private List _grids = new List(); public void Initialize(MazeGrid grid, Transform container, bool clearExisting = true) { if (visualProfile == null) { Debug.LogError("MazeRenderer needs a MazeVisualProfile before it can render.", this); return; } if (grid == null) { Debug.LogError("MazeRenderer received a null MazeGrid.", this); return; } if (clearExisting) { Clear(); } _container = container; if (!_grids.Contains(grid)) { _grids.Add(grid); grid.OnCellChanged += (x, z, type) => HandleCellChanged(grid, x, z, type); } // Initial render for (int z = 0; z < grid.Depth; z++) { for (int x = 0; x < grid.Width; x++) { UpdateCellVisual(grid, x, z, grid.GetCell(x, z), false); } } } public void Clear() { StopAllCoroutines(); foreach (var cell in _spawnedCells.Values) { if (cell == null) continue; if (Application.isPlaying) Destroy(cell); else DestroyImmediate(cell); } _spawnedCells.Clear(); foreach (var grid in _grids) { // Note: We can't easily unsubscribe because the lambda captures 'grid'. // In a production environment, we should use a proper event handler method. } _grids.Clear(); } [Button("Clear Spawned Maze")] private void ClearFromInspector() { Clear(); } private void HandleCellChanged(MazeGrid grid, int x, int z, MazeCellType type) { UpdateCellVisual(grid, x, z, type, true); UpdateNeighborVisual(grid, x + 1, z); UpdateNeighborVisual(grid, x - 1, z); UpdateNeighborVisual(grid, x, z + 1); UpdateNeighborVisual(grid, x, z - 1); } private void UpdateNeighborVisual(MazeGrid grid, int x, int z) { if (grid != null && grid.IsInBounds(x, z)) { if (IsPath(grid, x, z)) { MazeCellType type = grid.GetCell(x, z); UpdateCellVisual(grid, x, z, type, false); } } } private void UpdateCellVisual(MazeGrid grid, int x, int z, MazeCellType type, bool animate) { Vector3Int posKey = new Vector3Int(x, grid.Level, z); if (_spawnedCells.TryGetValue(posKey, out GameObject oldObj)) { if (oldObj != null) DestroyImmediate(oldObj); _spawnedCells.Remove(posKey); } if (type == MazeCellType.Wall) return; float logicalSpacing = visualProfile.nodeSpacing; // Distances between Nodes float halfSpacing = logicalSpacing / 2f; float safeScale = Mathf.Max(0.001f, visualProfile.scale); float spacingScale = logicalSpacing * safeScale; float yOffset = grid.Level * floorHeight; Vector3 localPos = new Vector3(x * spacingScale, yOffset, z * spacingScale); GameObject cellParent = new GameObject($"Cell_{x}_{grid.Level}_{z}"); cellParent.transform.SetParent(_container); cellParent.transform.localPosition = localPos; bool spawnedAnything = false; if (type == MazeCellType.Corridor || type == MazeCellType.Processing || type == MazeCellType.StairsUp || type == MazeCellType.StairsDown) { // 1. Spawn Node (Intersection, Corner, T, DeadEnd, or Stairs) GameObject nodePrefab; Quaternion nodeRot; Vector3 nodeOffset = Vector3.zero; if (type == MazeCellType.StairsUp || type == MazeCellType.StairsDown) { nodePrefab = visualProfile.GetPrefab(type); nodeRot = Quaternion.Euler(0, visualProfile.stairsOffset, 0); } else { (nodePrefab, nodeRot, nodeOffset) = GetNodePrefabAndRotation(grid, x, z); } if (nodePrefab != null) { GameObject node = Instantiate(nodePrefab, cellParent.transform); node.transform.localPosition = nodeOffset * safeScale; node.transform.localRotation = nodeRot; node.transform.localScale = Vector3.one * safeScale; spawnedAnything = true; } // 2. Spawn Edge X (Right path) if (IsPath(grid, x + 1, z)) { GameObject edgePrefab = visualProfile.corridorStraight; if (edgePrefab != null) { GameObject edgeX = Instantiate(edgePrefab, cellParent.transform); edgeX.transform.localPosition = new Vector3(halfSpacing * safeScale, 0, 0); // half units offset right edgeX.transform.localRotation = Quaternion.Euler(0, 90f, 0); // pointing along X edgeX.transform.localScale = Vector3.one * safeScale; spawnedAnything = true; } } // 3. Spawn Edge Z (Top path) if (IsPath(grid, x, z + 1)) { GameObject edgePrefab = visualProfile.corridorStraight; if (edgePrefab != null) { GameObject edgeZ = Instantiate(edgePrefab, cellParent.transform); edgeZ.transform.localPosition = new Vector3(0, 0, halfSpacing * safeScale); // half units offset forward edgeZ.transform.localRotation = Quaternion.identity; // pointing along Z edgeZ.transform.localScale = Vector3.one * safeScale; spawnedAnything = true; } } } else if (type == MazeCellType.Room) { // Spawn Floor if (visualProfile.roomFloorPrefab != null) { GameObject floor = Instantiate(visualProfile.roomFloorPrefab, cellParent.transform); floor.transform.localPosition = Vector3.zero; floor.transform.localScale = Vector3.one * safeScale; spawnedAnything = true; } // Spawn Ceiling if (visualProfile.roomCeilingPrefab != null) { GameObject ceiling = Instantiate(visualProfile.roomCeilingPrefab, cellParent.transform); ceiling.transform.localPosition = new Vector3(0, floorHeight, 0); ceiling.transform.localScale = Vector3.one * safeScale; spawnedAnything = true; } // Spawn Room Edges (Walls or Doors) MazeCellType top = grid.IsInBounds(x, z + 1) ? grid.GetCell(x, z + 1) : MazeCellType.Wall; SpawnRoomEdge(cellParent, top, new Vector3(0, 0, halfSpacing * safeScale), 0f, safeScale); MazeCellType right = grid.IsInBounds(x + 1, z) ? grid.GetCell(x + 1, z) : MazeCellType.Wall; SpawnRoomEdge(cellParent, right, new Vector3(halfSpacing * safeScale, 0, 0), 90f, safeScale); MazeCellType bottom = grid.IsInBounds(x, z - 1) ? grid.GetCell(x, z - 1) : MazeCellType.Wall; SpawnRoomEdge(cellParent, bottom, new Vector3(0, 0, -halfSpacing * safeScale), 180f, safeScale); MazeCellType left = grid.IsInBounds(x - 1, z) ? grid.GetCell(x - 1, z) : MazeCellType.Wall; SpawnRoomEdge(cellParent, left, new Vector3(-halfSpacing * safeScale, 0, 0), 270f, safeScale); spawnedAnything = true; // Always true if it reaches here } else { // Non-corridor logic (Start, End, etc) GameObject prefab = visualProfile.GetPrefab(type); if (prefab != null) { GameObject obj = Instantiate(prefab, cellParent.transform); obj.transform.localPosition = Vector3.zero; obj.transform.localRotation = Quaternion.identity; obj.transform.localScale = Vector3.one * safeScale; spawnedAnything = true; } } if (!spawnedAnything) { DestroyImmediate(cellParent); return; } _spawnedCells[posKey] = cellParent; if (animate && visualProfile.animationDuration > 0) { StartCoroutine(AnimateCell(cellParent.transform)); } } // ================================================================================= // THUẬT TOÁN BITMASK AUTO-TILING // ================================================================================= private (GameObject, Quaternion, Vector3) GetNodePrefabAndRotation(MazeGrid grid, int x, int z) { bool top = IsPath(grid, x, z + 1); bool right = IsPath(grid, x + 1, z); bool bottom = IsPath(grid, x, z - 1); bool left = IsPath(grid, x - 1, z); int mask = 0; if (top) mask += 1; if (right) mask += 2; if (bottom) mask += 4; if (left) mask += 8; GameObject prefabToSpawn = null; float yRotation = 0f; Vector3 offset = Vector3.zero; // Push dead ends to the boundary where edges end float endOffset = visualProfile.deadEndShift; switch (mask) { case 1: prefabToSpawn = visualProfile.corridorDeadEnd; yRotation = 0f; offset = new Vector3(0, 0, endOffset); break; case 2: prefabToSpawn = visualProfile.corridorDeadEnd; yRotation = 90f; offset = new Vector3(endOffset, 0, 0); break; case 4: prefabToSpawn = visualProfile.corridorDeadEnd; yRotation = 180f; offset = new Vector3(0, 0, -endOffset); break; case 8: prefabToSpawn = visualProfile.corridorDeadEnd; yRotation = 270f; offset = new Vector3(-endOffset, 0, 0); break; case 5: prefabToSpawn = visualProfile.corridorStraight; yRotation = 0f; break; case 10: prefabToSpawn = visualProfile.corridorStraight; yRotation = 90f; break; case 3: prefabToSpawn = visualProfile.corridorCorner; yRotation = 0f; break; case 6: prefabToSpawn = visualProfile.corridorCorner; yRotation = 90f; break; case 12: prefabToSpawn = visualProfile.corridorCorner; yRotation = 180f; break; case 9: prefabToSpawn = visualProfile.corridorCorner; yRotation = 270f; break; case 11: prefabToSpawn = visualProfile.corridorTJunction; yRotation = 0f; break; case 7: prefabToSpawn = visualProfile.corridorTJunction; yRotation = 90f; break; case 14: prefabToSpawn = visualProfile.corridorTJunction; yRotation = 180f; break; case 13: prefabToSpawn = visualProfile.corridorTJunction; yRotation = 270f; break; case 15: prefabToSpawn = visualProfile.corridorCross; yRotation = 0f; break; default: prefabToSpawn = visualProfile.corridorDeadEnd; yRotation = 0f; break; } float finalRotation = yRotation; if (prefabToSpawn == visualProfile.corridorTJunction) finalRotation += visualProfile.tJunctionOffset; if (prefabToSpawn == visualProfile.corridorDeadEnd) finalRotation += visualProfile.deadEndOffset; if (prefabToSpawn == visualProfile.corridorCorner) finalRotation += visualProfile.cornerOffset; if (prefabToSpawn == null) prefabToSpawn = visualProfile.corridorPrefab; return (prefabToSpawn, Quaternion.Euler(0, finalRotation, 0), offset); } private void SpawnRoomEdge(GameObject parent, MazeCellType neighborType, Vector3 offset, float yRot, float safeScale) { if (neighborType == MazeCellType.Room) return; // Open space to another room cell GameObject prefabToSpawn = null; if (neighborType == MazeCellType.Corridor || neighborType == MazeCellType.Processing || neighborType == MazeCellType.Start || neighborType == MazeCellType.End) { prefabToSpawn = visualProfile.roomDoorwayPrefab; } else { prefabToSpawn = visualProfile.roomWallPrefab; } if (prefabToSpawn != null) { GameObject edge = Instantiate(prefabToSpawn, parent.transform); edge.transform.localPosition = offset; edge.transform.localRotation = Quaternion.Euler(0, yRot, 0); edge.transform.localScale = Vector3.one * safeScale; } } private bool IsPath(MazeGrid grid, int x, int z) { if (grid == null || !grid.IsInBounds(x, z)) return false; MazeCellType type = grid.GetCell(x, z); return type == MazeCellType.Corridor || type == MazeCellType.Processing || type == MazeCellType.Start || type == MazeCellType.End || type == MazeCellType.Path || type == MazeCellType.StairsUp || type == MazeCellType.StairsDown; } // ================================================================================= // ANIMATION // ================================================================================= private IEnumerator AnimateCell(Transform target) { if (target == null) yield break; if (currentAnimationType == CellAnimationType.None) yield break; float duration = Mathf.Max(0.01f, visualProfile.animationDuration); float elapsed = 0; Vector3 finalScale = target.localScale; Vector3 finalPosition = target.localPosition; Quaternion finalRotation = target.localRotation; if (currentAnimationType == CellAnimationType.ScaleUp) { target.localScale = Vector3.one * 0.001f; } else if (currentAnimationType == CellAnimationType.DropDown) { target.localPosition = finalPosition + Vector3.up * 5f; } else if (currentAnimationType == CellAnimationType.SpinIn) { target.localScale = Vector3.one * 0.001f; target.localRotation = finalRotation * Quaternion.Euler(0, 180, 0); } while (elapsed < duration) { if (target == null) yield break; elapsed += Time.deltaTime; float t = Mathf.Clamp01(elapsed / duration); if (currentAnimationType == CellAnimationType.ScaleUp) { float s = Mathf.Sin(t * Mathf.PI * 0.5f); target.localScale = finalScale * Mathf.Max(0.001f, s); } else if (currentAnimationType == CellAnimationType.DropDown) { float s = Mathf.Sin(t * Mathf.PI * 0.5f); target.localPosition = Vector3.Lerp(finalPosition + Vector3.up * 5f, finalPosition, s); } else if (currentAnimationType == CellAnimationType.SpinIn) { float s = Mathf.Sin(t * Mathf.PI * 0.5f); target.localScale = finalScale * Mathf.Max(0.001f, s); target.localRotation = Quaternion.Lerp(finalRotation * Quaternion.Euler(0, 180, 0), finalRotation, s); } yield return null; } if (target != null) { target.localScale = finalScale; target.localPosition = finalPosition; target.localRotation = finalRotation; } } } }