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 _depth = new Dictionary(); private readonly Dictionary _parent = new Dictionary(); [SerializeField] private bool liftDoorAnyPosition = false; // true, если дверь в префабе может быть не по центру private readonly Dictionary _corridorOf = new Dictionary(); 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 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(); 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(_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 { 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(); // заполнится по факту построенных выходов 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(); 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)); } } } }