Hogyan készítsen fake ragdollt ezernyi alakra Unity DOTS-ban
Hogyan hajítson el ezernyi ellenfelet robbanással Unity DOTS-ban csontfizika nélkül: test egy pontként, determinisztikus kilövés, járható landolás, forgás.

Ha egy robbanás a játékban egyszerre több száz ellenfelet repít el, mindegyiküknek igazi ragdollt adni túl drága. Fake ragdollt Unity DOTS-ban úgy készít, hogy az ellenfél teljes testét egyetlen pontként szimulálja saját egyszerű fizikával, a modellt pedig csak a test középpontja körül forgatja. Az esés és a felállás közönséges animációs klipek, csontokat egyáltalán nem szimulálunk.
A módszert egy PC-s tower defense játékon mutatjuk be, amelyet Unityben, ECS-sel fejlesztünk (bővebben a referenciáink között). Egy bomba vagy fireball ott egyszerre több tucat, akár több száz ellenfelet repít el. Ha 2000 test van a levegőben, a szimuláció tickenként 0,53-0,59 ms-ba kerül (a tick a játékszimuláció egy rögzített hosszúságú lépése).
Miért nem igazi ragdoll
Egy igazi ragdoll karakterenként tíz vagy több, ízületekkel összekötött fizikai test. Több ezer karakternél a képernyőn ez nem reális.
A másik ok a determinizmus: ugyanabból az állapotból ugyanannak az eredménynek kell kijönnie. A mi játékunkban erre épül egy elvesztett hullám újrajátszása egy mentési pontból, és ezt a szimuláció állapotának hashével ellenőrizzük 1800 tick után.
A test mint egyetlen pont
Fizikai motor helyett használjon saját, egyszerű 2,5D fizikát: pozíció a rácson, magasság a talaj felett, ballisztikus repülés, visszapattanás a falakról és súrlódás. A mi játékunkban a bomba is így mozog, és a robbanás által eltalált ellenfél egy pillanatra ugyanolyan testté válik. Az ötletet az adta, ahogyan a katonák repülnek a Diplomacy is Not an Option című játékban.
A szimuláció csak a pozíciót, a magasságot és az elforgatás szögét ismeri. A modell forgatását ne írja bele. Az az entitás kiegészítő transzformációs mátrixába tartozik, amelyet az Entities csomag (ECS a Unityben) a nem egyenletes skálázáshoz támogat (dokumentáció).
A ledöntött ellenfél fázisai
A ledöntött ellenfél három fázison megy át, aztán visszatér a tömegbe:
találatkor:
számold ki a kilövési sebességet (erő, távolság a robbanástól, véletlen a seedből)
keress landolást járható cellán (lásd lent)
kapcsold be a "ledöntve" állapotot, szabadítsd fel a helyét a támadás célpontjánál
repülés: a test parabolán repül, a modell a test középpontja körül forog
landolás: testhelyzet a szögből (hát vagy has), rövid ráborulás
fekvés: rögzített számú ticket vár
felállás: felállási klip hátról vagy hasról, aztán vissza a tömegbe
A „ledöntve” állapotot olyan komponensként készítse el, amely ki- és bekapcsolható anélkül, hogy az entitást chunkok között kellene mozgatni (a chunk az a memóriablokk, amelyben az ECS az azonos komponenskészletű entitásokat tárolja). A navigáció, a tömegben való taszítás és a támadási döntés így a ledöntötteket egyszerűen kihagyja. Hogy hogyan működik a navigáció és a taszítás nagy tömegben, azt a horda navigációjáról szóló cikk Unity DOTS-ban írja le.
Kilövés: magasabbra, nem messzebbre
A legegyszerűbb kilövés 45° ± 10° szögben történik. 18 m/s bombaerőnél és 20 m/s² gravitációnál a repülés csúcspontja körülbelül 4 m. A test alacsonyan repül el a tömeg felett, és a hatás elvész benne.
Nagyobb erő nem segít, mert vele együtt nő a távolság is. Bontsa a sebességet két összetevőre, és mindegyiket külön szorozza: 60° ± 5° szög, vízszintes összetevő × 0,85, függőleges × 1,3. A csúcspont így körülbelül 10 m, a távolság marad nagyjából 15,5 m, a repülés pedig körülbelül 2 másodpercig tart.

Az irány ± 15°-kal tér el a robbanástól, az erő ± 20%-kal, a robbanás szélén pedig a felére csökken. A véletlent ne globális generátorból vegye, hanem a pálya seedjének, az ellenfél azonosítójának és a robbanás sorszámának hashéből. A ledöntési kéréseket rendezve dolgozza fel, mert a több szálon futó jobok tetszőleges sorrendben hozzák létre őket. Ugyanannak a hullámnak két futása így ugyanazokat az íveket adja.
A ledöntés igen vagy nem. Minden ledönthető ellenfél találatkor legalább elesik (a sebesség alulról 2 m/s-ra korlátozott), az ellenállás csak a távolságot változtatja. Az immunitást külön jelzővel adja meg, a mi játékunkban a legnagyobb ellenféltípusnak van ilyen. Ha az immunitás az ellenállásból következne (0,95-nél a sebesség 2 m/s alá esik), és a gyenge elhajítás törlődne, az ellenfél csak véletlenül lenne immunis.
Landolás járható cellán
Az ellenfél nem landolhat szakadékban, sziklán vagy egy torony alapterületén. A járhatóságot rácscellánként és minden ellenfél-méretosztályra külön határozzuk meg.
A levegőben falakról visszapattanás nem elég
Kézenfekvő ötlet a nem járható cellákat falként kezelni, amelyekről a repülő test visszapattan. Sziklánál ez természetesnek tűnik, szakadéknál nem: a szakadék felett levegő van, de a cella nem járható, így a test egy láthatatlan oszlopról pattan vissza a széle felett. Ráadásul semmi sem garantálja, hogy a visszapattanás után jó helyen ér földet.

Pályaelőrejelzés a kilövéskor
Megbízhatóbb a landolást már a kilövéskor meghatározni. Szimulálja le előre a repülést ugyanazzal a lépéssel, mint a valódi fizikát, és ellenőrizze a landolási cellát az ellenfél méretosztályára. Ha nem járható, próbáljon további változatokat rögzített sorrendben:
a szögekre [0, +25, -25, +50, -50, ... , +175, -175]:
a vízszintes sebesség szorzóira [1, 0.7, 0.4, 0.1]:
szimuláld a repülést
ha a landolási cella járható az ellenfél osztályának: használd ezt az irányt, és vége
ha semmi sem jött be: vízszintes sebesség = 0 (helyben ugrás)

A legrosszabb eset 60 repülésszimuláció ellenfelenként (15 irány × 4 szorzó). A függőleges sebesség nem változik, így a repülés magassága ugyanaz marad, csak a távolság rövidül. Ha repülés közben újabb robbanás éri az ellenfelet, futtassa újra az előrejelzést.
Hogy a test landolás után ne csússzon és ne pattanjon, adjon neki 0 talajvisszapattanási együtthatót és nagy súrlódást. Ott marad, ahol az előrejelzés mondta.
A repülési magasságból tervezett forgás
A legegyszerűbb a modellt véletlen sebességgel forgatni, például 180-540 °/s-mal, és landolás után 0,2 s alatt a legközelebbi fekvő helyzetbe billenteni. A billentés azonban látszik, ha a test szinte függőlegesen ér földet. Ha a forgási sebességet minden tickben úgy igazítja, hogy a szög landoláskor pontosan vízszintes legyen, az csak részben segít: még egy alacsony dobás is szaltóként pörög, és a forgásnak semmi köze ahhoz, milyen magasan repül a test.
Jobb a forgást a kilövéskor a repülési magasságból megtervezni:
magasság = vz² / (2 g), repülési idő vz-ből és a magasságból
cél = legközelebbi vízszintes helyzet véletlen irányban (negyed fordulat állásból)
cél += fél fordulat minden 2,5 m magasságért, legfeljebb 3
forgási sebesség = (cél - aktuális szög) / repülési idő

A landolás oldala a szögből következik: arccal a föld felé has, egyébként hát. Ez alapján választódik ki a felállási klip. A fekvő vagy felálló ellenfél újabb találatkor megtartja aktuális szögét, és nem ugrik vissza függőleges helyzetbe.
A modell a talajra helyezése
A modell a test középpontja körül forog, a talaj viszont a legalsó pontja alatt van, és ez a forgással változik. Az eltolás becslése két konstansból és a szög szinuszából nem illik azokhoz a modellekhez, amelyek az esési klipben szélesre tárják a karjukat: a mi játékunkban a csontváz modellje (gyakori ellenfél) akár 1,15 m-re, a nekromantáé akár 1,73 m-re süllyedt a föld alá.


Megbízhatóbb a legalsó pontot megmérni. Az animációk sütésekor (bake) számolja ki az esési klipre a test legalsó pontját 72 elforgatásnál 5°-os lépésekben, futás közben pedig interpoláljon a táblázatból. A legmélyebb süllyedés így 4 mm-re csökken. Egyetlen minimum a klip összes képkockáján át azonban nem elég: a test landolás után 0,15-0,28 m-rel a talaj felett lebegne, amíg a felállási klip le nem húzza. A táblázatnak ezért képkockák × elforgatások méretűnek kell lennie.
A lassú, tervezett forgásnál még egy hatás előjön. Ha a modellt a legalsó pontjával a repülési magasságra teszi, a test középpontja az állásból vízszintesbe fordulás közben a fél magasság és a fél vastagság különbségével lesüllyed. A test ekkor a repülés elején csak lassan emelkedik, és csak utána gyorsul. Emelje meg ezért a modellt az állásbeli és az aktuális szögnél mért fél kiterjedés különbségével, legfeljebb a repülési magassággal. A test középpontja így parabolán halad.

Halott ellenfelek: test, decal és por
A robbanásban meghalt ellenfélnek is el kell repülnie. Hozzon létre számára repülő testet: az entitás másolatát játékmenet-komponensek nélkül (életerő, jutalom, célpont). A test elrepül, földet ér, egy ideig fekszik, majd eltűnik. A helyén holttest-decal (a talajra vetített textúra) marad, mint a Warcraft 3-ban, és egy kis porfelhő. A mi játékunkban a felső határ 1000 repülő test és 1500 decal.
A decal felülnézet az esési klip egy képkockájáról, ugyanúgy elforgatva, mint a fekvő test, így a test decalra cserélése nem látszik. A felállási klip első képkockája nem jó, mert azon az alak a hátáról már felül. A porfelhőket vonja össze 1,5 m-es cellán belül. Egy bomba a tömegbe így néhány nagyobb felhőt hoz létre több száz apró helyett.
Csapdák a Unity Entitiesben
- A bakerből származó mátrix eltűnik. Ha a kiegészítő mátrixot bake közben adja hozzá, a transzformációs rendszer egyenletes skálánál eltávolítja, és a ledöntött ellenfelek nem mozognak. Bake közben ezért kérjen nem egyenletes skálát. Ekkor viszont a prefab teljes skálája ugyanabba a mátrixba kerül, tehát ne írja felül puszta forgatással. Különben egy 2×-es méretű ledöntött ellenfél fele akkora lesz, felállás után is. A skálát tárolja külön, a mátrixot pedig skálából és forgatásból állítsa össze.
- A gyökér magassága a klipek importjakor. Humanoid klipeknél a gyökér (root) függőleges mozgása elvész, és a karakterek felálláskor átesnek a padlón. Süsse bele a gyökér magasságát a pózba. A póz lábfejhez rögzítésének opcióját ne használja, az egy méterrel lejjebb tolja az egész testet.
- Zárt alakú mátrix. Az eltolás, a forgatás és a visszatolás mint három mátrixszorzás egy jobban mérhetően lassabb, mint az eredő mátrix kézzel megírt képlete.
- Burst régi verzióval. Ha módosítja egy Burst job által olvasott komponens mezőit, az editor összeomolhat vagy értelmetlen hibákat jelezhet. Az editor újraindítása segít.
Teljesítmény
Mérve a Unity Editorban M3 Prón, 10 000 csontvázzal egy benchmark jelenetben, 18-as kilövési erővel. Az idők a Burst jobok CPU-idejének összegei az összes szálon, nem a képkocka ideje. A grafikon és a táblázat pályaelőrejelzés és landolási táblázatok nélkül készült.

| ledöntött | szimuláció (ms/tick) | vizuális előkészítés (ms) |
|---|---|---|
| 500 | 0,28-0,30 | 0,15 |
| 2000 | 0,53 | 0,30-0,55 |
| 5000 | 0,74-0,75 | 0,49-0,91 |
| 10 000 | 0,96-1,01 | 0,68-1,24 |
- A szimuláció lineárisan nő, ezer ledöntöttenként nagyjából 0,25 ms-mal. Pályaelőrejelzéssel és landolási táblázatokkal 2000 ledöntöttnél 0,58-0,59 ms, vagyis kicsivel a 0,5 ms-os cél felett. Észszerű felső határ ezért egyszerre 3000 ledöntött ellenfél, ami körülbelül 0,8 ms.
- Egy robbanás egy fal melletti 359 fős csoportba a ledöntési kérések rendszerében egyetlen képkockában 1,3-1,9 ms-ba kerül. Ennek nagy részét a pályaelőrejelzés teszi ki.
- Az editor FPS-értékei nem hasonlíthatók össze: ugyanannak a konfigurációnak két futása akár 80 FPS-sel is eltért.
A megoldás korlátai
A vizuális előkészítés egy másik tesztjelenetben akár 1,3 ms-ra is nőtt, valószínűleg a landolási táblázat minden testnél történő olvasása miatt. Ez az első jelölt az optimalizálásra. Az FPS-t buildben és 1000 ellenfél egyidejű megölésének terhelését (cél: legfeljebb 1,5 ms többlet képkockánként) még nem mértük.
Ha hasonló tömeg- vagy effektszimuláción dolgozik Unityben, és segítségre van szüksége, írjon nekünk.


