Разрушаемость в Unity
Недавно я реализовал разрушаемые объекты окружения в своём аркадном авиасимуляторе. Сейчас расскажу, как именно я это сделал.
Общая логика работы разрушаемых объектов
- Нужно подготовить модель предмета, который требуется сломать и модель того же самого предмета, но в разбитом виде.
- Изначально игрок видит цельный объект. Когда он начинает ломать конструкцию, мы подсовываем модель разбитого объекта.
- Куски должны отламываться в том месте, куда ударил игрок, но оставшаяся часть объекта, состоящая из таких же осколков, при этом должна сохранить форму.
- Нужно решить проблему, из-за которой отдельные кусочки будут просто висеть в воздухе, ожидая физического воздействия.
Подготовка мешей
Итак, начнём с мешей. Нужно нарисовать объект в двух вариантах: целый и разрушенный. Для моделирования я использую Blender — в нём есть удобный аддон Cell Fracture. Он позволяет разломать объект произвольным образом на неровные куски. Для прототипа я сделал стену и колонну, которые буду разрушать.
После того как модели готовы, можно приступать к реализации логики разрушаемых объектов окружения.
Отделение физики осколков от самолёта
Перед тем как идти дальше, сделаю важную оговорку. В разрушении объектов для меня было важно, чтобы отлетающие куски не влияли на физику самолёта.
Это сильно аффектит фидбэк, но при этом мы полностью исключаем ситуации, когда из-за неудачно прилетевшего куска стены самолёт закручивает и он падает.
Программная реализация
DestructibleConstruction : MonoBehaviour
Класс отвечает за разрушаемый объект в целом. Он хранит информацию об осколках и переключает модель с целым объектом на модель разбитой конструкции.
Сам префаб объекта выглядит достаточно просто. Это пустой GameObject, которому подчинён меш целого объекта.
Поля класса:
Модель с кусочками мы будем подтягивать программно. Причина простая: когда кусков много, это может быть и незаметно, но если делать небольшое количество фрагментов, становится очевидно, что предмет ломается всегда одинаково.
Чтобы сгладить этот момент, мы делаем несколько вариантов разрушенной конструкции и в Awake случайным образом выбираем один из них.
Итак, в InstantiateVariant мы выбираем вариант осколков и добавляем его в объект. Затем в InitializeFragments обходим объекты осколков и инициализируем каждый из них:
- Слой изначально оставляем таким же, как у самолёта, иначе триггеры не будут срабатывать.
- Назначаем Rigidbody, при этом изначально отключаем физику и блокируем положение объекта в пространстве.
- Для Rigidbody рассчитываем массу пропорционально объёму меша.
- Добавляем BoxCollider для обработки столкновений в будущем, но пока устанавливаем для него флаг isTrigger = true (это сделано, чтобы самолёт не сталкивался с фрагментами).
- Добавляем SphereCollider — это уже полноценный триггер. Событие попадания самолёта в этот триггер будет сигналом для фрагмента о том, что пора отколоться.
- Назначаем объекту осколка MonoBehaviour Fragment.
Конструкция готова к разрушению.
Теперь самое интересное. Добавляем объекту DestructibleConstruction BoxCollider и в OnTriggerEnter вызываем BreakConstruction при условии, что объект — это самолёт игрока.
Тут всё примитивно. Выключаем целую конструкцию и включаем составленную из кусков. Важно понимать, что теперь фрагменты будут реагировать на триггеры, так что самое время посмотреть, как устроен класс Fragment.
Fragment : MonoBehaviour
Класс отвечает за отдельный кусок конструкции. По сути он ждёт взаимодействия, когда это взаимодействие происходит, Fragment включает физику своего куска и сообщает ему импульс в необходимом направлении.
Поля класса:
Всё самое интересное происходит при срабатывании тригера фрагмента.
Проверяем, что триггер сработал именно на самолёт, после чего начинаем откалывать объект.
Для начала вызываем публичный метод activateFragment: включаем физику для BoxCollider и снимаем у него флаг isTrigger, чтобы для него обрабатывались столкновения. Слой переключаем на Fragments, чтобы куски не влияли на самолёт.
Затем запускаем таймер, по истечении которого объект будет удалён. После включения физики фрагменту сообщаем импульс, который зависит от скорости самолёта и множителя impulseMultiplier.
Проблема висящих кусков
Мы реализовали пункты 1, 2 и 3. Остался четвёртый и самый интересный — нужно решить вопрос с висящими в воздухе кусочками.
Я применил следующий подход: назначаем некоторое множество фрагментов якорями, то есть считаем, что эти фрагменты закреплены и на них держится конструкция.
Так мы можем определить, какие куски могут висеть в воздухе — это те, которые соединены с этими якорными фрагментами.
Для того, чтобы понимать, какие куски между собой соединены, представим конструкцию в виде графа: Узлы графа — это фрагменты конструкции; Рёбра графа определяют соседние куски.
Если кусок откалывается, мы удаляем его из графа. В случае если граф становится несвязным, оставляем только те его компоненты (острова), которые содержат якорные фрагменты. Все остальные вершины принудительно откалываем от конструкции, поскольку они не имеют опоры.
В класс DestructibleConstruction добавляем поля:
В класс Fragment добавляем булевое поле isAnchor - его будем выставлять в true для якорных фрагментов.
Ниже скрины с примерами якорных коллайдеров. Для стены они расположены по её углам, для колонны — сверху и снизу.
Для работы с графом я реализовал следующие процедуры:
Внутри BuildFragmentGraph мы составляем первоначальный граф. Эту процедуру вызываем один раз при подмене целой конструкции на набор осколков.
Процедура CalculateFragmentGraph обходит граф и "откалывает" те куски, которые не связаны с якорями. Она вызывается из FixedUpdate всякий раз после того, как откололся фрагмент.
RemoveFragmentFromGraph удаляет фрагмент из графа. Эту процедуру мы также вызываем при активации фрагмента.
В итоге у нас не остаётся подвешенных в воздухе кусков конструкции, и при потере связи с якорем остров целиком обваливается.
В качестве резюме ещё одна небольшая демонстрация. На ней можно явно наблюдать момент, когда цельная конструкция подменяется осколками и как эти осколки реагируют на столкновения.
Спасибо за внимание!
Если проект заинтересовал, обязательно подписывайся тут или в телеге. Там я тоже регулярно делюсь прогрессом по разработке.