diff --git a/Assets/Scripts/GameSetup/Maze/MazeRenderer.cs b/Assets/Scripts/GameSetup/Maze/MazeRenderer.cs index 2a967976..f5128a35 100644 --- a/Assets/Scripts/GameSetup/Maze/MazeRenderer.cs +++ b/Assets/Scripts/GameSetup/Maze/MazeRenderer.cs @@ -102,203 +102,245 @@ namespace Hallucinate.GameSetup.Maze // } // } public class MazeRenderer : MonoBehaviour -{ - [SerializeField] private MazeVisualProfile visualProfile; - - private readonly Dictionary _spawnedCells = new Dictionary(); - private Transform _container; - private MazeGrid _currentGrid; // Thêm biến để lưu trữ grid phục vụ việc kiểm tra hàng xóm - - public void Initialize(MazeGrid grid, Transform container) { - _currentGrid = grid; - _container = container; - grid.OnCellChanged += HandleCellChanged; - - // Initial render - for (int z = 0; z < grid.Depth; z++) + [SerializeField] private MazeVisualProfile visualProfile; + + private readonly Dictionary _spawnedCells = new Dictionary(); + private Transform _container; + private MazeGrid _currentGrid; + + public void Initialize(MazeGrid grid, Transform container) { - for (int x = 0; x < grid.Width; x++) + _currentGrid = grid; + _container = container; + grid.OnCellChanged += HandleCellChanged; + + // Initial render + for (int z = 0; z < grid.Depth; z++) { - UpdateCellVisual(x, z, grid.GetCell(x, z), false); + for (int x = 0; x < grid.Width; x++) + { + UpdateCellVisual(x, z, grid.GetCell(x, z), false); + } } } - } - public void Clear() - { - StopAllCoroutines(); - - foreach (var cell in _spawnedCells.Values) + public void Clear() { - if (cell != null) Destroy(cell); - } - _spawnedCells.Clear(); - _currentGrid = null; - } + StopAllCoroutines(); - private void HandleCellChanged(int x, int z, MazeCellType type) - { - // 1. Cập nhật ô vừa thay đổi (Có chạy Animation) - UpdateCellVisual(x, z, type, true); - - // 2. Cập nhật 4 ô xung quanh để chúng tự nối ống với ô mới này (KHÔNG chạy Animation để tránh giật hình) - UpdateNeighborVisual(x + 1, z); - UpdateNeighborVisual(x - 1, z); - UpdateNeighborVisual(x, z + 1); - UpdateNeighborVisual(x, z - 1); - } - - private void UpdateNeighborVisual(int x, int z) - { - if (_currentGrid != null && _currentGrid.IsInBounds(x, z)) - { - // Chỉ cập nhật hình ảnh nếu hàng xóm là đường đi (tránh việc gọi vẽ lại tường gây tốn tài nguyên) - if (IsPath(x, z)) + foreach (var cell in _spawnedCells.Values) { - MazeCellType type = _currentGrid.GetCell(x, z); - UpdateCellVisual(x, z, type, false); + if (cell != null) Destroy(cell); + } + + _spawnedCells.Clear(); + _currentGrid = null; + } + + private void HandleCellChanged(int x, int z, MazeCellType type) + { + UpdateCellVisual(x, z, type, true); + + UpdateNeighborVisual(x + 1, z); + UpdateNeighborVisual(x - 1, z); + UpdateNeighborVisual(x, z + 1); + UpdateNeighborVisual(x, z - 1); + } + + private void UpdateNeighborVisual(int x, int z) + { + if (_currentGrid != null && _currentGrid.IsInBounds(x, z)) + { + if (IsPath(x, z)) + { + MazeCellType type = _currentGrid.GetCell(x, z); + UpdateCellVisual(x, z, type, false); + } } } - } - private void UpdateCellVisual(int x, int z, MazeCellType type, bool animate) - { - Vector2Int pos = new Vector2Int(x, z); - - if (_spawnedCells.TryGetValue(pos, out GameObject oldObj)) + private void UpdateCellVisual(int x, int z, MazeCellType type, bool animate) { - Destroy(oldObj); - _spawnedCells.Remove(pos); + Vector2Int pos = new Vector2Int(x, z); + + if (_spawnedCells.TryGetValue(pos, out GameObject oldObj)) + { + Destroy(oldObj); + _spawnedCells.Remove(pos); + } + + GameObject prefab = null; + Quaternion rotation = Quaternion.identity; + + if (type == MazeCellType.Corridor || type == MazeCellType.Processing) + { + (prefab, rotation) = GetCorridorPrefabAndRotation(x, z); + } + else + { + prefab = visualProfile.GetPrefab(type); + } + + if (prefab == null) return; + + float safeScale = Mathf.Max(0.001f, visualProfile.scale); + float modelScaleMultiplier = 0.335f; + + Vector3 worldPos = new Vector3(x * safeScale, 0, z * safeScale); + + GameObject newObj = Instantiate(prefab, worldPos, rotation, _container); + newObj.transform.localScale = Vector3.one * safeScale * modelScaleMultiplier; + _spawnedCells[pos] = newObj; + + if (animate && visualProfile.animationDuration > 0) + { + StartCoroutine(AnimateCell(newObj.transform)); + } } - GameObject prefab = null; - Quaternion rotation = Quaternion.identity; + // ================================================================================= + // THUẬT TOÁN BITMASK AUTO-TILING + // ================================================================================= + private (GameObject, Quaternion) GetCorridorPrefabAndRotation(int x, int z) + { + bool top = IsPath(x, z + 1); + bool right = IsPath(x + 1, z); + bool bottom = IsPath(x, z - 1); + bool left = IsPath(x - 1, z); - if (type == MazeCellType.Corridor || type == MazeCellType.Processing) - { - (prefab, rotation) = GetCorridorPrefabAndRotation(x, z); - } - else - { - prefab = visualProfile.GetPrefab(type); + 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; + } + + // --- CỘNG THÊM OFFSET (Đã xóa bỏ phần code thừa bị lặp lại) --- + 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)); } - if (prefab == null) return; - - float safeScale = Mathf.Max(0.001f, visualProfile.scale); - - // --- SỬA ĐỔI TẠI ĐÂY: Thu nhỏ prefab thành 0.5 --- - float modelScaleMultiplier = 0.5f; - - // Giữ nguyên safeScale cho worldPos để các ô vẫn cách đều nhau - Vector3 worldPos = new Vector3(x * safeScale, 0, z * safeScale); - - GameObject newObj = Instantiate(prefab, worldPos, rotation, _container); - - // Nhân thêm 0.5 vào kích thước cuối cùng của Object - newObj.transform.localScale = Vector3.one * safeScale * modelScaleMultiplier; - _spawnedCells[pos] = newObj; - - if (animate && visualProfile.animationDuration > 0) + private bool IsPath(int x, int z) { - StartCoroutine(AnimateCell(newObj.transform)); - } - } - - // ================================================================================= - // THUẬT TOÁN BITMASK AUTO-TILING - // ================================================================================= - private (GameObject, Quaternion) GetCorridorPrefabAndRotation(int x, int z) - { - bool top = IsPath(x, z + 1); - bool right = IsPath(x + 1, z); - bool bottom = IsPath(x, z - 1); - bool left = IsPath(x - 1, z); - - int mask = 0; - if (top) mask += 1; - if (right) mask += 2; - if (bottom) mask += 4; - if (left) mask += 8; - - GameObject prefabToSpawn = visualProfile.corridorDeadEnd; - float yRotation = 0f; - - switch (mask) - { - // === NGÕ CỤT (Giữ nguyên) === - 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; - - // === ĐƯỜNG THẲNG (Giữ nguyên) === - case 5: prefabToSpawn = visualProfile.corridorStraight; yRotation = 0f; break; - case 10: prefabToSpawn = visualProfile.corridorStraight; yRotation = 90f; break; - - // === GÓC CUA (Giữ nguyên) === - 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 = -90f; break; - - // === NGÃ BA (Đã điều chỉnh lại góc xoay) === - case 11: prefabToSpawn = visualProfile.corridorTJunction; yRotation = 0f; break; // Mở: Trên, Phải, Trái. Bịt: Dưới. - case 7: prefabToSpawn = visualProfile.corridorTJunction; yRotation = -90f; break; // Mở: Trên, Phải, Dưới. Bịt: Trái. - case 14: prefabToSpawn = visualProfile.corridorTJunction; yRotation = 180f; break; // Mở: Phải, Dưới, Trái. Bịt: Trên. - case 13: prefabToSpawn = visualProfile.corridorTJunction; yRotation = 90f; break; // Mở: Trên, Dưới, Trái. Bịt: Phải. - - // === NGÃ TƯ (Giữ nguyên) === - case 15: prefabToSpawn = visualProfile.corridorCross; yRotation = 0f; break; - - default: prefabToSpawn = visualProfile.corridorDeadEnd; yRotation = 0f; break; + if (_currentGrid == null || !_currentGrid.IsInBounds(x, z)) return false; + MazeCellType type = _currentGrid.GetCell(x, z); + return type == MazeCellType.Corridor + || type == MazeCellType.Processing + || type == MazeCellType.Start + || type == MazeCellType.End + || type == MazeCellType.Path; } - if (prefabToSpawn == null) prefabToSpawn = visualProfile.corridorPrefab; - - return (prefabToSpawn, Quaternion.Euler(0, yRotation, 0)); - } - - private bool IsPath(int x, int z) - { - if (_currentGrid == null || !_currentGrid.IsInBounds(x, z)) return false; - MazeCellType type = _currentGrid.GetCell(x, z); - return type == MazeCellType.Corridor - || type == MazeCellType.Processing - || type == MazeCellType.Start - || type == MazeCellType.End - || type == MazeCellType.Path; - } - - // ================================================================================= - // 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) + // ================================================================================= + // ANIMATION + // ================================================================================= + private IEnumerator AnimateCell(Transform target) { 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; - } + float duration = Mathf.Max(0.01f, visualProfile.animationDuration); + float elapsed = 0; + Vector3 finalScale = target.localScale; + target.localScale = Vector3.one * 0.001f; - if (target != null) - { - target.localScale = finalScale; + 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; + } } } } -} diff --git a/Assets/Scripts/GameSetup/Maze/MazeVisualProfile.cs b/Assets/Scripts/GameSetup/Maze/MazeVisualProfile.cs index bb5f7d8a..c7ba3303 100644 --- a/Assets/Scripts/GameSetup/Maze/MazeVisualProfile.cs +++ b/Assets/Scripts/GameSetup/Maze/MazeVisualProfile.cs @@ -5,6 +5,11 @@ namespace Hallucinate.GameSetup.Maze [CreateAssetMenu(fileName = "MazeVisualProfile", menuName = "Hallucinate/Maze/Visual Profile")] public class MazeVisualProfile : ScriptableObject { + [Header("Rotation Offsets")] + public float tJunctionOffset = 0f; + public float cornerOffset = 0f; + public float deadEndOffset = 0f; + [Header("Prefabs")] public GameObject wallPrefab; public GameObject corridorPrefab;