384 lines
16 KiB
C#
384 lines
16 KiB
C#
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 LevelManager levelManager;
|
||
|
||
private static readonly ExitsInRoomType[] AllDirs =
|
||
{
|
||
ExitsInRoomType.XAxisP, ExitsInRoomType.XAxisN,
|
||
ExitsInRoomType.ZAxisP, ExitsInRoomType.ZAxisN
|
||
};
|
||
|
||
void Start()
|
||
{
|
||
levelManager = LevelManager.Instance;
|
||
GenerateFloor();
|
||
}
|
||
|
||
bool PlaceElevator()
|
||
{
|
||
var start = levelManager.GetRooms().First(r => r.rt == RoomType.Start);
|
||
|
||
// максимум шагов, при равенстве выбираем самую дальнюю от старта по прямой
|
||
var best = _depth
|
||
.OrderByDescending(p => p.Value)
|
||
.ThenByDescending(p => (p.Key.Position - start.Position).sqrMagnitude)
|
||
.First();
|
||
|
||
if (best.Key == start || best.Value <= _minDepthLiftRoom)
|
||
{
|
||
return false;
|
||
|
||
}
|
||
best.Key.rt = RoomType.Lift; // добавь значение в enum RoomType
|
||
best.Key.Length = lengthLiftRoom;
|
||
best.Key.Width = widthLiftRoom;
|
||
best.Key.exits.Clear();
|
||
return true;
|
||
}
|
||
void GenerateFloor()
|
||
{
|
||
LevelManager.Rooms.Clear();
|
||
_depth.Clear();// статический список мог остаться с прошлого запуска
|
||
_curRooms = 0;
|
||
|
||
CreateStartRoom();
|
||
|
||
if (!PlaceElevator())
|
||
{
|
||
GenerateFloor();
|
||
}
|
||
|
||
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;
|
||
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);
|
||
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));
|
||
}
|
||
}
|
||
}
|
||
} |