Unity AI Development: Ein xNode-basiertes grafisches FSM-Tutorial

Veröffentlicht: 2022-08-12

In „Unity AI Development: A Finite-state Machine Tutorial“ haben wir ein einfaches Stealth-Spiel entwickelt – eine modulare FSM-basierte KI. Im Spiel patrouilliert ein feindlicher Agent im Spielraum. Wenn es den Spieler entdeckt, ändert der Feind seinen Zustand und folgt dem Spieler, anstatt zu patrouillieren.

In dieser zweiten Etappe unserer Unity-Reise werden wir eine grafische Benutzeroberfläche (GUI) erstellen, um die Kernkomponenten unserer Finite-State-Machine (FSM) schneller und mit einer verbesserten Unity-Entwicklererfahrung zu erstellen.

Eine schnelle Auffrischung

Der im vorherigen Tutorial beschriebene FSM wurde aus Architekturblöcken als C#-Skripts erstellt. Wir haben benutzerdefinierte ScriptableObject Aktionen und -Entscheidungen als Klassen hinzugefügt. Der ScriptableObject -Ansatz ermöglichte eine einfach wartbare und anpassbare FSM. In diesem Tutorial ersetzen wir die Drag-and-Drop- ScriptableObject des FSM durch eine grafische Option.

Ich habe auch ein aktualisiertes Skript für diejenigen unter Ihnen geschrieben, die es einfacher machen möchten, das Spiel zu gewinnen. Ersetzen Sie zur Implementierung einfach das Spielererkennungsskript durch dieses, das das Sichtfeld des Feindes einschränkt.

Erste Schritte mit xNode

Wir werden unseren grafischen Editor mit xNode erstellen, einem Framework für knotenbasierte Verhaltensbäume, das den Fluss unseres FSM visuell darstellt. Obwohl GraphView von Unity die Aufgabe erfüllen kann, ist seine API sowohl experimentell als auch spärlich dokumentiert. Die Benutzeroberfläche von xNode bietet ein hervorragendes Entwicklererlebnis und erleichtert das Prototyping und die schnelle Erweiterung unseres FSM.

Fügen wir unserem Projekt xNode als Git-Abhängigkeit mit dem Unity Package Manager hinzu:

  1. Klicken Sie in Unity auf Window > Package Manager , um das Package Manager-Fenster zu starten.
  2. Klicken Sie auf + (das Pluszeichen) in der oberen linken Ecke des Fensters und wählen Sie Paket von Git-URL hinzufügen aus , um ein Textfeld anzuzeigen.
  3. Geben oder fügen Sie https://github.com/siccity/xNode.git in das unbeschriftete Textfeld ein und klicken Sie auf die Schaltfläche Hinzufügen .

Jetzt sind wir bereit, tief einzutauchen und die Schlüsselkomponenten von xNode zu verstehen:

Node Repräsentiert einen Knoten, die grundlegendste Einheit eines Diagramms. In diesem xNode-Tutorial leiten wir von der Node -Klasse neue Klassen ab, die Knoten deklarieren, die mit benutzerdefinierten Funktionen und Rollen ausgestattet sind.
NodeGraph -Klasse Stellt eine Sammlung von Knoten ( Node -Klasseninstanzen) und die Kanten dar, die sie verbinden. In diesem xNode-Tutorial leiten wir von NodeGraph eine neue Klasse ab, die die Knoten manipuliert und auswertet.
NodePort -Klasse Repräsentiert ein Kommunikationsgatter, einen Port vom Typ Input oder vom Typ Output, der sich zwischen Node -Instanzen in einem NodeGraph . Die NodePort -Klasse ist einzigartig für xNode.
[Input] -Attribut Das Hinzufügen des Attributs [Input] zu einem Port kennzeichnet ihn als Eingang und ermöglicht es dem Port, Werte an den Knoten zu übergeben, zu dem er gehört. Stellen Sie sich das Attribut [Input] als einen Funktionsparameter vor.
[Output] -Attribut Das Hinzufügen des Attributs [Output] zu einem Port kennzeichnet ihn als Ausgang, wodurch der Port in die Lage versetzt wird, Werte von dem Knoten zu übergeben, zu dem er gehört. Stellen Sie sich das Attribut [Output] als den Rückgabewert einer Funktion vor.

Visualisierung der xNode Building Environment

In xNode arbeiten wir mit Graphen, bei denen jeder State und Transition die Form eines Knotens hat. Eingabe- und/oder Ausgabeverbindung(en) ermöglichen es dem Knoten, sich auf beliebige oder alle anderen Knoten in unserem Graphen zu beziehen.

Stellen wir uns einen Knoten mit drei Eingabewerten vor: zwei willkürliche und einen booleschen Wert. Der Knoten gibt einen der beiden willkürlichen Eingabewerte aus, je nachdem, ob die boolesche Eingabe wahr oder falsch ist.

Der Verzweigungsknoten, dargestellt durch ein großes Rechteck in der Mitte, enthält den Pseudocode „If C == True A Else B“. Auf der linken Seite befinden sich drei Rechtecke, von denen jedes einen Pfeil hat, der auf den Verzweigungsknoten zeigt: "A (beliebig)," "B (beliebig)" und "C (boolesch)." Der Branch-Knoten hat schließlich einen Pfeil, der auf ein „Output“-Rechteck zeigt.
Ein Beispiel für einen Branch

Um unser vorhandenes FSM in ein Diagramm umzuwandeln, ändern wir die Klassen State und Transition so, dass sie die Klasse Node anstelle der Klasse ScriptableObject erben. Wir erstellen ein Diagrammobjekt vom Typ NodeGraph , das alle unsere State und Transition enthält.

Ändern von BaseStateMachine zur Verwendung als Basistyp

Beginnen Sie mit dem Erstellen der grafischen Oberfläche, indem Sie unserer vorhandenen BaseStateMachine -Klasse zwei neue virtuelle Methoden hinzufügen:

Init Weist der CurrentState Eigenschaft den Anfangszustand zu
Execute Führt den aktuellen Zustand aus

Indem wir diese Methoden als virtuell deklarieren, können wir sie überschreiben, sodass wir das benutzerdefinierte Verhalten von Klassen definieren können, die die BaseStateMachine -Klasse für die Initialisierung und Ausführung erben:

 using System; using System.Collections.Generic; using UnityEngine; namespace Demo.FSM { public class BaseStateMachine : MonoBehaviour { [SerializeField] private BaseState _initialState; private Dictionary<Type, Component> _cachedComponents; private void Awake() { Init(); _cachedComponents = new Dictionary<Type, Component>(); } public BaseState CurrentState { get; set; } private void Update() { Execute(); } public virtual void Init() { CurrentState = _initialState; } public virtual void Execute() { CurrentState.Execute(this); } // Allows us to execute consecutive calls of GetComponent in O(1) time public new T GetComponent<T>() where T : Component { if(_cachedComponents.ContainsKey(typeof(T))) return _cachedComponents[typeof(T)] as T; var component = base.GetComponent<T>(); if(component != null) { _cachedComponents.Add(typeof(T), component); } return component; } } }

Als nächstes erstellen wir unter unserem FSM -Ordner:

FSMGraph Ein Ordner
BaseStateMachineGraph AC#-Klasse innerhalb FSMGraph

Vorerst erbt BaseStateMachineGraph nur die BaseStateMachine -Klasse:

 using UnityEngine; namespace Demo.FSM.Graph { public class BaseStateMachineGraph : BaseStateMachine { } }

Wir können BaseStateMachineGraph keine Funktionalität hinzufügen, bis wir unseren Basisknotentyp erstellt haben; machen wir das als nächstes.

Implementieren NodeGraph und Erstellen eines Basisknotentyps

Unter unserem neu erstellten FSMGraph Ordner erstellen wir:

FSMGraph Eine Klasse

Im Moment erbt FSMGraph nur die NodeGraph -Klasse (ohne zusätzliche Funktionalität):

 using UnityEngine; using XNode; namespace Demo.FSM.Graph { [CreateAssetMenu(menuName = "FSM/FSM Graph")] public class FSMGraph : NodeGraph { } }

Bevor wir Klassen für unsere Knoten erstellen, fügen wir Folgendes hinzu:

FSMNodeBase Eine Klasse, die von allen unseren Knoten als Basisklasse verwendet werden soll

Die FSMNodeBase -Klasse enthält eine Eingabe namens Entry vom Typ FSMNodeBase , damit wir Knoten miteinander verbinden können.

Wir werden auch zwei Hilfsfunktionen hinzufügen:

GetFirst Ruft den ersten Knoten ab, der mit der angeforderten Ausgabe verbunden ist
GetAllOnPort Ruft alle verbleibenden Knoten ab, die mit der angeforderten Ausgabe verbunden sind
 using System.Collections.Generic; using XNode; namespace Demo.FSM.Graph { public abstract class FSMNodeBase : Node { [Input(backingValue = ShowBackingValue.Never)] public FSMNodeBase Entry; protected IEnumerable<T> GetAllOnPort<T>(string fieldName) where T : FSMNodeBase { NodePort port = GetOutputPort(fieldName); for (var portIndex = 0; portIndex < port.ConnectionCount; portIndex++) { yield return port.GetConnection(portIndex).node as T; } } protected T GetFirst<T>(string fieldName) where T : FSMNodeBase { NodePort port = GetOutputPort(fieldName); if (port.ConnectionCount > 0) return port.GetConnection(0).node as T; return null; } } }

Letztendlich werden wir zwei Arten von Zustandsknoten haben; Lassen Sie uns eine Klasse hinzufügen, um diese zu unterstützen:

BaseStateNode Eine Basisklasse zur Unterstützung von StateNode und RemainInStateNode
 namespace Demo.FSM.Graph { public abstract class BaseStateNode : FSMNodeBase { } }

Ändern Sie als Nächstes die BaseStateMachineGraph -Klasse:

 using UnityEngine; namespace Demo.FSM.Graph { public class BaseStateMachineGraph : BaseStateMachine { public new BaseStateNode CurrentState { get; set; } } }

Hier haben wir die von der Basisklasse geerbte Eigenschaft CurrentState ausgeblendet und ihren Typ von BaseState in BaseStateNode .

Erstellen von Bausteinen für unser FSM-Diagramm

Um die Hauptbausteine ​​unseres FSM zu bilden, fügen wir als Nächstes drei neue Klassen zu unserem FSMGraph Ordner hinzu:

StateNode Stellt den Zustand eines Agenten dar. Bei der Ausführung iteriert StateNode über die TransitionNode s, die mit dem Ausgangsport des StateNode (von einer Hilfsmethode abgerufen). StateNode fragt jeden ab, ob der Knoten in einen anderen Zustand übergehen oder den Zustand des Knotens unverändert lassen soll.
RemainInStateNode Gibt an, dass ein Knoten im aktuellen Zustand bleiben soll.
TransitionNode Trifft die Entscheidung, in einen anderen Zustand zu wechseln oder im selben Zustand zu bleiben.

Im vorherigen Unity FSM-Lernprogramm durchläuft die State -Klasse die Übergangsliste. Hier in xNode dient StateNode als Äquivalent von State , um über die Knoten zu iterieren, die über unsere GetAllOnPort -Hilfsmethode abgerufen werden.

Fügen Sie nun den ausgehenden Verbindungen (den Übergangsknoten) ein [Output] -Attribut hinzu, um anzuzeigen, dass sie Teil der GUI sein sollen. Durch das Design von xNode stammt der Wert des Attributs aus dem Quellknoten: dem Knoten, der das mit dem [Output] -Attribut markierte Feld enthält. Da wir [Output] - und [Input] -Attribute verwenden, um Beziehungen und Verbindungen zu beschreiben, die von der xNode-GUI festgelegt werden, können wir diese Werte nicht wie gewohnt behandeln. Überlegen Sie, wie wir Actions versus Transitions durchlaufen:

 using System.Collections.Generic; namespace Demo.FSM.Graph { [CreateNodeMenu("State")] public sealed class StateNode : BaseStateNode { public List<FSMAction> Actions; [Output] public List<TransitionNode> Transitions; public void Execute(BaseStateMachineGraph baseStateMachine) { foreach (var action in Actions) action.Execute(baseStateMachine); foreach (var transition in GetAllOnPort<TransitionNode>(nameof(Transitions))) transition.Execute(baseStateMachine); } } }

In diesem Fall können an die Transitions Ausgabe mehrere Knoten angehängt werden; Wir müssen die GetAllOnPort -Hilfsmethode aufrufen, um eine Liste der [Output] -Verbindungen zu erhalten.

RemainInStateNode ist bei weitem unsere einfachste Klasse. RemainInStateNode führt keine Logik aus und zeigt unserem Agenten – in unserem Spiel dem Feind – lediglich an, in seinem aktuellen Zustand zu bleiben:

 namespace Demo.FSM.Graph { [CreateNodeMenu("Remain In State")] public sealed class RemainInStateNode : BaseStateNode { } }

Zu diesem Zeitpunkt ist die TransitionNode -Klasse noch unvollständig und wird nicht kompiliert. Die zugehörigen Fehler werden gelöscht, sobald wir die Klasse aktualisieren.

Um TransitionNode zu erstellen, müssen wir die Anforderung von xNode umgehen, dass der Wert der Ausgabe aus dem Quellknoten stammt – wie wir es beim Erstellen von StateNode . Ein Hauptunterschied zwischen StateNode und TransitionNode besteht darin, dass die Ausgabe von TransitionNode nur an einen Knoten angehängt werden kann. In unserem Fall GetFirst den einen Knoten ab, der an jeden unserer Ports angeschlossen ist (ein Zustandsknoten, zu dem im wahren Fall übergegangen wird, und ein anderer, zu dem im falschen Fall übergegangen wird):

 namespace Demo.FSM.Graph { [CreateNodeMenu("Transition")] public sealed class TransitionNode : FSMNodeBase { public Decision Decision; [Output] public BaseStateNode TrueState; [Output] public BaseStateNode FalseState; public void Execute(BaseStateMachineGraph stateMachine) { var trueState = GetFirst<BaseStateNode>(nameof(TrueState)); var falseState = GetFirst<BaseStateNode>(nameof(FalseState)); var decision = Decision.Decide(stateMachine); if (decision && !(trueState is RemainInStateNode)) { stateMachine.CurrentState = trueState; } else if(!decision && !(falseState is RemainInStateNode)) stateMachine.CurrentState = falseState; } } }

Werfen wir einen Blick auf die grafischen Ergebnisse unseres Codes.

Erstellen des visuellen Diagramms

Nachdem alle FSM-Klassen aussortiert sind, können wir damit fortfahren, unser FSM-Diagramm für den feindlichen Agenten des Spiels zu erstellen. Klicken Sie im Unity-Projektfenster mit der rechten Maustaste auf den Ordner EnemyAI und wählen Sie: Create > FSM > FSM Graph . Um unser Diagramm leichter zu identifizieren, benennen wir es in EnemyGraph .

Klicken Sie im xNode-Grafikeditorfenster mit der rechten Maustaste, um ein Dropdown-Menü anzuzeigen, das State , Transition und RemainInState auflistet . Wenn das Fenster nicht sichtbar ist, doppelklicken Sie auf die EnemyGraph -Datei, um das Fenster des xNode-Graph-Editors zu starten.

  1. So erstellen Sie die Zustände „ Chase und Patrol “:

    1. Klicken Sie mit der rechten Maustaste und wählen Sie State , um einen neuen Knoten zu erstellen.

    2. Benennen Sie den Knoten Chase .

    3. Kehren Sie zum Dropdown-Menü zurück und wählen Sie erneut State , um einen zweiten Knoten zu erstellen.

    4. Benennen Sie den Knoten Patrol .

    5. Ziehen Sie die vorhandenen Chase und Patrol per Drag & Drop in ihre neu erstellten entsprechenden Zustände.

  2. So erstellen Sie den Übergang:

    1. Klicken Sie mit der rechten Maustaste und wählen Sie Übergang , um einen neuen Knoten zu erstellen.

    2. Weisen Sie das LineOfSightDecision Objekt dem Decision des Übergangs zu.

  3. So erstellen Sie den RemainInState -Knoten:

    1. Klicken Sie mit der rechten Maustaste, und wählen Sie RemainInState aus, um einen neuen Knoten zu erstellen.
  4. Um den Graphen zu verbinden:

    1. Verbinden Sie den Transitions -Ausgang des Patrol -Knotens mit dem Entry -Eingang des Transition -Knotens.

    2. Verbinden Sie den True State -Ausgang des Transition -Knotens mit dem Entry -Eingang des Chase -Knotens.

    3. Verbinden Sie den False State -Ausgang des Transition -Knotens mit dem Entry -Eingang des Remain In State -Knotens.

Die Grafik sollte wie folgt aussehen:

Vier Knoten, die als vier Rechtecke dargestellt werden, jeder mit Entry-Eingabekreisen auf der linken oberen Seite. Von links nach rechts zeigt der Patrouillenstatus-Knoten eine Aktion an: Patrouillenaktion. Der Patrouillenstatus-Knoten enthält auch einen Transitions-Ausgangskreis auf seiner unteren rechten Seite, der mit dem Eingangskreis des Transitions-Knotens verbunden ist. Der Übergangsknoten zeigt eine Entscheidung an: LineOfSight. Es hat zwei Ausgangskreise auf der unteren rechten Seite, True State und False State. True State ist mit dem Entry-Kreis unserer dritten Struktur, dem Chase-State-Knoten, verbunden. Der Verfolgungszustandsknoten zeigt eine Aktion an: Verfolgungsaktion. Der Chase-Zustandsknoten hat einen Transitions-Ausgangskreis. Der zweite der beiden Ausgangskreise von Transition, False State, verbindet sich mit dem Eingangskreis unserer vierten und letzten Struktur, dem RemainInState-Knoten (der unter dem Chase-Zustandsknoten erscheint).
Der erste Blick auf unser FSM-Diagramm

Nichts in der Grafik zeigt an, welcher Knoten – der Patrol oder Chase – unser Anfangsknoten ist. Die BaseStateMachineGraph -Klasse erkennt vier Knoten, kann aber ohne vorhandene Indikatoren den Anfangszustand nicht auswählen.

Um dieses Problem zu beheben, erstellen wir Folgendes:

FSMInitialNode Eine Klasse, deren einzelne Ausgabe vom Typ StateNode den Namen InitialNode

Unsere Ausgabe InitialNode bezeichnet den Anfangszustand. Erstellen Sie als Nächstes in FSMInitialNode :

NextNode Eine Eigenschaft, die es uns ermöglicht, den Knoten abzurufen, der mit der InitialNode Ausgabe verbunden ist
 using XNode; namespace Demo.FSM.Graph { [CreateNodeMenu("Initial Node"), NodeTint("#00ff52")] public class FSMInitialNode : Node { [Output] public StateNode InitialNode; public StateNode NextNode { get { var port = GetOutputPort("InitialNode"); if (port == null || port.ConnectionCount == 0) return null; return port.GetConnection(0).node as StateNode; } } } }

Nachdem wir die Klasse FSMInitialNode erstellt haben, können wir sie mit dem Eingang Entry des Anfangszustands verbinden und den Anfangszustand über die Eigenschaft NextNode .

Kehren wir zu unserem Diagramm zurück und fügen den Anfangsknoten hinzu. Im xNode-Editorfenster:

  1. Klicken Sie mit der rechten Maustaste und wählen Sie Anfangsknoten , um einen neuen Knoten zu erstellen.
  2. Verbinden Sie den Ausgang des FSM-Knotens mit dem Entry des Patrol -Knotens.

Die Grafik sollte nun so aussehen:

Dasselbe Diagramm wie in unserem vorherigen Bild, mit einem hinzugefügten grünen Rechteck des FSM-Knotens links von den anderen vier Rechtecken. Er verfügt über einen Ausgangsknoten „Initial Node“ (dargestellt durch einen blauen Kreis), der mit dem „Entry“-Eingang des Patrouillenknotens verbunden ist (dargestellt durch einen dunkelroten Kreis).
Unser FSM-Diagramm mit dem an den Patrol State angehängten Anfangsknoten

Um unser Leben einfacher zu machen, fügen wir FSMGraph :

InitialState Ein Besitz

Wenn wir zum ersten Mal versuchen, den Wert der Eigenschaft InitialState abzurufen, durchläuft der Getter der Eigenschaft alle Knoten in unserem Diagramm, während er versucht, FSMInitialNode zu finden. Sobald FSMInitialNode wurde, verwenden wir die NextNode Eigenschaft, um unseren Anfangszustandsknoten zu finden:

 using System.Linq; using UnityEngine; using XNode; namespace Demo.FSM.Graph { [CreateAssetMenu(menuName = "FSM/FSM Graph")] public sealed class FSMGraph : NodeGraph { private StateNode _initialState; public StateNode InitialState { get { if (_initialState == null) _initialState = FindInitialStateNode(); return _initialState; } } private StateNode FindInitialStateNode() { var initialNode = nodes.FirstOrDefault(x => x is FSMInitialNode); if (initialNode != null) { return (initialNode as FSMInitialNode).NextNode; } return null; } } }

Als Nächstes verweisen wir in unserem BaseStateMachineGraph auf FSMGraph und überschreiben die Init - und Execute -Methoden unseres BaseStateMachine . Das Überschreiben von Init legt CurrentState als Anfangszustand des Diagramms fest, und das Überschreiben von Execute ruft Execute on CurrentState auf:

 using UnityEngine; namespace Demo.FSM.Graph { public class BaseStateMachineGraph : BaseStateMachine { [SerializeField] private FSMGraph _graph; public new BaseStateNode CurrentState { get; set; } public override void Init() { CurrentState = _graph.InitialState; } public override void Execute() { ((StateNode)CurrentState).Execute(this); } } }

Wenden wir nun den Graphen auf das Enemy-Objekt an und sehen ihn in Aktion.

Testen des FSM-Diagramms

In Vorbereitung auf das Testen im Projektfenster des Unity-Editors:

  1. Öffnen Sie das SampleScene-Asset.

  2. Suchen Sie unser Enemy Spielobjekt im Unity-Hierarchiefenster.

  3. Ersetzen Sie die BaseStateMachine Komponente durch die BaseStateMachineGraph Komponente:

    1. Klicken Sie auf Komponente hinzufügen und wählen Sie das richtige BaseStateMachineGraph Skript aus.

    2. Weisen Sie unser FSM-Diagramm EnemyGraph dem Feld Graph der BaseStateMachineGraph Komponente zu.

    3. Löschen Sie die BaseStateMachine Komponente (da sie nicht mehr benötigt wird), indem Sie mit der rechten Maustaste klicken und Komponente entfernen auswählen.

Das Enemy -Spielobjekt sollte wie folgt aussehen:

Von oben nach unten befindet sich im Inspector-Bildschirm neben Enemy ein Häkchen. „Spieler“ ist im Tag-Dropdown ausgewählt, „Gegner“ ist im Layer-Dropdown ausgewählt. Das Dropdown-Menü „Transformieren“ zeigt Position, Drehung und Skalierung an. Das Dropdown-Menü „Kapsel“ ist komprimiert, und die Dropdown-Menüs „Mesh-Renderer“, „Capsule Collider“ und „Nav Mesh Agent“ werden komprimiert mit einem Häkchen links daneben angezeigt. Das Dropdown-Menü Enemy Sight Sensor zeigt das Skript und die Ignorieren-Maske. Das Dropdown-Menü PatrolPoints zeigt das Skript und vier PatrolPoints. Neben dem Dropdown-Menü Base State Machine Graph (Script) befindet sich ein Häkchen. Script zeigt „BaseStateMachineGraph“, Initial State zeigt „None (Base State)“ und Graph zeigt „EnemyGraph (FSM Graph)“. es.
Unser Enemy Spielobjekt

Das ist es! Jetzt haben wir ein modulares FSM mit einem grafischen Editor. Ein Klick auf die Play- Schaltfläche zeigt, dass die grafisch erstellte Feind-KI genau wie unser zuvor erstellter ScriptableObject -Feind funktioniert.

Forging Ahead: Optimierung unseres FSM

Ein Wort der Vorsicht: Wenn Sie eine ausgefeiltere KI für Ihr Spiel entwickeln, nimmt die Anzahl der Zustände und Übergänge zu, und der FSM wird verwirrend und schwer lesbar. Der grafische Editor ähnelt einem Netz von Linien, die in mehreren Zuständen entstehen und an mehreren Übergängen enden – und umgekehrt, was das Debuggen des FSM erschwert.

Wie im vorherigen Tutorial laden wir Sie ein, den Code zu Ihrem eigenen zu machen, Ihr Stealth-Spiel zu optimieren und diese Bedenken auszuräumen. Stellen Sie sich vor, wie hilfreich es wäre, Ihre Zustandsknoten farblich zu codieren, um anzuzeigen, ob ein Knoten aktiv oder inaktiv ist, oder die Größe der RemainInState und Initial -Knoten zu ändern, um ihre Bildschirmfläche zu begrenzen.

Solche Verbesserungen sind nicht nur kosmetischer Natur. Farb- und Größenreferenzen würden helfen, zu erkennen, wo und wann debuggt werden muss. Eine gut sichtbare Grafik ist auch einfacher zu beurteilen, zu analysieren und zu verstehen. Alle nächsten Schritte liegen bei Ihnen – mit der Grundlage unseres grafischen Editors sind den Verbesserungen der Entwicklererfahrung keine Grenzen gesetzt.

Weiterführende Literatur im Toptal Engineering Blog:

  • Die 10 häufigsten Fehler, die Unity-Entwickler machen
  • Einheit mit MVC: Wie Sie Ihre Spieleentwicklung verbessern
  • 2D-Kameras in Unity beherrschen: Ein Tutorial für Spieleentwickler
  • Unity Best Practices und Tipps von Toptal-Entwicklern

Der Toptal Engineering Blog dankt Goran Lalic für sein Fachwissen und seine technische Überprüfung dieses Artikels.