Процедурная генерация зданий в Unity с помощью сплайнов

от автора

Здания — неотъемлемая часть большинства игровых миров, будь то оживлённый город или заброшенная деревня. Однако их создание может быть настоящей головной болью:

  • Ручное размещение: Модульные элементы, как LEGO, позволяют добиться точности, но для больших сцен это утомительно.

  • Полная процедурная генерация: Алгоритмы создают целые города, но требуют сложной настройки и могут давать непредсказуемые результаты.

  • Минимализм: Низкополигональные кубы экономят время, но выглядят слишком просто.

Каждая игра и команда требуют своего подхода. Я хотел сосредоточиться на геймплее, а не на ручной сборке зданий. Поэтому использовал инструмент на базе сплайнов в Unity, который позволяет быстро генерировать здания, сохраняя контроль над их формой и стилем.

Почему сплайны?

Сплайновый подход — это золотая середина между ручным дизайном и полной процедурной генерацией. Он позволяет:

  • Быстро задавать форму здания, рисуя сплайн в окне сцены.

  • Размещать готовые сетки (стены, окна, двери) вдоль сплайна.

  • Легко вносить изменения, перемещая точки сплайна или меняя сетки.

  • Поддерживать единообразие карты без утомительного ручного труда.

Минус в том, что система зависит от сеток, подходящих для сплайнов, и не идеальна для сложных крыш или детализированных дизайнов.

Создание инструмента Building Maker

Шаг 1: Подготовка сплайна

Для начала установите пакет Splines через Package Manager в Unity. Создайте сплайн в сцене — он станет основой для здания. Затем создайте скрипт BuildingMaker с атрибутами:

  • RequireComponent(typeof(SplineContainer)) — для привязки сплайна.

  • ExecuteInEditMode — чтобы скрипт работал в редакторе.

В методе Awake сохраните ссылку на сплайн-контейнер. Определите переменные для сеток (стена, окно, дверь) и расстояния между точками.

[RequireComponent(typeof(SplineContainer))] [ExecuteInEditMode] public class BuildingMaker : MonoBehaviour {     private SplineContainer splineContainer;     public Mesh wallMesh, windowMesh, doorMesh;     public float distance;      void Awake() {         splineContainer = GetComponent<SplineContainer>();     } }

Шаг 2: Генерация точек вдоль сплайна

Создайте метод CalculatePoints, который вычисляет точки и касательные вдоль сплайна на основе ширины сетки стены. Используйте функцию GetPointAtLinearDistance для равномерного размещения точек. Вызовите метод в OnValidate, чтобы обновлять точки при изменении параметров.

void OnValidate() {     if (wallMesh) distance = wallMesh.bounds.size.x;     CalculatePoints(); }  void CalculatePoints() {     var points = new List<Vector3>();     var tangents = new List<Vector3>();     var spline = splineContainer.Spline;     float t = 0, length = spline.GetLength();     while (t < 1) {         spline.Evaluate(t, out var point, out var tangent, out _);         points.Add(point);         tangents.Add(tangent);         t += distance / length;     } }

Для отладки визуализируйте точки с помощью OnDrawGizmosSelected:

void OnDrawGizmosSelected() {     foreach (var point in points) {         Gizmos.DrawSphere(point, 0.1f);     } }

Добавьте способ отслеживать изменения сплайна в OnEnable и отписку в OnDisable, чтобы точки обновлялись при редактировании сплайна.

Шаг 3: Размещение сеток

Создайте дочерний объект для хранения сгенерированных сеток и список CombineInstance для объединения. В цикле for проходите по точкам, задавая позицию и направление сетки. Используйте матрицу смещения для правильной ориентации, меняя оси X и Z, чтобы сетка «смотрела» краем вдоль сплайна.

public List<CombineInstance> instances = new List<CombineInstance>(); void GenerateMesh() {     instances.Clear();     for (int i = 0; i < points.Count; i++) {         var point = points[i];         var tangent = tangents[i];         var direction = (i < points.Count - 1 ? points[i + 1] : points[0]) - point;         var lookDir = new Vector3(tangent.z, 0, -tangent.x);         var offsetMatrix = Matrix4x4.TRS(point, Quaternion.LookRotation(lookDir), Vector3.one);         var instance = new CombineInstance { mesh = wallMesh, transform = offsetMatrix };         instances.Add(instance);     } }

Для заполнения зазора в последней точке масштабируйте стену по X, вычислив остаток расстояния.

Шаг 4: Добавление окон и дверей

Добавьте свойства для распределения окон и дверей (например, windowDistribution, doorDistribution, maxDoors). В цикле GenerateMesh выбирайте сетку (стена, окно или дверь) на основе параметров:

  • windowDistribution = 0: только стены.

  • windowDistribution = 1: случайный выбор.

  • windowDistribution > 1: окна через заданное количество точек.

Для дверей добавьте счётчик, чтобы ограничить их количество.

public float windowDistribution, doorDistribution; public int maxDoors; void GenerateMesh() {     int doorCount = 0;     for (int i = 0; i < points.Count; i++) {         Mesh selectedMesh = wallMesh;         if (doorCount < maxDoors && doorDistribution > 0 && i % doorDistribution == 0) {             selectedMesh = doorMesh;             doorCount++;         } else if (windowDistribution == 1 && Random.value > 0.5f) {             selectedMesh = windowMesh;         } else if (windowDistribution > 1 && i % windowDistribution == 0) {             selectedMesh = windowMesh;         }         // Добавление CombineInstance как выше     } }

Шаг 5: Генерация этажей

Добавьте переменную floorCount (минимум 1). Оберните цикл размещения сеток в цикл по этажам, смещая позицию по Y на высоту стены. Двери размещайте только на первом этаже.

public int floorCount = 1; void GenerateMesh() {     instances.Clear();     for (int floor = 0; floor < floorCount; floor++) {         for (int i = 0; i < points.Count; i++) {             var point = points[i] + Vector3.up * floor * wallMesh.bounds.size.y;             Mesh selectedMesh = (floor == 0 && doorCount < maxDoors && i % doorDistribution == 0) ? doorMesh : (windowDistribution > 1 && i % windowDistribution == 0) ? windowMesh : wallMesh;             // Добавление CombineInstance         }     } }

Шаг 6: Настройка высоты через гизмо

Создайте скрипт BuildingMakerEditor для интуитивной настройки высоты. В методе OnSceneGUI вычислите центр здания, высоту этажа и добавьте FreeMoveHandle для изменения floorCount.

[CustomEditor(typeof(BuildingMaker))] public class BuildingMakerEditor : Editor {     void OnSceneGUI() {         var maker = (BuildingMaker)target;         var center = maker.points.Aggregate(Vector3.zero, (sum, p) => sum + p) / maker.points.Count;         var floorHeight = maker.wallMesh.bounds.size.y;         var top = center + Vector3.up * floorHeight * maker.floorCount;         Handles.FreeMoveHandle(top, Quaternion.identity, 0.5f, Vector3.zero, Handles.SphereHandleCap);         // Обновление floorCount по Y-позиции     } }

Шаг 7: Генерация крыши

Для крыши создайте метод BuildRoofMeshData. Используйте структуры VertexInfo и Quad для упрощения работы с вершинами и UV. Вычислите центр здания, создайте quad для мансардной крыши, лерпя между базовыми и центральными точками. Для плоской крыши добавьте дополнительные quad, если параметр края меньше 1.

struct VertexInfo { public Vector3 position; /* Данные вершин */ } struct Quad {     public Quad(Vector3[] vertices, float offset) { /* Создание вершин, UV, треугольников */ } } void BuildRoofMeshData(List<CombineInstance> instances) {     var center = points.Aggregate(Vector3.zero, (sum, p) => sum + p) / points.Count;     var faces = new List<Quad>();     for (int i = 0; i < points.Count; i++) {         var p1 = points[i] + Vector3.up * floorCount * wallMesh.bounds.size.y;         var p2 = Vector3.Lerp(p1, center, edgePoint);         // Создание quad и добавление в faces     }     instances.Add(GetMeshFromQuads(faces)); }

Шаг 8: Оптимизация и запекание

Объедините сетки с помощью CombineMeshes с параметром mergeSubMeshes = true, сохраняя материалы. Добавьте переключатель lock для фиксации здания и скрипт для запекания сетки в проект.

public bool lock; void OnValidate() {     if (!lock) GenerateMesh(); }

Преимущества и ограничения метода

Преимущества:

  • Быстрая генерация зданий с помощью сплайнов.

  • Гибкость: изменение формы и высоты в реальном времени.

  • Подходит для городских уровней с умеренной детализацией.

Ограничения:

  • Зависимость от сеток, адаптированных для сплайнов.

  • Не подходит для сложных крыш или высокодетализированных дизайнов.

Инструмент Building Maker позволяет быстро создавать правдоподобные здания, экономя время на разработке уровней. Он идеален для инди-разработчиков, работающих над недетализированными городскими мирами. Попробуйте адаптировать этот подход под ваш проект, добавив свои сетки или настройки.

Александр Антипин, студия разработки Metabula Games


ссылка на оригинал статьи https://habr.com/ru/articles/925706/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *