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

  • Ручное размещение: Модульные элементы, как 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)


  1. Wolf4D
    07.07.2025 15:18

    Много кода, много слов, а в итоге что получилось? Статья абсолютно "слепая", ни одной иллюстрации


  1. Bunyaz39
    07.07.2025 15:18

    Подход интересный, но не до конца понял, как быть с углами и нестандартными фасадами. Сплайны хорошо, но ведь искажения сеток тоже бывают