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 _parent = new Dictionary(); private readonly Dictionary _corridorOf = new Dictionary(); 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(); SpawnMarkers(); // временно return _data; } // Роли для комнат без назначения: ужатые об край карты и обычные берут разные пулы void AssignRoles() { foreach (var r in _data.UnassignedRooms.ToList()) { var pool = r.isWallable ? 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 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(); lift.entrance = ToEntrance(dir); lift.isWallable = 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(_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 { 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(); // заполнится по факту построенных выходов newRoom.entrance = ToEntrance(dir); if (!FitRoom(prevRoom, newRoom, dir, out bool shrunk)) return false; newRoom.isWallable = 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(); 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)); } } } }