using System.Collections; using System.Collections.Generic; 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 { [SerializeField] private MazeVisualProfile visualProfile; public float floorHeight = 3.5f; public float Scale => visualProfile != null ? visualProfile.scale : 1f; 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 (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) Destroy(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(); } 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)) { Destroy(oldObj); _spawnedCells.Remove(posKey); } GameObject prefab = null; Quaternion rotation = Quaternion.identity; if (type == MazeCellType.Corridor || type == MazeCellType.Processing) { (prefab, rotation) = GetCorridorPrefabAndRotation(grid, x, z); } else { prefab = visualProfile.GetPrefab(type); } if (prefab == null) return; float safeScale = Mathf.Max(0.001f, visualProfile.scale); float modelScaleMultiplier = 0.25f; float yOffset = grid.Level * floorHeight; Vector3 localPos = new Vector3(x * safeScale, yOffset, z * safeScale); GameObject newObj = Instantiate(prefab, _container); newObj.transform.localPosition = localPos; newObj.transform.localRotation = rotation; newObj.transform.localScale = Vector3.one * safeScale * modelScaleMultiplier; _spawnedCells[posKey] = newObj; if (animate && visualProfile.animationDuration > 0) { StartCoroutine(AnimateCell(newObj.transform)); } } // ================================================================================= // THUẬT TOÁN BITMASK AUTO-TILING // ================================================================================= private (GameObject, Quaternion) GetCorridorPrefabAndRotation(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; switch (mask) { case 1: prefabToSpawn = visualProfile.corridorDeadEnd; yRotation = 180f; break; case 2: prefabToSpawn = visualProfile.corridorDeadEnd; yRotation = 270f; break; case 4: prefabToSpawn = visualProfile.corridorDeadEnd; yRotation = 0f; break; case 8: prefabToSpawn = visualProfile.corridorDeadEnd; yRotation = 90f; 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 = 180f; break; case 7: prefabToSpawn = visualProfile.corridorTJunction; yRotation = 270f; break; case 14: prefabToSpawn = visualProfile.corridorTJunction; yRotation = 0f; break; case 13: prefabToSpawn = visualProfile.corridorTJunction; yRotation = 90f; 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)); } 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.StairUp || type == MazeCellType.StairDown; } // ================================================================================= // ANIMATION // ================================================================================= private IEnumerator AnimateCell(Transform target) { if (target == null) yield break; float duration = Mathf.Max(0.01f, visualProfile.animationDuration); float elapsed = 0; Vector3 finalScale = target.localScale; target.localScale = Vector3.one * 0.001f; while (elapsed < duration) { if (target == null) yield break; elapsed += Time.deltaTime; float t = Mathf.Clamp01(elapsed / duration); float s = Mathf.Sin(t * Mathf.PI * 0.5f); target.localScale = finalScale * Mathf.Max(0.001f, s); yield return null; } if (target != null) { target.localScale = finalScale; } } } }