Як зробити fake ragdoll для тисяч ворогів у Unity DOTS

Як відкинути тисячі ворогів вибухом у Unity DOTS без фізики кісток: тіло як одна точка, детермінований підкид, приземлення на прохідну клітинку й обертання.

Фігури, підкинуті вибухом, летять дугами й обертаються в повітрі

Коли вибух у грі відкидає сотні ворогів одночасно, справжній ragdoll для кожного з них надто дорогий. Fake ragdoll в Unity DOTS робиться так: усе тіло ворога ви симулюєте як одну точку з власною простою фізикою, а модель лише обертаєте навколо центру тіла. Падіння та підйом є звичайними анімаційними кліпами, кістки не симулюються взагалі.

Підхід показуємо на tower defense грі для ПК, яку ми робимо в Unity з ECS (більше в референціях). Бомба або fireball там підкидає від десятків до сотень ворогів одночасно. Коли в повітрі 2 000 тіл, симуляція коштує від 0,53 до 0,59 мс на тік (тік є одним кроком ігрової симуляції фіксованої тривалості).

Чому не справжній ragdoll

Справжній ragdoll означає десять і більше фізичних тіл, з'єднаних суглобами, на кожного персонажа. Для тисяч персонажів на екрані це нереально.

Друга причина в детермінізмі: з того самого стану має виходити той самий результат. У нашій грі на цьому тримається повтор програної хвилі з контрольної точки, і ми перевіряємо це хешем стану симуляції після 1 800 тіків.

Тіло як одна точка

Замість фізичного рушія використайте власну просту 2,5D фізику: позиція на сітці, висота над землею, балістичний політ, відбиття від стін і тертя. У нашій грі так рухається й бомба, а ворог, у якого влучив вибух, на мить стає таким самим тілом. Натхненням було те, як відлітають солдати в грі Diplomacy is Not an Option.

Симуляція знає лише позицію, висоту й кут повороту. Обертання моделі в неї не записуйте. Воно належить до додаткової матриці трансформації сутності, яку пакет Entities (ECS в Unity) підтримує для нерівномірного масштабу (документація).

Фази збитого ворога

Збитий ворог проходить три фази й потім повертається в натовп:

при влучанні:
    обчисли швидкість підкиду (сила, відстань від вибуху, випадковість із seed)
    знайди приземлення на прохідну клітинку (див. нижче)
    увімкни стан "збитий", звільни місце, яке він займав біля цілі атаки
політ:        тіло летить параболою, модель обертається навколо центру тіла
приземлення:  положення за кутом (спина або живіт), коротке докочування
лежання:      чекає фіксовану кількість тіків
підйом:       кліп підйому зі спини або з живота, потім назад у натовп

Зробіть стан «збитий» компонентом, який можна вмикати й вимикати без переміщення сутності між chunk (блоками пам'яті, у яких ECS тримає сутності з однаковим набором компонентів). Навігація, розштовхування в натовпі та рішення про атаку тоді просто пропускають збитих. Як працюють навігація й розштовхування великого натовпу, описує стаття про навігацію орди в Unity DOTS.

Підкид: вище, а не далі

Найпростіший підкид іде під кутом 45° ± 10°. За сили бомби 18 м/с і гравітації 20 м/с² вершина польоту виходить близько 4 м. Тіло пролітає низько над натовпом, і ефект у ньому губиться.

Більша сила не допоможе, бо разом із нею зростає й дальність. Розділіть швидкість на дві складові й множте кожну окремо: кут 60° ± 5°, горизонтальна складова × 0,85, вертикальна × 1,3. Вершина тоді близько 10 м, дальність лишається близько 15,5 м, а політ триває приблизно 2 секунди.

Вигляд збоку: низька пунктирна дуга й висока дуга над будинком із силуетами персонажа, що обертається під час польоту
Пунктиром підкид під 45°, суцільною лінією підкид під 60° з окремими множниками. Будинок заввишки 5 м.

Напрямок відхиляється від вибуху на ± 15°, сила на ± 20 %, а на краю вибуху вона падає вдвічі. Не беріть випадковість із глобального генератора, а виводьте її з хешу seed рівня, ідентифікатора ворога й порядкового номера вибуху. Обробляйте запити на збиття відсортованими, бо job на кількох потоках створюють їх у довільному порядку. Два проходи тієї самої хвилі тоді дають ті самі дуги.

Збиття працює за принципом так або ні. Кожен ворог, якого можна збити, при влучанні щонайменше падає (швидкість обмежена знизу 2 м/с), опір змінює лише дальність. Імунітет давайте окремим прапорцем, у нашій грі його має найбільший тип ворога. Якби імунітет випливав з опору (при 0,95 швидкість виходить нижче 2 м/с), а слабке відкидання скасовувалося, ворог мав би імунітет лише випадково.

Приземлення на прохідну клітинку

Ворог не має приземлитися в ущелину, на скелю чи в контур вежі. Прохідність визначається для кожної клітинки сітки й окремо для кожного розмірного класу ворогів.

Відбиття від стін у повітрі не вистачає

Перше, що спадає на думку, це вважати непрохідні клітинки стінами, від яких летюче тіло відбивається. Біля скелі це виглядає природно, біля ущелини ні: над ущелиною повітря, але клітинка непрохідна, тож тіло відбивається від невидимого стовпа над її краєм. До того ж ніщо не гарантує, що після відбиття воно опиниться в доброму місці.

Вигляд збоку: тіло летить над ущелиною, посеред польоту відбивається від невидимого стовпа над її краєм і падає назад
Відбиття від непрохідної клітинки. Тіло відбивається від повітря над краєм ущелини.

Передбачення траєкторії під час підкиду

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

для кута в [0, +25, -25, +50, -50, ... , +175, -175]:
    для множника горизонтальної швидкості в [1, 0.7, 0.4, 0.1]:
        просимулюй політ
        якщо клітинка приземлення прохідна для класу ворога: візьми цей напрямок і кінець
якщо нічого не підійшло: горизонтальна швидкість = 0 (підскок на місці)
Вигляд згори на сітку з ущелиною й піднятою скелею, перекреслені відхилені приземлення й виділене перше прохідне
Вибух, ворог, відхилені приземлення й вибране. Сірі точки є всіма кандидатами на приземлення.

Найгірший випадок становить 60 симуляцій польоту на одного ворога (15 напрямків × 4 множники). Вертикальна швидкість не змінюється, тож висота польоту лишається тією самою, скорочується лише дальність. Якщо під час польоту ворога влучить ще один вибух, запустіть передбачення знову.

Ілюстрація у 2D: двоє ворогів упали б в ущелину (хрестики), передбачення скорочує їм політ.

Щоб тіло після приземлення не ковзало й не відскакувало, задайте йому коефіцієнт відбиття від землі 0 і велике тертя. Воно лишиться там, де сказало передбачення.

Обертання, сплановане за висотою польоту

Найпростіше крутити модель із випадковою швидкістю, наприклад від 180 до 540 °/с, а після приземлення за 0,2 с докотити її в найближче лежаче положення. Проте докочування видно, коли тіло приземляється майже вертикально. Підлаштовувати швидкість обертання щотіку так, щоб кут у момент приземлення виходив точно горизонтальним, допомагає лише частково: навіть низький кидок крутиться як сальто, а обертання ніяк не пов'язане з тим, як високо летить тіло.

Краще спланувати обертання під час підкиду з висоти польоту:

висота = vz² / (2 g), час польоту з vz і висоти
ціль = найближче горизонтальне положення у випадковому напрямку (чверть оберту зі стійки)
ціль += пів оберту за кожні 2,5 м висоти, не більше 3
швидкість обертання = (ціль - поточний кут) / час польоту
Дві дуги: низький кидок, за якого персонаж лише перекидається, і високий, за якого він робить три з чвертю оберти
Низький кидок (вершина близько 2 м) дає 90°, бомба (близько 10 м) 90° і три пів оберти.

Сторона приземлення випливає з кута: обличчям до землі означає живіт, інакше спина. За нею вибирається кліп підйому. Ворог, що лежить або підводиться, при наступному влучанні зберігає поточний кут і не стрибає назад у вертикальне положення.

Модель на землі

Модель обертається навколо центру тіла, але земля під її найнижчою точкою, а та змінюється з обертанням. Оцінка зсуву з двох констант і синуса кута не підходить для моделей із широко розкинутими руками в кліпі падіння: у нашій грі модель скелета (звичайний ворог) занурювалася в землю до 1,15 м, а модель некроманта до 1,73 м.

ОцінкаШість поворотів персонажа, частини під землею червоні
ВиміряноШість поворотів персонажа, кожен торкається землі найнижчою точкою
Ті самі повороти зі зсувом із констант і з виміряної таблиці.

Надійніше найнижчу точку виміряти. Під час запікання (bake) анімацій обчисліть для кліпу падіння найнижчу точку тіла для 72 поворотів із кроком 5°, а під час гри інтерполюйте з таблиці. Найглибше занурення тоді зменшується до 4 мм. Проте одного мінімуму по всіх кадрах кліпу не досить: тіло після приземлення висіло б на висоті від 0,15 до 0,28 м над землею, доки кліп підйому не стягне його вниз. Тому таблиця має мати розмір кадри × повороти.

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

Графік висоти центру тіла в часі: пунктирна ідеальна парабола, червона крива з провалом і синя, що повторює параболу
Висота центру тіла під час польоту: без підйому (червона) і з підйомом (синя).

Мертві вороги: тіло, decal і пил

Ворог, убитий вибухом, теж має відлетіти. Створіть для нього летюче тіло: копію сутності без ігрових компонентів (здоров'я, нагорода, ціль). Тіло пролітає, падає, якийсь час лежить і зникає. На його місці лишається decal трупа (текстура, спроєктована на землю), як у Warcraft 3, і хмарка пилу. У нашій грі ліміти становлять 1 000 летючих тіл і 1 500 decal.

Decal є виглядом згори на кадр із кліпу падіння, поверненим так само, як лежаче тіло, тож заміни тіла на decal не видно. Перший кадр кліпу підйому не підходить, бо в ньому персонаж уже сідає зі спини. Об'єднуйте хмарки пилу в межах клітинки 1,5 м. Бомба в натовп тоді створює кілька більших хмар замість сотень дрібних.

Пастки в Unity Entities

  • Матриця з baker зникає. Якщо додати додаткову матрицю під час bake, система трансформацій прибирає її за рівномірного масштабу, і збиті вороги не рухаються. Тому під час bake запросіть нерівномірний масштаб. Тоді, однак, увесь масштаб prefab переходить у ту саму матрицю, тож не перезаписуйте її самим обертанням. Інакше збитий ворог зі збільшенням 2× має половинний розмір навіть після підйому. Зберігайте масштаб окремо й складайте матрицю з масштабу та обертання.
  • Висота кореня під час імпорту кліпів. У humanoid кліпах вертикальний рух кореня (root) відкидається, і персонажі під час підйому провалюються під підлогу. Запечіть висоту кореня в позу. Опцію прив'язати позу до стоп не використовуйте, вона зсуває все тіло на метр униз.
  • Матриця в замкненому вигляді. Зсув, обертання й зворотний зсув як три множення матриць у job помітно повільніші за вручну написану формулу підсумкової матриці.
  • Burst зі старою версією. Після зміни полів компонента, який читає Burst job, редактор може падати або видавати нісенітниці. Допомагає перезапуск редактора.

Продуктивність

Виміряно в Unity Editor на M3 Pro, 10 000 скелетів у бенчмарк-сцені, сила підкиду 18. Часи є сумою процесорного часу Burst job по всіх потоках, а не часом кадру. Графік і таблиця без передбачення траєкторії й таблиць приземлення.

Графік часу на тік залежно від кількості збитих ворогів для симуляції та підготовки візуалу, з цільовою лінією 0,5 мс
Симуляція та підготовка візуалу без передбачення траєкторії. Ціль 0,5 мс для 2 000 збитих.
збитих симуляція (мс/тік) підготовка візуалу (мс)
500 від 0,28 до 0,30 0,15
2 000 0,53 від 0,30 до 0,55
5 000 від 0,74 до 0,75 від 0,49 до 0,91
10 000 від 0,96 до 1,01 від 0,68 до 1,24
  • Симуляція зростає лінійно, приблизно на 0,25 мс на тисячу збитих. З передбаченням траєкторії й таблицями приземлення це від 0,58 до 0,59 мс для 2 000 збитих, тобто трохи вище цілі 0,5 мс. Тому розумний ліміт становить 3 000 одночасно збитих, це близько 0,8 мс.
  • Вибух у групу з 359 ворогів біля стіни коштує в системі запитів на збиття від 1,3 до 1,9 мс за один кадр. Більшу частину займає передбачення траєкторії.
  • FPS з редактора порівнювати не можна: два проходи тієї самої конфігурації відрізнялися аж на 80 FPS.

Обмеження рішення

Підготовка візуалу в іншій тестовій сцені зросла аж до 1,3 мс, імовірно через читання таблиці приземлення для кожного тіла. Це перший кандидат на оптимізацію. FPS у білді й навантаження під час масового вбивства 1 000 ворогів одночасно (ціль щонайбільше 1,5 мс додатково на кадр) ми ще не вимірювали.

Якщо ви працюєте над схожою симуляцією натовпу чи ефектів у Unity і потребуєте допомоги, напишіть нам.

← Більше в категорії Tower Defense