Organize custom scripts and Shared under Assets/Scripts, and delete assembly definition files

This commit is contained in:
2026-07-01 20:36:56 +07:00
parent befc19bf37
commit 01048074ee
183 changed files with 180 additions and 3456 deletions

View File

@@ -0,0 +1,88 @@
using System.Collections;
using UnityEngine;
namespace Baba_yaga.GameSetup.Maze
{
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public class CrawlerAlgorithm : IMazeAlgorithm
{
private const int CrawlChance = 50;
private const int MinBoundary = 1;
private const int VerticalCrawlerCount = 3;
private const int HorizontalCrawlerCount = 3;
public void Generate(MazeGrid grid)
{
for (int i = 0; i < VerticalCrawlerCount; i++) CrawlV(grid, 0);
for (int i = 0; i < HorizontalCrawlerCount; i++) CrawlH(grid, 0);
}
public IEnumerator GenerateStepByStep(MazeGrid grid, int cellsPerFrame)
{
for (int i = 0; i < VerticalCrawlerCount; i++) yield return CrawlV(grid, cellsPerFrame);
for (int i = 0; i < HorizontalCrawlerCount; i++) yield return CrawlH(grid, cellsPerFrame);
}
private IEnumerator CrawlV(MazeGrid grid, int cellsPerFrame)
{
bool done = false;
int x = Random.Range(MinBoundary, grid.Width - MinBoundary);
int z = MinBoundary;
while (!done)
{
grid.SetCell(x, z, MazeCellType.Processing);
MazeManager.cellsProcessedThisFrame++;
if (MazeManager.cellsProcessedThisFrame >= cellsPerFrame)
{
MazeManager.cellsProcessedThisFrame = 0;
yield return null;
}
grid.SetCell(x, z, MazeCellType.Corridor);
if (Random.Range(0, 100) < CrawlChance)
{
x += Random.Range(-1, 2);
}
else
{
z += Random.Range(0, 2);
}
done |= (x < MinBoundary || x >= grid.Width - MinBoundary || z < MinBoundary || z >= grid.Depth - MinBoundary);
}
}
private IEnumerator CrawlH(MazeGrid grid, int cellsPerFrame)
{
bool done = false;
int x = MinBoundary;
int z = Random.Range(MinBoundary, grid.Depth - MinBoundary);
while (!done)
{
grid.SetCell(x, z, MazeCellType.Processing);
MazeManager.cellsProcessedThisFrame++;
if (MazeManager.cellsProcessedThisFrame >= cellsPerFrame)
{
MazeManager.cellsProcessedThisFrame = 0;
yield return null;
}
grid.SetCell(x, z, MazeCellType.Corridor);
if (Random.Range(0, 100) < CrawlChance)
{
x += Random.Range(0, 2);
}
else
{
z += Random.Range(-1, 2);
}
done |= (x < MinBoundary || x >= grid.Width - MinBoundary || z < MinBoundary || z >= grid.Depth - MinBoundary);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bd419f9be92beac48b6f551063165e1f

View File

@@ -0,0 +1,25 @@
using System.Collections.Generic;
namespace Baba_yaga.GameSetup.Maze.Extensions
{
public static class ListExtensions
{
private static System.Random _rng = new System.Random();
/// <summary>
/// Shuffles a list using the Fisher-Yates algorithm.
/// </summary>
public static void Shuffle<T>(this IList<T> list)
{
int n = list.Count;
while (n > 1)
{
n--;
int k = _rng.Next(n + 1);
T value = list[k];
list[k] = list[n];
list[n] = value;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7d9791b1b03c14f16a245b2d4577c5f9

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6e1bb9cd9af7ffe40ad1a740c3c30dd6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,20 @@
namespace Baba_yaga.GameSetup.Maze
{
/// <summary>
/// Interface for all maze generation algorithms.
/// Supports both immediate and step-by-step (animated) generation.
/// </summary>
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public interface IMazeAlgorithm
{
/// <summary>
/// Generates the maze immediately in one frame.
/// </summary>
void Generate(MazeGrid grid);
/// <summary>
/// Generates the maze step-by-step for visualization.
/// </summary>
System.Collections.IEnumerator GenerateStepByStep(MazeGrid grid, int cellsPerFrame);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 46b6a7796ba3c494581e4dcb884da064

View File

@@ -0,0 +1,32 @@
namespace Baba_yaga.GameSetup.Maze
{
/// <summary>
/// Represents a 2D coordinate on the maze grid.
/// Used as a lightweight value type to avoid GC allocations.
/// </summary>
public readonly struct MapLocation
{
public readonly int x;
public readonly int z;
public MapLocation(int _x, int _z)
{
x = _x;
z = _z;
}
// Static predefined directions to eliminate magic numbers in algorithms
public static MapLocation Right => new MapLocation(1, 0);
public static MapLocation Left => new MapLocation(-1, 0);
public static MapLocation Up => new MapLocation(0, 1);
public static MapLocation Down => new MapLocation(0, -1);
/// <summary>
/// Returns a list of all 4 cardinal directions.
/// </summary>
public static System.Collections.Generic.List<MapLocation> Directions => new System.Collections.Generic.List<MapLocation>
{
Right, Up, Left, Down
};
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 987a7c46c96326a44b3a5f179fe61161

View File

@@ -0,0 +1,43 @@
using UnityEngine;
namespace Baba_yaga.GameSetup.Maze
{
/// <summary>
/// Defines the state of each cell in the maze.
/// Used to replace magic numbers and drive visual changes.
/// </summary>
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public enum MazeCellType
{
Wall, // Solid block
Corridor, // Finalized path
Processing, // Currently being evaluated by algorithm (Debug)
Path, // Temporary path (e.g., Wilson's crawler)
Start, // Entry point
End, // Exit point
StairsUp,
StairsDown,
Room
}
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public enum PieceType
{
None,
Wall,
Vertical_Straight, // Đường thẳng dọc
Horizontal_Straight, // Đường thẳng ngang
Corner, // Góc cua
T_Junction, // Ngã ba
Crossroads, // Ngã tư
Stairs,
StairsUp// Cầu thang (Điểm nối)
}
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public struct MazePieceData
{
public PieceType piece; // Hình dạng mảnh ghép
// Bạn có thể thêm rotation nếu cần xoay hướng model sau này
public int rotation;
public GameObject model;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f54ef08fa4922eb4a968d46c7aa71faf

View File

@@ -0,0 +1,84 @@
using System;
using UnityEngine;
namespace Baba_yaga.GameSetup.Maze
{
/// <summary>
/// Holds the logical state of the maze grid.
/// Notifies listeners whenever a cell changes to trigger visual updates.
/// </summary>
[Serializable]
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public class MazeGrid
{
public int Width { get; set; }
public int Depth { get; set; }
public int Level { get; set; }
public MazePieceData[,] piecePlace;
public float scale = 1f;
private readonly MazeCellType[,] _cells;
/// <summary>
/// Event fired when a cell's type is changed.
/// Useful for the Renderer to trigger animations/FX.
/// </summary>
public event Action<int, int, MazeCellType> OnCellChanged;
public MazeGrid(int width, int depth)
{
Width = width;
Depth = depth;
piecePlace = new MazePieceData[width, depth];
_cells = new MazeCellType[width, depth];
// Initialize all as walls
for (int z = 0; z < depth; z++)
{
for (int x = 0; x < width; x++)
{
_cells[x, z] = MazeCellType.Wall;
piecePlace[x, z].piece = PieceType.Wall;
}
}
}
public void SetCell(int x, int z, MazeCellType type)
{
if (IsInBounds(x, z))
{
if (_cells[x, z] != type)
{
_cells[x, z] = type;
OnCellChanged?.Invoke(x, z, type);
}
}
}
public MazeCellType GetCell(int x, int z)
{
if (IsInBounds(x, z))
return _cells[x, z];
return MazeCellType.Wall; // Treat out of bounds as walls
}
public bool IsInBounds(int x, int z)
{
return x >= 0 && x < Width && z >= 0 && z < Depth;
}
public int CountSquareNeighbours(int x, int z, MazeCellType targetType)
{
int count = 0;
if (x <= 0 || x >= Width - 1 || z <= 0 || z >= Depth - 1) return 5;
if (GetCell(x - 1, z) == targetType) count++;
if (GetCell(x + 1, z) == targetType) count++;
if (GetCell(x, z + 1) == targetType) count++;
if (GetCell(x, z - 1) == targetType) count++;
return count;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a1a7a252ff0b1014a9690f08897e2e59

View File

@@ -0,0 +1,361 @@
using System.Collections;
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
namespace Baba_yaga.GameSetup.Maze
{
/// <summary>
/// Central controller for the Maze system.
/// Manages algorithm selection, debug speed, and regeneration.
/// </summary>
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public class MazeManager : MonoBehaviour
{
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public enum AlgorithmType { Recursive, Wilsons, Prims, Crawler, NoiseRecursive }
[BoxGroup("Generation")]
[InfoBox("Set the array size to control how many maze floors are generated. Runtime grid data is rebuilt when Regenerate runs.")]
[ValidateInput(nameof(HasAtLeastOneFloor), "Maze Manager needs at least one floor.")]
public MazeGrid[] mazes;
[BoxGroup("Generation")]
[MinValue(0.001f)]
public float floorHeight = 3.5f;
[BoxGroup("Generation")]
[MinValue(0)]
public int connectionsPerFloor = 2;
[BoxGroup("Generation")]
[SerializeField] private AlgorithmType selectedAlgorithm;
[BoxGroup("Generation/Grid Size")]
[PropertyRange(5, 200)]
[SerializeField] private int width = 30;
[BoxGroup("Generation/Grid Size")]
[PropertyRange(5, 200)]
[SerializeField] private int depth = 30;
[BoxGroup("Animation")]
[SerializeField] private bool animateGeneration = true;
[BoxGroup("Animation")]
[ShowIf(nameof(animateGeneration))]
[PropertyRange(1, 500)]
[LabelText("Generation Speed (Cells/Frame)")]
[SerializeField] private int generationSpeed = 50;
public static int cellsProcessedThisFrame;
[BoxGroup("Animation")]
[ShowIf(nameof(animateGeneration))]
public MazeRenderer.CellAnimationType cellAnimationType = MazeRenderer.CellAnimationType.ScaleUp;
[BoxGroup("Progress")]
[ProgressBar(0, 100)]
[ShowInInspector, ReadOnly]
private float completionPercentage;
[BoxGroup("References")]
[Required]
[SerializeField] private MazeRenderer mazeRenderer;
[BoxGroup("Rooms (Phase 2)")]
public bool generateRooms = true;
[BoxGroup("Rooms (Phase 2)")]
[ShowIf(nameof(generateRooms))]
public int numberOfRooms = 2;
[BoxGroup("Rooms (Phase 2)")]
[ShowIf(nameof(generateRooms))]
public Vector2Int minRoomSize = new Vector2Int(2, 2);
[BoxGroup("Rooms (Phase 2)")]
[ShowIf(nameof(generateRooms))]
public Vector2Int maxRoomSize = new Vector2Int(4, 4);
[BoxGroup("References")]
[Required]
[SerializeField] private Transform mazeContainer;
[FoldoutGroup("Manhole Prefabs")]
public GameObject straightManHoleLadder;
[FoldoutGroup("Manhole Prefabs")]
public GameObject straightManHoleUp;
[FoldoutGroup("Manhole Prefabs")]
public GameObject deadendManHoleLadder;
[FoldoutGroup("Manhole Prefabs")]
public GameObject deadendManHoleUp;
[ShowInInspector]
[ReadOnly]
[BoxGroup("Runtime")]
private int FloorCount => mazes?.Length ?? 0;
[ShowInInspector]
[ReadOnly]
[BoxGroup("Runtime")]
private string CurrentGrid => _grid == null ? "None" : $"{_grid.Width}x{_grid.Depth}, Level {_grid.Level}";
private MazeGrid _grid;
private Coroutine _generationCoroutine;
private HashSet<Vector3Int> _modifiedCells = new HashSet<Vector3Int>();
private void Start()
{
Regenerate();
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.R))
{
Regenerate();
}
}
[ContextMenu("Clear Maze")]
[Button("Clear Maze", ButtonSizes.Large)]
public void ClearMaze()
{
if (_generationCoroutine != null)
{
StopCoroutine(_generationCoroutine);
_generationCoroutine = null;
}
if (mazeRenderer != null)
{
mazeRenderer.Clear();
}
completionPercentage = 0f;
_modifiedCells?.Clear();
}
[ContextMenu("Regenerate")]
[Button("Regenerate Maze", ButtonSizes.Large)]
public void Regenerate()
{
if (mazeRenderer == null)
{
Debug.LogError("MazeManager needs a MazeRenderer reference before regenerating.", this);
return;
}
if (mazeContainer == null)
{
Debug.LogError("MazeManager needs a maze container reference before regenerating.", this);
return;
}
if (mazes == null || mazes.Length == 0)
{
mazes = new MazeGrid[1];
}
ClearMaze();
mazeRenderer.currentAnimationType = cellAnimationType;
if (animateGeneration)
{
_generationCoroutine = StartCoroutine(GenerateMazeRoutine());
}
else
{
GenerateMazeInstant();
}
}
private void GenerateMazeInstant()
{
_modifiedCells.Clear();
completionPercentage = 0f;
for (int i = 0; i < mazes.Length; i++)
{
mazes[i] = new MazeGrid(width, depth);
mazes[i].Level = i;
CarveRooms(mazes[i]);
IMazeAlgorithm algorithmForFloor = GetAlgorithm(selectedAlgorithm);
algorithmForFloor.Generate(mazes[i]);
}
GenerateConnections();
for (int i = 0; i < mazes.Length; i++)
{
mazeRenderer.Initialize(mazes[i], mazeContainer, i == 0);
}
if (mazes.Length > 0) _grid = mazes[0];
completionPercentage = 100f;
}
private IEnumerator GenerateMazeRoutine()
{
_modifiedCells.Clear();
completionPercentage = 0f;
for (int i = 0; i < mazes.Length; i++)
{
mazes[i] = new MazeGrid(width, depth);
mazes[i].Level = i;
int floorIndex = i;
mazes[i].OnCellChanged += (x, z, type) =>
{
if (type != MazeCellType.Wall)
{
_modifiedCells.Add(new Vector3Int(x, floorIndex, z));
int totalCells = width * depth * mazes.Length;
// Approximate the progress to reach roughly 100% since algorithms don't visit all cells
float fillRatio = 0.6f;
completionPercentage = Mathf.Clamp((_modifiedCells.Count / ((float)totalCells * fillRatio)) * 100f, 0, 99f);
}
};
CarveRooms(mazes[i]);
mazeRenderer.Initialize(mazes[i], mazeContainer, i == 0);
IMazeAlgorithm algorithmForFloor = GetAlgorithm(selectedAlgorithm);
cellsProcessedThisFrame = 0;
yield return StartCoroutine(algorithmForFloor.GenerateStepByStep(mazes[i], generationSpeed));
}
GenerateConnections();
if (mazes.Length > 0) _grid = mazes[0];
completionPercentage = 100f;
_generationCoroutine = null;
}
private void GenerateConnections()
{
// 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];
List<Vector2Int> possibleConnections = new List<Vector2Int>();
for (int z = 0; z < depth; z++)
{
for (int x = 0; x < width; x++)
{
// 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)
{
possibleConnections.Add(new Vector2Int(x, z));
}
}
}
ShuffleList(possibleConnections);
int connectionsMade = 0;
foreach (Vector2Int pos in possibleConnections)
{
if (connectionsMade >= connectionsPerFloor) break;
int x = pos.x;
int z = pos.y;
// Set stair cells
currentFloor.SetCell(x, z, MazeCellType.StairsUp);
nextFloor.SetCell(x, z, MazeCellType.StairsDown);
connectionsMade++;
}
}
}
private void CarveRooms(MazeGrid grid)
{
if (!generateRooms) return;
for (int i = 0; i < numberOfRooms; i++)
{
int w = Random.Range(minRoomSize.x, maxRoomSize.x + 1);
int d = Random.Range(minRoomSize.y, maxRoomSize.y + 1);
int startX = Random.Range(1, width - w - 1);
int startZ = Random.Range(1, depth - d - 1);
for (int x = startX; x < startX + w; x++)
{
for (int z = startZ; z < startZ + d; z++)
{
grid.SetCell(x, z, MazeCellType.Room);
}
}
// Carve guaranteed door to seed pathfinding
if (Random.value > 0.5f)
{
int doorX = Random.Range(startX, startX + w);
int doorZ = Random.value > 0.5f ? startZ + d : startZ - 1;
grid.SetCell(doorX, doorZ, MazeCellType.Corridor);
}
else
{
int doorX = Random.value > 0.5f ? startX + w : startX - 1;
int doorZ = Random.Range(startZ, startZ + d);
grid.SetCell(doorX, doorZ, MazeCellType.Corridor);
}
}
}
private void ShuffleList<T>(List<T> list)
{
for (int i = 0; i < list.Count; i++)
{
T temp = list[i];
int randomIndex = Random.Range(i, list.Count);
list[i] = list[randomIndex];
list[randomIndex] = temp;
}
}
private IMazeAlgorithm GetAlgorithm(AlgorithmType type)
{
return type switch
{
AlgorithmType.Recursive => new RecursiveAlgorithm(),
AlgorithmType.Wilsons => new WilsonsAlgorithm(),
AlgorithmType.Prims => new PrimsAlgorithm(),
AlgorithmType.Crawler => new CrawlerAlgorithm(),
AlgorithmType.NoiseRecursive => new NoiseRecursiveGenerator(),
_ => new RecursiveAlgorithm()
};
}
[Button("Find Scene References")]
private void FindSceneReferences()
{
if (mazeRenderer == null)
{
mazeRenderer = GetComponentInChildren<MazeRenderer>();
}
if (mazeContainer == null)
{
mazeContainer = transform;
}
}
private bool HasAtLeastOneFloor(MazeGrid[] floors)
{
return floors != null && floors.Length > 0;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3607adabe0c29c34591af73b414eb17a

View File

@@ -0,0 +1,489 @@
using System.Collections;
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
namespace Baba_yaga.GameSetup.Maze
{
/// <summary>
/// Responsible for the visual representation of the maze.
/// Handles spawning, pooling, and animations with safety checks.
/// </summary>
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public class MazeRenderer : MonoBehaviour
{
[BoxGroup("Visuals")]
[Required]
[InlineEditor]
[SerializeField] private MazeVisualProfile visualProfile;
[BoxGroup("Visuals")]
[MinValue(0.001f)]
public float floorHeight = 3.5f;
public float Scale => visualProfile != null ? visualProfile.scale : 1f;
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public enum CellAnimationType { None, ScaleUp, DropDown, SpinIn }
[HideInInspector]
public CellAnimationType currentAnimationType = CellAnimationType.ScaleUp;
[ShowInInspector]
[ReadOnly]
[BoxGroup("Runtime")]
private int SpawnedCellCount => _spawnedCells.Count;
[ShowInInspector]
[ReadOnly]
[BoxGroup("Runtime")]
private int RenderedFloorCount => _grids.Count;
private readonly Dictionary<Vector3Int, GameObject> _spawnedCells = new Dictionary<Vector3Int, GameObject>();
private Transform _container;
private List<MazeGrid> _grids = new List<MazeGrid>();
public void Initialize(MazeGrid grid, Transform container, bool clearExisting = true)
{
if (visualProfile == null)
{
Debug.LogError("MazeRenderer needs a MazeVisualProfile before it can render.", this);
return;
}
if (grid == null)
{
Debug.LogError("MazeRenderer received a null MazeGrid.", this);
return;
}
if (clearExisting)
{
Clear();
}
_container = container;
if (!_grids.Contains(grid))
{
_grids.Add(grid);
grid.OnCellChanged += (x, z, type) => HandleCellChanged(grid, x, z, type);
}
// Initial render
for (int z = 0; z < grid.Depth; z++)
{
for (int x = 0; x < grid.Width; x++)
{
UpdateCellVisual(grid, x, z, grid.GetCell(x, z), false);
}
}
}
public void Clear()
{
StopAllCoroutines();
foreach (var cell in _spawnedCells.Values)
{
if (cell == null) continue;
if (Application.isPlaying)
Destroy(cell);
else
DestroyImmediate(cell);
}
_spawnedCells.Clear();
foreach (var grid in _grids)
{
// 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();
}
[Button("Clear Spawned Maze")]
private void ClearFromInspector()
{
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 = grid.GetCell(x, z);
UpdateCellVisual(grid, x, z, type, false);
}
}
}
private void UpdateCellVisual(MazeGrid grid, int x, int z, MazeCellType type, bool animate)
{
Vector3Int posKey = new Vector3Int(x, grid.Level, z);
if (_spawnedCells.TryGetValue(posKey, out GameObject oldObj))
{
if (oldObj != null) DestroyImmediate(oldObj);
_spawnedCells.Remove(posKey);
}
if (type == MazeCellType.Wall) return;
float logicalSpacing = visualProfile.nodeSpacing; // Distances between Nodes
float halfSpacing = logicalSpacing / 2f;
float safeScale = Mathf.Max(0.001f, visualProfile.scale);
float spacingScale = logicalSpacing * safeScale;
float yOffset = grid.Level * floorHeight;
Vector3 localPos = new Vector3(x * spacingScale, yOffset, z * spacingScale);
GameObject cellParent = new GameObject($"Cell_{x}_{grid.Level}_{z}");
cellParent.transform.SetParent(_container);
cellParent.transform.localPosition = localPos;
bool spawnedAnything = false;
if (type == MazeCellType.Corridor || type == MazeCellType.Processing || type == MazeCellType.StairsUp || type == MazeCellType.StairsDown)
{
// 1. Spawn Node (Intersection, Corner, T, DeadEnd, or Stairs)
GameObject nodePrefab;
Quaternion nodeRot;
Vector3 nodeOffset = Vector3.zero;
if (type == MazeCellType.StairsUp || type == MazeCellType.StairsDown)
{
nodePrefab = visualProfile.GetPrefab(type);
nodeRot = Quaternion.Euler(0, visualProfile.stairsOffset, 0);
}
else
{
(nodePrefab, nodeRot, nodeOffset) = GetNodePrefabAndRotation(grid, x, z);
}
if (nodePrefab != null)
{
GameObject node = Instantiate(nodePrefab, cellParent.transform);
node.transform.localPosition = nodeOffset * safeScale;
node.transform.localRotation = nodeRot;
node.transform.localScale = Vector3.one * safeScale;
spawnedAnything = true;
}
// 2. Spawn Edge X (Right path)
if (IsPath(grid, x + 1, z))
{
GameObject edgePrefab = visualProfile.corridorStraight;
if (edgePrefab != null)
{
GameObject edgeX = Instantiate(edgePrefab, cellParent.transform);
edgeX.transform.localPosition = new Vector3(halfSpacing * safeScale, 0, 0); // half units offset right
edgeX.transform.localRotation = Quaternion.Euler(0, 90f, 0); // pointing along X
edgeX.transform.localScale = Vector3.one * safeScale;
spawnedAnything = true;
}
}
// 3. Spawn Edge Z (Top path)
if (IsPath(grid, x, z + 1))
{
GameObject edgePrefab = visualProfile.corridorStraight;
if (edgePrefab != null)
{
GameObject edgeZ = Instantiate(edgePrefab, cellParent.transform);
edgeZ.transform.localPosition = new Vector3(0, 0, halfSpacing * safeScale); // half units offset forward
edgeZ.transform.localRotation = Quaternion.identity; // pointing along Z
edgeZ.transform.localScale = Vector3.one * safeScale;
spawnedAnything = true;
}
}
}
else if (type == MazeCellType.Room)
{
// Spawn Floor
if (visualProfile.roomFloorPrefab != null)
{
GameObject floor = Instantiate(visualProfile.roomFloorPrefab, cellParent.transform);
floor.transform.localPosition = Vector3.zero;
floor.transform.localScale = Vector3.one * safeScale;
spawnedAnything = true;
}
// Spawn Ceiling
if (visualProfile.roomCeilingPrefab != null)
{
GameObject ceiling = Instantiate(visualProfile.roomCeilingPrefab, cellParent.transform);
ceiling.transform.localPosition = new Vector3(0, floorHeight, 0);
ceiling.transform.localScale = Vector3.one * safeScale;
spawnedAnything = true;
}
// Spawn Room Edges (Walls or Doors)
MazeCellType top = grid.IsInBounds(x, z + 1) ? grid.GetCell(x, z + 1) : MazeCellType.Wall;
SpawnRoomEdge(cellParent, top, new Vector3(0, 0, halfSpacing * safeScale), 0f, safeScale);
MazeCellType right = grid.IsInBounds(x + 1, z) ? grid.GetCell(x + 1, z) : MazeCellType.Wall;
SpawnRoomEdge(cellParent, right, new Vector3(halfSpacing * safeScale, 0, 0), 90f, safeScale);
MazeCellType bottom = grid.IsInBounds(x, z - 1) ? grid.GetCell(x, z - 1) : MazeCellType.Wall;
SpawnRoomEdge(cellParent, bottom, new Vector3(0, 0, -halfSpacing * safeScale), 180f, safeScale);
MazeCellType left = grid.IsInBounds(x - 1, z) ? grid.GetCell(x - 1, z) : MazeCellType.Wall;
SpawnRoomEdge(cellParent, left, new Vector3(-halfSpacing * safeScale, 0, 0), 270f, safeScale);
spawnedAnything = true; // Always true if it reaches here
}
else
{
// Non-corridor logic (Start, End, etc)
GameObject prefab = visualProfile.GetPrefab(type);
if (prefab != null)
{
GameObject obj = Instantiate(prefab, cellParent.transform);
obj.transform.localPosition = Vector3.zero;
obj.transform.localRotation = Quaternion.identity;
obj.transform.localScale = Vector3.one * safeScale;
spawnedAnything = true;
}
}
if (!spawnedAnything)
{
DestroyImmediate(cellParent);
return;
}
_spawnedCells[posKey] = cellParent;
if (animate && visualProfile.animationDuration > 0)
{
StartCoroutine(AnimateCell(cellParent.transform));
}
}
// =================================================================================
// THUẬT TOÁN BITMASK AUTO-TILING
// =================================================================================
private (GameObject, Quaternion, Vector3) GetNodePrefabAndRotation(MazeGrid grid, int x, int 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;
if (right) mask += 2;
if (bottom) mask += 4;
if (left) mask += 8;
GameObject prefabToSpawn = null;
float yRotation = 0f;
Vector3 offset = Vector3.zero;
// Push dead ends to the boundary where edges end
float endOffset = visualProfile.deadEndShift;
switch (mask)
{
case 1:
prefabToSpawn = visualProfile.corridorDeadEnd;
yRotation = 0f;
offset = new Vector3(0, 0, endOffset);
break;
case 2:
prefabToSpawn = visualProfile.corridorDeadEnd;
yRotation = 90f;
offset = new Vector3(endOffset, 0, 0);
break;
case 4:
prefabToSpawn = visualProfile.corridorDeadEnd;
yRotation = 180f;
offset = new Vector3(0, 0, -endOffset);
break;
case 8:
prefabToSpawn = visualProfile.corridorDeadEnd;
yRotation = 270f;
offset = new Vector3(-endOffset, 0, 0);
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 = 0f;
break;
case 7:
prefabToSpawn = visualProfile.corridorTJunction;
yRotation = 90f;
break;
case 14:
prefabToSpawn = visualProfile.corridorTJunction;
yRotation = 180f;
break;
case 13:
prefabToSpawn = visualProfile.corridorTJunction;
yRotation = 270f;
break;
case 15:
prefabToSpawn = visualProfile.corridorCross;
yRotation = 0f;
break;
default:
prefabToSpawn = visualProfile.corridorDeadEnd;
yRotation = 0f;
break;
}
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), offset);
}
private void SpawnRoomEdge(GameObject parent, MazeCellType neighborType, Vector3 offset, float yRot, float safeScale)
{
if (neighborType == MazeCellType.Room)
return; // Open space to another room cell
GameObject prefabToSpawn = null;
if (neighborType == MazeCellType.Corridor || neighborType == MazeCellType.Processing || neighborType == MazeCellType.Start || neighborType == MazeCellType.End)
{
prefabToSpawn = visualProfile.roomDoorwayPrefab;
}
else
{
prefabToSpawn = visualProfile.roomWallPrefab;
}
if (prefabToSpawn != null)
{
GameObject edge = Instantiate(prefabToSpawn, parent.transform);
edge.transform.localPosition = offset;
edge.transform.localRotation = Quaternion.Euler(0, yRot, 0);
edge.transform.localScale = Vector3.one * safeScale;
}
}
private bool IsPath(MazeGrid grid, int x, int 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.StairsUp
|| type == MazeCellType.StairsDown;
}
// =================================================================================
// ANIMATION
// =================================================================================
private IEnumerator AnimateCell(Transform target)
{
if (target == null) yield break;
if (currentAnimationType == CellAnimationType.None) yield break;
float duration = Mathf.Max(0.01f, visualProfile.animationDuration);
float elapsed = 0;
Vector3 finalScale = target.localScale;
Vector3 finalPosition = target.localPosition;
Quaternion finalRotation = target.localRotation;
if (currentAnimationType == CellAnimationType.ScaleUp)
{
target.localScale = Vector3.one * 0.001f;
}
else if (currentAnimationType == CellAnimationType.DropDown)
{
target.localPosition = finalPosition + Vector3.up * 5f;
}
else if (currentAnimationType == CellAnimationType.SpinIn)
{
target.localScale = Vector3.one * 0.001f;
target.localRotation = finalRotation * Quaternion.Euler(0, 180, 0);
}
while (elapsed < duration)
{
if (target == null) yield break;
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
if (currentAnimationType == CellAnimationType.ScaleUp)
{
float s = Mathf.Sin(t * Mathf.PI * 0.5f);
target.localScale = finalScale * Mathf.Max(0.001f, s);
}
else if (currentAnimationType == CellAnimationType.DropDown)
{
float s = Mathf.Sin(t * Mathf.PI * 0.5f);
target.localPosition = Vector3.Lerp(finalPosition + Vector3.up * 5f, finalPosition, s);
}
else if (currentAnimationType == CellAnimationType.SpinIn)
{
float s = Mathf.Sin(t * Mathf.PI * 0.5f);
target.localScale = finalScale * Mathf.Max(0.001f, s);
target.localRotation = Quaternion.Lerp(finalRotation * Quaternion.Euler(0, 180, 0), finalRotation, s);
}
yield return null;
}
if (target != null)
{
target.localScale = finalScale;
target.localPosition = finalPosition;
target.localRotation = finalRotation;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f30df611110713742ab984f5bead5d88

View File

@@ -0,0 +1,147 @@
using Sirenix.OdinInspector;
using UnityEngine;
namespace Baba_yaga.GameSetup.Maze
{
[CreateAssetMenu(fileName = "MazeVisualProfile", menuName = "BABA_YAGA/Maze/Visual Profile")]
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public class MazeVisualProfile : ScriptableObject
{
[BoxGroup("Rotation Offsets")]
public float tJunctionOffset = 0f;
[BoxGroup("Rotation Offsets")]
public float cornerOffset = 0f;
[BoxGroup("Rotation Offsets")]
public float deadEndOffset = 0f;
[BoxGroup("Rotation Offsets")]
public float stairsOffset = 0f;
[BoxGroup("Cell Prefabs")]
[Required]
public GameObject wallPrefab;
[BoxGroup("Cell Prefabs")]
[Required]
public GameObject corridorPrefab;
[BoxGroup("Cell Prefabs")]
public GameObject processingPrefab;
[BoxGroup("Cell Prefabs")]
public GameObject pathPrefab;
[BoxGroup("Cell Prefabs")]
public GameObject startPrefab;
[BoxGroup("Cell Prefabs")]
public GameObject endPrefab;
[BoxGroup("Cell Prefabs")]
[Required]
public GameObject stairUpPrefab;
[BoxGroup("Cell Prefabs")]
[Required]
public GameObject stairDownPrefab;
[BoxGroup("Corridor Pieces")]
[Required]
public GameObject corridorStraight;
[BoxGroup("Corridor Pieces")]
[Required]
public GameObject corridorCorner;
[BoxGroup("Corridor Pieces")]
[Required]
public GameObject corridorTJunction;
[BoxGroup("Corridor Pieces")]
[Required]
public GameObject corridorCross;
[BoxGroup("Corridor Pieces")]
[Required]
public GameObject corridorDeadEnd;
[BoxGroup("Room Pieces (Phase 2)")]
public GameObject roomFloorPrefab;
[BoxGroup("Room Pieces (Phase 2)")]
public GameObject roomWallPrefab;
[BoxGroup("Room Pieces (Phase 2)")]
public GameObject roomCeilingPrefab;
[BoxGroup("Room Pieces (Phase 2)")]
public GameObject roomDoorwayPrefab;
[BoxGroup("Visualization")]
[MinValue(0.001f)]
public float scale = 0.167f;
[BoxGroup("Visualization")]
[MinValue(1f)]
[InfoBox("The physical distance between each grid cell. Default is 6 based on 3x3 intersections and 3x2 halls.")]
public float nodeSpacing = 6f;
[BoxGroup("Visualization")]
[InfoBox("How far to push dead-end caps from the center so they touch the hallway edge. Default is 1.0.")]
public float deadEndShift = 1.0f;
[BoxGroup("Visualization")]
[MinValue(0f)]
public float animationDuration = 0.25f;
[ShowInInspector]
[ReadOnly]
[BoxGroup("Validation")]
private int MissingRequiredPrefabCount
{
get
{
var count = 0;
if (wallPrefab == null) count++;
if (corridorPrefab == null) count++;
if (stairUpPrefab == null) count++;
if (stairDownPrefab == null) count++;
if (corridorStraight == null) count++;
if (corridorCorner == null) count++;
if (corridorTJunction == null) count++;
if (corridorCross == null) count++;
if (corridorDeadEnd == null) count++;
return count;
}
}
public GameObject GetPrefab(MazeCellType type)
{
return type switch
{
MazeCellType.Wall => wallPrefab,
MazeCellType.Corridor => corridorPrefab,
MazeCellType.Processing => processingPrefab,
MazeCellType.Path => pathPrefab,
MazeCellType.Start => startPrefab,
MazeCellType.End => endPrefab,
MazeCellType.StairsUp => stairUpPrefab,
MazeCellType.StairsDown => stairDownPrefab,
_ => null
};
}
[Button("Log Missing Required Prefabs")]
private void LogMissingRequiredPrefabs()
{
if (MissingRequiredPrefabCount == 0)
{
Debug.Log($"{name}: all required maze prefabs are assigned.", this);
return;
}
Debug.LogWarning($"{name}: {MissingRequiredPrefabCount} required maze prefab reference(s) are missing.", this);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d3ff96571406a624381b7b0e596a4d1b

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 05fdc25279e7ac148a44fde646c93546
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,65 @@
using System;
using System.Runtime.InteropServices;
using UnityEngine;
namespace Baba_yaga.GameSetup.Maze.Native
{
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze.Native", sourceAssembly: "Opsive.UltimateCharacterController")]
public class NativeNoiseProvider : IDisposable
{
private const string DLL_NAME = "BackroomsNoise";
[DllImport(DLL_NAME)]
private static extern IntPtr CreateNoiseGenerator(int seed, float frequency, int noiseType);
[DllImport(DLL_NAME)]
private static extern float GetNoiseValue(IntPtr handle, float x, float z);
[DllImport(DLL_NAME)]
private static extern void GetNoiseBuffer(IntPtr handle, float startX, float startZ, int width, int depth, float[] buffer);
[DllImport(DLL_NAME)]
private static extern void DestroyNoiseGenerator(IntPtr handle);
private IntPtr _handle;
public bool IsInitialized => _handle != IntPtr.Zero;
public NativeNoiseProvider(int seed, float frequency = 0.01f, int noiseType = 0)
{
try
{
_handle = CreateNoiseGenerator(seed, frequency, noiseType);
}
catch (DllNotFoundException)
{
Debug.LogWarning($"Native library '{DLL_NAME}' not found. Ensure it is compiled and placed in Plugins folder.");
}
}
public float GetNoise(float x, float z)
{
if (!IsInitialized) return 0f;
return GetNoiseValue(_handle, x, z);
}
public void FillBuffer(float startX, float startZ, int width, int depth, float[] buffer)
{
if (!IsInitialized || buffer == null) return;
GetNoiseBuffer(_handle, startX, startZ, width, depth, buffer);
}
public void Dispose()
{
if (IsInitialized)
{
DestroyNoiseGenerator(_handle);
_handle = IntPtr.Zero;
}
}
~NativeNoiseProvider()
{
Dispose();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4fd030227a1b87a4f8826f9b317fbf87

View File

@@ -0,0 +1,159 @@
using System.Collections;
using System.Collections.Generic;
using Baba_yaga.GameSetup.Maze.Extensions;
using Baba_yaga.GameSetup.Maze.Native;
using UnityEngine;
namespace Baba_yaga.GameSetup.Maze
{
/// <summary>
/// Advanced generator that combines C++ Native Noise with a Recursive Backtracking algorithm.
/// Creates a hybrid layout of large rooms and chaotic corridors.
/// </summary>
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public class NoiseRecursiveGenerator : IMazeAlgorithm
{
private readonly List<MapLocation> _directions = MapLocation.Directions;
private float[] _noiseMap;
private int _seed = 1337;
// Thresholds
private const float RoomThreshold = 0.5f;
private const float CorridorThreshold = -0.3f;
private const int DeadEndNeighbourThreshold = 2;
public void Generate(MazeGrid grid)
{
InitializeNoise(grid);
// Step 1: Pre-place rooms based on noise peaks
for (int z = 0; z < grid.Depth; z++)
{
for (int x = 0; x < grid.Width; x++)
{
float noise = GetNoiseAt(x, z, grid.Width);
if (noise > RoomThreshold)
{
grid.SetCell(x, z, MazeCellType.Corridor);
}
}
}
// Step 2: Run recursive carving in the "connectable" zones
// We start from a few points to ensure coverage
for (int i = 0; i < 5; i++)
{
int startX = Random.Range(1, grid.Width - 1);
int startZ = Random.Range(1, grid.Depth - 1);
GenerateRecursive(grid, startX, startZ);
}
}
public IEnumerator GenerateStepByStep(MazeGrid grid, int cellsPerFrame)
{
InitializeNoise(grid);
// Visual feedback for noise pre-placement
for (int z = 0; z < grid.Depth; z++)
{
for (int x = 0; x < grid.Width; x++)
{
float noise = GetNoiseAt(x, z, grid.Width);
if (noise > RoomThreshold)
{
grid.SetCell(x, z, MazeCellType.Processing);
}
}
if (z % 5 == 0) yield return null;
}
for (int z = 0; z < grid.Depth; z++)
{
for (int x = 0; x < grid.Width; x++)
{
if (grid.GetCell(x, z) == MazeCellType.Processing)
grid.SetCell(x, z, MazeCellType.Corridor);
}
}
yield return GenerateRecursiveStepByStep(grid, 5, 5, cellsPerFrame);
}
private void InitializeNoise(MazeGrid grid)
{
_noiseMap = new float[grid.Width * grid.Depth];
using (var provider = new NativeNoiseProvider(_seed, 0.05f))
{
if (provider.IsInitialized)
{
provider.FillBuffer(0, 0, grid.Width, grid.Depth, _noiseMap);
}
else
{
// Fallback to Unity Perlin
for (int z = 0; z < grid.Depth; z++)
{
for (int x = 0; x < grid.Width; x++)
{
_noiseMap[z * grid.Width + x] = Mathf.PerlinNoise(x * 0.1f, z * 0.1f) * 2f - 1f;
}
}
}
}
}
private float GetNoiseAt(int x, int z, int width)
{
if (_noiseMap == null) return 0f;
return _noiseMap[z * width + x];
}
private void GenerateRecursive(MazeGrid grid, int x, int z)
{
if (!grid.IsInBounds(x, z)) return;
if (grid.GetCell(x, z) != MazeCellType.Wall) return;
if (GetNoiseAt(x, z, grid.Width) < CorridorThreshold) return;
if (grid.GetCell(x, z) == MazeCellType.Corridor) return;
if (grid.CountSquareNeighbours(x, z, MazeCellType.Corridor) >= DeadEndNeighbourThreshold) return;
grid.SetCell(x, z, MazeCellType.Corridor);
List<MapLocation> shuffledDirs = new List<MapLocation>(_directions);
shuffledDirs.Shuffle();
foreach (var dir in shuffledDirs)
{
GenerateRecursive(grid, x + dir.x, z + dir.z);
}
}
private IEnumerator GenerateRecursiveStepByStep(MazeGrid grid, int x, int z, int cellsPerFrame)
{
if (!grid.IsInBounds(x, z)) yield break;
if (grid.GetCell(x, z) != MazeCellType.Wall) yield break;
if (GetNoiseAt(x, z, grid.Width) < CorridorThreshold) yield break;
if (grid.GetCell(x, z) == MazeCellType.Corridor) yield break;
if (grid.CountSquareNeighbours(x, z, MazeCellType.Corridor) >= DeadEndNeighbourThreshold) yield break;
grid.SetCell(x, z, MazeCellType.Processing);
MazeManager.cellsProcessedThisFrame++;
if (MazeManager.cellsProcessedThisFrame >= cellsPerFrame)
{
MazeManager.cellsProcessedThisFrame = 0;
yield return null;
}
grid.SetCell(x, z, MazeCellType.Corridor);
List<MapLocation> shuffledDirs = new List<MapLocation>(_directions);
shuffledDirs.Shuffle();
foreach (var dir in shuffledDirs)
{
yield return GenerateRecursiveStepByStep(grid, x + dir.x, z + dir.z, cellsPerFrame);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 22e17049420e98f43a692fd3e7d7d261

View File

@@ -0,0 +1,112 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Baba_yaga.GameSetup.Maze
{
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public class PrimsAlgorithm : IMazeAlgorithm
{
private const int InitialX = 2;
private const int InitialZ = 2;
private const int MaxIterations = 10000;
private const int TargetCorridorNeighbours = 1;
public void Generate(MazeGrid grid)
{
int x = InitialX;
int z = InitialZ;
grid.SetCell(x, z, MazeCellType.Corridor);
List<MapLocation> walls = GetNeighbouringWalls(grid, x, z);
int iterations = 0;
while (walls.Count > 0 && iterations < MaxIterations)
{
int rIndex = Random.Range(0, walls.Count);
MapLocation w = walls[rIndex];
walls.RemoveAt(rIndex);
if (grid.CountSquareNeighbours(w.x, w.z, MazeCellType.Corridor) == TargetCorridorNeighbours)
{
grid.SetCell(w.x, w.z, MazeCellType.Corridor);
foreach (var nw in GetNeighbouringWalls(grid, w.x, w.z))
{
if (!walls.Contains(nw)) walls.Add(nw);
}
}
iterations++;
}
}
public IEnumerator GenerateStepByStep(MazeGrid grid, int cellsPerFrame)
{
int x = InitialX;
int z = InitialZ;
grid.SetCell(x, z, MazeCellType.Corridor);
yield return null;
List<MapLocation> walls = GetNeighbouringWalls(grid, x, z);
foreach(var w in walls) grid.SetCell(w.x, w.z, MazeCellType.Processing);
int iterations = 0;
while (walls.Count > 0 && iterations < MaxIterations)
{
int rIndex = Random.Range(0, walls.Count);
MapLocation w = walls[rIndex];
walls.RemoveAt(rIndex);
if (grid.CountSquareNeighbours(w.x, w.z, MazeCellType.Corridor) == TargetCorridorNeighbours)
{
grid.SetCell(w.x, w.z, MazeCellType.Corridor);
MazeManager.cellsProcessedThisFrame++;
if (MazeManager.cellsProcessedThisFrame >= cellsPerFrame)
{
MazeManager.cellsProcessedThisFrame = 0;
yield return null;
}
foreach (var nw in GetNeighbouringWalls(grid, w.x, w.z))
{
if (grid.GetCell(nw.x, nw.z) == MazeCellType.Wall)
{
grid.SetCell(nw.x, nw.z, MazeCellType.Processing);
walls.Add(nw);
}
}
}
else
{
// If it's no longer a candidate, turn it back to Wall
grid.SetCell(w.x, w.z, MazeCellType.Wall);
}
iterations++;
}
}
private List<MapLocation> GetNeighbouringWalls(MazeGrid grid, int x, int z)
{
List<MapLocation> neighbours = new List<MapLocation>();
foreach (var dir in MapLocation.Directions)
{
int nx = x + dir.x;
int nz = z + dir.z;
// Correction
nx = x + dir.x;
nz = z + dir.z;
if (grid.IsInBounds(nx, nz))
{
MazeCellType type = grid.GetCell(nx, nz);
if (type == MazeCellType.Wall || type == MazeCellType.Processing)
{
neighbours.Add(new MapLocation(nx, nz));
}
}
}
return neighbours;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: edcdd3c0aa9656a4797b83cc675aa629

View File

@@ -0,0 +1,77 @@
using System.Collections;
using System.Collections.Generic;
using Baba_yaga.GameSetup.Maze.Extensions;
using UnityEngine;
namespace Baba_yaga.GameSetup.Maze
{
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public class RecursiveAlgorithm : IMazeAlgorithm
{
private const int StartX = 5;
private const int StartZ = 5;
private const int DeadEndNeighbourThreshold = 2;
private readonly List<MapLocation> _directions = MapLocation.Directions;
public void Generate(MazeGrid grid)
{
GenerateRecursive(grid, StartX, StartZ);
}
public IEnumerator GenerateStepByStep(MazeGrid grid, int cellsPerFrame)
{
yield return GenerateRecursiveStepByStep(grid, StartX, StartZ, cellsPerFrame);
}
private void GenerateRecursive(MazeGrid grid, int x, int z)
{
if (grid.GetCell(x, z) != MazeCellType.Wall) return;
if (grid.CountSquareNeighbours(x, z, MazeCellType.Corridor) >= DeadEndNeighbourThreshold) return;
grid.SetCell(x, z, MazeCellType.Corridor);
List<MapLocation> shuffledDirs = new List<MapLocation>(_directions);
shuffledDirs.Shuffle();
foreach (var dir in shuffledDirs)
{
int nx = x + dir.x;
int nz = z + dir.z;
if (grid.IsInBounds(nx, nz))
{
GenerateRecursive(grid, nx, nz);
}
}
}
private IEnumerator GenerateRecursiveStepByStep(MazeGrid grid, int x, int z, int cellsPerFrame)
{
if (grid.GetCell(x, z) != MazeCellType.Wall) yield break;
if (grid.CountSquareNeighbours(x, z, MazeCellType.Corridor) >= DeadEndNeighbourThreshold) yield break;
grid.SetCell(x, z, MazeCellType.Processing);
MazeManager.cellsProcessedThisFrame++;
if (MazeManager.cellsProcessedThisFrame >= cellsPerFrame)
{
MazeManager.cellsProcessedThisFrame = 0;
yield return null;
}
grid.SetCell(x, z, MazeCellType.Corridor);
List<MapLocation> shuffledDirs = new List<MapLocation>(_directions);
shuffledDirs.Shuffle();
foreach (var dir in shuffledDirs)
{
int nx = x + dir.x;
int nz = z + dir.z;
if (grid.IsInBounds(nx, nz))
{
yield return GenerateRecursiveStepByStep(grid, nx, nz, cellsPerFrame);
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2460c0e9379da9741b3d3387aa6c7a8e

View File

@@ -0,0 +1,182 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Baba_yaga.GameSetup.Maze
{
/// <summary>
/// Wilson's Algorithm implementation based on the original provided logic.
/// Ensures paths are sparse and correctly finalized using specific neighbor constraints.
/// </summary>
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
public class WilsonsAlgorithm : IMazeAlgorithm
{
private const int MinBoundary = 2;
private const int MaxIterationSafety = 5000;
private const int MaxWalkSteps = 5000;
private readonly List<MapLocation> _directions = MapLocation.Directions;
private List<MapLocation> _notUsed = new List<MapLocation>();
public void Generate(MazeGrid grid)
{
// 1. Create a starting finalized cell (Type.Corridor represents state 2)
int x = Random.Range(MinBoundary, grid.Width - 1);
int z = Random.Range(MinBoundary, grid.Depth - 1);
grid.SetCell(x, z, MazeCellType.Corridor);
int safety = 0;
while (GetAvailableCells(grid) > 1 && safety < MaxIterationSafety)
{
RandomWalkSync(grid);
safety++;
}
}
public IEnumerator GenerateStepByStep(MazeGrid grid, int cellsPerFrame)
{
int x = Random.Range(MinBoundary, grid.Width - 1);
int z = Random.Range(MinBoundary, grid.Depth - 1);
grid.SetCell(x, z, MazeCellType.Corridor);
yield return null;
int safety = 0;
while (GetAvailableCells(grid) > 1 && safety < MaxIterationSafety)
{
yield return RandomWalk(grid, cellsPerFrame);
safety++;
}
}
/// <summary>
/// Counts neighbors that are already part of the finalized maze (State 2 / Corridor).
/// </summary>
private int CountFinalizedNeighbours(MazeGrid grid, int x, int z)
{
int count = 0;
foreach (var d in _directions)
{
if (grid.GetCell(x + d.x, z + d.z) == MazeCellType.Corridor)
{
count++;
}
}
return count;
}
private int GetAvailableCells(MazeGrid grid)
{
_notUsed.Clear();
for (int z = 1; z < grid.Depth - 1; z++)
{
for (int x = 1; x < grid.Width - 1; x++)
{
if (CountFinalizedNeighbours(grid, x, z) == 0)
{
_notUsed.Add(new MapLocation(x, z));
}
}
}
return _notUsed.Count;
}
private void RandomWalkSync(MazeGrid grid)
{
if (_notUsed.Count == 0) return;
List<MapLocation> inWalk = new List<MapLocation>();
int rStartIndex = Random.Range(0, _notUsed.Count);
int cx = _notUsed[rStartIndex].x;
int cz = _notUsed[rStartIndex].z;
inWalk.Add(new MapLocation(cx, cz));
int loop = 0;
bool validPath = false;
while (cx > 0 && cx < grid.Width - 1 && cz > 0 && cz < grid.Depth - 1 && loop < MaxWalkSteps && !validPath)
{
// Mark as temporary walk (State 0 / Processing)
// Note: We don't set grid cell here in sync mode to avoid triggering events unnecessarily
// but we keep track of neighbors.
if (CountFinalizedNeighbours(grid, cx, cz) > 1) break;
MapLocation rd = _directions[Random.Range(0, _directions.Count)];
int nx = cx + rd.x;
int nz = cz + rd.z;
// User's original constraint: CountSquareNeighbours (nx, nz) < 2
if (CountFinalizedNeighbours(grid, nx, nz) < 2)
{
cx = nx;
cz = nz;
inWalk.Add(new MapLocation(cx, cz));
}
validPath = CountFinalizedNeighbours(grid, cx, cz) == 1;
loop++;
}
if (validPath)
{
foreach (MapLocation m in inWalk)
grid.SetCell(m.x, m.z, MazeCellType.Corridor);
}
}
private IEnumerator RandomWalk(MazeGrid grid, int cellsPerFrame)
{
if (_notUsed.Count == 0) yield break;
List<MapLocation> inWalk = new List<MapLocation>();
int rStartIndex = Random.Range(0, _notUsed.Count);
int cx = _notUsed[rStartIndex].x;
int cz = _notUsed[rStartIndex].z;
inWalk.Add(new MapLocation(cx, cz));
int loop = 0;
bool validPath = false;
while (cx > 0 && cx < grid.Width - 1 && cz > 0 && cz < grid.Depth - 1 && loop < MaxWalkSteps && !validPath)
{
grid.SetCell(cx, cz, MazeCellType.Processing); // State 0
MazeManager.cellsProcessedThisFrame++;
if (MazeManager.cellsProcessedThisFrame >= cellsPerFrame)
{
MazeManager.cellsProcessedThisFrame = 0;
yield return null;
}
if (CountFinalizedNeighbours(grid, cx, cz) > 1) break;
MapLocation rd = _directions[Random.Range(0, _directions.Count)];
int nx = cx + rd.x;
int nz = cz + rd.z;
if (CountFinalizedNeighbours(grid, nx, nz) < 2)
{
cx = nx;
cz = nz;
inWalk.Add(new MapLocation(cx, cz));
}
validPath = CountFinalizedNeighbours(grid, cx, cz) == 1;
loop++;
}
if (validPath)
{
foreach (MapLocation m in inWalk)
{
grid.SetCell(m.x, m.z, MazeCellType.Corridor); // State 2
}
}
else
{
foreach (MapLocation m in inWalk)
grid.SetCell(m.x, m.z, MazeCellType.Wall); // State 1
}
inWalk.Clear();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 61156f8986612ca49a0672ad5542380f