Souljack/Assets/_Project/Scripts/LevelGenerating/WallsGenerator.cs

502 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Random = UnityEngine.Random;
namespace _Project.Scripts.LevelGenerating
{
public class WallsGenerator : MonoBehaviour
{
[SerializeField] private int floorsCount;
[SerializeField] private int lengthWallMin;
[SerializeField] private int lengthWallMax;
[SerializeField] private int lengthStartRoom;
[SerializeField] private int lengthLiftRoom;
[SerializeField] private int widthLiftRoom;
[SerializeField] private int lengthFloor;
[SerializeField] private int corridorLength;
[SerializeField] private int corridorWigth;
[SerializeField] private int _minDepthLiftRoom;
[SerializeField] private GameObject GameObjectA;
[SerializeField] private GameObject GameObjectB;
[Header("Генерация")] [SerializeField] private int maxRooms = 15;
[SerializeField, Range(0f, 1f)] private float branchChance = 0.7f;
[SerializeField] private float roomMargin = 2f; // зазор между комнатами
private int _curRooms;
private readonly Dictionary<Room, int> _depth = new Dictionary<Room, int>();
private readonly Dictionary<Room, (Room parent, ExitsInRoomType dir)> _parent
= new Dictionary<Room, (Room, ExitsInRoomType)>();
[SerializeField] private bool liftDoorAnyPosition = false; // true, если дверь в префабе может быть не по центру
private readonly Dictionary<Room, Room> _corridorOf = new Dictionary<Room, Room>();
private LevelManager levelManager;
private static readonly ExitsInRoomType[] AllDirs =
{
ExitsInRoomType.XAxisP, ExitsInRoomType.XAxisN,
ExitsInRoomType.ZAxisP, ExitsInRoomType.ZAxisN
};
void Start()
{
levelManager = LevelManager.Instance;
GenerateFloor();
}
bool PlaceElevator()
{
while (true)
{
if (TryAttachLift(true, roomMargin)) return true; // норма: глубина >= минимума
if (TryAttachLift(false, roomMargin)) return true; // без требования глубины
if (TryAttachLift(false, 0f)) return true; // без зазора
if (!PruneRandomLeaf()) return false; // освобождаем место и повторяем
}
}
private ExitsInRoomType[] LiftEntryDirs()
{
if (lengthLiftRoom > widthLiftRoom) return new[] { ExitsInRoomType.ZAxisP, ExitsInRoomType.ZAxisN };
if (widthLiftRoom > lengthLiftRoom) return new[] { ExitsInRoomType.XAxisP, ExitsInRoomType.XAxisN };
return AllDirs;
}
// Сдвиги лифта вдоль стены, чтобы коридор всё равно попал в стену
private IEnumerable<int> LiftShifts(ExitsInRoomType dir)
{
yield return 0;
if (!liftDoorAnyPosition) yield break;
bool xDir = dir == ExitsInRoomType.XAxisP || dir == ExitsInRoomType.XAxisN;
int cross = xDir ? widthLiftRoom : lengthLiftRoom;
int max = (cross - corridorWigth) / 2;
for (int s = 1; s <= max; s++) { yield return s; yield return -s; }
}
bool TryAttachLift(bool requireMinDepth, float margin)
{
var start = levelManager.GetRooms().First(r => r.rt == RoomType.Start);
var parents = _depth.Keys
.OrderByDescending(r => _depth[r])
.ThenByDescending(r => (r.Position - start.Position).sqrMagnitude)
.ToList();
foreach (var parent in parents)
{
if (requireMinDepth && _depth[parent] + 1 < _minDepthLiftRoom) continue;
foreach (var dir in LiftEntryDirs().OrderBy(_ => Random.value))
{
if (parent.exits.Contains(dir)) continue;
foreach (int shift in LiftShifts(dir))
{
Room lift = new Room { Length = lengthLiftRoom, Width = widthLiftRoom };
PlaceByEntry(EntryPoint(parent, dir), dir, lift);
bool xDir = dir == ExitsInRoomType.XAxisP || dir == ExitsInRoomType.XAxisN;
lift.Position += xDir ? new Vector2(0, shift) : new Vector2(shift, 0);
if (!CanPlaceLift(parent, lift, dir, margin)) continue;
lift.rt = RoomType.Lift;
lift.exits = new List<ExitsInRoomType>();
lift.entrance = ToEntrance(dir);
lift.isWallable = false;
levelManager.AddRoom(lift);
_depth[lift] = _depth[parent] + 1;
_parent[lift] = (parent, dir);
_corridorOf[lift] = CreateCorridor(parent, dir);
parent.exits.Add(dir);
_curRooms++;
return true;
}
}
}
return false;
}
bool CanPlaceLift(Room parent, Room lift, ExitsInRoomType dir, float margin)
{
if (!InBounds(lift)) return false;
if (!IsAreaFree(CorridorRect(parent, dir))) return false;
Rect area = RoomRect(lift);
float m = Mathf.Min(margin, corridorLength);
area.xMin -= m; area.yMin -= m; area.xMax += m; area.yMax += m;
return IsAreaFree(area, parent);
}
// Удаляет случайную тупиковую комнату вместе с её коридором
bool PruneRandomLeaf()
{
var parentsSet = new HashSet<Room>(_parent.Values.Select(v => v.parent));
var leaves = _parent.Keys.Where(r => !parentsSet.Contains(r)).ToList();
if (leaves.Count == 0) return false;
Room leaf = leaves[Random.Range(0, leaves.Count)];
var (p, dir) = _parent[leaf];
LevelManager.Rooms.Remove(leaf);
if (_corridorOf.TryGetValue(leaf, out var cor))
{
LevelManager.Rooms.Remove(cor);
_corridorOf.Remove(leaf);
}
p.exits.Remove(dir);
_depth.Remove(leaf);
_parent.Remove(leaf);
_curRooms--;
return true;
}
// Позиция комнаты заданного размера относительно точки входа
private static void PlaceByEntry(Vector2 e, ExitsInRoomType dir, Room n)
{
switch (dir)
{
case ExitsInRoomType.XAxisP: n.Position = new Vector2(e.x, e.y - n.Width / 2f); break;
case ExitsInRoomType.XAxisN: n.Position = new Vector2(e.x - n.Length, e.y - n.Width / 2f); break;
case ExitsInRoomType.ZAxisP: n.Position = new Vector2(e.x - n.Length / 2f, e.y); break;
default: n.Position = new Vector2(e.x - n.Length / 2f, e.y - n.Width); break;
}
}
private bool InBounds(Room r) =>
r.Position.x >= 0 && r.Position.y >= 0 &&
r.Position.x + r.Length <= lengthFloor &&
r.Position.y + r.Width <= lengthFloor;
// Комнаты проверяются с зазором, коридоры без него (иначе зазор заденет собственный коридор лифта)
void GenerateFloor()
{
const int maxAttempts = 50;
bool ok = false;
for (int i = 0; i < maxAttempts && !ok; i++)
{
LevelManager.Rooms.Clear();
_depth.Clear();
_parent.Clear();
_corridorOf.Clear();
_curRooms = 0;
CreateStartRoom();
ok = PlaceElevator();
}
if (!ok) Debug.LogWarning("Лифт не удалось разместить, этаж собран без него");
Instantiate(GameObjectA, new Vector3(0, -1, 0), Quaternion.identity);
foreach (var room in levelManager.GetRooms())
{
float x = room.Position.x;
float z = room.Position.y; // Vector2.y -> Z в 3D
float length = room.Length;
float width = room.Width;
Instantiate(GameObjectB, new Vector3(x, 0, z), Quaternion.identity);
Instantiate(GameObjectB, new Vector3(x + length, 0, z), Quaternion.identity);
Instantiate(GameObjectB, new Vector3(x + length, 0, z + width), Quaternion.identity);
Instantiate(GameObjectB, new Vector3(x, 0, z + width), Quaternion.identity);
}
}
// ---------- Вспомогательные ----------
private static ExitsInRoomType Opposite(ExitsInRoomType d)
{
switch (d)
{
case ExitsInRoomType.XAxisP: return ExitsInRoomType.XAxisN;
case ExitsInRoomType.XAxisN: return ExitsInRoomType.XAxisP;
case ExitsInRoomType.ZAxisP: return ExitsInRoomType.ZAxisN;
default: return ExitsInRoomType.ZAxisP;
}
}
private static EntranceToRoomType ToEntrance(ExitsInRoomType d)
{
switch (d)
{
case ExitsInRoomType.XAxisP: return EntranceToRoomType.XAxisN;
case ExitsInRoomType.XAxisN: return EntranceToRoomType.XAxisP;
case ExitsInRoomType.ZAxisP: return EntranceToRoomType.ZAxisN;
default: return EntranceToRoomType.ZAxisP;
}
}
private static Rect RoomRect(Room r) => new Rect(r.Position.x, r.Position.y, r.Length, r.Width);
// Прямоугольник коридора, ведущего из prev в направлении dir
private Rect CorridorRect(Room prev, ExitsInRoomType dir)
{
switch (dir)
{
case ExitsInRoomType.XAxisP:
return new Rect(prev.Position.x + prev.Length,
prev.Position.y + (prev.Width - corridorWigth) / 2f,
corridorLength, corridorWigth);
case ExitsInRoomType.XAxisN:
return new Rect(prev.Position.x - corridorLength,
prev.Position.y + (prev.Width - corridorWigth) / 2f,
corridorLength, corridorWigth);
case ExitsInRoomType.ZAxisP:
return new Rect(prev.Position.x + (prev.Length - corridorWigth) / 2f,
prev.Position.y + prev.Width,
corridorWigth, corridorLength);
default: // ZAxisN
return new Rect(prev.Position.x + (prev.Length - corridorWigth) / 2f,
prev.Position.y - corridorLength,
corridorWigth, corridorLength);
}
}
// Центр стены новой комнаты, куда упирается коридор
private Vector2 EntryPoint(Room prev, ExitsInRoomType dir)
{
switch (dir)
{
case ExitsInRoomType.XAxisP:
return new Vector2(prev.Position.x + prev.Length + corridorLength,
prev.Position.y + prev.Width / 2f);
case ExitsInRoomType.XAxisN:
return new Vector2(prev.Position.x - corridorLength,
prev.Position.y + prev.Width / 2f);
case ExitsInRoomType.ZAxisP:
return new Vector2(prev.Position.x + prev.Length / 2f,
prev.Position.y + prev.Width + corridorLength);
default:
return new Vector2(prev.Position.x + prev.Length / 2f,
prev.Position.y - corridorLength);
}
}
private bool IsAreaFree(Rect rect, Room ignore = null)
{
foreach (var other in levelManager.GetRooms())
{
if (other == ignore) continue;
if (rect.Overlaps(RoomRect(other))) return false;
}
return true;
}
// ---------- Коридор ----------
Room CreateCorridor(Room prev, ExitsInRoomType dir)
{
Rect r = CorridorRect(prev, dir);
Room corridor = new Room(r.position,
Mathf.RoundToInt(r.width), Mathf.RoundToInt(r.height),
RoomType.Corridor, false,
ToEntrance(dir), new List<ExitsInRoomType> { dir });
LevelManager.Rooms.Add(corridor);
return corridor;
}
private bool FitRoom(Room prev, Room n, ExitsInRoomType dir, out bool shrunk)
{
Vector2 e = EntryPoint(prev, dir);
int origLen = n.Length, origWid = n.Width;
switch (dir)
{
case ExitsInRoomType.XAxisP:
case ExitsInRoomType.XAxisN:
{
float maxAlong = dir == ExitsInRoomType.XAxisP ? lengthFloor - e.x : e.x;
float maxCross = 2f * Mathf.Min(e.y, lengthFloor - e.y);
n.Length = Mathf.Min(n.Length, Mathf.FloorToInt(maxAlong));
n.Width = Mathf.Min(n.Width, Mathf.FloorToInt(maxCross));
float x = dir == ExitsInRoomType.XAxisP ? e.x : e.x - n.Length;
n.Position = new Vector2(x, e.y - n.Width / 2f);
break;
}
default: // Z
{
float maxAlong = dir == ExitsInRoomType.ZAxisP ? lengthFloor - e.y : e.y;
float maxCross = 2f * Mathf.Min(e.x, lengthFloor - e.x);
n.Width = Mathf.Min(n.Width, Mathf.FloorToInt(maxAlong));
n.Length = Mathf.Min(n.Length, Mathf.FloorToInt(maxCross));
float y = dir == ExitsInRoomType.ZAxisP ? e.y : e.y - n.Width;
n.Position = new Vector2(e.x - n.Length / 2f, y);
break;
}
}
shrunk = n.Length != origLen || n.Width != origWid;
return n.Length >= lengthWallMin && n.Width >= lengthWallMin;
}
bool TryPlaceRoom(Room prevRoom, ExitsInRoomType dir)
{
if (prevRoom == null || _curRooms >= maxRooms) return false;
Room newRoom = GenerateRandomRoom();
newRoom.rt = RoomType.Undefined;
newRoom.exits = new List<ExitsInRoomType>(); // заполнится по факту построенных выходов
newRoom.entrance = ToEntrance(dir);
if (!FitRoom(prevRoom, newRoom, dir, out bool shrunk)) return false;
newRoom.isWallable = shrunk;
// Пересечения: комната (с зазором, без prevRoom) и коридор к ней
Rect roomArea = RoomRect(newRoom);
float m = Mathf.Min(roomMargin, corridorLength);
roomArea.xMin -= m; roomArea.yMin -= m;
roomArea.xMax += m; roomArea.yMax += m;
if (!IsAreaFree(roomArea, prevRoom)) return false;
if (!IsAreaFree(CorridorRect(prevRoom, dir))) return false;
// Всё хорошо - фиксируем
levelManager.AddRoom(newRoom);
_depth[newRoom] = _depth[prevRoom] + 1;
_parent[newRoom] = (prevRoom, dir);
_corridorOf[newRoom] = CreateCorridor(prevRoom, dir); // вместо отдельного CreateCorridor(...)
_curRooms++;
if (!prevRoom.exits.Contains(dir)) prevRoom.exits.Add(dir);
// Ветвление: случайный порядок, назад не идём
foreach (var d in AllDirs.OrderBy(_ => Random.value))
{
if (d == Opposite(dir)) continue;
if (Random.value > branchChance) continue;
TryPlaceRoom(newRoom, d);
}
return true;
}
Room GenerateRandomRoom()
{
// Range(int,int) не включает верхнюю границу, поэтому +1
int a = Random.Range(lengthWallMin, lengthWallMax + 1);
int b = Random.Range(lengthWallMin, lengthWallMax + 1);
Room room = new Room();
room.Length = a;
room.Width = b;
return room;
}
// ---------- Стартовая комната ----------
void CreateStartRoom()
{
float randValue = Random.value;
int xR, zR;
ExitsInRoomType firstDir;
Room room = new Room(lengthStartRoom, lengthStartRoom, RoomType.Start);
room.exits = new List<ExitsInRoomType>();
room.entrance = EntranceToRoomType.Null;
if (randValue < 0.25f) // 1 квадрант
{
xR = Random.Range(0, lengthFloor / 2 - lengthStartRoom);
if (xR + lengthStartRoom <= lengthFloor / 4)
{
firstDir = ExitsInRoomType.XAxisP;
zR = Random.Range(0, lengthFloor / 2 - lengthStartRoom);
}
else
{
firstDir = ExitsInRoomType.ZAxisP;
zR = Random.Range(0, lengthFloor / 4 - lengthStartRoom);
}
}
else if (randValue < 0.5f) // 2 квадрант
{
xR = Random.Range(lengthFloor / 2, lengthFloor - lengthStartRoom);
if (xR + lengthStartRoom <= lengthFloor * 3 / 4)
{
firstDir = ExitsInRoomType.ZAxisP;
zR = Random.Range(0, lengthFloor / 4 - lengthStartRoom);
}
else
{
firstDir = ExitsInRoomType.XAxisN;
zR = Random.Range(0, lengthFloor / 2 - lengthStartRoom);
}
}
else if (randValue < 0.75f) // 3 квадрант
{
xR = Random.Range(lengthFloor / 2, lengthFloor - lengthStartRoom);
if (xR + lengthStartRoom <= lengthFloor * 3 / 4)
{
firstDir = ExitsInRoomType.ZAxisN;
zR = Random.Range(lengthFloor * 3 / 4, lengthFloor - lengthStartRoom);
}
else
{
firstDir = ExitsInRoomType.XAxisN;
zR = Random.Range(lengthFloor / 2, lengthFloor - lengthStartRoom);
}
}
else // 4 квадрант
{
xR = Random.Range(0, lengthFloor / 2 - lengthStartRoom);
if (xR + lengthStartRoom <= lengthFloor / 4)
{
firstDir = ExitsInRoomType.XAxisP;
zR = Random.Range(lengthFloor / 2, lengthFloor - lengthStartRoom);
}
else
{
firstDir = ExitsInRoomType.ZAxisN;
zR = Random.Range(lengthFloor * 3 / 4, lengthFloor - lengthStartRoom);
}
}
room.Position = new Vector2(xR, zR);
LevelManager.Rooms.Add(room);
_depth[room] = 0;
_curRooms = 1; // стартовая комната считается
// Предпочтительное направление, а при неудаче - остальные
if (TryPlaceRoom(room, firstDir)) return;
foreach (var d in AllDirs.Where(x => x != firstDir).OrderBy(_ => Random.value))
if (TryPlaceRoom(room, d)) return;
}
// ---------- Отладка ----------
void OnDrawGizmos()
{
var rooms = LevelManager.Rooms;
if (rooms == null) return;
foreach (var r in rooms)
{
switch (r.rt)
{
case RoomType.Start:
Gizmos.color = Color.green;
break;
case RoomType.Corridor:
Gizmos.color = Color.yellow;
break;
case RoomType.Lift:
Gizmos.color = Color.rebeccaPurple;
break;
default:
Gizmos.color = Color.cyan;
break;
}
// Gizmos.color = r.rt == RoomType.Start ? Color.green
// : r.rt == RoomType.Corridor ? Color.yellow : Color.cyan;
Vector3 c = new Vector3(r.Position.x + r.Length / 2f, 0, r.Position.y + r.Width / 2f);
Gizmos.DrawWireCube(c, new Vector3(r.Length, 0.1f, r.Width));
}
}
}
}