Здания — неотъемлемая часть большинства игровых миров, будь то оживлённый город или заброшенная деревня. Однако их создание может быть настоящей головной болью:
Ручное размещение: Модульные элементы, как 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
Комментарии (2)
Bunyaz39
07.07.2025 15:18Подход интересный, но не до конца понял, как быть с углами и нестандартными фасадами. Сплайны хорошо, но ведь искажения сеток тоже бывают
Wolf4D
Много кода, много слов, а в итоге что получилось? Статья абсолютно "слепая", ни одной иллюстрации