This commit is contained in:
Lucastaa
2026-05-08 08:58:01 +07:00
16 changed files with 408 additions and 367 deletions

View File

@@ -7,6 +7,7 @@ namespace Hallucinate.GameSetup.Maze
/// Holds the logical state of the maze grid.
/// Notifies listeners whenever a cell changes to trigger visual updates.
/// </summary>
[Serializable]
public class MazeGrid
{
public int Width { get; set; }

View File

@@ -5,34 +5,43 @@ using UnityEngine;
namespace Hallucinate.GameSetup.Maze
{
/// <summary>
/// Central controller for the Multi-floor Maze system.
/// Handles initialization order and cross-floor connections.
/// Central controller for the Maze system.
/// Manages algorithm selection, debug speed, and regeneration.
/// </summary>
public class MazeManager : MonoBehaviour
{
public enum AlgorithmType { Recursive, Wilsons, Prims, Crawler }
[Header("System Settings")]
[Tooltip("Số lượng tầng mê cung cần sinh ra")]
[Range(1, 10)] public int floorCount = 2;
public float floorHeight = 4.0f;
[Header("System Settings")]
public MazeGrid[] mazes;
public float floorHeight = 3.5f;
public int connectionsPerFloor = 2;
[Header("Grid Settings")]
[SerializeField] private AlgorithmType selectedAlgorithm;
[SerializeField] private int width = 20;
[SerializeField] private int depth = 20;
[SerializeField] private int width = 30;
[SerializeField] private int depth = 30;
[Header("Debug Settings")]
[SerializeField] private bool debugMode = true;
[Range(0.001f, 0.5f)]
[SerializeField] private float visualizationInterval = 0.05f;
[Header("References")]
[SerializeField] private MazeRenderer rendererPrefab;
[SerializeField] private MazeRenderer mazeRenderer;
[SerializeField] private Transform mazeContainer;
private List<MazeGrid> _mazeFloors = new List<MazeGrid>();
private List<MazeRenderer> _activeRenderers = new List<MazeRenderer>();
[Header("Corridor Setting")]
public GameObject straightManHoleLadder;
public GameObject straightManHoleUp;
public GameObject deadendManHoleLadder;
public GameObject deadendManHoleUp;
private MazeGrid _grid;
private Coroutine _generationCoroutine;
private void Start()
{
Debug.Log("--- Tớ là: " + gameObject.name + " đang gọi Regenerate! ---");
Regenerate();
}
@@ -44,117 +53,94 @@ namespace Hallucinate.GameSetup.Maze
}
}
//[ContextMenu("Regenerate")]
[ContextMenu("Regenerate")]
public void Regenerate()
{
StopAllCoroutines();
ClearExistingMaze();
// 1. Khởi tạo dữ liệu các tầng (Logic Data)
for (int i = 0; i < floorCount; i++)
if (_generationCoroutine != null)
{
// FIX: Khởi tạo Object trước khi gán thuộc tính
MazeGrid newFloor = new MazeGrid(width, depth);
newFloor.Level = i;
// 2. Chạy thuật toán sinh mê cung cho tầng này (Sync để đảm bảo có dữ liệu trước khi nối tầng)
IMazeAlgorithm algorithm = GetAlgorithm(selectedAlgorithm);
algorithm.Generate(newFloor);
_mazeFloors.Add(newFloor);
StopCoroutine(_generationCoroutine);
}
// 3. Tạo điểm nối cầu thang (Stairs) giữa các tầng kề nhau
ConnectFloors();
mazeRenderer.Clear();
// 4. Hiển thị 3D cho tất cả các tầng
RenderAllFloors();
}
private void ConnectFloors()
{
if (_mazeFloors.Count < 2) return;
for (int i = 0; i < _mazeFloors.Count - 1; i++)
// Step 1: Initialize all maze floors
for (int i = 0; i < mazes.Length; i++)
{
MazeGrid currentFloor = _mazeFloors[i];
MazeGrid nextFloor = _mazeFloors[i + 1];
mazes[i] = new MazeGrid(width, depth);
mazes[i].Level = i;
// Generate each floor using the selected algorithm
IMazeAlgorithm algorithmForFloor = GetAlgorithm(selectedAlgorithm);
algorithmForFloor.Generate(mazes[i]);
}
List<Vector2Int> overlapPaths = new List<Vector2Int>();
// Step 2: Create connections between adjacent floors
for (int i = 0; i < mazes.Length - 1; i++)
{
MazeGrid currentFloor = mazes[i];
MazeGrid nextFloor = mazes[i + 1];
// Tìm các tọa độ (x, z) mà cả 2 tầng đều là đường đi (Corridor)
for (int z = 1; z < depth - 1; z++)
List<Vector2Int> possibleConnections = new List<Vector2Int>();
for (int z = 0; z < depth; z++)
{
for (int x = 1; x < width - 1; x++)
for (int x = 0; x < width; x++)
{
if (currentFloor.GetCell(x, z) == MazeCellType.Corridor &&
nextFloor.GetCell(x, z) == MazeCellType.Corridor)
// Check if both floors have a corridor at this position
bool isCurrentFloorPath = currentFloor.GetCell(x, z) == MazeCellType.Corridor;
bool isNextFloorPath = nextFloor.GetCell(x, z) == MazeCellType.Corridor;
if (isCurrentFloorPath && isNextFloorPath)
{
overlapPaths.Add(new Vector2Int(x, z));
possibleConnections.Add(new Vector2Int(x, z));
}
}
}
// Trộn ngẫu nhiên danh sách để điểm nối rải rác
ShuffleList(overlapPaths);
ShuffleList(possibleConnections);
int actualConnections = Mathf.Min(connectionsPerFloor, overlapPaths.Count);
for (int j = 0; j < actualConnections; j++)
int connectionsMade = 0;
foreach (Vector2Int pos in possibleConnections)
{
Vector2Int pos = overlapPaths[j];
// Đánh dấu logic để Renderer tự chọn Prefab cầu thang từ VisualProfile
currentFloor.SetCell(pos.x, pos.y, MazeCellType.StairsUp);
nextFloor.SetCell(pos.x, pos.y, MazeCellType.StairsDown);
if (connectionsMade >= connectionsPerFloor) break;
int x = pos.x;
int z = pos.y;
// Set stair cells
currentFloor.SetCell(x, z, MazeCellType.StairUp);
nextFloor.SetCell(x, z, MazeCellType.StairDown);
connectionsMade++;
}
}
}
private void RenderAllFloors()
{
Debug.Log("--- Đang chạy hàm này! ---");
for (int i = 0; i < _mazeFloors.Count; i++)
// Step 3: Render all floors
if (mazes.Length > 0)
{
if (_activeRenderers.Count > 100) {
Debug.LogError("DỪNG LẠI! Quá nhiều tầng rồi!");
return;
}
// Tạo một Renderer instance cho mỗi tầng
MazeRenderer floorRenderer = Instantiate(rendererPrefab, mazeContainer);
floorRenderer.gameObject.name = $"Floor_Renderer_{i}";
// Đặt vị trí Y của tầng
floorRenderer.transform.localPosition = new Vector3(0, i * floorHeight, 0);
// Khởi tạo hiển thị cho Grid tương ứng
floorRenderer.Initialize(_mazeFloors[i], floorRenderer.transform);
_activeRenderers.Add(floorRenderer);
}
}
private void ClearExistingMaze()
{
foreach (var renderer in _activeRenderers)
{
if (renderer != null)
{
renderer.Clear();
Destroy(renderer.gameObject);
}
}
_activeRenderers.Clear();
_mazeFloors.Clear();
// Xóa sạch các object con cũ trong container (nếu có)
if (mazeContainer != null)
{
foreach (Transform child in mazeContainer)
for (int i = 0; i < mazes.Length; i++)
{
Destroy(child.gameObject);
mazeRenderer.Initialize(mazes[i], mazeContainer, i == 0);
}
_grid = mazes[0];
}
else
{
_grid = new MazeGrid(width, depth);
mazeRenderer.Initialize(_grid, mazeContainer);
IMazeAlgorithm algorithm = GetAlgorithm(selectedAlgorithm);
if (debugMode)
{
_generationCoroutine = StartCoroutine(algorithm.GenerateStepByStep(_grid, visualizationInterval));
}
else
{
algorithm.Generate(_grid);
}
}
}
private void ShuffleList<T>(List<T> list)
{
for (int i = 0; i < list.Count; i++)
@@ -178,4 +164,4 @@ namespace Hallucinate.GameSetup.Maze
};
}
}
}
}

View File

@@ -8,124 +8,39 @@ namespace Hallucinate.GameSetup.Maze
/// Responsible for the visual representation of the maze.
/// Handles spawning, pooling, and animations with safety checks.
/// </summary>
// public class MazeRenderer : MonoBehaviour
// {
// [SerializeField] private MazeVisualProfile visualProfile;
//
// private readonly Dictionary<Vector2Int, GameObject> _spawnedCells = new Dictionary<Vector2Int, GameObject>();
// 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;
public float floorHeight = 3.5f;
private readonly Dictionary<Vector2Int, GameObject> _spawnedCells = new Dictionary<Vector2Int, GameObject>();
public float Scale => visualProfile != null ? visualProfile.scale : 1f;
private readonly Dictionary<Vector3Int, GameObject> _spawnedCells = new Dictionary<Vector3Int, GameObject>();
private Transform _container;
private MazeGrid _currentGrid;
private List<MazeGrid> _grids = new List<MazeGrid>();
public void Initialize(MazeGrid grid, Transform container)
public void Initialize(MazeGrid grid, Transform container, bool clearExisting = true)
{
_currentGrid = grid;
if (clearExisting)
{
Clear();
}
_container = container;
if (!_grids.Contains(grid))
{
_grids.Add(grid);
grid.OnCellChanged += (x, z, type) => HandleCellChanged(grid, x, z, type);
}
// ĐỪNG đăng ký cái này vội: 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);
UpdateCellVisual(grid, x, z, grid.GetCell(x, z), false);
}
}
// ĐĂNG KÝ Ở ĐÂY: Sau khi đã vẽ xong hết rồi mới nghe ngóng thay đổi
grid.OnCellChanged += HandleCellChanged;
}
public void Clear()
@@ -138,52 +53,53 @@ namespace Hallucinate.GameSetup.Maze
}
_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))
foreach (var grid in _grids)
{
if (IsPath(x, z))
// 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 = _currentGrid.GetCell(x, z);
UpdateCellVisual(x, z, type, false);
MazeCellType type = grid.GetCell(x, z);
UpdateCellVisual(grid, x, z, type, false);
}
}
}
private void UpdateCellVisual(int x, int z, MazeCellType type, bool animate)
private void UpdateCellVisual(MazeGrid grid, int x, int z, MazeCellType type, bool animate)
{
Vector2Int pos = new Vector2Int(x, z);
Vector3Int posKey = new Vector3Int(x, grid.Level, z);
// 1. DÙNG DestroyImmediate ĐỂ XÓA THẬT SỰ TRƯỚC KHI TẠO MỚI
if (_spawnedCells.TryGetValue(pos, out GameObject oldObj))
if (_spawnedCells.TryGetValue(posKey, out GameObject oldObj))
{
if (oldObj != null)
{
DestroyImmediate(oldObj);
}
_spawnedCells.Remove(pos);
Destroy(oldObj);
_spawnedCells.Remove(posKey);
}
GameObject prefab = null;
Quaternion rotation = Quaternion.identity;
// Logic xét duyệt loại prefab
if (type == MazeCellType.Corridor || type == MazeCellType.Processing)
{
(prefab, rotation) = GetCorridorPrefabAndRotation(x, z);
(prefab, rotation) = GetCorridorPrefabAndRotation(grid, x, z);
}
else
{
@@ -193,17 +109,17 @@ namespace Hallucinate.GameSetup.Maze
if (prefab == null) return;
float safeScale = Mathf.Max(0.001f, visualProfile.scale);
float modelScaleMultiplier = 0.167f;
float modelScaleMultiplier = 0.25f;
Vector3 localPos = new Vector3(x * safeScale, 0, z * safeScale);
float yOffset = grid.Level * floorHeight;
Vector3 localPos = new Vector3(x * safeScale, yOffset, z * safeScale);
// GameObject newObj = Instantiate(prefab, worldPos, rotation, _container);
GameObject newObj = Instantiate(prefab, _container);
newObj.transform.localPosition = localPos;
newObj.transform.localRotation = rotation;
newObj.transform.localScale = Vector3.one * safeScale * modelScaleMultiplier;
_spawnedCells[pos] = newObj;
_spawnedCells[posKey] = newObj;
if (animate && visualProfile.animationDuration > 0)
{
@@ -214,12 +130,12 @@ namespace Hallucinate.GameSetup.Maze
// =================================================================================
// THUẬT TOÁN BITMASK AUTO-TILING
// =================================================================================
private (GameObject, Quaternion) GetCorridorPrefabAndRotation(int x, int z)
private (GameObject, Quaternion) GetCorridorPrefabAndRotation(MazeGrid grid, 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);
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;
@@ -303,7 +219,6 @@ namespace Hallucinate.GameSetup.Maze
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;
@@ -314,15 +229,17 @@ namespace Hallucinate.GameSetup.Maze
return (prefabToSpawn, Quaternion.Euler(0, finalRotation, 0));
}
private bool IsPath(int x, int z)
private bool IsPath(MazeGrid grid, int x, int z)
{
if (_currentGrid == null || !_currentGrid.IsInBounds(x, z)) return false;
MazeCellType type = _currentGrid.GetCell(x, 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.Path
|| type == MazeCellType.StairUp
|| type == MazeCellType.StairDown;
}
// =================================================================================

View File

@@ -17,12 +17,8 @@ namespace Hallucinate.GameSetup.Maze
public GameObject pathPrefab;
public GameObject startPrefab;
public GameObject endPrefab;
public GameObject stairsUp;
[Header("Room Pieces")]
public GameObject wallpiece;
public GameObject floorpiece;
public GameObject cellingpiece;
public GameObject stairUpPrefab;
public GameObject stairDownPrefab;
[Header("Corridor Types")]
public GameObject corridorStraight;
@@ -45,7 +41,8 @@ namespace Hallucinate.GameSetup.Maze
MazeCellType.Path => pathPrefab,
MazeCellType.Start => startPrefab,
MazeCellType.End => endPrefab,
MazeCellType.StairsUp => stairsUp,
MazeCellType.StairUp => stairUpPrefab,
MazeCellType.StairDown => stairDownPrefab,
_ => null
};
}