This commit is contained in:
2026-07-04 06:29:20 +07:00
parent bdbb76a42a
commit 6b4a5a8e12
8 changed files with 726 additions and 145 deletions

View File

@@ -67,8 +67,9 @@ namespace Baba_yaga.GameSetup.MazeRework
/// <summary>
/// Generates a 2D layout of the maze of size config.width by config.depth using a specific seed.
/// Generates the maze grid.
/// </summary>
public MazeReworkCellType[,] Generate(int seed)
public MazeReworkCellType[,] Generate(int seed, Vector2Int? forcedStart = null, Vector2Int? forcedDirection = null)
{
if (_config == null) return null;
@@ -95,14 +96,30 @@ namespace Baba_yaga.GameSetup.MazeRework
InitializeVisitedMap(grid, visited, rooms, width, depth);
// 4. Select starting coordinates
int startX = _config.startLocation.x;
int startZ = _config.startLocation.y;
EnsureValidOddCoordinates(width, depth, ref startX, ref startZ);
int startX = forcedStart.HasValue ? forcedStart.Value.x : _config.startLocation.x;
int startZ = forcedStart.HasValue ? forcedStart.Value.y : _config.startLocation.y;
if (!forcedStart.HasValue)
{
EnsureValidOddCoordinates(width, depth, ref startX, ref startZ);
}
// 5. Carve Corridors using the selected algorithm
visited[startX, startZ] = true;
grid[startX, startZ] = MazeReworkCellType.Corridor;
IMazeReworkAlgorithm algorithm = GetAlgorithm();
if (forcedDirection.HasValue)
{
int fx = startX + forcedDirection.Value.x;
int fz = startZ + forcedDirection.Value.y;
if (fx >= 0 && fx < width && fz >= 0 && fz < depth && !visited[fx, fz])
{
visited[fx, fz] = true;
grid[fx, fz] = MazeReworkCellType.Corridor;
algorithm.Carve(grid, visited, fx, fz, width, depth, rng);
}
}
algorithm.Carve(grid, visited, startX, startZ, width, depth, rng);
// 6. Hunt & Kill scan to connect isolated rooms/corridors
@@ -112,7 +129,7 @@ namespace Baba_yaga.GameSetup.MazeRework
CarveLoops(grid, rng, width, depth);
// 8. Place Start & End points
PlaceStartAndEnd(grid, rooms, width, depth);
PlaceStartAndEnd(grid, rooms, width, depth, forcedStart, forcedDirection, null);
return grid;
}
@@ -132,7 +149,7 @@ namespace Baba_yaga.GameSetup.MazeRework
return GenerateAnimated(seed);
}
public (MazeReworkCellType[,] grid, List<MazeCellChange> history) GenerateAnimated(int seed)
public (MazeReworkCellType[,] grid, List<MazeCellChange> history) GenerateAnimated(int seed, Vector2Int? forcedStart = null, Vector2Int? forcedDirection = null)
{
if (_config == null) return (null, null);
@@ -165,16 +182,32 @@ namespace Baba_yaga.GameSetup.MazeRework
InitializeVisitedMap(grid, visited, rooms, width, depth);
// 4. Starting coordinates
int startX = _config.startLocation.x;
int startZ = _config.startLocation.y;
int startX = forcedStart.HasValue ? forcedStart.Value.x : _config.startLocation.x;
int startZ = forcedStart.HasValue ? forcedStart.Value.y : _config.startLocation.y;
EnsureValidOddCoordinates(width, depth, ref startX, ref startZ);
// 5. Carve Corridors — algorithm fires back through the record callback
// 5. Carve Corridors
currentPhase = MazeAnimationPhase.Carving;
visited[startX, startZ] = true;
grid[startX, startZ] = MazeReworkCellType.Corridor;
record(startX, startZ, MazeReworkCellType.Corridor, MazeCellHighlight.None); // DFS will highlight itself
GetAlgorithm().Carve(grid, visited, startX, startZ, width, depth, rng, record);
record(startX, startZ, MazeReworkCellType.Corridor, MazeCellHighlight.Evaluating);
IMazeReworkAlgorithm algorithm = GetAlgorithm();
if (forcedDirection.HasValue)
{
int fx = startX + forcedDirection.Value.x;
int fz = startZ + forcedDirection.Value.y;
if (fx >= 0 && fx < width && fz >= 0 && fz < depth && !visited[fx, fz])
{
visited[fx, fz] = true;
grid[fx, fz] = MazeReworkCellType.Corridor;
record(fx, fz, MazeReworkCellType.Corridor, MazeCellHighlight.Evaluating);
algorithm.Carve(grid, visited, fx, fz, width, depth, rng, record);
}
}
algorithm.Carve(grid, visited, startX, startZ, width, depth, rng, record);
// 6. Hunt & Kill connecting pass
currentPhase = MazeAnimationPhase.Connecting;
@@ -184,9 +217,9 @@ namespace Baba_yaga.GameSetup.MazeRework
currentPhase = MazeAnimationPhase.Loops;
CarveLoops(grid, rng, width, depth, recordNoHighlight);
// 8. Start & End placement
// 8. Place Start & End points (no highlighting needed for this step in animation)
currentPhase = MazeAnimationPhase.StartEnd;
PlaceStartAndEnd(grid, rooms, width, depth, recordNoHighlight);
PlaceStartAndEnd(grid, rooms, width, depth, forcedStart, forcedDirection, recordNoHighlight);
return (grid, history);
}
@@ -436,29 +469,37 @@ namespace Baba_yaga.GameSetup.MazeRework
}
}
private void PlaceStartAndEnd(MazeReworkCellType[,] grid, List<Room> rooms, int width, int depth,
private void PlaceStartAndEnd(MazeReworkCellType[,] grid, List<Room> rooms, int width, int depth, Vector2Int? forcedStart, Vector2Int? forcedDirection,
Action<int, int, MazeReworkCellType> onCellChanged = null)
{
if (rooms != null && rooms.Count > 0)
Vector2Int startPt;
if (forcedStart.HasValue)
{
var startRoom = rooms[0];
var startPt = startRoom.GetCenter();
grid[startPt.x, startPt.y] = MazeReworkCellType.Start;
onCellChanged?.Invoke(startPt.x, startPt.y, MazeReworkCellType.Start);
var endRoom = rooms[rooms.Count - 1];
var endPt = endRoom.GetCenter();
if (endPt == startPt && rooms.Count > 1) { endRoom = rooms[1]; endPt = endRoom.GetCenter(); }
grid[endPt.x, endPt.y] = MazeReworkCellType.End;
onCellChanged?.Invoke(endPt.x, endPt.y, MazeReworkCellType.End);
startPt = forcedStart.Value;
}
else if (rooms != null && rooms.Count > 0)
{
startPt = rooms[0].GetCenter();
}
else
{
int sx = _config.startLocation.x, sz = _config.startLocation.y;
EnsureValidOddCoordinates(width, depth, ref sx, ref sz);
grid[sx, sz] = MazeReworkCellType.Start;
onCellChanged?.Invoke(sx, sz, MazeReworkCellType.Start);
startPt = new Vector2Int(sx, sz);
}
grid[startPt.x, startPt.y] = MazeReworkCellType.Start;
onCellChanged?.Invoke(startPt.x, startPt.y, MazeReworkCellType.Start);
Vector2Int endPt = startPt;
if (rooms != null && rooms.Count > 0)
{
var endRoom = rooms[rooms.Count - 1];
endPt = endRoom.GetCenter();
if (endPt == startPt && rooms.Count > 1) { endPt = rooms[1].GetCenter(); }
}
else
{
int ex = width - 2, ez = depth - 2;
EnsureValidOddCoordinates(width, depth, ref ex, ref ez);
if (grid[ex, ez] == MazeReworkCellType.Wall)
@@ -473,12 +514,36 @@ namespace Baba_yaga.GameSetup.MazeRework
{ ex = tx; ez = tz; found = true; }
}
}
if (ex != sx || ez != sz)
{
grid[ex, ez] = MazeReworkCellType.End;
onCellChanged?.Invoke(ex, ez, MazeReworkCellType.End);
}
endPt = new Vector2Int(ex, ez);
}
if (endPt == startPt)
{
// Fallback: find any path cell that isn't the start point, starting from the opposite corner
int ex = 1, ez = 1;
bool found = false;
for (int r = 0; r < Mathf.Max(width, depth) && !found; r++)
{
for (int ddx = -r; ddx <= r && !found; ddx++)
{
for (int ddz = -r; ddz <= r && !found; ddz++)
{
int tx = ex + ddx, tz = ez + ddz;
if (tx >= 0 && tx < width && tz >= 0 && tz < depth && IsPathCell(grid[tx, tz]))
{
if (tx != startPt.x || tz != startPt.y)
{
ex = tx; ez = tz; found = true;
}
}
}
}
}
endPt = new Vector2Int(ex, ez);
}
grid[endPt.x, endPt.y] = MazeReworkCellType.End;
onCellChanged?.Invoke(endPt.x, endPt.y, MazeReworkCellType.End);
}
#endregion

View File

@@ -1,3 +1,4 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Sirenix.OdinInspector;
@@ -114,109 +115,158 @@ namespace Baba_yaga.GameSetup.MazeRework
if (animateGeneration && animator != null)
{
// Animated mode: generates floor 0 and replays the creation step-by-step
int seed = config.useRandomSeed ? Random.Range(0, 1000000) : config.seed;
var (grid, history) = generator.GenerateAnimated(seed);
_grids.Add(grid);
BeginningWorldPosition = FindBeginningWorldPosition(grid);
animator.AnimateGeneration(history, grid, spawner, 0f, mazeContainer, () =>
if (Application.isPlaying)
{
Debug.Log($"[MazeReworkManager] Animation complete. Beginning world position: {BeginningWorldPosition}");
});
StartCoroutine(GenerateAndSpawnAnimatedRoutine(generator));
}
else
{
Debug.LogWarning("[MazeReworkManager] Animation is only supported in Play Mode. Falling back to instantaneous generation.");
GenerateInstantaneous(generator);
}
}
else
{
// Standard instantaneous generation
for (int i = 0; i < floorCount; i++)
{
int seed = config.useRandomSeed ? Random.Range(0, 1000000) : config.seed + i * 1000;
var grid = generator.Generate(seed);
_grids.Add(grid);
}
// Connect floors with staircases if multiple floors are present
if (floorCount > 1)
{
GenerateConnections();
}
// Spawn prefabs for all floors
for (int i = 0; i < floorCount; i++)
{
float yOffset = i * floorHeight;
spawner.Spawn(_grids[i], yOffset, mazeContainer);
}
// Compute and cache the world-space Beginning position from floor 0
BeginningWorldPosition = FindBeginningWorldPosition(_grids[0]);
Debug.Log($"[MazeReworkManager] Beginning world position set to: {BeginningWorldPosition}");
GenerateInstantaneous(generator);
}
}
private void GenerateConnections()
private IEnumerator GenerateAndSpawnAnimatedRoutine(MazeReworkGenerator generator)
{
int seed = config.useRandomSeed ? System.DateTime.Now.Millisecond : config.seed;
System.Random rng = new System.Random(seed);
for (int i = 0; i < floorCount - 1; i++)
Vector2Int? nextStartPos = null;
Vector2Int? nextStartDir = null;
for (int i = 0; i < floorCount; i++)
{
var currentFloor = _grids[i];
var nextFloor = _grids[i + 1];
List<Vector2Int> possibleConnections = new List<Vector2Int>();
int width = config.width;
int depth = config.depth;
for (int z = 1; z < depth - 1; z++)
int seed = config.useRandomSeed ? System.Guid.NewGuid().GetHashCode() : config.seed + i * 1000;
var (grid, history) = generator.GenerateAnimated(seed, nextStartPos, nextStartDir);
if (i > 0 && nextStartPos.HasValue)
{
for (int x = 1; x < width - 1; x++)
{
// A staircase requires path cells (corridors or rooms) to exist at the exact same x, z on both floors
bool isCurrentFloorPath = IsPathCell(currentFloor[x, z]);
bool isNextFloorPath = IsPathCell(nextFloor[x, z]);
grid[nextStartPos.Value.x, nextStartPos.Value.y] = MazeReworkCellType.StairsDown;
}
if (isCurrentFloorPath && isNextFloorPath)
if (i < floorCount - 1)
{
Vector2Int endPos = new Vector2Int(-1, -1);
for (int z = 0; z < config.depth; z++)
{
for (int x = 0; x < config.width; x++)
{
possibleConnections.Add(new Vector2Int(x, z));
if (grid[x, z] == MazeReworkCellType.End) { endPos = new Vector2Int(x, z); break; }
}
if (endPos.x != -1) break;
}
if (endPos.x != -1)
{
grid[endPos.x, endPos.y] = MazeReworkCellType.StairsUp;
nextStartPos = endPos;
nextStartDir = GetSingleConnectionDirection(grid, endPos.x, endPos.y);
}
}
// Shuffle connections
for (int k = possibleConnections.Count - 1; k > 0; k--)
_grids.Add(grid);
if (i == 0) BeginningWorldPosition = FindBeginningWorldPosition(grid);
bool floorAnimationComplete = false;
float yOffset = i * floorHeight;
animator.AnimateGeneration(history, grid, spawner, yOffset, mazeContainer, () =>
{
int idx = rng.Next(k + 1);
var temp = possibleConnections[k];
possibleConnections[k] = possibleConnections[idx];
possibleConnections[idx] = temp;
}
floorAnimationComplete = true;
});
int connectionsMade = 0;
foreach (Vector2Int pos in possibleConnections)
{
if (connectionsMade >= connectionsPerFloor) break;
int x = pos.x;
int z = pos.y;
// Set stair cells (stairs up on lower floor, stairs down on upper floor)
currentFloor[x, z] = MazeReworkCellType.StairsUp;
nextFloor[x, z] = MazeReworkCellType.StairsDown;
connectionsMade++;
}
yield return new WaitUntil(() => floorAnimationComplete);
// Tiny pause before starting next floor
yield return new WaitForSeconds(0.5f);
}
Debug.Log($"[MazeReworkManager] All floors animated! Beginning world position: {BeginningWorldPosition}");
}
private void GenerateInstantaneous(MazeReworkGenerator generator)
{
Vector2Int? nextStartPos = null;
Vector2Int? nextStartDir = null;
for (int i = 0; i < floorCount; i++)
{
int seed = config.useRandomSeed ? System.Guid.NewGuid().GetHashCode() : config.seed + i * 1000;
var grid = generator.Generate(seed, nextStartPos, nextStartDir);
// If this is not the first floor, replace the Start with StairsDown to connect to the floor below
if (i > 0 && nextStartPos.HasValue)
{
grid[nextStartPos.Value.x, nextStartPos.Value.y] = MazeReworkCellType.StairsDown;
}
// If there is a floor above this one, convert the End cell to a StairsUp
if (i < floorCount - 1)
{
Vector2Int endPos = new Vector2Int(-1, -1);
for (int z = 0; z < config.depth; z++)
{
for (int x = 0; x < config.width; x++)
{
if (grid[x, z] == MazeReworkCellType.End)
{
endPos = new Vector2Int(x, z);
break;
}
}
if (endPos.x != -1) break;
}
if (endPos.x != -1)
{
grid[endPos.x, endPos.y] = MazeReworkCellType.StairsUp;
nextStartPos = endPos;
nextStartDir = GetSingleConnectionDirection(grid, endPos.x, endPos.y);
}
}
_grids.Add(grid);
}
// Spawn prefabs for all floors
for (int i = 0; i < floorCount; i++)
{
float yOffset = i * floorHeight;
spawner.Spawn(_grids[i], yOffset, mazeContainer);
}
// Compute and cache the world-space Beginning position from floor 0
BeginningWorldPosition = FindBeginningWorldPosition(_grids[0]);
Debug.Log($"[MazeReworkManager] Beginning world position set to: {BeginningWorldPosition}");
}
private bool IsPathCell(MazeReworkCellType type)
{
return type == MazeReworkCellType.Corridor ||
type == MazeReworkCellType.Room ||
type == MazeReworkCellType.Start ||
type == MazeReworkCellType.End;
type == MazeReworkCellType.End ||
type == MazeReworkCellType.StairsDown ||
type == MazeReworkCellType.StairsUp;
}
private Vector2Int GetSingleConnectionDirection(MazeReworkCellType[,] grid, int cx, int cz)
{
int width = grid.GetLength(0);
int depth = grid.GetLength(1);
Vector2Int[] dirs = new Vector2Int[] { new Vector2Int(0, 1), new Vector2Int(1, 0), new Vector2Int(0, -1), new Vector2Int(-1, 0) };
foreach(var d in dirs)
{
int nx = cx + d.x;
int nz = cz + d.y;
if (nx >= 0 && nx < width && nz >= 0 && nz < depth)
{
if (IsPathCell(grid[nx, nz]))
{
return d;
}
}
}
return new Vector2Int(1, 0); // fallback
}
private Vector3 FindBeginningWorldPosition(MazeReworkCellType[,] grid)

View File

@@ -27,6 +27,8 @@ namespace Baba_yaga.GameSetup.MazeRework
public GameObject beginningPrefab;
[Tooltip("Prefab for the End cell (player exit point). Auto-rotates based on neighbors.")]
public GameObject endPrefab;
[Tooltip("Prefab for connecting to the next floor. Spawns on the lower floor and acts as a dead-end piece.")]
public GameObject stairPrefab;
[Header("Preview Mode Settings (Phase 1)")]
[Tooltip("If true, the spawner will only spawn simple preview blocks instead of full 3D modular pieces.")]
@@ -58,6 +60,10 @@ namespace Baba_yaga.GameSetup.MazeRework
public float beginningRotationOffset = 0f;
[Tooltip("Rotation offset added to End prefab.")]
public float endRotationOffset = 0f;
[Tooltip("Rotation offset added to Stair prefab.")]
public float stairRotationOffset = 0f;
[Tooltip("Height offset added to Stair prefab.")]
public float stairHeightOffset = 0f;
[Header("Spacing Settings")]
[Tooltip("Physical distance between each grid cell center.")]
@@ -70,8 +76,8 @@ namespace Baba_yaga.GameSetup.MazeRework
public float Rotation;
}
private readonly Dictionary<Vector2Int, SpawnedCellData> _spawnedGridCells = new Dictionary<Vector2Int, SpawnedCellData>();
private readonly Dictionary<Vector2Int, GameObject> _spawnedHighlights = new Dictionary<Vector2Int, GameObject>();
private readonly Dictionary<Vector3, SpawnedCellData> _spawnedGridCells = new Dictionary<Vector3, SpawnedCellData>();
private readonly Dictionary<Vector3, GameObject> _spawnedHighlights = new Dictionary<Vector3, GameObject>();
private void Start()
{
@@ -169,7 +175,7 @@ namespace Baba_yaga.GameSetup.MazeRework
{
if (x < 0 || x >= width || z < 0 || z >= depth) return;
Vector2Int pos = new Vector2Int(x, z);
Vector3 pos = new Vector3(x, yOffset, z);
MazeReworkCellType type = grid[x, z];
MazeCellHighlight hType = highlights != null ? highlights[x, z] : MazeCellHighlight.None;
@@ -237,6 +243,29 @@ namespace Baba_yaga.GameSetup.MazeRework
targetRot = baseRot + endRotationOffset;
targetName = "End";
}
else if (type == MazeReworkCellType.StairsUp)
{
if (stairPrefab != null)
{
(_, float baseRot) = GetModularPrefabAndRotation(grid, x, z, width, depth);
targetPrefab = stairPrefab;
targetRot = baseRot + stairRotationOffset;
targetName = "StairConnection";
yOffset += stairHeightOffset;
}
else
{
(targetPrefab, targetRot) = GetModularPrefabAndRotation(grid, x, z, width, depth);
targetName = $"{type}";
}
}
else if (type == MazeReworkCellType.StairsDown)
{
// The stair prefab from the lower floor physically spans into this space.
// We spawn NOTHING here, but it logically connects to the corridor network.
targetPrefab = null;
targetName = "StairsDownEmpty";
}
else
{
(targetPrefab, targetRot) = GetModularPrefabAndRotation(grid, x, z, width, depth);