Здания — неотъемлемая часть большинства игровых миров, будь то оживлённый город или заброшенная деревня. Однако их создание может быть настоящей головной болью:
-
Ручное размещение: Модульные элементы, как 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/
Добавить комментарий