Isometrisches Plugin für Unity3D


Es ist eine Geschichte darüber, wie man ein Plugin für Unity Asset Store schreibt, die bekannten isometrischen Probleme in Spielen löst und damit ein wenig Kaffeegeld verdient und wie man den erweiterbaren Unity-Editor versteht. Bilder, Code, Grafiken und Gedanken im Inneren.


Prolog


Es war also eine Nacht, als ich herausfand, dass ich so ziemlich nichts zu tun hatte. Das kommende Jahr war in meinem Berufsleben nicht wirklich vielversprechend (im Gegensatz zu einem persönlichen, aber das ist eine ganz andere Geschichte). Wie auch immer, ich hatte die Idee, etwas zu schreiben, das Spaß macht, um der alten Zeiten willen, das ganz persönlich wäre, etwas für sich, aber immer noch einen kleinen kommerziellen Vorteil hat (ich mag dieses warme Gefühl, wenn Ihr Projekt für jemand anderen interessant ist, außer für Ihren Arbeitgeber). Und all dies ging Hand in Hand mit der Tatsache, dass ich lange darauf gewartet habe, die Möglichkeiten der Unity-Editor-Erweiterung zu prüfen und festzustellen, ob die Plattform für den Verkauf der eigenen Erweiterungen der Engine etwas Gutes enthält.


Ich habe mich eines Tages dem Studium des Asset Store gewidmet: Modelle, Skripte, Integrationen mit verschiedenen Diensten. Und zunächst schien alles bereits geschrieben und integriert worden zu sein, und es gab sogar eine Reihe von Optionen mit unterschiedlichen Qualitäts- und Detailstufen, genau wie Preise und Support. Also habe ich es sofort eingegrenzt auf:


  • Nur Code (schließlich bin ich Programmierer)
  • Nur 2D (da ich 2D einfach liebe und sie in Unity eine anständige Out-of-the-Box-Unterstützung dafür geleistet haben)

Und dann erinnerte ich mich daran, wie viele Kakteen ich gegessen habe und wie viele Mäuse gestorben sind, als wir zuvor ein isometrisches Spiel gemacht haben. Sie werden nicht glauben, wie viel Zeit wir für die Suche nach tragfähigen Lösungen aufgewendet haben und wie viele Kopien wir bei den Versuchen, diese Isometrie zu sortieren und zu zeichnen, gebrochen haben. Als ich mich bemühte, meine Hände ruhig zu halten, suchte ich nach verschiedenen Schlüsselwörtern und weniger Schlüsselwörtern und konnte nichts außer einem riesigen Stapel isometrischer Kunst finden, bis ich mich schließlich entschied, ein isometrisches Plugin von Grund auf neu zu erstellen.


Ziele setzen


Als erstes musste ich kurz beschreiben, welche Probleme dieses Plugin lösen sollte und welchen Nutzen der Entwickler isometrischer Spiele daraus ziehen würde. Die Isometrieprobleme sind also wie folgt:


  • Objekte nach Entfernung sortieren, um sie richtig zu zeichnen
  • Erweiterung zum Erstellen, Positionieren und Verschieben von isometrischen Objekten im Editor

Daher habe ich mir mit den Hauptzielen für die erste formulierte Version eine Frist von 2-3 Tagen für die erste Entwurfsversion gesetzt. Sie können also nicht aufschieben, da Begeisterung eine fragile Sache ist und wenn Sie in den ersten Tagen nichts bereit haben, besteht eine große Chance, dass Sie es ruinieren. Und die Neujahrsfeiertage sind nicht so lang, wie es scheint, selbst in Russland, und ich wollte die erste Version innerhalb von etwa zehn Tagen veröffentlichen.


Sortieren


Kurz gesagt, Isometrie ist ein Versuch von 2D-Sprites, wie 3D-Modelle auszusehen. Das führt natürlich zu Dutzenden von Problemen. Das wichtigste ist, dass die Sprites in der Reihenfolge sortiert werden müssen, in der sie gezeichnet werden sollen, um Probleme mit gegenseitiger Überlappung zu vermeiden.



Auf dem Screenshot können Sie sehen, wie zuerst das grüne Sprite gezeichnet wird (2,1) und dann das blaue (1,1).


Der Screenshot zeigt die falsche Sortierung, wenn das blaue Sprite zuerst gezeichnet wird

In diesem einfachen Fall ist das Sortieren kein Problem, und es wird Optionen geben, zum Beispiel:


  • Sortieren nach Position von Y auf dem Bildschirm, die * (isoX + isoY) 0,5 + isoZ ** ist
  • Zeichnen aus der entferntesten isometrischen Gitterzelle von links nach rechts, von oben nach unten [(3,3), (2,3), (3,2), (1,3), (2,2), (3, 1), ...]
  • und eine ganze Reihe anderer interessanter und nicht wirklich interessanter Wege

Sie sind alle ziemlich gut, schnell und funktionieren, aber nur für solche einzelligen Objekte oder Spalten, die in isoZ-Richtung erweitert wurden :) Schließlich war ich an einer allgemeineren Lösung interessiert, die für die Objekte funktioniert, die in Richtung einer Koordinate erweitert wurden. oder sogar die "Zäune", die absolut keine Breite haben, aber in die gleiche Richtung wie die notwendige Höhe verlängert sind.



Der Screenshot zeigt die richtige Art und Weise, erweiterte Objekte 3x1 und 1x3 mit "Zäunen" von 3x0 und 0x3 zu sortieren

Und hier beginnen unsere Probleme und bringen uns an den Ort, an dem wir uns für den weiteren Weg entscheiden müssen:


  • Teilen Sie "mehrzellige" Objekte in "einzellige" Objekte, d. h. um sie vertikal zu schneiden und dann die entstandenen Streifen zu sortieren


  • Denken Sie an die neue Sortiermethode, die komplizierter und interessanter ist



Ich entschied mich für die zweite Option, da ich nicht besonders darauf aus war, jedes Objekt auf knifflige Weise zu bearbeiten, zu schneiden (sogar automatisch) und eine spezielle Herangehensweise an die Logik. Für die Aufzeichnung verwendeten sie den ersten Weg in einigen berühmten Spielen wie Fallout 1 und Fallout 2 . Sie können diese Streifen tatsächlich sehen, wenn Sie in die Daten der Spiele gelangen.


Die zweite Option impliziert also keine Sortierkriterien. Dies bedeutet, dass es keinen vorberechneten Wert gibt, nach dem Sie Objekte sortieren können. Wenn Sie mir nicht glauben (und ich denke, viele Leute, die nie mit Isometrie gearbeitet haben, tun es nicht), nehmen Sie ein Stück Papier und zeichnen Sie kleine Objekte mit den Maßen 2x8 und zum Beispiel 2x2 . Wenn Sie es irgendwie schaffen, einen Wert für die Berechnung seiner Tiefe und Sortierung herauszufinden, fügen Sie einfach ein 8x2- Objekt hinzu und versuchen Sie, sie an verschiedenen Positionen relativ zueinander zu sortieren.


Es gibt also keinen solchen Wert, aber wir können immer noch Abhängigkeiten zwischen ihnen (grob gesagt, welche überlappen welche) für die topologische Sortierung verwenden . Wir können die Abhängigkeiten der Objekte mithilfe von Projektionen isometrischer Koordinaten auf der isometrischen Achse berechnen.



Der Screenshot zeigt den blauen Würfel, der vom roten abhängig ist


Der Screenshot zeigt den grünen Würfel, der vom blauen abhängig ist

Ein Pseudocode zur Abhängigkeitsbestimmung für zwei Achsen (funktioniert auch mit der Z-Achse):


bool IsIsoObjectsDepends(IsoObject obj_a, IsoObject obj_b) { var obj_a_max_size = obj_a.position + obj_a.size; return obj_b.position.x < obj_a_max_size.x && obj_b.position.y < obj_a_max_size.y; } 

Mit einem solchen Ansatz bauen wir Abhängigkeiten zwischen allen Objekten auf, gehen rekursiv zwischen ihnen über und markieren die Z-Koordinate der Anzeige. Die Methode ist ziemlich universell und funktioniert vor allem. Eine ausführliche Beschreibung dieses Algorithmus finden Sie beispielsweise hier oder hier . Sie verwenden diesen Ansatz auch in der beliebten isometrischen Flash-Bibliothek ( as3isolib ).


Und alles war einfach großartig, außer dass die zeitliche Komplexität dieses Ansatzes O (N ^ 2) ist, da wir jedes Objekt mit jedem anderen vergleichen müssen, um die Abhängigkeiten zu erstellen. Ich habe die Optimierung für spätere Versionen verlassen und nur eine verzögerte Neusortierung hinzugefügt, damit nichts sortiert wird, bis sich etwas bewegt. Wir werden also etwas später über die Optimierung sprechen.


Editor-Erweiterung


Von nun an hatte ich folgende Ziele:


  • Das Sortieren von Objekten musste im Editor funktionieren (nicht nur in einem Spiel)
  • es musste eine andere Art von Gizmos-Pfeil geben (Pfeile zum Bewegen von Objekten)
  • Optional würde es eine Ausrichtung mit Kacheln geben, wenn das Objekt bewegt wird
  • Kachelgrößen werden automatisch angewendet und im isometrischen Weltinspektor festgelegt
  • AABB- Objekte werden entsprechend ihrer isometrischen Größe gezeichnet
  • Ausgabe von isometrischen Koordinaten im Objektinspektor, durch Ändern der Position des Objekts in der Spielwelt

Und all diese Ziele wurden erreicht. Unity erlaubt es wirklich, seinen Editor erheblich zu erweitern. Sie können im Objektinspektor neue Registerkarten, Fenster, Schaltflächen und neue Felder hinzufügen. Wenn Sie möchten, können Sie sogar einen benutzerdefinierten Inspektor für eine Komponente des genauen Typs erstellen, den Sie benötigen. Sie können auch zusätzliche Informationen im Editorfenster ausgeben (in meinem Fall für AABB-Objekte) und auch Standard-Verschiebungs-Gizmos von Objekten ersetzen. Das Problem des Sortierens im Editor wurde über dieses magische ExecuteInEditMode- Tag gelöst, mit dem Komponenten des Objekts im Editor-Modus ausgeführt werden können, dh auf dieselbe Weise wie in einem Spiel.


All dies geschah natürlich nicht ohne Schwierigkeiten und Tricks aller Art, aber es gab kein einziges Problem, für das ich mehr als ein paar Stunden aufgewendet hatte (Google, Foren und Communitys haben mir sicher geholfen, alle Probleme zu lösen entstanden, die in der Dokumentation nicht erwähnt wurden).



Der Screenshot zeigt meine Gizmos für Bewegungsobjekte in der isometrischen Welt

Lassen Sie los


Also habe ich die erste Version fertig gemacht und den Screenshot gemacht. Ich habe sogar ein Symbol gezeichnet und eine Beschreibung geschrieben. Es ist Zeit. Also habe ich einen Nominalpreis von 5 US-Dollar festgelegt, das Plugin in den Store hochgeladen und darauf gewartet, dass es von Unity genehmigt wird. Ich habe nicht viel über den Preis nachgedacht, da ich noch nicht wirklich viel Geld verdienen wollte. Mein Ziel war es herauszufinden, ob es eine allgemeine Nachfrage gibt, und wenn ja, würde ich sie gerne schätzen. Außerdem wollte ich Entwicklern von isometrischen Spielen helfen, denen es irgendwie an Möglichkeiten und Ergänzungen mangelte.


In 5 ziemlich schmerzhaften Tagen (ich habe ungefähr die gleiche Zeit damit verbracht, die erste Version zu schreiben, aber ich wusste, was ich tat, ohne mich weiter zu wundern und zu überdenken, was mir die höhere Geschwindigkeit im Vergleich zu Leuten gab, die gerade angefangen hatten, mit Isometrie zu arbeiten) Ich erhielt eine Antwort von Unity, dass das Plugin genehmigt wurde und ich es bereits im Laden sehen konnte, genauso wie seine (bisher) null Verkäufe. Es hat im lokalen Forum eingecheckt, Google Analytics in die Seite des Plugins im Store integriert und mich darauf vorbereitet, darauf zu warten, dass das Gras wächst.


Es dauerte nicht lange bis zum ersten Verkauf, als die Rückmeldungen im Forum und im Laden auftauchten. Für die verbleibenden Tage des 12. Januar wurden Kopien meines Plugins verkauft, was ich als Zeichen des öffentlichen Interesses betrachtete und beschloss, fortzufahren.


Optimierung


Ich war also mit zwei Dingen unzufrieden:


  • Zeitliche Komplexität der Sortierung - O (N ^ 2)
  • Probleme mit der Speicherbereinigung und der allgemeinen Leistung

Algorithmus


Mit 100 Objekten und O (N ^ 2) musste ich 10.000 Iterationen durchführen, um Abhängigkeiten zu finden. Außerdem musste ich alle übergeben und die Anzeige Z zum Sortieren markieren. Dafür hätte es eine Lösung geben sollen. Also habe ich eine Vielzahl von Optionen ausprobiert und konnte nicht schlafen, wenn ich über dieses Problem nachdachte. Wie auch immer, ich werde Ihnen nicht alle Methoden erzählen, die ich ausprobiert habe, aber ich werde die beschreiben, die ich bisher am besten gefunden habe.


Als erstes sortieren wir natürlich nur sichtbare Objekte. Was es bedeutet ist, dass wir ständig wissen müssen, was in unserem Schuss ist. Wenn es ein neues Objekt gibt, müssen wir es beim Sortieren hinzufügen, und wenn eines der alten verschwunden ist, ignorieren Sie es. Jetzt erlaubt Unity nicht, die Begrenzungsbox des Objekts zusammen mit seinen untergeordneten Elementen im Szenenbaum zu bestimmen. Übergeben Sie die Kinder (übrigens jedes Mal, da sie hinzugefügt und entfernt werden können) würde nicht funktionieren - zu langsam. Wir können OnBecameVisible und andere Ereignisse auch nicht verwenden, da diese nur für übergeordnete Objekte funktionieren. Wir können jedoch alle Renderer- Komponenten aus dem erforderlichen Objekt und seinen untergeordneten Objekten abrufen . Natürlich klingt es nicht nach unserer besten Option, aber ich konnte keinen anderen Weg finden, der universell und von der Leistung her akzeptabel ist.


 List<Renderer> _tmpRenderers = new List<Renderer>(); bool IsIsoObjectVisible(IsoObject iso_object) { iso_object.GetComponentsInChildren<Renderer>(_tmpRenderers); for ( var i = 0; i < _tmpRenderers.Count; ++i ) { if ( _tmpRenderers[i].isVisible ) { return true; } } return false; } 

Es gibt einen kleinen Trick bei der Verwendung der GetComponentsInChildren- Funktion, mit der Komponenten ohne Zuordnung im erforderlichen Puffer abgerufen werden können, im Gegensatz zu einer anderen, die ein neues Array von Komponenten zurückgibt


Zweitens musste ich noch etwas gegen O (N ^ 2) unternehmen. Ich habe eine Reihe von Raumaufteilungstechniken ausprobiert, bevor ich an einem einfachen zweidimensionalen Gitter im Anzeigebereich anhielt, in dem ich meine isometrischen Objekte projiziere. Jeder solche Sektor enthält eine Liste von isometrischen Objekten, die ihn kreuzen. Die Idee ist also einfach: Wenn Projektionen der Objekte nicht gekreuzt werden, macht es keinen Sinn, Abhängigkeiten zwischen den Objekten aufzubauen. Dann übergehen wir alle sichtbaren Objekte und bauen Abhängigkeiten nur in den Sektoren auf, in denen dies erforderlich ist, wodurch die zeitliche Komplexität des Algorithmus verringert und die Leistung erhöht wird. Wir berechnen die Größe jedes Sektors als Durchschnitt zwischen den Größen aller Objekte. Ich fand das Ergebnis mehr als zufriedenstellend.


Allgemeine Leistung


Natürlich könnte ich einen separaten Artikel dazu schreiben ... Okay, versuchen wir es kurz zu machen. Zuerst kassieren wir die Komponenten (wir verwenden GetComponent , um sie zu finden, was nicht schnell ist). Ich empfehle jedem, sich selbst zu beobachten, wenn er mit etwas arbeitet, das mit Update zu tun hat. Sie müssen immer bedenken, dass dies bei jedem Frame der Fall ist, daher müssen Sie sehr vorsichtig sein. Denken Sie auch an alle interessanten Funktionen wie den Operator custom == . Es gibt viel zu beachten, aber am Ende lernen Sie jeden einzelnen davon im eingebauten Profiler kennen. Es macht es viel einfacher, sie auswendig zu lernen und zu benutzen :)


Außerdem lernst du den Schmerz des Müllsammlers wirklich verstehen. Benötigen Sie eine höhere Leistung? Vergessen Sie dann alles, was Speicher zuweisen kann, was in C # (insbesondere im alten Mono- Compiler) von allem ausgeführt werden kann, von foreach (!) Bis zu aufkommenden Lambdas, geschweige denn LINQ, das Ihnen jetzt selbst in den einfachsten Fällen verboten ist. Am Ende erhält man anstelle von C # mit seinem syntaktischen Zucker einen Anschein von C mit lächerlichen Fähigkeiten.


Hier werde ich einige Links zu dem Thema geben, das Sie vielleicht hilfreich finden:
Teil1 , Teil2 , Teil3 .


Ergebnisse


Ich habe noch nie jemanden gekannt, der diese Optimierungstechnik verwendet, daher war ich besonders froh, die Ergebnisse zu sehen. Und wenn in den ersten Versionen buchstäblich 50 sich bewegende Objekte benötigt wurden, um das Spiel in eine Diashow umzuwandeln, funktioniert es jetzt ziemlich gut, selbst wenn sich 800 Objekte in einem Frame befinden: Alles dreht sich mit Höchstgeschwindigkeit und wird nur für neu sortiert 3-6 ms, was für diese Anzahl von Objekten in der Isometrie sehr gut ist. Außerdem hat es nach der Initialisierung fast keinen Speicher für einen Frame zugewiesen :)


Weitere Möglichkeiten


Nachdem ich Feedbacks und Vorschläge gelesen hatte, gab es einige Funktionen, die ich in früheren Versionen hinzugefügt habe.


2D / 3D-Mischung


Das Mischen von 2D und 3D in isometrischen Spielen ist eine interessante Möglichkeit, um das Zeichnen verschiedener Bewegungs- und Rotationsoptionen (z. B. 3D-Modelle animierter Charaktere) zu minimieren. Es ist nicht wirklich schwer, erfordert aber die Integration in das Sortiersystem. Alles, was Sie brauchen, ist, eine Begrenzungsbox des Modells mit all seinen untergeordneten Elementen zu erhalten und das Modell dann um die Breite der Box entlang der Anzeige Z zu verschieben.


 Bounds IsoObject3DBounds(IsoObject iso_object) { var bounds = new Bounds(); iso_object.GetComponentsInChildren<Renderer>(_tmpRenderers); if ( _tmpRenderers.Count > 0 ) { bounds = _tmpRenderers[0].bounds; for ( var i = 1; i < _tmpRenderers.Count; ++i ) { bounds.Encapsulate(_tmpRenderers[i].bounds); } } return bounds; } 

Dies ist ein Beispiel dafür, wie Sie Bounding Box des Modells mit all seinen Kindern erhalten können



und so sieht es aus, wenn es fertig ist

Benutzerdefinierte isometrische Einstellungen


Das ist relativ einfach. Ich wurde gebeten, die Einstellung des isometrischen Winkels, des Seitenverhältnisses und der Fliesenhöhe zu ermöglichen. Nachdem Sie einige mathematische Schmerzen erlitten haben, erhalten Sie ungefähr Folgendes:




Physik


Und hier wird es interessanter. Da die Isometrie die 3D-Welt simuliert, soll die Physik auch dreidimensional sein, mit Höhe und allem. Ich habe mir diesen faszinierenden Trick ausgedacht. Ich repliziere alle Komponenten der Physik wie Rigidbody , Collider usw. für die isometrische Welt. Nach diesen Beschreibungen und Einstellungen erstelle ich die Kopie der unsichtbaren physischen dreidimensionalen Welt mit der Engine selbst und dem eingebauten PhysX . Danach nehme ich die berechneten Simulationsdaten und erhalte diese Bacl in duplizierenden Komponenten für die isometrische Welt. Dann mache ich dasselbe, um Stöße zu simulieren und Ereignisse auszulösen.



Das Toolset physische Demo GIF

Nachwort und Schlussfolgerungen


Nachdem ich alle Vorschläge aus dem Forum umgesetzt hatte, beschloss ich, den Preis auf 40 Dollar zu erhöhen, damit es nicht wie ein weiteres billiges Plugin mit fünf Codezeilen aussieht :) Ich werde mich sehr freuen, Fragen zu beantworten und Höre auf deine Ratschläge. Da es das erste Mal ist, dass ich etwas über Habr schreibe, begrüße ich jede Art von Kritik, danke! Und jetzt etwas, das ich zuletzt gespeichert habe, die Umsatzstatistik des Monats:


Monat5 $40 $
Januar120
Februar220
März170
April90
Mai90
Juni90
Juli74
August04
September05

Link zur Seite der Unity Asset Store: Isometric 2.5D Toolset

Source: https://habr.com/ru/post/de436372/


All Articles