Tutorials lehren nicht die optimale Vorgehensweise oder den effizientesten Weg, ein bestimmtes Problem zu lösen, sondern nur den einfachsten Weg. Diese sind manchmal der Grund, warum Entwicklern (meistens Anfängern) beigebracht wird, dass sie bestimmte Muster verwenden sollen, die überhaupt nicht empfehlenswert sind, wenn ihnen Performance, sauberer Code und ordentliche Code-Design-Muster wichtig sind. Irgendwann werden diese Dinge auch bei Anfängern eine grosse Rolle spielen, wenn sie es jetzt noch nicht tun.
Inhalt
- Warum sollten Find-Methoden vermieden werden?
- Referenz durch den Inspector
- Eine Art Injektion
- Singleton-ähnlich
Warum sollten Find-Methoden vermieden werden?
Ob GameObject.Find
, GameObject.FindWithTag
, GameObject.FindGameObjectsWithTag
, FindObjectOfType
oder FindObjectsOfType
… all diese Namen solltest du in den meisten Fällen ziemlich schnell vergessen und meiden. Dies hat verschiedene Gründe.
Schlechte Performance
Find
-Methoden sind extrem langsam. Sie müssen die gesamte Hierarchie der momentan geladenen Szene durchsuchen, um ein bestimmtes oder mehrere Objekte zu finden. Je grösser die Szene, desto langsamer wird diese Funktion. Das bedeutet, dass besonders bei grösseren Projekten diese Find
-Methoden eine Sünde für dein Spiel sind, aber auch bei jeglichen anderen Projekten wird davon dringend abgeraten.
Oft wird behauptet, dass Find
-Methoden in Start()
oder Awake()
ja grundsätzlich in Ordnung seien. Diese Behauptung beherbergt nur einen Teil der Wahrheit. Auch dort rettet man sich nicht vor der Langsamkeit dieser Funktionen. So verlängert man die Ladezeiten und macht sich selbst und seinem Spiel keinen gefallen. Ausserdem gibt es noch andere Gründe, die Find
Methoden ihren schlechten Ruf schenken, nicht nur die Performance, wobei dies das Schlimmste an ihnen ist. Darauf kommen wir gleich zu sprechen.
Nebst dem Hauptproblem, dass diese Methoden die gesamte Hierarchie durchsuchen müssen, gibt es auch einen zweiten Punkt, der diese Methoden so langsam macht. Dies betrifft wiederum hauptsächlich nur die GameObject.Find
Methode. Diese verlangt einen Parameter vom Typ string
. Intern wird der Name jedes Objektes mit der angegebenen Zeichenkette verglichen und dies ist besonders Performance-lastig, da somit wegen dieser Zeichenkette mehr Speicher im Heap zugewiesen wird. Dies ist ziemlich teuer und löst auch den Garbage Collector aus, was wieder auf die CPU drückt.
Teilweise “string-Abhängigkeit”
Dies betrifft nicht alle, aber dennoch einige der Find
-Methoden, die gleichzeitig auch am häufigsten verwendet werden. Methoden wie GameObject.Find
, GameObject.FindWithTag
und GameObject.FindGameObjectsWithTag
verlangen einen string
Parameter.
Damit hängen grundsätzlich zwei Probleme zusammen. Einerseits der oben bereits erwähnte Nachteil betreffend die Performance und andererseits die Unsicherheit eines solchen Vorgehens.
Zeichenketten sind nicht sehr zuverlässig.
Hierzu ein Beispiel:
using UnityEngine;
public class SomeBehavior : MonoBehaviour
{
private GameObject greenHouse;
private void Start()
{
greenHouse = GameObject.Find("GreenHouse");
}
}
Was wäre, wenn du dich versehentlich vertippt hättest und das GameObject eigentlich den Namen “Green House” oder “GrenHouse” trägt?
Du würdest eine Fehlermeldung erhalten und deine gesamte Logik, die von einer korrekten Referenz dieser Variable abhängt, würde nicht mehr funktionieren. Dann musst du den Fehler zuerst finden, ausbügeln und dies gegebenenfalls mehrmals tun. Das ist ein wenig ärgerlich und führt zu einem kleinen, aber überflüssigen Zeitverlust.Was wäre, wenn du beschliessen würdest, den Namen des Objektes zu verändern?
Du würdest wieder eine Fehlermeldung erhalten und müsstest alleFind
-Methoden, die dieses Objekt betreffen, verändern, um dies zu beheben. Das ist wieder ein unnötiger Aufwand, den man sich sparen kann.Was wäre, wenn du versehentlich ein Objekt mit demselben Namen hättest?
Es existiert die Möglichkeit, dass das falsche Objekt ausgewählt wird. Dies resultiert entweder in Fehlermeldungen oder unerwünschtem Verhalten. Das ist besonders bei sehr grossen Szenen mit eventuell mehreren ähnlichen Objekten eine nicht ganz kontrollierbare Angelegenheit.
Wie man sieht, sind Zeichenketten hier nicht sehr zuverlässig. Kleine fahrlässige Fehler rauben einem viel Zeit, die man besser hätte investieren können.
Welche Alternativen gibt es?
Referenz durch den Inspector
Der bei weitem beste und effizienteste Weg, eine Referenz in Unity zu bilden, ist mithilfe des “Inspectors” realisierbar. Dabei macht man eine Variable (Feld) im “Inspector” sichtbar, um später die Referenz per “Drag and Drop” mit der Maus in das erschienene Feld zu ziehen.
Wenn beide Objekte mit den beiden Instanzen der Klassen, die miteinander kommunizieren sollten, NICHT instanziiert werden, also von Anfang an in der Szene sind, solltest du diesen Weg wählen.
Beispiel
Manager.cs:
using UnityEngine;
public class Manager : MonoBehaviour
{
[SerializeField] private ExampleClass example; // Referenz zur `ExampleClass` Instanz, die du im "Inspector" per "Drag and Drop" zuweisen sollst
private void Start()
{
// Nur als Beispiel gedacht. Du rufst jetzt zum Beispiel eine nicht-statische Methode in `ExampleClass` auf (in der Instanz, die du zugewiesen hast)
example.ExampleMethod();
}
}
ExampleClass.cs:
using UnityEngine;
public class ExampleClass : MonoBehaviour
{
public void ExampleMethod() // Beispiel-Methode
{
Debug.Log("Geschafft! Methode wurde erfolgreich aufgerufen.");
}
}
Zuweisung im Inspector
Das [SerializeField]
Attribut gewährleistet, dass ein Feld im “Inspector” angezeigt wird, auch wenn die Variable nicht public
ist.ExampleClass
ist hier der Typ der Variable; das bedeutet, wir möchten eine Referenz zu einer Instanz der ExampleClass
Klasse und example
ist der Variablenname, mit welchem diese Variable verwendet werden kann.
Jetzt müssen wir noch angeben, welche Instanz wir denn mit dieser Variable referenzieren möchten. Dies kann man in Unity im “Inspector” tun, insofern die Instanz als Komponente an einem GameObject in der Szene hängt. Unity führt dann im Hintergrund ein wenig Magie durch und weist der Variable einen Wert zu.
Die Zuweisung funktioniert wie folgt…
- Zuerst haben wir ein GameObject mit der
Manager
Komponente.
- Dann haben wir auch noch ein GameObject, das die
ExampleClass
Komponente enthält.
- Und zuletzt muss man nur noch das GameObject, das die gewünschte Komponente enthält, in das Feld der
Manager
Komponente ziehen.
So einfach geht das und das funktioniert genauso gut auch mit Referenzen zu einem GameObject
oder irgendeiner anderen Klasse, die als Komponente an einem Objekt hängt.
Eine Art Injektion
Der Nachteil der obigen Variante mit der alleinigen Zuweisung im “Inspector” ist, dass es nur funktioniert, wenn auch beide Objekte von Anfang an in der Szene sind und NICHT instanziiert werden. Trotzdem gibt es Alternativen. Für die nachfolgenden Beispiele bleiben wir weiterhin bei den Manager
und ExampleClass
Klassen.
Falls jetzt das GameObject, das die ExampleClass
Komponente enthält, irgendwann instanziiert wird, also NICHT von Anfang an in der Szene ist, kann man die Komponente nicht einfach per “Drag and Drop” zuweisen, da die Komponente nur als Teil eines “Prefab” existiert. Ein “Prefab” ist sozusagen ein “Bauplan” eines GameObjects, der beliebig oft instanziiert werden kann, sodass genau so ein GameObject in der Szene entsteht. Bevor so ein Objekt jedoch instanziiert wird, existiert es gar nicht und kann nicht irgendwie zugewiesen werden. Hier verwenden wir einen einfachen Trick, eine sogenannte “Property Injection”.
Es gibt sicherlich eine Klasse, die für die Instanziierung dieses Objektes mit der ExampleClass
Komponente verantwortlich ist. Nennen wir sie einmal Spawner
. Diese enthält jetzt beispielsweise eine Methode, in der dieses Example
Objekt instanziiert wird, wenn diese Methode aufgerufen wird.
Wir möchten weiterhin eine Referenz zu dieser ExampleClass
Instanz in der Manager
Klasse. Angenommen wird, dass die zwei verschiedenen Objekte mit den Manager
und Spawner
Komponenten von Anfang an in der Szene sind.
Manager.cs:
using UnityEngine;
public class Manager : MonoBehaviour
{
public ExampleClass Example { get; set; } // Dies wird die Referenz zur `ExampleClass` Instanz speichern
private void CallMethod()
{
// Nur als Beispiel gedacht. Du rufst jetzt zum Beispiel eine nicht-statische Methode in `ExampleClass` auf (in der Instanz, die du zugewiesen hast)
Example?.ExampleMethod();
}
}
Spawner.cs:
using UnityEngine;
public class Spawner : MonoBehaviour
{
[SerializeField] private GameObject examplePrefab; // Referenz zum Prefab, das instanziiert werden soll - im "Inspector" zuweisen
[SerializeField] private Manager manager; // Referenz zum `Manager`, der die Referenz zugewiesen bekommen soll - im "Inspector" zuweisen
private void Spawn()
{
GameObject exampleObj = Instantiate(examplePrefab, transform.position, Quaternion.identity); // Instanziierung des Objektes
manager.Example = exampleObj.GetComponent<ExampleClass>(); // Weist die Instanz der `Example` Eigenschaft in der `Manager` Klasse zu
}
}
Der Code der ExampleClass.cs Datei bleibt gleich für dieses Beispiel. Die Manager
Klasse hat jedoch einige Änderungen erfahren und die Spawner
Klasse ist neu dazugekommen…
Nun haben wir die im “Inspector” angezeigte Variable in Form eines “Feldes” mit dem
[SerializeField]
Attribut in eine get-set Eigenschaft umgewandelt, die nicht im “Inspector” angezeigt wird, da das nicht nötig ist. Warum das jetzt eine Eigenschaft ist, hängt einzig und allein mit der Empfehlung in C# für nach aussen sichtbare Variablen zusammen. In diesem Fall ist es jedoch nicht ganz wichtig, da es sowieso eine get-set Eigenschaft ist.
Dies haben wir getan, weil die Variable nicht mehr im “Inspector”, sondern per Code von einer anderen Klasse aus zugewiesen wird.Die
CallMethod()
Methode ist nur eine Beispiel-Methode, in der die nicht-statischeExampleMethod()
aufgerufen wird, um die Nutzung einer zugewiesenen Variable zu demonstrieren. Dies ist also für das derzeitige Problem nicht relevant.In der
Spawner
Klasse haben wir nun zwei Variablen (Felder) oben, beide im “Inspector” sichtbar, da wir beide per “Drag and Drop” im “Inspector” zuweisen müssen. Einerseits ist da dieexamplePrefab
Variable, die eine einfache Referenz zum Prefab in den Assets darstellt, damit wir dieses instanziieren können. Andererseits haben wir diemanager
Variable, die eine Referenz zur bereits bestehendenManager
Instanz hält.Innerhalb der
Spawn()
Methode instanziieren wir zuerst das Prefab und speichern es als Referenz in einer lokalen VariableexampleObj
ab.Danach holen wir die
ExampleClass
Komponente dieses neu instanziierten Objektes (mit der GetComponent<>() Methode) und weisen diese derExample
Eigenschaft in derManager
Instanz zu.
Somit hat der Manager
endlich seine Referenz zur ExampleClass
Instanz.
Im Falle, dass Manager instanziiert wird und eine Referenz zum ExampleScript in der Szene braucht
Nun kann man dieses Beispiel ganz einfach umdrehen, im Falle, dass das Objekt mit der Manager
Komponente instanziiert wird und ausgerechnet diese Komponente eine Referenz zur bereits bestehenden ExampleClass
benötigt.
Manager.cs:
using UnityEngine;
public class Manager : MonoBehaviour
{
public ExampleClass Example { get; set; } // Dies wird die Referenz zur `ExampleClass` Instanz speichern
private void CallMethod()
{
// Nur als Beispiel gedacht. Du rufst jetzt zum Beispiel eine nicht-statische Methode in `ExampleClass` auf (in der Instanz, die du zugewiesen hast)
Example?.ExampleMethod();
}
}
Spawner.cs:
using UnityEngine;
public class Spawner : MonoBehaviour
{
[SerializeField] private GameObject managerPrefab; // Referenz zum Prefab, das instanziiert werden soll (Manager) - im Inspector zuweisen
[SerializeField] private ExampleClass example; // Referenz zur bestehenden `ExampleClass` Instanz - im Inspector zuweisen
private void Spawn()
{
GameObject managerObj = Instantiate(managerPrefab, transform.position, Quaternion.identity); // Instanziierung des Objektes
managerObj.GetComponent<Manager>().Example = example; // Zuweisung der `Example` Eigenschaft in der `Manager` Klasse
}
}
Die einzigen Änderungen haben in der Spawner
Klasse stattgefunden. Die signifikanteste Änderung betrifft die letzte Zeile der Spawn()
Methode. Sehen wir uns das genauer an…
So sah die Zeile vorher aus:
manager.Example = exampleObj.GetComponent<ExampleClass>();
Da holen wir zuerst die ExampleClass
Komponente vom neulich instanziierten Objekt und weisen sie der Example
Eigenschaft der bereits bestehenden und referenzierten Manager
Instanz zu.
Nun sieht die Zeile aber so aus:
managerObj.GetComponent<Manager>().Example = example;
Hier haben wir zuerst eine Referenz zu dem gerade instanziierten Objekt, welches die Manager
Komponente enthält. Diese holen wir uns auch mit
GetComponent<>()
und greifen zusätzlich auf die Example
Eigenschaft darin zu. Dieser Eigenschaft weisen wir wieder einen Wert zu, nämlich den Wert von example
. Zur Erinnerung: example
hält eine Referenz zu einer bereits bestehenden ExampleClass
Instanz.
Achtung
Dieser Weg kann in verschiedenen Varianten vorkommen. Es ist kein striktes Vorgehen, sondern man ist ziemlich flexibel und kann beispielsweise mit einer Methode arbeiten, wobei man die Referenz als Parameter übergibt. Du darfst natürlich gerne damit herumspielen.
Singleton-ähnlich
Zuletzt ist auch die “Singleton-Technik” eine gängige Methode, um sich Zugriff auf eine Instanz zu schaffen. Sie wird meistens in Situationen verwendet, wo ein nachträglich instanziiertes Objekt eine Referenz zu etwas in der Szene braucht. Hierbei verwenden wir wieder den Manager
, allerdings fungiert dieser jetzt als zentrales Element der Szene, das sozusagen die Schnittstelle zwischen benötigten Referenzen und den Instanzen, die diese Referenzen benötigen, ist.
Grundsätzlich braucht es nicht einmal ein richtiges “Singleton”. Es ist eher eine statische Referenz zu einer Instanz dieses Managers
nötig.
Das Objekt mit ExampleClass
wird nachträglich instanziiert. Du möchtest dort beispielsweise eine Referenz zu einem UI-Image in der Szene haben.
Manager.cs:
using UnityEngine;
using UnityEngine.UI;
public class Manager : MonoBehaviour
{
public static Manager Instance { get; private set; } // Statische Referenz zu der `Manager` Instanz in der Szene
[field: SerializeField] public Image SomeImage { get; set; } // Referenz zum UI-Image - im Inspector zuweisen
private void Awake()
{
Instance = this; // Weise DIESE Instanz der `Instance` Variable zu. Nun kann jede Klasse/Instanz, egal woher, auf diese zugreifen
}
}
ExampleClass.cs:
using UnityEngine;
public class ExampleClass : MonoBehaviour
{
private Manager manager; // Anfangs leere Variable, die dann eine Referenz zur `Manager` Instanz halten wird.
private Image someImage; // Anfangs leere Variable, die dann eine Referenz zum UI-Image halten wird.
private void Start()
{
manager = Manager.Instance; // Referenz zum `Manager` holen
someImage = manager.SomeImage; // Referenz zum `Image` aus dem `Manager` holen
}
}
Nochmals, um es zu wiederholen…
Zuerst erstellen wir eine statische Variable, auf die man von überall zugreifen kann, ohne jegliche Referenz zu einer
Manager
Instanz, da diese Variable eben statisch ist. Diese Variable wird eine Referenz zu einerManager
Instanz beherbergen.Danach haben wir eine Eigenschaft, die eine Referenz zu einem UI-Image in der Szene hält.
[field: SerializeField]
stellt hierbei sicher, dass diese Eigenschaft im Inspector angezeigt wird, sodass wir dasImage
wieder per “Drag and Drop” zuweisen können.field:
erstellt hierbei ein sogenanntes "Backing Field" , welches dafür benötigt wird. Darauf werde ich nicht näher eingehen, da das ein wenig ausserhalb des Fokus dieses Artikels liegt.Zuletzt weisen wir diese jetzige, momentane Instanz der
Instance
Variable zu. Dies tun wir inAwake()
. Das ist auch ziemlich wichtig, um mögliche Konflikte zu vermeiden, indem man sicherstellt, dass die Zuweisung so früh wie möglich erfolgt, sodass, wenn möglich, keine Instanz versucht, davor auf die Variable zuzugreifen.Nun kann die Klasse, die am Objekt hängt, das im Laufe der Zeit irgendwann einmal instanziiert wird, einfach auf diese
Instance
Variable zugreifen, die die Referenz bereits enthält.
Dies kann beispielsweise in Start() getan werden, da das praktisch direkt nach der “Geburt” des Objektes aufgerufen wird.Da wir jetzt eine Referenz zum
Manager
haben, können wir ganz einfach der “öffentlichen” (public
)SomeImage
Variable die Referenz zum “Image” entnehmen.
Dies ist jetzt nicht ganz ein Singleton. Um daraus ein Singleton zu machen, müsste man sicherstellen, dass nur eine Instanz dieser Manager
Klasse in der Szene vorhanden ist. Dies ist aber für dieses Problem irrelevant.
Unity besitzt einzigartige Ansätze für manche ziemlich simplen Dinge, die aber schlussendlich trotzdem überhaupt nicht schwer zu begreifen, geschweige denn sonderlich schwer zu verwenden sind. Für einen erfahrenen .NET-Entwickler ist das Konzept vom Zuweisen von Referenzen im “Inspector” ein wenig eigenartig, da “Magie im Hintergrund” ein Fremdwort für diesen ist. Und trotzdem bietet es aufgrund fehlender “Dependency Injection” und einem Komponenten-basierten System die beste Alternative zu altbekannten Wegen in C#.