347 lines
12 KiB
C#
347 lines
12 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace Hallucinate.GameSetup.Maze
|
|
{
|
|
/// <summary>
|
|
/// 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;
|
|
|
|
private readonly Dictionary<Vector2Int, GameObject> _spawnedCells = new Dictionary<Vector2Int, GameObject>();
|
|
private Transform _container;
|
|
private MazeGrid _currentGrid;
|
|
|
|
public void Initialize(MazeGrid grid, Transform container)
|
|
{
|
|
_currentGrid = grid;
|
|
_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();
|
|
_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))
|
|
{
|
|
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));
|
|
}
|
|
}
|
|
|
|
// =================================================================================
|
|
// 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 = 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));
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|