Осколки остаются, физика заканчивается: жизненный цикл обломков в Chaos Destruction

Откуда взялась задача

На одном из проектов мы активно использовали Chaos Destruction. Разрушаемой была заметная часть окружения: стены, панели, ящики, элементы уровня. Системой разрушений целиком занимался я, а требования прилетали с двух сторон.

Арт и лиды хотели, чтобы всё разрушалось физически убедительно: панели отваливаются, куски стен падают, осколки сталкиваются друг с другом и с другими физическими объектами. Было и принципиальное требование: осколки не исчезают. Упал кусок стены и остался лежать, значит, там он и лежит. По обломкам на полу игрок понимает, что здесь произошло, и для арта это был художественный след, который нельзя стирать.

Техарт смотрел на это с другой стороны. На одном из проходов по производительности стало видно, что в сцене лежит много осколков, которые почти не двигаются, но продолжают участвовать в физике. Они толкаются в куче, цепляются за соседей, не засыпают тогда, когда нам нужно, и всё это складывается в нагрузку на CPU.

Просто выключить физику сразу после отделения нельзя: осколку нужно отыграть разрушение, упасть, занять своё место. Иначе куски повиснут в воздухе или поверхность под ногами игрока поменяется в неожиданный момент. Нужно было устроить и арт, и техарт, причём так, чтобы настройка каждого следующего ящика не превращалась в отдельную инженерную задачу.

Демо-сцена на 1800 осколков. Стены разрушены, обломки остались лежать, а счётчик на стене — сколько кандидатов оптимизатор ещё ведёт — падает до нуля.

Почему не хватило штатных средств

Нужные механизмы в движке есть: у Chaos имеются сон, отключение тел, настройки удаления, события разрушения. Только разложено всё это по разным местам (Geometry Collection, физика, коллизии, десятки галочек), и собрать из них цельный, предсказуемый механизм очистки для всего проекта оказалось сложно. А когда я его всё-таки собрал, он не всегда работал так, как ожидалось, и было непонятно, почему.

Строить ещё одну панель с теми же свойствами я не хотел. Хотелось управлять вещью попонятнее: тем, что происходит с осколком на каждом этапе его жизни.

Жизненный цикл вместо настроек

Если смотреть глазами осколка, его жизнь простая. Пока он в составе целого объекта, он ничего не «кушает», и следить за ним незачем. Когда оторвался, он летит, падает, толкается с миром, и вот здесь он стоит CPU. Остановился — и через какое-то время должен сам «знать», что ему пора заснуть или умереть, а при необходимости оставить вместо себя простую коллизию.

Жизненный цикл осколка. Видимая геометрия не меняется ни на одном этапе.

Два уточнения, на которые дальше опирается вся статья.

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

И ещё: сон и отключение легко принять за одно и то же, хотя это разные состояния. Спящее тело остаётся в физической сцене: у него есть коллизия, его можно разбудить. Отключённого тела в физической сцене нет. Поэтому одним объектам хватает сна, а другим нужно действительно закончить физическую жизнь.

Сон

Отключение

Отключение + proxy

Осколок виден

да

да

да

Остаётся в физической сцене

да, спит

нет

простая невидимая форма

Можно разбудить

да

нет

нет

Персонаж стоит на нём

да

нет — проваливается

да

Сталкивается с физикой

да, и просыпается

нет

нет (по умолчанию)

Так появился лёгкий управляющий слой поверх штатных классов Chaos. Движок считает разрушение и движение; мой слой решает, когда симуляция уже не нужна, что оставить для геймплея и как показать это человеку, который настраивает сцену. Из этого вырос плагин Chaos Debris Optimizer (дальше речь о версии 1.1.0 под UE 5.7–5.8, все цифры взяты из настроек по умолчанию).

Когда осколок можно считать остановившимся

Первая реальная сложность. Три куска отделились одновременно: один сразу лёг рядом, второй покатился по наклонной, третий получил пинок от соседа. Единый срок жизни «умереть через N секунд» здесь не работает: он либо обрывает движение, либо держит лишние тела.

Поэтому осколку сначала даётся время отыграть, а потом проверяется его состояние. Скорость ниже порога — можно действовать, выше — проверим ещё раз через секунду. Ждать бесконечно тоже нельзя: микроконтакты в куче, движение опоры, повторные столкновения могут держать фрагмент «живым» очень долго. У повторных проверок есть предел в 10 повторов (MaxRetries), после него включается принудительная обработка. Это компромисс: срок физической активности ограничен, даже если тело формально ещё дёргается.

Отдельный случай, когда мы ждём, пока Chaos сам усыпит тело. Сигнал самый естественный, но в плотной куче он приходит поздно или не приходит вовсе, потому что куски держат друг друга в полусне. Я добавил свою проверку стабилизации: если фрагмент пять проверок подряд почти не движется, его физическую жизнь можно закончить, не дожидаясь штатного сна. Пришлось учесть и «танец на ребре», когда кусок не перемещается, но продолжает вращаться. Для него отдельное условие.

В упрощённом виде одна проверка осколка выглядит так. Вызывается она не каждый кадр, а по таймеру повторных проверок:

// Псевдокод: логика проверки, не исходник плагина.// Режим «ждать сна движка»: своя проверка стабилизации работает именно в нёмbool LooksSettled(float LinearSpeed, float AngularSpeed){ if (LinearSpeed < SpeedLimit) return true; // почти не перемещается // «Танец на ребре»: перемещения почти нет, но кусок крутится на месте return LinearSpeed  SpinLimit;}void CheckFragment(FFragment& Fragment){ if (Fragment.IsAsleepInEngine()) { FinishPhysics(Fragment); // Chaos усыпил сам — дальше не ждём return; } if (LooksSettled(Fragment.LinearSpeed(), Fragment.AngularSpeed())) { // Разовая низкая скорость — ещё не остановка: нужно несколько проверок подряд if (++Fragment.CalmChecks >= ChecksRequired) FinishPhysics(Fragment); } else { Fragment.CalmChecks = 0; // дёрнулся — счёт заново }}

Здесь есть ловушка, на которую я наступил сам: низкая скорость ещё не значит, что под куском есть опора. Свободно летящий фрагмент в верхней точке траектории тоже на мгновение медленный. Слишком мягкие пороги останавливают физику раньше, чем это выглядит естественно. Настраивать пороги нужно на реальных траекториях; куча, которая уже лежит, для этого не годится.

В итоге время, скорость и состояние движка стали тремя отдельными сигналами, а что с ними делать, решает выбранная политика: сон, отключение, сон-потом-отключение или ожидание движка.

Осколки, которые проходили мимо учёта

Я начал с очевидного: ловить событие разрушения и регистрировать отделившийся кусок. Быстро выяснилось, что реальное разрушение в одну волну событий не укладывается. Сначала отваливается крупный кусок, потом разрушается уже он. А при включённой связности движок на лету собирает внутренние кластеры, которых не было в исходной геометрии. Часть тел просто проходила мимо учёта и продолжала жить своей физической жизнью.

Одним событиям я с тех пор не доверяю. Примерно раз в полсекунды оптимизатор сверяется с фактическим состоянием частиц на физическом потоке: какие подходящие тела прямо сейчас существуют в симуляции. Уже отключённые, неподходящие и ещё управляемые родителем отфильтровываются, найденные попадают в тот же учёт, что и событийные. Для пользователя поздно найденный фрагмент ничем не отличается: это такой же осколок с теми же правилами.

События ловят первую волну, сверка с физикой — то, что пришло позже или появилось на лету.

Сверка эта не бесплатна и не глобальна: она привязана к конкретному компоненту и его коллекциям. Несколько оптимизаторов на одном solver будут ходить по нему каждый сам по себе, и при профилировании об этом надо помнить.

Кластер — это не один осколок

Не всё, что выглядит одним большим обломком, устроено в Chaos как одно тело. Есть конечные фрагменты, а есть кластеры: несколько частей, объединённых в одно физическое тело. Снаружи мне хотелось одно правило для всех, но внутри их пришлось различать.

Отключить родителя кластера недостаточно: его дочерние части остаются со своими связями, и в сцене может повиснуть бесхозная коллизия. Для кластеров сделан отдельный путь. Сначала запомнить непосредственных детей, потом отключить кластер штатным механизмом кластеризации, затем отключить детей и очистить их связь с бывшим родителем. Это работает для прямых потомков; глубокую вложенность я не считаю автоматически проверенной.

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

Почему кластер нельзя просто отключить целиком.

И по умолчанию кластеры я не отключаю, а усыпляю. Политика эта осторожнее, её можно переключить, но пока она не тронута, кластеры засыпают даже при общем режиме «отключать».

Физики нет, а стоять на обломках нужно

После того как физика осколка закончилась, остаётся вопрос: нужна ли игроку, кроме картинки, ещё и поверхность. Декоративному мусору достаточно рендериться. А куча обломков может лежать на пути персонажа, и если отключить тела без замены, изображение останется, а опора исчезнет.

Выход нашёлся, когда я развёл три вещи: что игрок видит, что ещё симулируется и какая коллизия реально нужна геймплею. Картинка остаётся как есть. Симуляция заканчивается. А коллизию, когда она нужна, даёт невидимый proxy-компонент: коробка, сфера, капсула или выпуклая оболочка. Данные о положении и форме снимаются до отключения исходного тела (для кластеров прямо на физическом потоке, одна оболочка на весь кластер).

Такая коллизия по умолчанию участвует только в запросах (трассировках и свипах), а не в симуляционных столкновениях. Персонаж, который проверяет поверхность свипами, по ней ходит, а новая оболочка при этом не начинает расталкивать соседние обломки, которые ещё движутся. Если нужно, чтобы по ней каталась физическая граната, это уже другой режим и другие каналы коллизии.

Участие в навигации вынесено в отдельный переключатель и по умолчанию выключено: наступить на поверхность и проложить по ней маршрут — разные задачи. У proxy есть и свой срок жизни, так что обломок остаётся видимым, а его коллизию можно убрать позже.

Персонаж стоит на куче обломков. Слева — игра с отладочной отрисовкой: оранжевые контуры — объединённые выпуклые proxy-оболочки, зелёные рамки — их габариты. Справа — физическая сцена в тот же момент: тел обломков в ней уже нет.

Выпуклая оболочка тоже стоит денег

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

Помогли лимит на создание оболочек за проход обработки и очередь. По умолчанию лимита нет: он включается настройкой, а в пресете для выпуклых оболочек уже задан. Геометрия снимается заранее, пока доступны исходные частицы, а сами компоненты создаются постепенно. Расплата такая: частицу можно отключить раньше, чем появится отложенная оболочка, и короткий промежуток без опоры возможен. Для поверхности прямо под игроком это надо проверять отдельно и, если что, взять форму попроще, другой лимит или другой момент перехода.

Соседние оболочки можно ещё и объединять в одну. Компонентов становится меньше, но перестроение общей формы дорого и сглаживает промежутки между обломками. Я включил это как опцию с отложенным перестроением и потолком в 2000 накопленных вершин. Иногда несколько простых коробок лучше ложатся в бюджет, чем постоянно перестраиваемая общая оболочка.

Сколько стоит сам оптимизатор

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

Для плотных разрушений есть отдельная группа настроек. Первые проверки немного разносятся во времени, чтобы большая волна кандидатов не подошла к одному порогу одновременно. Количество переходов за проход ограничено. Однотипные команды уходят в физику одним пакетом.

Обещают эти механизмы не всё. Лимит количества операций не равен лимиту миллисекунд: разные операции стоят по-разному. Бюджеты у каждого компонента свои, общего бюджета на уровень нет. По умолчанию всё это выключено, и включать стоит под конкретную наблюдаемую проблему.

Правила, которые зависят от сцены

Один жизненный цикл не означает одного решения для всех объектов. Поверх него есть несколько правил под конкретные ситуации.

Если кусок улетел ниже заданной высоты, он отключается без всякой оболочки: игровая поверхность там уже не нужна. Кусок, который остался рядом с местом отделения, остаётся спящим и периодически пересматривается, а если сместился, возвращается в обычную обработку. В конце рабочего цикла идёт контрольный проход по остаточным телам как страховка. И есть две ручные команды: убить всё остаточное и, более агрессивная, убить вообще всё, включая то, что ещё летит (она нужна для триггеров-«зон смерти»).

На практике всё это собирается в пресеты под тип разрушения. В комплекте их шесть. Пресет при этом только отправная точка, художественное решение остаётся за командой.

Как увидеть разницу, которой не видно

Контроль был одной из исходных причин всей работы. Если на полу лежат обломки, по картинке не понять, симулируются ли они, спят или уже отключены. А в нашем случае картинка как раз и должна оставаться одинаковой, так что проверять глазами недостаточно.

Поэтому оптимизатор выдаёт события на регистрацию, сон и отключение, счётчики и отладочную отрисовку переходов: видно, где применён сон, где обычное отключение, где сработало правило высоты или стабилизация. Proxy рисуются отдельным слоем.

Отладочная отрисовка в режиме «сон, потом отключение»: жёлтые сферы — кусок усыплён, красные — отключён. Обломки при этом остаются на месте.Одна и та же сцена: слева игра, справа Chaos Visual Debugger. В игре обломки остаются лежать, в физической сцене они пропадают.

Есть два места, где легко обмануться. Счётчик оптимизатора показывает число кандидатов в его учёте, а не число активных тел в Chaos; обработанный кандидат мог закончиться сном. Событие «отключил» означает только, что команда отправлена: физический поток применит её позже, и снимок в том же тике может ещё не видеть результат. Для глубокой проверки остаются штатные инструменты. Chaos Visual Debugger показывает, что реально происходит в физической сцене, а оптимизатор объясняет, почему он принял решение.

Как это проверять

Процентов ускорения я здесь не привожу: без воспроизводимого набора замеров такие числа создают ложную точность. Расскажу лучше, как мерить у себя.

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

Что осталось нерешённым

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

Есть и технический долг. Часть чтений внутренних физических данных у меня делается с игрового потока; в коде это помечено, но образцом потокобезопасности я бы это не назвал. Правильный путь тут известен: передавать такие данные согласованными снимками с физического потока, как уже сделано для runtime-кластеров. Отложенные оболочки могут оставить короткий промежуток без опоры. Глубокая вложенность кластеров проверена не до конца.

Про сетевую игру. Оптимизатор работает как локальная оптимизация, своего протокола синхронизации у него нет. На выделенном сервере оболочки не создаются, так что клиентскую поверхность нельзя автоматически считать пригодной для серверных проверок.

Что в итоге

Арту нужны были убедительное разрушение и след, который не исчезает, техарту — контролируемая стоимость и понимание, что происходит в сцене. Помирить их получилось без ещё одной панели со свойствами Chaos и без общего срока жизни для мусора: сработало управление состояниями осколка поверх штатного движка.

Для простых экспериментов я отдельно сделал бесплатный Chaos Debris Optimizer Lite на Fab. Он намеренно упрощён: обработка по фиксированной задержке для плоских некластерных коллекций, без ожидания стабилизации и без замещающей коллизии. На нём можно попробовать саму идею; описанное выше он покрывает далеко не целиком.

Источник: habr.com

0 0 голоса
Рейтинг новости
1
0
Подписаться
Уведомить о
0 комментариев