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

582 lines
26 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
{
// =====================================================================
// НЕ ЗАБЫТЬ:
// 1. Поля инспектора - в МЕТРАХ. Внутри генератора всё в МОДУЛЯХ (0.5 м), см. ReadSettings().
// 2. Размеры комнат только чётные в модулях (целые метры): метод Even().
// 3. Данные этажа лежат в FloorData (_data). Статический LevelManager.Rooms больше не используется.
// 4. TODO: переименовать Room.isWallable (сейчас значит "комнату ужали об край карты").
// 5. TODO: спавн маркеров в Generate() временный. Он переедет в стадию "оболочка" (стены, пол).
// 6. Этаж с боссом (крыша) сюда не попадает: это FloorKind.Special, собирается вручную.
// =====================================================================
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 minRoomCount;
[SerializeField] private int _minDepthLiftRoom;
[SerializeField] private float _minDistanceStartLift; // метры
[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; // зазор между комнатами, метры
[SerializeField] private bool liftDoorAnyPosition = false; // true, если дверь лифта может быть не по центру
// ---- Те же настройки в модулях (заполняет ReadSettings) ----
private int _wallMin, _wallMax, _startSize, _liftLen, _liftWid, _floor, _corrLen, _corrWid, _margin;
private float _minDist;
// ---- Состояние генерации ----
private FloorData _data;
private int _curRooms;
private readonly Dictionary<Room, (Room parent, ExitsInRoomType dir)> _parent
= new Dictionary<Room, (Room, ExitsInRoomType)>();
private readonly Dictionary<Room, Room> _corridorOf = new Dictionary<Room, Room>();
public FloorData Data => _data;
private static readonly ExitsInRoomType[] AllDirs =
{
ExitsInRoomType.XAxisP, ExitsInRoomType.XAxisN,
ExitsInRoomType.ZAxisP, ExitsInRoomType.ZAxisN
};
private static readonly RoomType[] ClippedRoles = { RoomType.Laboratory, RoomType.Office, RoomType.Penthouse };
private static readonly RoomType[] FullRoles = { RoomType.Storage, RoomType.SwitchboardRoom, RoomType.BoilerRoom };
void Start()
{
ReadSettings();
Generate(0); // позже этим будет управлять оркестратор
}
void ReadSettings()
{
_wallMin = LevelGrid.FromMeters(lengthWallMin);
_wallMax = LevelGrid.FromMeters(lengthWallMax);
_startSize = LevelGrid.FromMeters(lengthStartRoom);
_liftLen = LevelGrid.FromMeters(lengthLiftRoom);
_liftWid = LevelGrid.FromMeters(widthLiftRoom);
_floor = LevelGrid.FromMeters(lengthFloor);
_corrLen = LevelGrid.FromMeters(corridorLength);
_corrWid = LevelGrid.FromMeters(corridorWigth);
_margin = Mathf.RoundToInt(roomMargin * LevelGrid.PerMeter);
_minDist = _minDistanceStartLift * LevelGrid.PerMeter;
}
void OnValidate()
{
lengthWallMax = Mathf.Max(lengthWallMax, lengthWallMin);
minRoomCount = Mathf.Min(minRoomCount, maxRooms + 1); // +1 за лифт
if (lengthFloor < lengthStartRoom + corridorLength + Mathf.Max(lengthLiftRoom, widthLiftRoom))
Debug.LogWarning("lengthFloor слишком мал для старта + коридора + лифта");
}
// ---------- Генерация этажа ----------
void ResetState(int floorIndex)
{
_data = FloorData.CreateGenerated(floorIndex, _floor);
_parent.Clear();
_corridorOf.Clear();
_curRooms = 0;
}
public FloorData Generate(int floorIndex)
{
bool ok = false;
// Фаза 1: все ограничения, без обрезки веток
for (int i = 0; i < 50 && !ok; i++)
{
ResetState(floorIndex);
CreateStartRoom();
ok = TryAttachLift(true, _margin, _minDist) && _curRooms >= minRoomCount;
}
// Фаза 2 (аварийная): лифт обязателен, остальное ослабляется
for (int i = 0; i < 20 && !ok; i++)
{
ResetState(floorIndex);
CreateStartRoom();
ok = PlaceElevator();
if (ok) Debug.LogWarning("Этаж собран в аварийном режиме: ограничения ослаблены");
}
if (!ok) Debug.LogError("Лифт не помещается: проверь размеры карты, лифта и коридора");
AssignRoles();
ShellBuilder.Build(_data); // сегменты стен (только данные, без спавна)
SpawnMarkers(); // временно
return _data;
}
// Роли для комнат без назначения: ужатые об край карты и обычные берут разные пулы
void AssignRoles()
{
foreach (var r in _data.UnassignedRooms.ToList())
{
var pool = r.isClipped ? ClippedRoles : FullRoles;
r.rt = pool[Random.Range(0, pool.Length)];
}
}
// Временная визуализация, переедет в стадию "оболочка"
void SpawnMarkers()
{
Instantiate(GameObjectA, new Vector3(0, -1, 0), Quaternion.identity);
foreach (var room in _data.Rooms)
{
var p = room.Position;
Instantiate(GameObjectB, LevelGrid.ToWorld(new Vector2Int(p.x, p.y)), Quaternion.identity);
Instantiate(GameObjectB, LevelGrid.ToWorld(new Vector2Int(p.x + room.Length, p.y)), Quaternion.identity);
Instantiate(GameObjectB, LevelGrid.ToWorld(new Vector2Int(p.x + room.Length, p.y + room.Width)), Quaternion.identity);
Instantiate(GameObjectB, LevelGrid.ToWorld(new Vector2Int(p.x, p.y + room.Width)), Quaternion.identity);
}
}
// ---------- Лифт ----------
bool PlaceElevator()
{
while (true)
{
if (TryAttachLift(true, _margin, _minDist)) return true;
if (TryAttachLift(false, _margin, _minDist)) return true;
if (TryAttachLift(false, _margin, 0f)) return true;
if (TryAttachLift(false, 0, 0f)) return true;
if (!PruneRandomLeaf()) return false; // последнее средство: освобождаем место
}
}
// Вход в лифт только в длинную стену
private ExitsInRoomType[] LiftEntryDirs()
{
if (_liftLen > _liftWid) return new[] { ExitsInRoomType.ZAxisP, ExitsInRoomType.ZAxisN };
if (_liftWid > _liftLen) 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 ? _liftWid : _liftLen;
int max = (cross - _corrWid) / 2;
for (int s = 1; s <= max; s++) { yield return s; yield return -s; }
}
bool TryAttachLift(bool requireMinDepth, int margin, float minDistance)
{
var start = _data.Start;
if (start == null) return false;
var parents = _data.Depth.Keys
.OrderByDescending(r => _data.Depth[r])
.ThenByDescending(r => (r.Center - start.Center).sqrMagnitude)
.ToList();
foreach (var parent in parents)
{
int liftDepth = _data.Depth[parent] + 1;
if (requireMinDepth && liftDepth < _minDepthLiftRoom) continue;
foreach (var dir in LiftEntryDirs().OrderBy(_ => Random.value))
{
if (parent.exits.Contains(dir)) continue;
bool xDir = dir == ExitsInRoomType.XAxisP || dir == ExitsInRoomType.XAxisN;
foreach (int shift in LiftShifts(dir))
{
Room lift = new Room { Length = _liftLen, Width = _liftWid };
PlaceByEntry(EntryPoint(parent, dir), dir, lift);
lift.Position += xDir ? new Vector2Int(0, shift) : new Vector2Int(shift, 0);
if (minDistance > 0f && Vector2.Distance(lift.Center, start.Center) < minDistance)
continue;
if (!CanPlaceLift(parent, lift, dir, margin)) continue;
lift.rt = RoomType.Lift; // тип нужно выставить ДО AddRoom, чтобы FloorData запомнил Lift
lift.exits = new List<ExitsInRoomType>();
lift.entrance = ToEntrance(dir);
lift.isClipped = false;
_data.AddRoom(lift, liftDepth);
_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, int margin)
{
if (!InBounds(lift)) return false;
if (!IsAreaFree(CorridorRect(parent, dir))) return false;
return IsAreaFree(Inflate(RoomRect(lift), Mathf.Min(margin, _corrLen)), 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];
_data.Remove(leaf);
if (_corridorOf.TryGetValue(leaf, out var cor))
{
_data.Remove(cor);
_corridorOf.Remove(leaf);
}
p.exits.Remove(dir);
_parent.Remove(leaf);
_curRooms--;
return true;
}
// ---------- Геометрия (всё в модулях) ----------
private static void PlaceByEntry(Vector2Int e, ExitsInRoomType dir, Room n)
{
switch (dir)
{
case ExitsInRoomType.XAxisP: n.Position = new Vector2Int(e.x, e.y - n.Width / 2); break;
case ExitsInRoomType.XAxisN: n.Position = new Vector2Int(e.x - n.Length, e.y - n.Width / 2); break;
case ExitsInRoomType.ZAxisP: n.Position = new Vector2Int(e.x - n.Length / 2, e.y); break;
default: n.Position = new Vector2Int(e.x - n.Length / 2, e.y - n.Width); break;
}
}
private bool InBounds(Room r) =>
r.Position.x >= 0 && r.Position.y >= 0 &&
r.Position.x + r.Length <= _floor &&
r.Position.y + r.Width <= _floor;
// Чётное значение (целые метры), чтобы центровка (a - b) / 2 была целой
private static int Even(int v) => v & ~1;
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 RectInt RoomRect(Room r) => new RectInt(r.Position.x, r.Position.y, r.Length, r.Width);
private static RectInt Inflate(RectInt r, int m) =>
new RectInt(r.x - m, r.y - m, r.width + 2 * m, r.height + 2 * m);
// Прямоугольник коридора, ведущего из prev в направлении dir
private RectInt CorridorRect(Room prev, ExitsInRoomType dir)
{
switch (dir)
{
case ExitsInRoomType.XAxisP:
return new RectInt(prev.Position.x + prev.Length,
prev.Position.y + (prev.Width - _corrWid) / 2,
_corrLen, _corrWid);
case ExitsInRoomType.XAxisN:
return new RectInt(prev.Position.x - _corrLen,
prev.Position.y + (prev.Width - _corrWid) / 2,
_corrLen, _corrWid);
case ExitsInRoomType.ZAxisP:
return new RectInt(prev.Position.x + (prev.Length - _corrWid) / 2,
prev.Position.y + prev.Width,
_corrWid, _corrLen);
default: // ZAxisN
return new RectInt(prev.Position.x + (prev.Length - _corrWid) / 2,
prev.Position.y - _corrLen,
_corrWid, _corrLen);
}
}
// Центр стены новой комнаты, куда упирается коридор
private Vector2Int EntryPoint(Room prev, ExitsInRoomType dir)
{
switch (dir)
{
case ExitsInRoomType.XAxisP:
return new Vector2Int(prev.Position.x + prev.Length + _corrLen,
prev.Position.y + prev.Width / 2);
case ExitsInRoomType.XAxisN:
return new Vector2Int(prev.Position.x - _corrLen,
prev.Position.y + prev.Width / 2);
case ExitsInRoomType.ZAxisP:
return new Vector2Int(prev.Position.x + prev.Length / 2,
prev.Position.y + prev.Width + _corrLen);
default:
return new Vector2Int(prev.Position.x + prev.Length / 2,
prev.Position.y - _corrLen);
}
}
private bool IsAreaFree(RectInt rect, Room ignore = null)
{
foreach (var other in _data.Rooms)
{
if (other == ignore) continue;
if (rect.Overlaps(RoomRect(other))) return false;
}
return true;
}
// ---------- Коридор ----------
Room CreateCorridor(Room prev, ExitsInRoomType dir)
{
RectInt r = CorridorRect(prev, dir);
// Конструктор Room принимает (width = размер по Z, length = размер по X).
// RectInt: width = размер по X, height = размер по Z - поэтому аргументы переставлены.
Room corridor = new Room(r.position, r.height, r.width,
RoomType.Corridor, false,
ToEntrance(dir), new List<ExitsInRoomType> { dir });
_data.AddCorridor(corridor);
return corridor;
}
// ---------- Размещение комнаты ----------
// Задаёт размер и позицию комнаты от точки входа: вдоль оси выхода комната растёт на полную
// длину, поперёк - симметрично (центр на коридоре). Если не влезает, ужимает.
private bool FitRoom(Room prev, Room n, ExitsInRoomType dir, out bool shrunk)
{
Vector2Int e = EntryPoint(prev, dir);
int origLen = n.Length, origWid = n.Width;
switch (dir)
{
case ExitsInRoomType.XAxisP:
case ExitsInRoomType.XAxisN:
{
int maxAlong = dir == ExitsInRoomType.XAxisP ? _floor - e.x : e.x;
int maxCross = 2 * Mathf.Min(e.y, _floor - e.y);
n.Length = Even(Mathf.Min(n.Length, maxAlong));
n.Width = Even(Mathf.Min(n.Width, maxCross));
int x = dir == ExitsInRoomType.XAxisP ? e.x : e.x - n.Length;
n.Position = new Vector2Int(x, e.y - n.Width / 2);
break;
}
default: // Z
{
int maxAlong = dir == ExitsInRoomType.ZAxisP ? _floor - e.y : e.y;
int maxCross = 2 * Mathf.Min(e.x, _floor - e.x);
n.Width = Even(Mathf.Min(n.Width, maxAlong));
n.Length = Even(Mathf.Min(n.Length, maxCross));
int y = dir == ExitsInRoomType.ZAxisP ? e.y : e.y - n.Width;
n.Position = new Vector2Int(e.x - n.Length / 2, y);
break;
}
}
shrunk = n.Length != origLen || n.Width != origWid;
return n.Length >= _wallMin && n.Width >= _wallMin;
}
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.isClipped = shrunk;
// Пересечения: комната (с зазором, без prevRoom) и коридор к ней
RectInt roomArea = Inflate(RoomRect(newRoom), Mathf.Min(_margin, _corrLen));
if (!IsAreaFree(roomArea, prevRoom)) return false;
if (!IsAreaFree(CorridorRect(prevRoom, dir))) return false;
// Всё хорошо - фиксируем
_data.AddRoom(newRoom, _data.Depth[prevRoom] + 1);
_parent[newRoom] = (prevRoom, dir);
_corridorOf[newRoom] = CreateCorridor(prevRoom, dir);
_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);
return new Room { Length = LevelGrid.FromMeters(a), Width = LevelGrid.FromMeters(b) };
}
// ---------- Стартовая комната ----------
void CreateStartRoom()
{
float randValue = Random.value;
int xR, zR;
ExitsInRoomType firstDir;
Room room = new Room(_startSize, _startSize, RoomType.Start);
room.exits = new List<ExitsInRoomType>();
room.entrance = EntranceToRoomType.Null;
if (randValue < 0.25f) // 1 квадрант
{
xR = Random.Range(0, _floor / 2 - _startSize);
if (xR + _startSize <= _floor / 4)
{
firstDir = ExitsInRoomType.XAxisP;
zR = Random.Range(0, _floor / 2 - _startSize);
}
else
{
firstDir = ExitsInRoomType.ZAxisP;
zR = Random.Range(0, _floor / 4 - _startSize);
}
}
else if (randValue < 0.5f) // 2 квадрант
{
xR = Random.Range(_floor / 2, _floor - _startSize);
if (xR + _startSize <= _floor * 3 / 4)
{
firstDir = ExitsInRoomType.ZAxisP;
zR = Random.Range(0, _floor / 4 - _startSize);
}
else
{
firstDir = ExitsInRoomType.XAxisN;
zR = Random.Range(0, _floor / 2 - _startSize);
}
}
else if (randValue < 0.75f) // 3 квадрант
{
xR = Random.Range(_floor / 2, _floor - _startSize);
if (xR + _startSize <= _floor * 3 / 4)
{
firstDir = ExitsInRoomType.ZAxisN;
zR = Random.Range(_floor * 3 / 4, _floor - _startSize);
}
else
{
firstDir = ExitsInRoomType.XAxisN;
zR = Random.Range(_floor / 2, _floor - _startSize);
}
}
else // 4 квадрант
{
xR = Random.Range(0, _floor / 2 - _startSize);
if (xR + _startSize <= _floor / 4)
{
firstDir = ExitsInRoomType.XAxisP;
zR = Random.Range(_floor / 2, _floor - _startSize);
}
else
{
firstDir = ExitsInRoomType.ZAxisN;
zR = Random.Range(_floor * 3 / 4, _floor - _startSize);
}
}
room.Position = new Vector2Int(xR, zR);
_data.AddRoom(room, 0); // rt = Start, поэтому FloorData запомнит _data.Start
_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()
{
if (_data == null) return;
foreach (var r in _data.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;
case RoomType.Laboratory: Gizmos.color = Color.white; break;
case RoomType.Office: Gizmos.color = Color.blue; break;
case RoomType.Penthouse: Gizmos.color = Color.magenta; break;
case RoomType.Storage: Gizmos.color = new Color(1f, 0.5f, 0f); break;
case RoomType.SwitchboardRoom: Gizmos.color = Color.red; break;
case RoomType.BoilerRoom: Gizmos.color = Color.gray; break;
default: Gizmos.color = Color.cyan; break;
}
Vector3 c = LevelGrid.ToWorld(r.Center);
Gizmos.DrawWireCube(c, new Vector3(r.Length * LevelGrid.Module, 0.1f, r.Width * LevelGrid.Module));
}
// Сегменты стен: чёрные - стена, красные - угол
foreach (var s in _data.Walls)
{
Gizmos.color = s.Kind == SegmentKind.Corner ? Color.red : Color.black;
Vector3 pos = LevelGrid.ToWorld(new Vector2(s.Cell.x + 0.5f, s.Cell.y + 0.5f), 0.5f);
Gizmos.DrawCube(pos, new Vector3(LevelGrid.Module, 1f, LevelGrid.Module) * 0.9f);
}
}
}
}