Tipps und Tricks für die Arbeit mit Unity3D

Ursprünglicher Autor: Herman Tulleken
  • Übersetzung


Ich habe vor 4 Jahren den ersten Artikel „50 Unity Tips“ veröffentlicht . Trotz der Tatsache, dass das meiste davon immer noch relevant ist, hat sich aus folgenden Gründen viel geändert:

  • Die Einheit wurde besser. Zum Beispiel kann ich jetzt dem FPS-Zähler vertrauen. Die Möglichkeit, Eigenschaftsschubladen zu verwenden, hat die Notwendigkeit verringert, benutzerdefinierte Editoren zu schreiben. Die Art und Weise, mit Fertighäusern zu arbeiten, ist für vordefinierte verschachtelte Fertighäuser und deren Alternativen weniger anspruchsvoll geworden. Skriptfähige Objekte sind freundlicher geworden.

  • Die Integration in Visual Studio hat sich verbessert, das Debuggen ist viel einfacher geworden und der Bedarf an "Affen" -Debugging hat abgenommen.

  • Tools und Bibliotheken von Drittanbietern sind besser geworden. Im Asset Store wurden viele Assets angezeigt, die Aspekte wie visuelles Debuggen und Protokollieren vereinfachen. Der größte Teil des Codes für unser eigenes (kostenloses) Erweiterungs- Plugin ist in meinem ersten Artikel beschrieben (und ein Großteil davon wird hier beschrieben).

  • Verbesserte Versionskontrolle. (Aber vielleicht habe ich gerade gelernt, es effizienter zu nutzen). Zum Beispiel müssen Sie jetzt nicht mehr mehrere oder Sicherungskopien für Fertighäuser erstellen.

  • Ich bin erfahrener geworden. In den letzten 4 Jahren habe ich an vielen Unity-Projekten gearbeitet, darunter eine Reihe von Spielprototypen , abgeschlossene Spiele wie Father.IO und unser wichtigstes Unity Grids- Asset .

Dieser Artikel ist eine Version des Originalartikels, die unter Berücksichtigung aller oben genannten Punkte überarbeitet wurde.

Bevor ich mit den Tipps fortfahre, werde ich zunächst eine kurze Notiz hinterlassen (wie im ersten Artikel). Diese Tipps gelten möglicherweise nicht für alle Unity-Projekte:

  • Sie basieren auf meiner Erfahrung in Projekten in kleinen Teams (von 3 bis 20 Personen).

  • Strukturierung, Wiederverwendbarkeit, Klarheit des Codes und andere Aspekte haben ihren Preis: Dies hängt von der Größe des Teams, der Größe des Projekts und den Zielen des Projekts ab. Zum Beispiel werden Sie all dies nicht für einen Gamejam verwenden .

  • Die Verwendung vieler Tipps ist Geschmackssache (es kann unterschiedliche, aber dennoch gute Techniken für die hier aufgeführten Tipps geben).

Auf der Unity-Website finden Sie auch Empfehlungen für die Arbeit an Projekten (die meisten zielen jedoch darauf ab, die Produktivität von Projekten zu verbessern) (alle in englischer Sprache) :


Arbeitsprozess


1. Entscheiden Sie sich von Anfang an für die Skala und erstellen Sie alles auf einer Skala. Wenn Sie dies nicht tun, müssen Sie die Assets möglicherweise später wiederholen (z. B. wird die Animation nicht immer richtig skaliert). Für 3D-Spiele ist es wahrscheinlich am besten, 1 Unity-Einheit zu nehmen, die 1 Meter entspricht. Für 2D-Spiele, die keine Beleuchtung und Physik verwenden, ist normalerweise 1 Einheitseinheit gleich 1 Pixel (in der "Arbeits" -Auflösung) geeignet. Wählen Sie für die Benutzeroberfläche (und 2D-Spiele) die Arbeitsauflösung aus (wir verwenden HD oder 2xHD) und erstellen Sie alle Assets, die in dieser Auflösung skaliert werden sollen.

2. Lassen Sie jede Szene laufen.Auf diese Weise können Sie nicht zwischen den Szenen wechseln, um das Spiel zu starten und so den Testprozess zu beschleunigen. Dies kann schwierig sein, wenn Sie persistente Objekte verwenden, die zwischen Szenen-Downloads übergeben werden und in allen Szenen erforderlich sind. Eine Möglichkeit, dies zu erreichen, besteht darin, die übertragenen Objekte singleton zu machen, die sich selbst laden, wenn sie sich nicht in der Szene befinden. Singletones werden in einem anderen Tipp ausführlicher behandelt.

3. Wenden Sie die Quellcodeverwaltung an und lernen Sie, wie Sie sie effektiv einsetzen.

  • Serialisieren Sie Assets als Text. Dadurch werden Szenen und Fertighäuser zwar nicht kompatibler, Änderungen können jedoch einfacher nachverfolgt werden.

  • Beherrsche die Strategie des Teilens von Szenen und Fertighäusern. Normalerweise sollten mehrere Personen nicht an einer Szene oder einem Fertighaus arbeiten. In einem kleinen Team kann es ausreichen, vor Beginn der Arbeit an einer Szene oder einem Fertighaus alle zu bitten, nicht daran zu arbeiten. Es kann nützlich sein, physische Token zu verwenden, um anzuzeigen, wer gerade an der Szene arbeitet (Sie können nur dann an der Szene arbeiten, wenn Sie das entsprechende Token auf Ihrem Tisch haben).

  • Verwenden Sie Tags als Lesezeichen.

  • Wählen Sie eine Verzweigungsstrategie und bleiben Sie dabei. Da es unmöglich ist, die Verbindung von Szenen und Fertighäusern reibungslos zu gestalten, kann die Verzweigung ziemlich kompliziert werden. Unabhängig davon, welche Verzweigungsmethode Sie wählen, sollte sie mit Ihrer Strategie für die gemeinsame Nutzung von Szenen und Fertighäusern funktionieren.

  • Verwenden Sie Submodule mit Vorsicht. Submodule können eine großartige Möglichkeit sein, wiederverwendbaren Code zu unterstützen, aber es gibt mehrere Gefahren:

    • Metadateien für verschiedene Projekte sind im Allgemeinen nicht gleich. Dies ist normalerweise kein Problem für Code, der kein MonoBehaviour oder skriptfähige Objekte verwendet, aber für MonoBehaviour und skriptfähige Objekte, die Submodule verwenden, kann dies zu Codeverlust führen.

    • Wenn Sie an mehreren Projekten arbeiten (von denen eines oder mehrere Submodule verwenden), kann es manchmal zu einer „Lawine von Aktualisierungen“ kommen, wenn Sie mehrere Iterationen von Pull-Merge-Commit-Push für verschiedene Projekte ausführen müssen, um den Code in allen Projekten zu stabilisieren ( und wenn während dieses Prozesses jemand anderes Änderungen vornimmt, kann die Lawine kontinuierlich werden). Eine Möglichkeit, diesen Effekt zu minimieren, besteht darin, Änderungen an den Submodulen der Projekte vorzunehmen, die sich auf sie beziehen. Gleichzeitig müssen Projekte, die Submodule verwenden, immer ziehen, und sie müssen niemals pushen.

4. Trennen Sie Testszenen immer vom Code. Übernehmen Sie Commits für temporäre Assets und Skripte in das Repository und entfernen Sie sie aus dem Projekt, wenn Sie mit der Arbeit fertig sind.

5. Aktualisieren Sie gleichzeitig die Tools (insbesondere Unity). Unity ist viel besser darin, Verbindungen aufrechtzuerhalten, wenn ein Projekt aus anderen als der aktuellen Version geöffnet wird. Manchmal gehen jedoch immer noch Verbindungen verloren, wenn Teammitglieder in verschiedenen Versionen arbeiten.

6. Importieren Sie Assets von Drittanbietern in ein sauberes Projekt und importieren Sie von dort aus ein neues Paket für Ihre Verwendung. Beim direkten Import in ein Projekt können Assets manchmal zu Problemen führen:

  • Kollisionen (Dateien oder Namen) können auftreten, insbesondere bei Assets, die Dateien im Stammverzeichnis des Plugins- Ordners enthalten , oder bei Assets, die in ihren Beispielen Standard-Assets verwenden.

  • Sie sind möglicherweise ungeordnet und verteilen ihre Dateien in Ihrem Projekt. Dies wird zu einem besonderen Problem, wenn Sie es nicht verwenden und entfernen möchten.

Verwenden Sie die folgenden Anweisungen, um Ihre Assets sicherer zu machen:

1. Erstellen Sie ein neues Projekt und importieren Sie die Assets.
2. Führen Sie die Beispiele aus und stellen Sie sicher, dass sie funktionieren.
3. Organisieren Sie das Asset in einer geeigneteren Ordnerstruktur. (Normalerweise passe ich das Asset nicht an meine eigene Ordnerstruktur an. Ich überprüfe jedoch, ob sich alle Dateien im selben Ordner befinden und dass sich an wichtigen Stellen keine Dateien befinden, die vorhandene Dateien meines Projekts überschreiben können.)
4. Führen Sie die Beispiele aus und stellen Sie sicher, dass sie vorhanden sind arbeitet immer noch. (Manchmal ist das Asset beim Verschieben seiner Komponenten „kaputt gegangen“, aber normalerweise tritt dieses Problem nicht auf.)
5. Löschen Sie nun die Komponenten, die Sie nicht benötigen (z. B. Beispiele).
6. Stellen Sie sicher, dass das Asset noch kompiliert wird und dass die Fertighäuser noch alle Verbindungen haben. Wenn noch etwas unveröffentlicht ist, testen Sie es.
7. Wählen Sie nun alle Assets aus und exportieren Sie das Paket.
8. Importieren Sie es in Ihr Projekt.

7. Automatisieren Sie den Erstellungsprozess. Dies ist auch in kleinen Projekten nützlich, insbesondere aber, wenn:

  • Sie müssen viele verschiedene Versionen des Spiels erstellen,
  • Sie müssen Baugruppen für andere Teammitglieder mit unterschiedlichen technischen Erfahrungen erstellen oder
  • Sie müssen kleine Änderungen am Projekt vornehmen, bevor Sie es erstellen können.

Informationen dazu finden Sie unter Unity Builds Scripting: Grundlegende und erweiterte Funktionen.

8. Dokumentieren Sie Ihre Einstellungen. Die meiste Dokumentation sollte im Code sein, aber etwas muss außerhalb davon dokumentiert werden. Entwickler dazu zu zwingen, den Code nach Einstellungen zu durchsuchen, bedeutet, ihre Zeit zu verschwenden. Dokumentierte Einstellungen erhöhen die Effizienz (wenn Dokumente auf dem neuesten Stand gehalten werden). Dokumentieren Sie Folgendes:

  • Tags verwenden.
  • Verwendung von Ebenen (für Kollisionen, Keulen und Raycasting - geben Sie an, in welcher Ebene sich die Ebene befinden soll).
  • GUI-Tiefe für Ebenen (was oben platziert werden sollte).
  • Szeneneinstellungen.
  • Die Struktur komplexer Fertighäuser.
  • Ausgewählte Redewendungen.
  • Erstellen Sie Anpassungen.

Allgemeine Code-Tipps


9. Fügen Sie Ihren gesamten Code in einen Namespace ein. Dies vermeidet den Codekonflikt Ihrer eigenen Bibliotheken und des Codes von Drittanbietern. Verlassen Sie sich jedoch nicht auf Namespaces, wenn Sie versuchen, Codekonflikte mit wichtigen Klassen zu vermeiden. Auch wenn Sie andere Namespaces verwenden, verwenden Sie die Objekt-, Aktions- oder Ereignisklassen nicht als Namen.

10. Verwenden Sie Behauptungen. Anweisungen sind nützlich, um Invarianten im Code zu testen und logische Fehler zu beseitigen. Ansprüche sind über die Unity.Assertions.Assert- Klasse verfügbar . Sie überprüfen den Zustand und schreiben eine Nachricht an die Konsole, wenn sie falsch ist. Wenn Sie nicht wissen, warum Anweisungen nützlich sein können, lesen Sie Die Vorteile der Programmierung mit Assertions (auch Assert-Anweisungen genannt)..

11. Verwenden Sie Zeichenfolgen nur zum Anzeigen von Text. Verwenden Sie insbesondere keine Zeichenfolgen, um Objekte oder Fertighäuser zu identifizieren. Es gibt Ausnahmen (es gibt noch einige Elemente in Unity, auf die nur über einen Namen zugegriffen werden kann). Definieren Sie in solchen Fällen Zeichenfolgen wie Konstanten in Dateien wie AnimationNames oder AudioModuleNames. Wenn solche Klassen nicht mehr verwaltet werden können, verwenden Sie verschachtelte Klassen, um etwas wie AnimationNames.Player.Run einzuführen.

12. Verwenden Sie Invoke und SendMessage nicht. Diese MonoBehaviour-Methoden rufen andere Methoden beim Namen auf. Nach Namen aufgerufene Methoden sind im Code schwer zu verfolgen (Sie können "Verwendungen" nicht finden, und SendMessage hat einen weiten Bereich, der noch schwieriger zu verfolgen ist).

Mit Coroutine und den Aktionen C # können Sie ganz einfach Ihre eigene Version von Invoke schreiben:

public static Coroutine Invoke(this MonoBehaviour monoBehaviour, Action action, float time)
{
return monoBehaviour.StartCoroutine(InvokeImpl(action, time));
}
private static IEnumerator InvokeImpl(Action action, float time)
{
yield return new WaitForSeconds(time);
action();
}

Dann können Sie es in MonoBehaviour folgendermaßen verwenden:

this.Invoke(ShootEnemy); //где ShootEnemy - это невозвращающий значения (void) метод без параметров.

( Ergänzung: Jemand hat vorgeschlagen, die ExecuteEvent- Klasse , die Teil des Unity-Ereignissystems ist , als Alternative zu verwenden . Bisher weiß ich nicht viel darüber, aber es scheint sinnvoll, sie genauer zu untersuchen.)

13. Lassen Sie nicht zu, dass gespawnte Objekte die Hierarchie durcheinander bringen, wenn Leistung des Spiels. Legen Sie das Objekt in der Szene als übergeordnetes Objekt für sie fest, damit Sie beim Spielen des Spiels leichter Objekte finden können. Sie können ein leeres Spielobjekt oder sogar einen Singleton (siehe weiter unten in diesem Artikel) ohne Verhalten verwenden, um den Zugriff im Code zu erleichtern. Nennen Sie dieses Objekt DynamicObjects.

14. Seien Sie genau, wenn Sie null als gültige Werte verwenden, und vermeiden Sie diese, wo dies möglich ist.

Nullwerte sind nützlich, wenn Sie nach ungültigem Code suchen. Wenn Sie jedoch die Gewohnheit haben, Null zu ignorieren, wird falscher Code erfolgreich ausgeführt und Sie werden lange Zeit keine Fehler bemerken. Darüber hinaus kann es tief im Code deklariert werden, da jede Schicht Nullvariablen ignoriert. Ich versuche, null überhaupt nicht als gültigen Wert zu verwenden.

Ich bevorzuge die folgende Redewendung: Nicht auf Null prüfen und den Code herausfallen lassen, wenn ein Problem auftritt. Manchmal überprüfe ich bei wiederverwendbaren Methoden die Variable auf null und löse eine Ausnahme aus, anstatt sie an andere Methoden zu übergeben, bei denen dies zu einem Fehler führen kann.

In einigen Fällen kann null gültig sein und daher anders behandelt werden. In solchen Fällen müssen Sie einen Kommentar hinzufügen, der die Gründe angibt, aus denen der Wert möglicherweise null ist.

Für im Inspektor konfigurierte Werte wird häufig ein allgemeines Skript verwendet. Der Benutzer kann einen Wert angeben, andernfalls wird der Standardwert verwendet. Der beste Weg, dies zu tun, ist die Verwendung der optionalen Klasse ‹T›, die die Werte von T umschließt. (Dies ist ein bisschen wie Nullable ‹T›.) Sie können das Kontrollkästchen mit einem speziellen Eigenschaften-Renderer rendern und das Wertefeld nur anzeigen, wenn das Kontrollkästchen aktiviert ist. (Leider ist es nicht möglich, die generische Klasse direkt zu verwenden. Sie müssen die Klassen für bestimmte Werte von T erweitern.)

[Serializable]
public class Optional
{
   public bool useCustomValue;
   public T value;
}

In Ihrem Code können Sie es folgendermaßen verwenden:

health = healthMax.useCustomValue ? healthMax.Value : DefaultHealthMax;

Ergänzung: Viele Leute sagen mir, dass es besser ist, struct zu verwenden (erzeugt keinen Müll und kann nicht null sein). Dies bedeutet jedoch, dass Sie es nicht als Basisklasse für nicht generische Klassen verwenden können, sodass Sie es für Felder verwenden können, die Sie im Inspektor verwenden können.

15. Wenn Sie Coroutinen verwenden, lernen Sie, diese effektiv einzusetzen. Coroutinen können ein bequemer Weg sein, um viele Probleme zu lösen. Sie sind jedoch schwer zu debuggen, und mit ihrer Hilfe können Sie den Code leicht in ein Chaos verwandeln, in dem niemand, selbst Sie, es herausfinden kann.

Sie müssen verstehen:

  • So führen Sie Coroutinen parallel aus.
  • So führen Sie Coroutinen nacheinander aus.
  • So erstellen Sie neue Coroutinen aus vorhandenen.
  • So erstellen Sie benutzerdefinierte Coroutinen mit CustomYieldInstruction.

//Это сама корутина
IEnumerator RunInParallel()
{
   yield return StartCoroutine(Coroutine1());
   yield return StartCoroutine(Coroutine2());
}
public void RunInSequence()
{
   StartCoroutine(Coroutine1());
   StartCoroutine(Coroutine1());
}
Coroutine WaitASecond()
{
   return new WaitForSeconds(1);
}

16. Verwenden Sie Erweiterungsmethoden, um mit Komponenten zu arbeiten, die eine gemeinsame Schnittstelle haben. ( Ergänzung: Es scheint, dass GetComponent und andere Methoden jetzt auch für Schnittstellen funktionieren, daher ist dieser Rat überflüssig.) Manchmal ist es praktisch, Komponenten abzurufen, die eine bestimmte Schnittstelle implementieren, oder Objekte mit solchen Komponenten zu finden.

In der folgenden Implementierung wird typeof anstelle der generischen Versionen dieser Funktionen verwendet. Generische Versionen funktionieren nicht mit Schnittstellen, und typeof funktioniert nicht. Die folgende Methode umschließt es mit generischen Methoden.

public static TInterface GetInterfaceComponent(this Component thisComponent)
   where TInterface : class
{
   return thisComponent.GetComponent(typeof(TInterface)) as TInterface;
}

17. Verwenden Sie Erweiterungsmethoden, um die Syntax komfortabler zu gestalten. Zum Beispiel:

public static class TransformExtensions
{
   public static void SetX(this Transform transform, float x)
   {
      Vector3 newPosition =
         new Vector3(x, transform.position.y, transform.position.z);
      transform.position = newPosition;
   }
   ...
}

18. Verwenden Sie die weichere GetComponent-Alternative. Manchmal kann das gewaltsame Hinzufügen von Abhängigkeiten über RequireComponent unangenehm sein. Dies ist nicht immer möglich oder akzeptabel, insbesondere wenn Sie GetComponent für eine fremde Klasse aufrufen. Alternativ kann die folgende GameObject-Erweiterung verwendet werden, wenn das Objekt eine Fehlermeldung auslösen soll, wenn es nicht gefunden wird.

public static T GetRequiredComponent(this GameObject obj) where T : MonoBehaviour
{
   T component = obj.GetComponent();
   if(component == null)
   {
      Debug.LogError("Ожидается компонент типа "
         + typeof(T) + ", но он отсутствует", obj);
   }
   return component;
}

19. Vermeiden Sie es, unterschiedliche Redewendungen zu verwenden, um dasselbe zu tun. In vielen Fällen gibt es verschiedene Redewendungen. Wählen Sie in solchen Fällen eine Redewendung aus und verwenden Sie sie für das gesamte Projekt. Und hier ist warum:

  • Einige Redewendungen sind schlecht kompatibel. Die Verwendung eines Idioms lenkt die Entwicklung in eine Richtung, die für ein anderes Idiom nicht geeignet ist.

  • Durch die Verwendung einer Redewendung für das gesamte Projekt können die Projektteilnehmer besser verstehen, was passiert. Gleichzeitig werden Struktur und Code verständlicher und die Fehlerwahrscheinlichkeit verringert.

Beispiele für Redewendungsgruppen:

  • Coroutinen sind endliche Zustandsmaschinen.
  • Eingebaute Fertighäuser - angebrachte Fertighäuser - Gott-Fertighäuser.
  • Strategien für den Datenaustausch.
  • Möglichkeiten, Sprites für Zustände in 2D-Spielen zu verwenden.
  • Die Struktur von Fertighäusern.
  • Laichstrategien.
  • Möglichkeiten, Objekte zu finden: nach Typ / Name / Tag / Ebene / Link.
  • Möglichkeiten zum Gruppieren von Objekten: nach Typ / Name / Tag / Ebene / Array von Links.
  • Möglichkeiten zum Aufrufen von Methoden anderer Komponenten.
  • Suche nach Objektgruppen / Selbstregistrierung.
  • Steuerung der Ausführungsreihenfolge (unter Verwendung der Einstellung für die Ausführungsreihenfolge von Unity - Ertragslogik - Erwachen / Starten und Aktualisieren / Spätes Aktualisieren - manuelle Methoden - beliebige Architektur
  • Die Auswahl von Objekten / Positionen / Zielen im Spiel mit der Maus: Der Auswahlmanager ist die lokale Selbstverwaltung.
  • Datenspeicherung beim Szenenwechsel: über PlayerPrefs oder mithilfe von Objekten, die beim Laden einer neuen Szene nicht zerstört werden (Destroy).
  • Möglichkeiten zum Kombinieren (Mischen, Hinzufügen und Überlagern) von Animationen.
  • Eingabeverarbeitung (zentral - lokal)

20. Erstellen und pflegen Sie Ihre eigene Zeitklasse, um das Arbeiten mit Pausen zu vereinfachen. Wrap Time.DeltaTime und Time.TimeSinceLevelLoad, um Pausen und Zeitskalen zu steuern. Die Verwendung einer Klasse erfordert Disziplin, macht jedoch alles viel einfacher, insbesondere wenn sie mit verschiedenen Timern ausgeführt wird (z. B. Schnittstellenanimationen und Spielanimationen).

Ergänzung: Unity unterstützt unscaledTime und unscaledDeltaTime, wodurch die native Zeitklasse in vielen Situationen überflüssig wird. Es kann jedoch weiterhin nützlich sein, wenn sich die Skalierung der globalen Zeit auf Komponenten auswirkt, die Sie nicht auf unerwünschte Weise geschrieben haben.

21. Benutzerklassen, die aktualisiert werden müssen, sollten keinen Zugriff auf die globale statische Zeit haben. Stattdessen sollten sie ein Zeitdelta als Parameter für die Update-Methode erhalten. Auf diese Weise können Sie diese Klassen verwenden, wenn Sie das oben beschriebene Pausensystem implementieren oder wenn Sie das Verhalten einer benutzerdefinierten Klasse beschleunigen oder verlangsamen möchten.

22. Verwenden Sie ein gemeinsames Framework für WWW-Anrufe. In Spielen mit viel Kommunikation mit dem Server gibt es normalerweise Dutzende von WWW-Aufrufen. Unabhängig davon, ob Sie die rohe WWW-Unity-Klasse oder das Plugin verwenden, ist es praktisch, eine dünne Schicht darüber zu schreiben, die wie eine Boilerplate funktioniert.

Normalerweise definiere ich die Call-Methode (getrennt für Get und Post), die CallImpl- und MakeHandler-Coroutinen. Im Wesentlichen erstellt die Call-Methode mithilfe der MakeHandler-Methode einen „Superhander“ aus dem Parser, einen Handler für Erfolg und Fehler. Es ruft auch die CallImpl-Coroutine auf, die die URL bildet, den Anruf tätigt, auf den Abschluss wartet und dann den „Super-Handler“ aufruft.

So sieht es aus:

public void Call(string call, Func parser, Action onSuccess, Action onFailure)
{
	var handler = MakeHandler(parser, onSuccess, onFailure);
	StartCoroutine(CallImpl(call, handler));
} 
public IEnumerator CallImpl(string call, Action handler)
{
	var www = new WWW(call);
	yield return www;
	handler(www);
}
public Action MakeHandler(Func parser, Action onSuccess, Action onFailure)
{
   return (WWW www) =>
   {
      if(NoError(www)) 
      {
         var parsedResult = parser(www.text);
         onSuccess(parsedResult);
      }
      else
      {
         onFailure("Текст ошибки");
      }
   }
}

Dieser Ansatz bietet mehrere Vorteile.

  • Es wird vermieden, eine große Menge an Boilerplate-Code zu schreiben
  • Sie können zunächst die erforderlichen Elemente verarbeiten (z. B. Anzeigen einer Lade-UI-Komponente oder Behandeln bestimmter häufiger Fehler).

23. Wenn Sie viel Text haben, fügen Sie ihn in eine Datei ein. Fügen Sie es nicht in die Bearbeitungsfelder des Inspektors ein. Stellen Sie es so ein, dass Sie es schnell ändern können, ohne den Unity-Editor zu öffnen und insbesondere ohne die Szene speichern zu müssen.

24. Wenn Sie lokalisieren möchten, trennen Sie alle Zeilen an einer Stelle. Es gibt verschiedene Möglichkeiten, dies zu tun. Eine davon besteht darin, die Textklasse mit einem Zeichenfolgenfeld vom Typ public für jede Zeile zu definieren. Standardmäßig wird beispielsweise Englisch festgelegt. Andere Sprachen sind untergeordnete Klassen und initialisieren Felder mit Sprachgegenstücken neu.

Eine komplexere Methode (geeignet für große Textmengen oder eine große Anzahl von Sprachen) besteht darin, die Tabelle zu lesen und eine Logik zu erstellen, um die gewünschte Zeile basierend auf der ausgewählten Sprache auszuwählen.

Klassendesign


25. Entscheiden Sie, wie die inspizierten Felder verwendet werden sollen, und machen Sie sie zum Standard. Es gibt zwei Möglichkeiten: Machen Sie die Felder öffentlich oder privat und markieren Sie sie als [SerializeField]. Letzteres ist „korrekter“, aber weniger bequem (und diese Methode wird von Unity selbst nicht sehr populär gemacht). Was auch immer Sie wählen, machen Sie es zu einem Standard, damit die Entwickler in Ihrem Team wissen, wie das öffentliche Feld zu interpretieren ist.

  • Inspizierte Felder sind öffentlich. In diesem Fall bedeutet public: „Die Variable kann vom Designer während der Ausführung der Anwendung sicher geändert werden. Stellen Sie den Wert nicht im Code ein. "

  • Die geprüften Felder sind privat und als serialisierbar markiert. In diesem Fall bedeutet public: "Sie können diese Variable sicher im Code ändern" (daher gibt es nicht viele davon und es gibt keine öffentlichen Felder in MonoBehaviours und ScriptableObjects).

26. Machen Sie Komponentenvariablen niemals öffentlich, es sei denn, sie müssen im Inspektor konfiguriert werden. Andernfalls sie wird den Designer ändern, vor allem , wenn es nicht klar ist , was sie tun. In einigen seltenen Fällen kann dies nicht vermieden werden (z. B. wenn ein Editor-Skript eine Variable verwenden sollte). In diesem Fall müssen Sie das Attribut HideInInspector verwenden , um es im Inspektor auszublenden.

27. Verwenden Sie Eigenschaftsschubladen, um Felder benutzerfreundlicher zu gestalten. Mit Eigenschaftsschubladen können Steuerelemente im Inspektor konfiguriert werden. Auf diese Weise können Sie Steuerelemente erstellen, die für den Datentyp am besten geeignet sind, und Schutz einfügen (z. B. die Werte von Variablen begrenzen). Verwenden Sie das Header- Attributzum Organisieren der Felder und das Tooltip- Attribut , um Designern zusätzliche Dokumentation zur Verfügung zu stellen.

28. Bevorzugen Sie lieber Eigenschaftsschubladen als benutzerdefinierte Editoren . Eigenschaftsschubladen werden nach Feldtyp implementiert, was bedeutet, dass sie viel weniger Zeit für die Implementierung benötigen. Sie können auch bequemer wiederholt verwendet werden. Nach der Implementierung für einen Typ können sie in jeder Klasse für denselben Typ verwendet werden. Benutzerdefinierte Editoren sind in MonoBehaviour implementiert, sodass sie schwerer wiederzuverwenden sind und mehr Arbeit erfordern.

29. Versiegeln Sie standardmäßig MonoBehaviours (verwenden Sie den Modifikator Sealed). Im Allgemeinen ist MonoBehaviours Unity für die Vererbung nicht sehr praktisch:

  • Die Art und Weise, wie Unity Nachrichtenmethoden wie Start und Update aufruft, erschwert die Funktionsweise dieser Methoden in Unterklassen. Wenn Sie nicht vorsichtig sind, wird das falsche Element aufgerufen, oder Sie vergessen, die Basismethode aufzurufen.

  • Wenn Sie benutzerdefinierte Editoren verwenden, müssen Sie normalerweise die Vererbungshierarchie für Editoren kopieren. Wenn jemand eine Ihrer Klassen erweitern muss, müssen Sie Ihren eigenen Editor erstellen oder sich auf das beschränken, was Sie erstellt haben.

Verwenden Sie in Fällen, in denen eine Vererbung erforderlich ist , nicht die Unity-Nachrichtenmethoden, wenn dies vermieden werden kann. Wenn Sie sie weiterhin verwenden , machen Sie sie nicht virtuell. Bei Bedarf können Sie eine leere virtuelle Funktion definieren, die von der Nachrichtenmethode aufgerufen wird und die die untergeordnete Klasse überschreiben kann, um zusätzliche Aktionen auszuführen.

public class MyBaseClass
{
   public sealed void Update()
   {
      CustomUpdate();
      ... // update этого класса 
   }
   //Вызывается до того, как этот класс выполняет свой update
   //Переопределение для выполнения вашего кода update.
   virtual public void CustomUpdate(){};
}
public class Child : MyBaseClass
{
   override public void CustomUpdate()
   {
      //Выполняем какие-то действия
   }
}

Dadurch wird verhindert, dass die Klasse Ihren Code versehentlich überschreibt, es können jedoch weiterhin Unity-Nachrichten aktiviert werden. Ich mag diese Reihenfolge nicht, weil sie problematisch wird. Im obigen Beispiel muss die untergeordnete Klasse möglicherweise Operationen ausführen, unmittelbar nachdem die Klasse ihre eigene Aktualisierung abgeschlossen hat.

30. Trennen Sie die Schnittstelle von der Spielelogik.Schnittstellenkomponenten sollten im Allgemeinen nichts über das Spiel wissen, in dem sie verwendet werden. Übergeben Sie ihnen die Daten, die Sie anzeigen möchten, und abonnieren Sie die Ereignisse, die überprüft werden, wenn der Benutzer mit den UI-Komponenten interagiert. Schnittstellenkomponenten dürfen nicht der Spielelogik folgen. Sie können die Eingabedaten filtern und ihre Richtigkeit überprüfen, aber die Grundregeln sollten nicht in ihnen implementiert werden. In vielen Puzzlespielen sind Feldelemente eine Erweiterung der Benutzeroberfläche und sollten keine Regeln enthalten. (Zum Beispiel sollte eine Schachfigur nicht die dafür zulässigen Züge berechnen.)

Die Eingabeinformationen müssen auch von der Logik getrennt werden, die auf der Grundlage dieser Informationen wirkt. Verwenden Sie einen Eingabecontroller, der den Akteur über die Notwendigkeit einer Bewegung informiert. Der Akteur entscheidet, wann er sich bewegen soll.

Hier ist ein reduziertes Beispiel einer UI-Komponente, mit der der Benutzer eine Waffe aus einer bestimmten Liste auswählen kann. Das einzige, was diese Klassen über das Spiel wissen, ist die Waffenklasse (und nur, weil die Waffenklasse eine nützliche Datenquelle ist, die dieser Container anzeigen sollte). Das Spiel weiß auch nichts über den Container; Sie muss nur das OnWeaponSelect-Ereignis registrieren.

public WeaponSelector : MonoBehaviour
{
   public event Action OnWeaponSelect {add; remove; } 
   //GameManager может регистрировать это событие
   public void OnInit(List  weapons)
   {
      foreach(var weapon in weapons)
      {
          var button = ... //Создаёт дочернюю кнопку и добавляет её в иерархию          
          buttonOnInit(weapon, () => OnSelect(weapon)); 
          // дочерняя кнопка отображает опцию, 
          // и отправляет сообщение о нажатии этому компоненту
      }
   }
   public void OnSelect(Weapon weapon)
  {
      if(OnWepaonSelect != null) OnWeponSelect(weapon);
   }
}
public class WeaponButton : MonoBehaviour
{
    private Action<> onClick;
    public void OnInit(Weapon weapon, Action onClick)
    {
        ... //установка спрайта и текста оружия
        this.onClick = onClick;
    }
    public void OnClick() //Привязываем этот метод как OnClick компонента UI Button
    {
       Assert.IsTrue(onClick != null);  //Не должно происходить
       onClick();
    }    
}

31. Separate Konfiguration, Status und unterstützende Informationen.

  • Konfigurationsvariablen sind Variablen, die im Objekt konfiguriert werden, um das Objekt über seine Eigenschaften zu definieren. Zum Beispiel maxHealth .
  • Zustandsvariablen sind Variablen, die den aktuellen Zustand eines Objekts vollständig bestimmen. Dies sind Variablen, die gespeichert werden müssen, wenn Ihr Spiel das Speichern unterstützt. Zum Beispiel currentHealth .
  • Buchhaltungsvariablen werden für Geschwindigkeits-, Komfort- und Übergangszustände verwendet. Sie können vollständig aus Zustandsvariablen bestimmt werden. Zum Beispiel previousHealth .

Wenn Sie diese Variablentypen trennen, werden Sie verstehen, dass Sie ändern können, was gespeichert werden muss, was über das Netzwerk gesendet / empfangen werden muss. Hier ist ein einfaches Beispiel für eine solche Trennung.

public class Player
{
   [Serializable]
   public class PlayerConfigurationData
   {
      public float maxHealth;
   }
   [Serializable]
   public class PlayerStateData
   {
      public float health;
   }
   public PlayerConfigurationData configuration;
   private PlayerState stateData;
   //вспомогательная информация
   private float previousHealth;
   public float Health
   {
      public get { return stateData.health; }
      private set { stateData.health = value; }
   }
}

32. Verwenden Sie keine indizierten Arrays vom Typ public. Definieren Sie beispielsweise keine Reihe von Waffen, keine Reihe von Kugeln und keine Reihe von Partikeln auf folgende Weise:

public void SelectWeapon(int index)
{ 
   currentWeaponIndex = index;
   Player.SwitchWeapon(weapons[currentWeapon]);
}
public void Shoot()
{
   Fire(bullets[currentWeapon]);
   FireParticles(particles[currentWeapon]);
}

Das Problem liegt hier eher nicht im Code, sondern in der Komplexität der fehlerfreien Einrichtung im Inspektor.

Definieren Sie besser eine Klasse, die alle drei Variablen kapselt, und erstellen Sie daraus ein Array:

[Serializable]
public class Weapon
{
   public GameObject prefab;
   public ParticleSystem particles;
   public Bullet bullet;
}

Ein solcher Code sieht besser aus, aber was noch wichtiger ist, es ist schwieriger, Fehler beim Einrichten von Daten im Inspektor zu machen.

33. Vermeiden Sie die Verwendung von Arrays für Nichtsequenzstrukturen. Zum Beispiel hat ein Spieler drei Arten von Angriffen. Jeder verwendet die aktuelle Waffe, erzeugt jedoch unterschiedliche Kugeln und unterschiedliches Verhalten.

Sie können versuchen, drei Aufzählungszeichen in ein Array zu stopfen, und dann diese Art von Logik verwenden:

public void FireAttack()
{
   /// поведение
   Fire(bullets[0]);
}
public void IceAttack()
{
   /// поведение
   Fire(bullets[1]);
}
public void WindAttack()
{
   /// поведение
   Fire(bullets[2]);
}

Aufzählungen sehen im Code vielleicht hübscher aus ...

public void WindAttack()
{
   /// behaviour
   Fire(bullets[WeaponType.Wind]);
}

... aber nicht im Inspektor.

Es ist besser, separate Variablen zu verwenden, damit Sie anhand der Namen besser verstehen, welche Inhalte dort geschrieben werden sollen. Erstellen Sie eine Klasse, um alles bequem zu machen.

[Serializable]
public class Bullets
{
   public Bullet fireBullet;
   public Bullet iceBullet;
   public Bullet windBullet;
}

Dies bedeutet, dass keine anderen Feuer-, Eis- oder Winddaten vorhanden sind.

34. Gruppieren Sie die Daten in serialisierbare Klassen, damit im Inspektor alles bequemer aussieht. Einige Elemente haben möglicherweise Dutzende von Einstellungen. Die richtige Variable zu finden kann ein Albtraum sein. Befolgen Sie diese Anweisungen, um Ihr Leben zu vereinfachen:

  • Definieren Sie separate Klassen für Gruppen von Variablen. Machen Sie sie öffentlich und serialisierbar
  • Definieren Sie in der Hauptklasse öffentliche Variablen für jeden oben definierten Typ.
  • Initialisieren Sie diese Variablen nicht in Awake oder Start. Sie sind serialisierbar, sodass Unity sich selbst um sie kümmert.
  • Sie können Standardwerte angeben, indem Sie sie in der Definition zuweisen.

Dadurch werden Gruppen von Variablen erstellt, die im Inspektor einfacher zu verwalten sind.

[Serializable]
public class MovementProperties //Не MonoBehaviour!
{
   public float movementSpeed;
   public float turnSpeed = 1; //указываем значение по умолчанию
}
public class HealthProperties //Не MonoBehaviour!
{
   public float maxHealth;
   public float regenerationRate;
}
public class Player : MonoBehaviour
{
   public MovementProperties movementProeprties;
   public HealthPorperties healthProeprties;
}

35. Machen Sie Nicht-MonoBehavior-Klassen serialisierbar, auch wenn sie nicht für öffentliche Felder verwendet werden. Auf diese Weise können Sie die Klassenfelder im Inspektor anzeigen, wenn Sie sich im Debug-Modus befinden. Dies funktioniert auch für verschachtelte Klassen (privat oder öffentlich).

36. Versuchen Sie, die im Inspektor im Code konfigurierten Daten nicht zu ändern. Die im Inspektor konfigurierte Variable ist eine Konfigurationsvariable und muss beim Ausführen der Anwendung als Konstante und nicht als Statusvariable behandelt werden. Wenn Sie diese Regel befolgen, können Sie leichter Methoden schreiben, die den Status der Komponente auf den ursprünglichen zurücksetzen, und Sie werden klar verstehen, was die Variable tut.

public class Actor : MonoBehaviour
{
   public float initialHealth = 100;
   private float currentHealth;
   public void Start()
   {
      ResetState();
   }   
   private void Respawn()
   {
      ResetState();
   } 
   private void ResetState()
   {
      currentHealth = initialHealth;
   }
}

Muster


Muster sind Möglichkeiten, um häufig auftretende Probleme mit Standardmethoden zu lösen. Robert Nystroms Buch "Game Programming Patterns" (kostenlos online verfügbar) ist eine wertvolle Ressource, um zu verstehen, wie Muster zur Lösung von Problemen bei der Entwicklung von Spielen eingesetzt werden können. In Unity selbst gibt es viele solcher Muster: Instantiate ist ein Beispiel für ein Prototypmuster. MonoBehaviour ist eine Version der Vorlagenmethode, die Benutzeroberfläche und die Animation verwenden das Beobachtermuster und die neue Animations-Engine verwendet Zustandsmaschinen.

Diese Tipps beziehen sich auf die Verwendung von Mustern speziell in Unity.

37. Verwenden Sie zur Vereinfachung Singleton ("Einzelgänger" -Muster). Die folgende Klasse erstellt automatisch einen Singleton für jede Klasse, die ihn erbt:

public class Singleton : MonoBehaviour where T : MonoBehaviour
{
   protected static T instance;
   //Возвращает экземпляр этого синглтона.
   public static T Instance
   {
      get
      {
         if(instance == null)
         {
            instance = (T) FindObjectOfType(typeof(T));
            if (instance == null)
            {
               Debug.LogError("В сцене нужен экземпляр " + typeof(T) + 
                  ", но он отсутствует.");
            }
         }
         return instance;
      }
   }
}

Singletones sind nützlich für Manager wie ParticleManager , AudioManager oder GUIManager .

(Viele Programmierer sind gegen Klassen, die vage als XManager bezeichnet werden, weil dies darauf hinweist, dass die Klasse einen schlechten Ruf hat oder zu viele Aufgaben hat, die nichts miteinander zu tun haben. Im Allgemeinen stimme ich ihnen zu. Es gibt jedoch nur wenige Manager in Spielen und sie führen die gleichen Aufgaben in Spielen aus, so dass diese Klassen eigentlich Redewendungen sind.)

  • Verwenden Sie keine Singletones für eindeutige Instanzen von Nicht-Manager-Prefabs (z. B. Player). Halten Sie sich an dieses Prinzip, um die Vererbungshierarchie und die Einführung bestimmter Arten von Änderungen nicht zu erschweren. Speichern Sie Links zu ihnen besser im GameManager (oder in einer besser geeigneten Gottklasse ;-)).
  • Определите свойства static и методы для переменных и методов public, которые часто используются за пределами класса. Это позволит вам писать GameManager.Player вместо GameManager.Instance.player.

Wie in anderen Tipps erläutert, sind Singletones nützlich, um Standard-Spawnpunkte und Objekte zu erstellen, die zwischen Szenen-Downloads und dem Speichern globaler Daten übertragen werden.

38. Verwenden Sie Zustandsautomaten, um unterschiedliche Verhaltensweisen in unterschiedlichen Zuständen zu erstellen oder Code beim Ändern von Zuständen auszuführen. Eine Light-State-Maschine hat viele Status. Für jeden Status können Sie die Aktionen angeben, die ausgeführt werden, wenn Sie einen Status eingeben oder sich in einem Status befinden, sowie die Aktualisierungsaktion. Dadurch wird der Code sauberer und weniger fehleranfällig. Ein gutes Zeichen dafür, dass die Zustandsmaschine für Sie nützlich ist: Der Code der Update-Methode enthält if- oder switch-Konstrukte, die ihr Verhalten ändern, oder Variablen wie hasShownGameOverMessage.

public void Update()
{
   if(health <= 0)
   {
      if(!hasShownGameOverMessage) 
      {
         ShowGameOverMessage();
         hasShownGameOverMessage = true; //При респауне значение становится false
      }
   }
   else
   {
      HandleInput();
   }   
}

Mit mehr Zuständen kann diese Art von Code verwirrend werden, die Zustandsmaschine wird es viel klarer machen.

39. Verwenden Sie Felder vom Typ UnityEvent, um das Beobachtermuster im Inspektor zu erstellen. Mit der UnityEvent-Klasse können Sie Methoden binden, die im Inspektor bis zu vier Parameter empfangen, und zwar über dieselbe Benutzeroberfläche wie Ereignisse in Schaltflächen. Dies ist besonders nützlich, wenn Sie mit Eingaben arbeiten.

40. Verwenden Sie das Beobachtermuster, um festzustellen, wann sich ein Feldwert ändert. Das Problem, Code nur beim Ändern einer Variablen auszuführen, tritt häufig in Spielen auf. Wir haben eine Standardlösung für dieses Problem mithilfe der generischen Klasse erstellt, mit der Ereignisse mit variablen Änderungen registriert werden können. Das Folgende ist ein Gesundheitsbeispiel. So wird es erstellt:

/*Наблюдаемое значение*/ health = new ObservedValue(100);
health.OnValueChanged += () => { if(health.Value <= 0) Die(); };

Jetzt können Sie es überall ändern, ohne seinen Wert an jeder Stelle zu überprüfen, zum Beispiel wie folgt:

if(hit) health.Value -= 10;

Wenn der Gesundheitszustand unter 0 fällt, wird die Die-Methode aufgerufen. Detaillierte Diskussionen und Implementierungen finden Sie in diesem Beitrag .

41. Verwenden Sie das Actor-Muster für Fertighäuser. (Dies ist ein „nicht standardmäßiges“ Muster. Die Grundidee stammt aus der Präsentation von Kieran Lord.) Der

Schauspieler ist der Hauptbestandteil des Fertighauses. Normalerweise ist dies die Komponente, die die "Individualität" des Fertighauses bietet und mit der der Code der höheren Ebene am häufigsten interagiert. Der Akteur verwendet häufig andere Komponenten - Helfer - für dasselbe Objekt (und manchmal für untergeordnete Objekte), um seine Arbeit zu erledigen.

Wenn Sie ein Schaltflächenobjekt über das Menü "Einheit" erstellen, wird ein Spielobjekt mit Sprite- und Schaltflächenkomponenten (und ein untergeordnetes Element mit der Textkomponente) erstellt. In diesem Fall ist der Schauspieler Button. Die Hauptkamera verfügt normalerweise auch über mehrere Komponenten (GUI-Ebene, Flare-Ebene, Audio-Listener), die an die Kamerakomponente angeschlossen sind. Kamera ist hier ein Schauspieler.

Damit der Akteur ordnungsgemäß funktioniert, sind möglicherweise andere Komponenten erforderlich. Mit den folgenden Attributen einer Akteurkomponente können Sie ein Fertighaus zuverlässiger und nützlicher machen:

  • Verwenden Sie RequiredComponent , um alle Komponenten anzugeben, die ein Akteur im selben Spielobjekt benötigt. (Ein Akteur kann dann GetComponent immer sicher aufrufen, ohne prüfen zu müssen, ob der zurückgegebene Wert null ist.)
  • Используйте DisallowMultipleComponent, чтобы избежать прикрепления нескольких экземпляров того же компонента. Актор всегда сможет вызвать GetComponent, не беспокоясь о поведении, которое должно быть, когда прикреплено несколько компонентов).
  • Используйте SelectionBase, если у объекта-актора есть дочерние объекты. Так его будет проще выбрать в окне сцены.

[RequiredComponent(typeof(HelperComponent))]
[DisallowMultipleComponent]
[SelectionBase]
public class Actor : MonoBehaviour
{
   ...//
}

42. Verwenden Sie zufällige und gemusterte Datenstromgeneratoren. (Dies ist ein nicht standardmäßiges Muster, aber wir finden es äußerst nützlich.)

Der Generator ähnelt einem Zufallszahlengenerator: Es handelt sich um ein Objekt mit der Next-Methode, das aufgerufen wird, um ein neues Element eines bestimmten Typs abzurufen. Während des Entwurfsprozesses können Generatoren modifiziert werden, um eine breite Palette von Mustern und verschiedene Arten von Zufälligkeiten zu erzeugen. Sie sind nützlich, weil Sie damit die Logik zum Generieren eines neuen Elements getrennt von dem Teil des Codes speichern können, in dem das Element benötigt wird, wodurch der Code viel sauberer wird.

Hier einige Beispiele:

var generator = Generator
   .RamdomUniformInt(500)
   .Select(x => 2*x); //Генерирует чётные числа от 0 до 998
var generator = Generator
   .RandomUniformInt(1000)
   .Where(n => n % 2 == 0); //Делает то же самое
var generator = Generator
    .Iterate(0, 0, (m, n) => m + n); //Числа Фибоначчи
var generator = Generator
   .RandomUniformInt(2)
   .Select(n => 2*n - 1)
   .Aggregate((m, n) => m + n); //Случайные скачки с шагом 1 или -1
var generator = Generator
   .Iterate(0, Generator.RandomUniformInt(4), (m, n) => m + n - 1)
   .Where(n >= 0); //Случайная последовательность, увеличивающая среднее

Wir haben bereits Generatoren verwendet, um Hindernisse zu erzeugen, Hintergrundfarben zu ändern, prozedurale Musik zu erstellen, eine Folge von Buchstaben zu generieren, um Wörter zu erstellen, wie in Wortspielen und vieles mehr. Mit dem folgenden Design können Generatoren erfolgreich zur Steuerung von Coroutinen eingesetzt werden, die sich in unterschiedlichen Intervallen wiederholen:

while (true)
{
   //Что-то делаем
   yield return new WaitForSeconds(timeIntervalGenerator.Next());
}

Lesen Sie diesen Beitrag , um mehr über Generatoren zu erfahren.

Fertighäuser und skriptfähige Objekte


43. Verwenden Sie für alles Fertighäuser. Die einzigen Spielobjekte in der Szene, die keine Fertighäuser (oder Teile von Fertighäusern) sind, sollten Ordner sein. Selbst eindeutige Objekte, die nur einmal verwendet werden, müssen vorgefertigt sein. Dies macht es einfach, Änderungen vorzunehmen, für die kein Szenenwechsel erforderlich ist.

44. Fertighäuser an Fertighäuser binden; Binden Sie keine Instanzen an Instanzen. Links zu Fertighäusern werden gespeichert, wenn Sie das Fertighaus in die Szene ziehen, Links zu Instanzen jedoch nicht. Durch die Verknüpfung mit Fertighäusern werden nach Möglichkeit die Kosten für die Einrichtung der Szene und die Notwendigkeit von Szenenänderungen reduziert.

Stellen Sie nach Möglichkeit automatisch Verbindungen zwischen Instanzen her. Wenn Sie Instanzen verknüpfen müssen, stellen Sie die Verknüpfungen programmgesteuert her. Beispielsweise kann sich der Fertigspieler beim Start im GameManager registrieren, oder der GameManager kann den Fertigspieler beim Start finden.

45. Erstellen Sie keine Raster mit den Wurzeln von Fertighäusern, wenn Sie andere Skripte hinzufügen möchten. Wenn Sie ein Fertighaus aus einem Raster erstellen, machen Sie das übergeordnete Raster zunächst zu einem leeren Spielobjekt und lassen Sie es die Wurzel sein. Binden Sie Skripte an den Stamm, nicht an das Raster. So können Sie das Raster leichter durch ein anderes Raster ersetzen, ohne die im Inspektor konfigurierten Werte zu verlieren.

46. ​​Verwenden Sie für die übertragenen Konfigurationsdaten skriptfähige Objekte, keine Prefabs.

Wenn Sie dies tun:

  • Die Szenen werden kleiner
  • Sie können nicht versehentlich Änderungen an einer Szene (an der vorgefertigten Instanz) vornehmen.

47. Verwenden Sie für diese Ebenen skriptfähige Objekte. Level-Daten werden häufig in XML oder JSON gespeichert. Die Verwendung von Skriptobjekten bietet jedoch mehrere Vorteile:

  • Sie können im Editor bearbeitet werden. Es wird einfacher sein, die Daten zu überprüfen, und diese Methode ist für nicht-technische Designer bequemer. Darüber hinaus können Sie benutzerdefinierte Editoren verwenden, um die Bearbeitung noch einfacher zu gestalten.
  • Sie müssen sich nicht um das Lesen / Schreiben und Parsen von Daten kümmern.
  • Das Trennen und Einbetten sowie das Verwalten der resultierenden Assets wird einfacher. Sie können also Ebenen aus Bausteinen und nicht aus einer massiven Konfiguration erstellen.

48. Verwenden Sie skriptfähige Objekte, um das Verhalten im Inspektor zu konfigurieren. Skriptobjekte sind normalerweise Konfigurationsdaten zugeordnet, aber Sie können auch „Methoden“ als Daten verwenden.

Stellen Sie sich ein Szenario vor, in dem Sie einen Feindtyp haben und jeder Feind eine Reihe von Superkräften hat. Sie können sie zu gewöhnlichen Klassen machen und ihre Liste in der Enemy-Klasse abrufen, aber ohne einen benutzerdefinierten Editor können Sie die Liste der verschiedenen Superkräfte (jede mit ihren eigenen Eigenschaften) im Inspektor nicht konfigurieren. Aber wenn Sie diese Superkräfte-Assets erstellen (als ScriptableObjects implementieren), werden Sie Erfolg haben!

So funktioniert es:

public class Enemy : MonoBehaviour
{
   public SuperPower superPowers;
   public UseRandomPower()
   {
       superPowers.RandomItem().UsePower(this);
   }
}
public class BasePower : ScriptableObject
{
   virtual void UsePower(Enemy self)
   {
   }
}
[CreateAssetMenu("BlowFire", "Blow Fire")
public class BlowFire : SuperPower
{
   public strength;
   override public void UsePower(Enemy self)
   {
      ///программа использования суперсилы blow fire
   }
}

Vergessen Sie bei der Verwendung dieses Musters nicht die folgenden Einschränkungen:

  • Скриптуемые объекты не могут надёжно быть абстрактными. Вместо этого используйте конкретные базовые глассы и выдавайте NotImplementedExceptions в методах, которые должны быть абстрактными. Также можно определить атрибут Abstract и отметить им классы и методы, которые должны быть абстрактными.

  • Generic скриптуемые объекты не могут быть сериализированы. Однако можно использовать generic базовые классы и сериализировать только подклассы, определяющие все generic.

49. Verwenden Sie skriptfähige Objekte, um Fertighäuser zu spezialisieren. Wenn sich die Konfiguration von zwei Objekten nur in einigen Eigenschaften unterscheidet, werden normalerweise zwei Instanzen in die Szene eingefügt und diese Eigenschaften in Instanzen festgelegt. In der Regel ist es besser, eine separate Klasse von Eigenschaften zu erstellen, die sich zwischen den beiden Typen unterscheiden kann, eine separate Klasse des Skriptobjekts.

Dies bietet mehr Flexibilität:

  • Sie können von der Spezialisierungsklasse erben, um bestimmte Eigenschaften für verschiedene Objekttypen zu erstellen.
  • Das Einstellen der Szene wird viel sicherer (Sie wählen einfach das gewünschte skriptfähige Objekt aus, anstatt alle Eigenschaften festzulegen, um das Objekt vom gewünschten Typ zu machen).
  • Während der Anwendungsausführung ist es einfacher, diese Objekte über Code zu verwalten.
  • Wenn es mehrere Instanzen von zwei Typen gibt, sind Sie sich der Konstanz ihrer Eigenschaften sicher, wenn Sie Änderungen vornehmen.
  • Sie können Sätze von Konfigurationsvariablen in Sätze aufteilen, die gemischt und abgeglichen werden können.

Hier ist ein einfaches Beispiel für ein solches Setup.

[CreateAssetMenu("HealthProperties.asset", "Health Properties")]
public class HealthProperties : ScriptableObject
{
   public float maxHealth;
   public float resotrationRate;
}
public class Actor : MonoBehaviour
{
   public HealthProperties healthProperties;
}

Bei einer großen Anzahl von Spezialisierungen können Sie eine Spezialisierung als reguläre Klasse definieren und ihre Liste in einem skriptfähigen Objekt verwenden, das einem geeigneten Ort zugeordnet ist, an dem Sie sie anwenden können (z. B. in GameManager ). Um seine Sicherheit, Geschwindigkeit und Bequemlichkeit zu gewährleisten, ist etwas mehr „Kleber“ erforderlich. Das Folgende ist ein Beispiel für die kleinstmögliche Verwendung.

public enum ActorType
{
   Vampire, Wherewolf
}
[Serializable]
public class HealthProperties
{
   public ActorType type;
   public float maxHealth;
   public float resotrationRate;
}
[CreateAssetMenu("ActorSpecialization.asset", "Actor Specialization")]
public class ActorSpecialization : ScriptableObject
{
   public List healthProperties;
   public this[ActorType]
   {
       get { return healthProperties.First(p => p.type == type); } //Небезопасная версия!
   }
}
public class GameManager : Singleton  
{
   public ActorSpecialization actorSpecialization;
   ...
}
public class Actor : MonoBehaviour
{
   public ActorType type;
   public float health;
   //Пример использования
   public Regenerate()
   {
      health 
         += GameManager.Instance.actorSpecialization[type].resotrationRate;
   }
}

50. Verwenden Sie das Attribut CreateAssetMenu, um dem Menü Asset / Create automatisch eine ScriptableObject-Erstellung hinzuzufügen.

Debuggen


51. Научитесь эффективно использовать инструменты отладки Unity.

  • Добавляйте объекты context в конструкции Debug.Log, чтобы знать, где они генерируются.
  • Используйте Debug.Break для паузы игры в редакторе (например, это полезно, когда вы хотите выполнить условия ошибки и вам нужно исследовать свойства компонента в этом кадре).
  • Используйте функции Debug.DrawRay и Debug.DrawLine для визуальной отладки (например, DrawRay очень полезна при отладке причин «непопадания» ray cast).
  • Используйте для визуальной отладки Gizmos. Можно также использовать gizmo renderer за пределами mono behaviours с помощью атрибута DrawGizmo.
  • Используйте инспектор в режиме отладки (чтобы видеть с помощью инспектора значения полей private при выполнении приложения в Unity).

52. Erfahren Sie, wie Sie den Debugger Ihrer IDE effizient nutzen können. Siehe beispielsweise Debuggen von Unity-Spielen in Visual Studio .

53. Verwenden Sie einen visuellen Debugger, der Diagramme von Änderungen der Werte im Laufe der Zeit zeichnet. Es ist äußerst praktisch zum Debuggen von Physik, Animationen und anderen dynamischen Prozessen und insbesondere für unregelmäßig auftretende Fehler. Sie können diesen Fehler in der Grafik sehen und andere Variablen verfolgen, die sich zum Zeitpunkt des Fehlers ändern. Die visuelle Inspektion macht auch bestimmte Arten von seltsamem Verhalten offensichtlich, z. B. zu häufig geänderte Werte oder Abweichungen ohne ersichtlichen Grund. Wir verwenden Monitorkomponenten , es gibt jedoch auch andere visuelle Debugging-Tools.

54. Verwenden Sie die praktische Aufnahme in der Konsole.Verwenden Sie die Editorerweiterung, die eine farbcodierte Ausgabe nach Kategorien ermöglicht, und filtern Sie die Ausgabe nach diesen Kategorien. Wir verwenden Editor Console Pro , es gibt jedoch auch andere Erweiterungen.

55. Verwenden Sie Unity-Testwerkzeuge, insbesondere zum Testen von Algorithmen und mathematischem Code. Siehe zum Beispiel das Tutorial zu Unity Test Tools oder den Unit-Test mit Lichtgeschwindigkeit mit dem Beitrag zu Unity Test Tools .

56. Verwenden Sie Unity-Testtools, um grobe Tests durchzuführen. Unity-Test-Tools eignen sich nicht nur für formale Tests. Sie können auch für praktische „grobe“ Tests verwendet werden, die im Editor durchgeführt werden, ohne eine Szene zu starten.

57. Verwenden Sie Tastaturkürzel, um Screenshots zu machen. Viele Fehler hängen mit der visuellen Anzeige zusammen, und es ist viel einfacher, sie zu melden, wenn Sie einen Screenshot machen können. Ein ideales System sollte über PlayerPrefs-Zähler verfügen, damit Screenshots nicht überschrieben werden. Screenshots müssen nicht im Projektordner gespeichert werden, damit Mitarbeiter sie nicht versehentlich im Repository festschreiben.

58. Verwenden Sie Tastaturkürzel, um Schnappschüsse wichtiger Variablen zu drucken. Mit ihnen können Sie Informationen registrieren, wenn während des Spiels unerwartete Ereignisse auftreten, die untersucht werden können. Die Menge der Variablen hängt natürlich vom Spiel ab. Typische Fehler, die im Spiel auftreten, können Tipps für Sie sein. Zum Beispiel die Position des Spielers und der Feinde oder der „Denkzustand“ des KI-Schauspielers (sagen wir, wie er zu folgen versucht).

59. Implementieren Sie Debugging-Optionen, um das Testen zu vereinfachen. Beispiele:

  • Schalte alle Gegenstände frei.
  • Deaktiviere Feinde.
  • Schalten Sie die GUI aus.
  • Machen Sie einen Spieler unverwundbar.
  • Deaktiviere das gesamte Gameplay.

Achten Sie darauf, dass Sie nicht versehentlich Debugging-Optionen in das Repository übertragen. Das Ändern dieser Optionen kann andere Entwickler im Team verwirren.

60. Definieren Sie die Konstanten für die Debug-Hotkeys und speichern Sie sie an einem Ort. Debug-Schlüssel werden im Gegensatz zu Spieleingaben normalerweise nicht an einem Ort verarbeitet. Um Hotkey-Konflikte zu vermeiden, definieren Sie zunächst Konstanten. Eine Alternative besteht darin, alle Schlüssel an einem Ort zu verarbeiten, unabhängig davon, ob sie über Debugging-Funktionen verfügen oder nicht. (Der Nachteil dieses Ansatzes besteht darin, dass diese Klasse möglicherweise nur zusätzliche Objektreferenzen benötigt).

61. Zeichnen oder spawnen Sie für die prozedurale Netzgenerierung kleine Kugeln an den Eckpunkten. Auf diese Weise können Sie sicherstellen, dass sich die Scheitelpunkte an den richtigen Stellen befinden und die richtige Größe haben, bevor Sie mit Dreiecken und UV-Strahlen arbeiten, um Gitter anzuzeigen.

Leistung


62. Seien Sie vorsichtig mit den allgemeinen Entwurfs- und Strukturrichtlinien für die Leistung.

  • Oft basieren solche Tipps auf Mythen und werden nicht durch Tests getestet.
  • Manchmal werden Empfehlungen durch Tests überprüft, aber die Tests sind von schlechter Qualität.
  • Es kommt vor, dass Tipps mit Qualitätstests getestet werden, aber sie sind unrealistisch oder in einem anderen Kontext anwendbar. (Zum Beispiel können Sie einfach beweisen, dass die Verwendung von Arrays schneller ist als generische Listen. Im Kontext eines realen Spiels ist dieser Unterschied jedoch fast immer unbedeutend. Sie können auch hinzufügen, dass die Ergebnisse möglicherweise auf anderen Geräten als den Zielgeräten für Sie durchgeführt werden in deinem Fall sind nutzlos.)
  • Manchmal ist der Rat richtig, aber bereits veraltet.
  • Manchmal ist eine Empfehlung hilfreich. Möglicherweise ist jedoch ein Kompromiss erforderlich: Manchmal sind langsame, aber pünktlich abgeschlossene Spiele besser als schnell, aber verzögert. Hochoptimierte Spiele enthalten möglicherweise eher kniffligen Code, der die Veröffentlichung verzögert.
  • Es ist nützlich, Leistungstipps zu berücksichtigen, um die Ursachen für echte Probleme schneller als oben beschrieben zu finden.

63. Beginnen Sie so bald wie möglich, das Spiel regelmäßig auf Zielgeräten zu testen. Geräte haben unterschiedliche Leistungsmerkmale. Lass dich nicht überraschen. Je früher Sie etwas über Probleme lernen, desto effektiver können Sie sie lösen.

64. Erfahren Sie, wie Sie den Profiler effektiv verwenden, um die Ursachen von Leistungsproblemen zu verfolgen.


65. Verwenden Sie bei Bedarf einen Profiler eines Drittanbieters, um eine genauere Profilerstellung zu erhalten. Manchmal kann der Unity-Profiler kein klares Bild davon liefern, was gerade passiert: Möglicherweise gehen ihm die Profilrahmen aus, oder die Tiefenprofilierung verlangsamt das Spiel so sehr, dass die Testergebnisse keinen Sinn ergeben. In diesem Fall verwenden wir unseren eigenen Profiler. Alternative finden Sie jedoch im Asset Store.

66. Messen Sie den Effekt von Leistungsverbesserungen. Wenn Sie Änderungen vornehmen, um die Leistung zu verbessern, messen Sie diese, um sicherzustellen, dass die Änderung die Leistung wirklich verbessert. Wenn die Änderung nicht gemessen oder unbedeutend ist, verwerfen Sie sie.

67. Schreiben Sie keinen weniger lesbaren Code, um die Leistung zu verbessern. Ausnahmen:

  • Sie haben ein Problem im Code des Profilers gefunden, Sie haben die Verbesserung nach der Änderung gemessen und die Verbesserung ist so gut, dass sich die Verschlechterung des Supports lohnt.

    ODER
  • Sie wissen genau, was Sie tun.

Benennung von Standard- und Ordnerstruktur


68. Befolgen Sie die dokumentierte Namenskonvention und Ordnerstruktur. Dank der standardisierten Namens- und Ordnerstruktur ist es einfacher, nach Objekten zu suchen und diese zu verstehen.

Höchstwahrscheinlich möchten Sie Ihre eigene Namenskonvention und Ordnerstruktur erstellen. Hier ist ein Beispiel.

Allgemeine Grundsätze für die Benennung


  1. Nennen Sie einen Spaten einen Spaten. Der Vogel sollte Vogel heißen.
  2. Выбирайте имена, которые можно произнести и запомнить. Если вы делаете игру про майя, не называйте уровень QuetzalcoatisReturn (ВозвращениеКетцалкоатля).
  3. Поддерживайте постоянство. Если вы выбрали имя, придерживайтесь его. Не называйте что-то buttonHolder в одном случае и buttonContainer в другом.
  4. Используйте Pascal case, например: ComplicatedVerySpecificObject. Не используйте пробелы, символы подчёркивания или дефисы, с одним исключением (см. раздел «Присвоение имён для различных аспектов одного элемента»).
  5. Не используйте номера версий или слова для обозначения степени выполнения (WIP, final).
  6. Не используйте аббревиатуры: DVamp@W должен называться DarkVampire@Walk.
  7. Используйте терминологию дизайн-документа: если в документе анимация смерти называется Die, то используйте DarkVampire@Die, а не DarkVampire@Death.
  8. Оставляйте наиболее конкретное описание слева: DarkVampire, а не VampireDark; PauseButton, а не ButtonPaused. Например, будет проще найти кнопку паузы в инспекторе, если не все названия кнопок начинаются со слова Button. [Многие предпочитают обратный принцип, потому что так группировка визуально выглядит более очевидной. Однако имена, в отличие от папок, не предназначены для группировки. Имена нужны для различения объектов одного типа, чтобы можно было находить их быстро и просто.]
  9. Некоторые имена образуют последовательности. Используйте в этих именах числа, например, PathNode0, PathNode1. Всегда начинайте нумерацию с 0, а не с 1.
  10. Не используйте числа для элементов, не образующих последовательность. Например, Bird0, Bird1, Bird2 должны называться Flamingo, Eagle, Swallow.

Присвоение имён для различных аспектов одного элемента


Verwenden Sie Unterstriche zwischen dem Hauptnamen und dem Teil, der den „Aspekt“ des Elements beschreibt. Zum Beispiel:

  • GUI- Schaltflächenstatus EnterButton_Active, EnterButton_Inactive
  • Texturen DarkVampire_Diffuse, DarkVampire_Normalmap
  • Skyboxes JungleSky_Top, JungleSky_North
  • LOD- Gruppen DarkVampire_LOD0, DarkVampire_LOD1

Verwenden Sie diese Konvention nicht, um zwischen verschiedenen Arten von Elementen zu unterscheiden. Rock_Small, Rock_Large sollte beispielsweise SmallRock, LargeRock heißen.

Struktur


Das Szenendiagramm, der Projektordner und der Skriptordner sollten eine ähnliche Vorlage haben. Nachfolgend finden Sie Beispiele, die verwendet werden können.

Ordnerstruktur


MyGame
Helper
Design
Scratchpad
Materials
Meshes
Actors
DarkVampire
LightVampire

Structures
Buildings

Props
Plants


Resources
Actors
Items

Prefabs
Actors
Items

Scenes
Menus
Levels
Scripts
Tests
Textures
UI
Effects

UI
MyLibray

Plugins
SomeOtherAsset1
SomeOtherAsset2
...

Структура сцены


Die Haupt
die Debug -
Managers
Kameras
Lichter
die Benutzeroberfläche
der Leinwand
das HUD
PauseMenu
...
Welt
Boden
Props
Structures
...
Gameplay
Schauspieler
die Gegenstände
...
Dynamic die Objekte

Struktur des Skriptordners


Debuggen von
Gameplay
Actors
Items
...
Framework
Graphics
UI
...

Jetzt auch beliebt: