Compare commits

2 Commits

Author SHA1 Message Date
Lucastaa
51edae7bdd Merge branch 'main' of https://scove-vault.duckdns.org/scove/HALLUCINATION 2026-05-01 20:50:41 +07:00
Lucastaa
b43255d1f2 Change 2026-05-01 20:50:08 +07:00
2 changed files with 219 additions and 172 deletions

View File

@@ -102,203 +102,245 @@ namespace Hallucinate.GameSetup.Maze
// }
// }
public class MazeRenderer : MonoBehaviour
{
[SerializeField] private MazeVisualProfile visualProfile;
private readonly Dictionary<Vector2Int, GameObject> _spawnedCells = new Dictionary<Vector2Int, GameObject>();
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<Vector2Int, GameObject> _spawnedCells = new Dictionary<Vector2Int, GameObject>();
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;
}
}
}
}
}

View File

@@ -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;