Wie man Fake-Ragdolls für Tausende Gegner in Unity DOTS baut

Wie Sie Tausende Gegner in Unity DOTS ohne Knochenphysik wegschleudern: Körper als ein Punkt, deterministischer Wurf, begehbare Landung und Rotation.

Von einer Explosion weggeschleuderte Figuren fliegen auf Bögen und drehen sich in der Luft

Wenn eine Explosion im Spiel Hunderte Gegner gleichzeitig wegschleudert, ist ein echter Ragdoll für jeden einzelnen zu teuer. Einen Fake-Ragdoll in Unity DOTS bauen Sie, indem Sie den ganzen Körper des Gegners als einen einzigen Punkt mit eigener, einfacher Physik simulieren und das Modell nur um die Körpermitte drehen. Fallen und Aufstehen sind gewöhnliche Animationsclips, Knochen werden überhaupt nicht simuliert.

Wir zeigen das Vorgehen an einem Tower-Defense-Spiel für PC, das wir in Unity mit ECS entwickeln (mehr in unseren Referenzen). Eine Bombe oder ein Fireball schleudert dort Dutzende bis Hunderte Gegner auf einmal weg. Bei 2.000 Körpern in der Luft kostet die Simulation 0,53 bis 0,59 ms pro Tick (ein Tick ist ein Schritt der Spielsimulation mit fester Länge).

Warum kein echter Ragdoll

Ein echter Ragdoll besteht pro Figur aus zehn oder mehr Physikkörpern, die über Gelenke verbunden sind. Bei Tausenden Figuren auf dem Bildschirm ist das nicht realistisch.

Der zweite Grund ist Determinismus: Derselbe Zustand muss dasselbe Ergebnis liefern. Unser Spiel braucht das, um eine verlorene Welle ab einem Checkpoint zu wiederholen, und wir prüfen es mit einem Hash des Simulationszustands nach 1.800 Ticks.

Der Körper als ein Punkt

Verwenden Sie statt einer Physik-Engine eine eigene, einfache 2,5D-Physik: Position im Raster, Höhe über dem Boden, ballistischer Flug, Abprallen an Wänden und Reibung. In unserem Spiel bewegt sich auch die Bombe so, und ein von der Explosion getroffener Gegner wird für einen Moment zum gleichen Körper. Die Inspiration war, wie Soldaten in Diplomacy is Not an Option durch die Luft fliegen.

Die Simulation kennt nur Position, Höhe und Drehwinkel. Schreiben Sie die Drehung des Modells nicht in die Simulation. Sie gehört in die zusätzliche Transformationsmatrix der Entity, die das Paket Entities (ECS in Unity) für nicht-uniforme Skalierung unterstützt (Dokumentation).

Phasen eines umgeworfenen Gegners

Ein umgeworfener Gegner durchläuft drei Phasen und kehrt dann in die Menge zurück:

beim Treffer:
    berechne die Wurfgeschwindigkeit (Kraft, Abstand zur Explosion, Zufall aus dem Seed)
    finde eine Landung auf einer begehbaren Zelle (siehe unten)
    schalte den Zustand "umgeworfen" ein, gib den Platz am Angriffsziel frei
Flug:      der Körper fliegt auf einer Parabel, das Modell dreht sich um die Körpermitte
Landung:   Lage aus der Drehung (Rücken oder Bauch), kurzes Nachkippen
Liegen:    wartet eine feste Anzahl von Ticks
Aufstehen: Aufsteh-Clip vom Rücken oder vom Bauch, dann zurück in die Menge

Bauen Sie den Zustand „umgeworfen“ als Komponente, die sich ein- und ausschalten lässt, ohne die Entity zwischen Chunks zu verschieben (Speicherblöcken, in denen ECS Entities mit derselben Komponentenkombination ablegt). Navigation, Separation in der Menge und Angriffsentscheidungen überspringen umgeworfene Gegner dann einfach. Wie Navigation und Separation in einer großen Menge funktionieren, beschreibt der Artikel über Horden-Navigation in Unity DOTS.

Wurf: höher, nicht weiter

Der einfachste Wurf geht in einem Winkel von 45° ± 10°. Bei einer Bombenkraft von 18 m/s und einer Gravitation von 20 m/s² liegt der Scheitelpunkt bei etwa 4 m. Der Körper fliegt tief über die Menge, und der Effekt geht darin unter.

Mehr Kraft hilft nicht, weil damit auch die Weite wächst. Teilen Sie die Geschwindigkeit in zwei Komponenten und skalieren Sie jede separat: Winkel 60° ± 5°, horizontale Komponente × 0,85, vertikale × 1,3. Der Scheitelpunkt liegt dann bei etwa 10 m, die Weite bleibt bei etwa 15,5 m, und der Flug dauert ungefähr 2 Sekunden.

Seitenansicht: ein niedriger gestrichelter Bogen und ein hoher Bogen über ein Haus mit Silhouetten einer Figur, die sich im Flug dreht
Gestrichelt ein Wurf mit 45°, durchgezogen ein Wurf mit 60° und getrennten Faktoren. Das Haus ist 5 m hoch.

Die Richtung weicht um ± 15° von der Explosion ab, die Kraft um ± 20 %, und am Rand der Explosion sinkt sie auf die Hälfte. Nehmen Sie den Zufall nicht aus einem globalen Generator, sondern aus einem Hash von Level-Seed, Kennung des Gegners und laufender Nummer der Explosion. Verarbeiten Sie die Umwerf-Anfragen sortiert, denn Jobs auf mehreren Threads erzeugen sie in beliebiger Reihenfolge. Zwei Durchläufe derselben Welle ergeben so dieselben Bögen.

Umwerfen ist ja oder nein. Jeder umwerfbare Gegner fällt bei einem Treffer mindestens um (die Geschwindigkeit ist nach unten auf 2 m/s begrenzt), die Resistenz ändert nur die Weite. Geben Sie Immunität ein eigenes Flag, in unserem Spiel hat es der größte Gegnertyp. Ergäbe sich die Immunität aus der Resistenz (bei 0,95 liegt die Geschwindigkeit unter 2 m/s) und würden schwache Würfe verworfen, wäre der Gegner nur zufällig immun.

Landung auf einer begehbaren Zelle

Ein Gegner darf nicht in einer Schlucht, auf einer Klippe oder in der Grundfläche eines Turms landen. Begehbarkeit wird pro Rasterzelle und für jede Größenklasse von Gegnern separat bestimmt.

Abprallen an Wänden in der Luft reicht nicht

Naheliegend ist, nicht begehbare Zellen als Wände zu behandeln, von denen der fliegende Körper abprallt. An einer Klippe sieht das natürlich aus, an einer Schlucht nicht: Über einer Schlucht ist Luft, aber die Zelle ist nicht begehbar, also prallt der Körper an einer unsichtbaren Säule über ihrem Rand ab. Außerdem garantiert nichts, dass er nach dem Abprall an einer guten Stelle landet.

Seitenansicht: Ein Körper fliegt über eine Schlucht, prallt mitten im Flug an einer unsichtbaren Säule über ihrem Rand ab und fällt zurück
Abprall an einer nicht begehbaren Zelle. Der Körper prallt an der Luft über dem Rand der Schlucht ab.

Vorhersage der Flugbahn beim Wurf

Zuverlässiger ist es, die Landung gleich beim Wurf zu bestimmen. Simulieren Sie den Flug im Voraus mit demselben Schritt wie die echte Physik und prüfen Sie die Landezelle für die Größenklasse des Gegners. Ist sie nicht begehbar, probieren Sie weitere Varianten in fester Reihenfolge:

für Winkel in [0, +25, -25, +50, -50, ... , +175, -175]:
    für Faktor der horizontalen Geschwindigkeit in [1, 0.7, 0.4, 0.1]:
        simuliere den Flug
        wenn die Landezelle für die Klasse des Gegners begehbar ist: nimm diese Richtung und Ende
wenn nichts passt: horizontale Geschwindigkeit = 0 (Hüpfer auf der Stelle)
Draufsicht auf ein Raster mit Schlucht und erhöhter Klippe, durchgestrichene verworfene Landungen und die erste begehbare hervorgehoben
Explosion, Gegner, verworfene Landungen und die gewählte. Die grauen Punkte sind alle Kandidaten für die Landung.

Im schlimmsten Fall sind das 60 Flugsimulationen pro Gegner (15 Richtungen × 4 Faktoren). Die vertikale Geschwindigkeit ändert sich nicht, die Flughöhe bleibt also gleich, nur die Weite wird kürzer. Trifft den Gegner während des Flugs eine weitere Explosion, starten Sie die Vorhersage erneut.

Illustration in 2D: Zwei Gegner würden in der Schlucht landen (Kreuze), die Vorhersage verkürzt ihren Flug.

Damit der Körper nach der Landung weder rutscht noch abprallt, geben Sie ihm einen Restitutionskoeffizienten am Boden von 0 und hohe Reibung. Er bleibt dort liegen, wo die Vorhersage es gesagt hat.

Rotation nach der Flughöhe geplant

Am einfachsten ist es, das Modell mit zufälliger Geschwindigkeit zu drehen, etwa 180 bis 540 °/s, und es nach der Landung innerhalb von 0,2 s in die nächste Liegeposition kippen zu lassen. Das Nachkippen ist aber sichtbar, wenn der Körper fast aufrecht landet. Die Drehgeschwindigkeit in jedem Tick so nachzuregeln, dass der Winkel bei der Landung genau waagerecht ist, hilft nur teilweise: Auch ein niedriger Wurf dreht sich wie ein Salto, und die Drehung hat nichts damit zu tun, wie hoch der Körper fliegt.

Besser ist es, die Rotation beim Wurf aus der Flughöhe zu planen:

Höhe = vz² / (2 g), Flugzeit aus vz und Höhe
Ziel = nächste waagerechte Lage in zufälliger Richtung (Vierteldrehung aus dem Stand)
Ziel += halbe Drehung je 2,5 m Höhe, höchstens 3
Drehgeschwindigkeit = (Ziel - aktueller Winkel) / Flugzeit
Zwei Bögen: ein niedriger Wurf, bei dem die Figur nur umkippt, und ein hoher, bei dem sie dreieinviertel Drehungen macht
Ein niedriger Wurf (Scheitel etwa 2 m) dreht um 90°, eine Bombe (etwa 10 m) um 90° plus drei halbe Drehungen.

Die Landeseite folgt aus dem Winkel: Gesicht zum Boden heißt Bauch, sonst Rücken. Danach wird der Aufsteh-Clip gewählt. Ein liegender oder aufstehender Gegner behält bei einem weiteren Treffer seinen aktuellen Winkel und springt nicht in die aufrechte Lage zurück.

Das Modell auf den Boden setzen

Das Modell dreht sich um die Körpermitte, der Boden liegt aber unter seinem tiefsten Punkt, und der ändert sich mit der Drehung. Eine Schätzung des Versatzes aus zwei Konstanten und dem Sinus des Winkels passt nicht zu Modellen mit weit ausgebreiteten Armen im Fall-Clip: In unserem Spiel sank das Skelett-Modell (ein häufiger Gegner) bis zu 1,15 m in den Boden ein, das Modell des Nekromanten bis zu 1,73 m.

SchätzungSechs Drehungen einer Figur, die Teile unter dem Boden sind rot
GemessenSechs Drehungen einer Figur, jede berührt den Boden mit ihrem tiefsten Punkt
Dieselben Drehungen mit Versatz aus Konstanten und aus der gemessenen Tabelle.

Zuverlässiger ist es, den tiefsten Punkt zu messen. Berechnen Sie beim Baken der Animationen für den Fall-Clip den tiefsten Punkt des Körpers bei 72 Drehungen in 5°-Schritten und interpolieren Sie zur Laufzeit aus der Tabelle. Das tiefste Einsinken fällt so auf 4 mm. Ein einziges Minimum über alle Frames des Clips reicht allerdings nicht: Der Körper würde nach der Landung 0,15 bis 0,28 m über dem Boden schweben, bis der Aufsteh-Clip ihn herunterzieht. Die Tabelle muss deshalb die Dimension Frames × Drehungen haben.

Die langsame geplante Drehung zeigt noch einen Effekt. Setzen Sie das Modell mit seinem tiefsten Punkt auf die Flughöhe, sinkt die Körpermitte beim Übergang vom Stehen in die Waagerechte um die Differenz zwischen halber Höhe und halber Dicke der Figur. Der Körper steigt dann am Anfang des Flugs nur langsam und beschleunigt erst danach. Heben Sie das Modell deshalb um die Differenz zwischen seiner halben Ausdehnung im Stand und beim aktuellen Winkel an, höchstens um die Flughöhe. Die Körpermitte folgt dann der Parabel.

Diagramm der Höhe der Körpermitte über die Zeit: eine gestrichelte ideale Parabel, eine rote Kurve mit Delle und eine blaue, die der Parabel folgt
Höhe der Körpermitte während des Flugs: ohne Anhebung (rot) und mit Anhebung (blau).

Tote Gegner: Körper, Decal und Staub

Ein von einer Explosion getöteter Gegner soll ebenfalls fliegen. Erzeugen Sie dafür einen fliegenden Körper: eine Kopie der Entity ohne Gameplay-Komponenten (Lebenspunkte, Belohnung, Ziel). Der Körper fliegt, landet, liegt eine Weile und verschwindet. An seiner Stelle bleiben ein Leichen-Decal (eine auf den Boden projizierte Textur) wie in Warcraft 3 und eine kleine Staubwolke. In unserem Spiel liegt die Obergrenze bei 1.000 fliegenden Körpern und 1.500 Decals.

Das Decal ist eine Draufsicht auf einen Frame des Fall-Clips, gedreht wie der liegende Körper, sodass der Tausch von Körper zu Decal nicht zu sehen ist. Der erste Frame des Aufsteh-Clips eignet sich nicht, weil die Figur darin vom Rücken schon aufgerichtet ist. Fassen Sie Staubwolken in einer Zelle von 1,5 m zusammen. Eine Bombe in die Menge erzeugt so ein paar größere Wolken statt Hunderter kleiner.

Fallen in Unity Entities

  • Die Matrix aus dem Baker verschwindet. Wenn Sie die zusätzliche Matrix beim Baken hinzufügen, entfernt das Transform-System sie bei uniformer Skalierung, und umgeworfene Gegner bewegen sich nicht. Fordern Sie beim Baken deshalb nicht-uniforme Skalierung an. Dann wandert allerdings die gesamte Skalierung des Prefabs in dieselbe Matrix, überschreiben Sie sie also nicht mit der reinen Rotation. Sonst hat ein umgeworfener Gegner mit Skalierung 2× nur die halbe Größe, auch nach dem Aufstehen. Speichern Sie die Skalierung separat und setzen Sie die Matrix aus Skalierung und Rotation zusammen.
  • Wurzelhöhe beim Import der Clips. Bei Humanoid-Clips wird die vertikale Bewegung der Wurzel (Root) verworfen, und Figuren fallen beim Aufstehen durch den Boden. Baken Sie die Wurzelhöhe in die Pose. Die Option, die Pose an den Füßen zu verankern, sollten Sie nicht verwenden: Sie verschiebt den ganzen Körper um einen Meter nach unten.
  • Matrix in geschlossener Form. Verschiebung, Rotation und Rückverschiebung als drei Matrixmultiplikationen sind im Job messbar langsamer als eine von Hand geschriebene Formel für die resultierende Matrix.
  • Burst mit alter Version. Nachdem Sie die Felder einer Komponente geändert haben, die ein Burst-Job liest, kann der Editor abstürzen oder Unsinn melden. Ein Neustart des Editors hilft.

Leistung

Gemessen im Unity Editor auf einem M3 Pro, 10.000 Skelette in einer Benchmark-Szene, Wurfkraft 18. Die Zeiten sind die Summe der CPU-Zeit der Burst-Jobs über alle Threads, nicht die Frame-Zeit. Diagramm und Tabelle sind ohne Flugbahnvorhersage und Landetabellen.

Diagramm der Zeit pro Tick nach Anzahl umgeworfener Gegner für Simulation und Vorbereitung der Darstellung, mit Ziellinie bei 0,5 ms
Simulation und Vorbereitung der Darstellung ohne Flugbahnvorhersage. Ziel 0,5 ms bei 2.000 umgeworfenen.
umgeworfen Simulation (ms/Tick) Vorbereitung der Darstellung (ms)
500 0,28 bis 0,30 0,15
2.000 0,53 0,30 bis 0,55
5.000 0,74 bis 0,75 0,49 bis 0,91
10.000 0,96 bis 1,01 0,68 bis 1,24
  • Die Simulation wächst linear, etwa um 0,25 ms pro tausend umgeworfene Gegner. Mit Flugbahnvorhersage und Landetabellen sind es bei 2.000 umgeworfenen 0,58 bis 0,59 ms, also knapp über dem Ziel von 0,5 ms. Eine sinnvolle Obergrenze sind daher 3.000 gleichzeitig umgeworfene Gegner, das sind etwa 0,8 ms.
  • Eine Explosion in eine Gruppe von 359 Gegnern an einer Wand kostet im System der Umwerf-Anfragen 1,3 bis 1,9 ms in einem einzigen Frame. Den größten Teil davon macht die Flugbahnvorhersage aus.
  • FPS aus dem Editor lassen sich nicht vergleichen: Zwei Durchläufe derselben Konfiguration unterschieden sich um bis zu 80 FPS.

Grenzen der Lösung

Die Vorbereitung der Darstellung stieg in einer anderen Testszene auf bis zu 1,3 ms, vermutlich wegen des Lesens der Landetabelle für jeden Körper. Das ist der erste Kandidat für eine Optimierung. FPS in einem Build und die Last beim gleichzeitigen Töten von 1.000 Gegnern (Ziel: höchstens 1,5 ms zusätzlich pro Frame) sind noch nicht gemessen.

Wenn Sie an einer ähnlichen Simulation von Menschenmengen oder Effekten in Unity arbeiten und Hilfe brauchen, schreiben Sie uns.

← Mehr in Tower Defense