diff --git a/Assets/Scenes/Main Scene.unity b/Assets/Scenes/Main Scene.unity index 17c174a8..580119ba 100644 --- a/Assets/Scenes/Main Scene.unity +++ b/Assets/Scenes/Main Scene.unity @@ -628,7 +628,7 @@ Transform: m_GameObject: {fileID: 1437922948} serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} - m_LocalPosition: {x: 3.5546, y: -5.79626, z: 4.39807} + m_LocalPosition: {x: 3.5546, y: -66, z: 4.39807} m_LocalScale: {x: 50, y: 1, z: 50} m_ConstrainProportionsScale: 0 m_Children: [] diff --git a/Assets/Scripts/GameSetup/Maze/MazeRenderer.cs b/Assets/Scripts/GameSetup/Maze/MazeRenderer.cs index ab39e694..2a967976 100644 --- a/Assets/Scripts/GameSetup/Maze/MazeRenderer.cs +++ b/Assets/Scripts/GameSetup/Maze/MazeRenderer.cs @@ -8,97 +8,297 @@ 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; + // + // private readonly Dictionary _spawnedCells = new Dictionary(); + // private Transform _container; + // + // public void Initialize(MazeGrid grid, Transform container) + // { + // _container = container; + // grid.OnCellChanged += HandleCellChanged; + // + // // Initial render + // for (int z = 0; z < grid.Depth; z++) + // { + // 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) + // { + // if (cell != null) Destroy(cell); + // } + // _spawnedCells.Clear(); + // } + // + // private void HandleCellChanged(int x, int z, MazeCellType type) + // { + // UpdateCellVisual(x, z, type, true); + // } + // + // private void UpdateCellVisual(int x, int z, MazeCellType type, bool animate) + // { + // Vector2Int pos = new Vector2Int(x, z); + // + // if (_spawnedCells.TryGetValue(pos, out GameObject oldObj)) + // { + // Destroy(oldObj); + // _spawnedCells.Remove(pos); + // } + // + // GameObject prefab = visualProfile.GetPrefab(type); + // if (prefab == null) return; + // + // // Ensure scale is always positive to avoid BoxCollider issues + // float safeScale = Mathf.Max(0.001f, visualProfile.scale); + // Vector3 worldPos = new Vector3(x * safeScale, 0, z * safeScale); + // + // GameObject newObj = Instantiate(prefab, worldPos, Quaternion.identity, _container); + // newObj.transform.localScale = Vector3.one * safeScale; + // _spawnedCells[pos] = newObj; + // + // if (animate && visualProfile.animationDuration > 0) + // { + // StartCoroutine(AnimateCell(newObj.transform)); + // } + // } + // + // 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; // Use tiny positive instead of zero + // + // 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); + // + // // Ensure s is never negative + // target.localScale = finalScale * Mathf.Max(0.001f, s); + // yield return null; + // } + // + // if (target != null) + // { + // target.localScale = finalScale; + // } + // } + // } 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) { - [SerializeField] private MazeVisualProfile visualProfile; + _currentGrid = grid; + _container = container; + grid.OnCellChanged += HandleCellChanged; - private readonly Dictionary _spawnedCells = new Dictionary(); - private Transform _container; - - public void Initialize(MazeGrid grid, Transform container) + // Initial render + for (int z = 0; z < grid.Depth; z++) { - _container = container; - grid.OnCellChanged += HandleCellChanged; - - // Initial render - for (int z = 0; z < grid.Depth; z++) + for (int x = 0; x < grid.Width; x++) { - 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) - { - if (cell != null) Destroy(cell); - } - _spawnedCells.Clear(); - } - - private void HandleCellChanged(int x, int z, MazeCellType type) - { - UpdateCellVisual(x, z, type, true); - } - - private void UpdateCellVisual(int x, int z, MazeCellType type, bool animate) - { - Vector2Int pos = new Vector2Int(x, z); - - if (_spawnedCells.TryGetValue(pos, out GameObject oldObj)) - { - Destroy(oldObj); - _spawnedCells.Remove(pos); - } - - GameObject prefab = visualProfile.GetPrefab(type); - if (prefab == null) return; - - // Ensure scale is always positive to avoid BoxCollider issues - float safeScale = Mathf.Max(0.001f, visualProfile.scale); - Vector3 worldPos = new Vector3(x * safeScale, 0, z * safeScale); - - GameObject newObj = Instantiate(prefab, worldPos, Quaternion.identity, _container); - newObj.transform.localScale = Vector3.one * safeScale; - _spawnedCells[pos] = newObj; - - if (animate && visualProfile.animationDuration > 0) - { - StartCoroutine(AnimateCell(newObj.transform)); - } - } - - 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; // Use tiny positive instead of zero - - 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); - - // Ensure s is never negative - target.localScale = finalScale * Mathf.Max(0.001f, s); - yield return null; - } - - if (target != null) - { - target.localScale = finalScale; + UpdateCellVisual(x, z, grid.GetCell(x, z), false); } } } + + public void Clear() + { + StopAllCoroutines(); + + foreach (var cell in _spawnedCells.Values) + { + if (cell != null) Destroy(cell); + } + _spawnedCells.Clear(); + _currentGrid = null; + } + + 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)) + { + 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)) + { + 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); + + // --- 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) + { + 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 (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) + { + 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 362aa0ca..bb5f7d8a 100644 --- a/Assets/Scripts/GameSetup/Maze/MazeVisualProfile.cs +++ b/Assets/Scripts/GameSetup/Maze/MazeVisualProfile.cs @@ -12,6 +12,13 @@ namespace Hallucinate.GameSetup.Maze public GameObject pathPrefab; public GameObject startPrefab; public GameObject endPrefab; + + [Header("Corridor Types")] + public GameObject corridorStraight; + public GameObject corridorCorner; + public GameObject corridorTJunction; + public GameObject corridorCross; + public GameObject corridorDeadEnd; [Header("Visualization Settings")] public float scale = 1f; diff --git a/Assets/Settings/Project Setting/DefaultMazeProfile.asset b/Assets/Settings/Project Setting/DefaultMazeProfile.asset index 6f4d5813..02b585af 100644 --- a/Assets/Settings/Project Setting/DefaultMazeProfile.asset +++ b/Assets/Settings/Project Setting/DefaultMazeProfile.asset @@ -13,10 +13,15 @@ MonoBehaviour: m_Name: DefaultMazeProfile m_EditorClassIdentifier: Assembly-CSharp::Hallucinate.GameSetup.Maze.MazeVisualProfile wallPrefab: {fileID: 865692088774546613, guid: c49f25c5b1c3e4b43a2bc56717387124, type: 3} - corridorPrefab: {fileID: 7041334646084879511, guid: 4872dd25f62fbfa48b25a2b636aa6865, type: 3} + corridorPrefab: {fileID: 919132149155446097, guid: 4721de3f311aa364e9609e41c5a28665, type: 3} processingPrefab: {fileID: 1560533784803380970, guid: 1e8b6ed6b01405e4b9e358abc8f7a058, type: 3} pathPrefab: {fileID: 0} startPrefab: {fileID: 0} endPrefab: {fileID: 0} + corridorStraight: {fileID: 919132149155446097, guid: 4721de3f311aa364e9609e41c5a28665, type: 3} + corridorCorner: {fileID: 919132149155446097, guid: 075b1b03af4930445823f0aadfe82cef, type: 3} + corridorTJunction: {fileID: 919132149155446097, guid: c57b571fe619e894c833cf1d9e5c057a, type: 3} + corridorCross: {fileID: 919132149155446097, guid: 25a6a5f070d975c48a85d820c7f78438, type: 3} + corridorDeadEnd: {fileID: 919132149155446097, guid: 710f9f3daf0b6fa48ab017ff20db25ac, type: 3} scale: 1 animationDuration: 0.25