Jump to content
Unity Insider Forum

Noch mehr Scripten in Unity - Teil 4: Böse Funktionen beseitigen


Sascha

Recommended Posts

Dieses Tutorial beschäftigt sich mit Funktionen, die die UnityEngine abietet, von denen (nicht nur) ich der Meinung bin, dass man sie trotzdem möglichst nicht bis nie benutzen sollte. Ich werde kurz erklären, warum man sie meiden möchte und was man tun kann, um exakt das selbe zu erreichen, ohne sie zu benutzen.

 

Kleine Anmerkung: Ich finde es etwas schade, dass die offiziellen Einstiegspunkte für Unity-Scripting diese Methoden teilweise vorschlagen. Ich kann euch versichern, dass ich weiß, das Unity Tech. selbst die Verwendung vorschlägt, und dass es trotzdem unschön ist.

 

HINWEIS: In diesem Tutorial steht in den Spoilern immer der schlechte Code, der Code darunter ist dazu äquivalent, aber eben ohne die böse Funktion.

 

Noch mehr Scripten in Unity - Teil 4: Böse Funktionen beseitigen

 

Fangen wir mit dem Staatsfeind nummer eins an:

GameObject.Find

Was macht es?

  • Findet ein GameObject in der aktuellen Szene anhand des Namens.

Warum ist es schlecht?

  • Der Name eines GameObjects sollte einen rein dekorativen Zweck haben, da man sich auf den Namen nicht wirklich verlassen kann. Spätestens, wenn man im Team arbeitet, kann man sich nicht mehr sicher sein, dass nicht das falsche oder kein Objekt mehr gefunden wird.
  • Es ist inperformant. Ich weiß nicht, ob die ganze Hierarchie durchsucht wird oder Unity eine Dictionary benutzt, aber schneller als die folgende Lösung wird GameObject.Find niemals sein.

Was mache ich stattdessen?

Wie bereits vorhergehend beschrieben, sollen alle GameObjects möglichst autonom agieren. Da GameObject.Find dafür da ist, eine Verbindung zwischen zwei GameObjects herzustellen, ist der erste Lösungsansatz, zu überdenken, ob da wirklich zwei Objekte kommunizieren sollen. Kann man nicht ein neues Script schreiben, und dieses tut dann seinen Kram alleine?

 

Wenn das nicht der Fall ist, und zwei Objekte wirklich miteinander kommunizieren sollen, dann hilft eine Eigenschaft.

Originalcode:

function Update()
{
 GameObject.Find("Auto").transform.Translate(Vector3.forward);
}

 

var auto : GameObject;

function Update()
{
 auto.transform.Translate(Vector3.forward);
}

Jetzt muss nur noch das Auto im Unity Editor in die Eigenschaft "Auto" gezogen werden.

 

Erfahreneren wird aufgefallen sein: Ich benutze immer nur die Transform-Komponente des Autos. In diesem Fall kann ich auch gleich das hier machen:

var auto : Transform; //Typ einer Komponente, die ein GameObject haben muss, um
//in die Eigenschaft gezogen werden zu können

function Update()
{
 auto.Translate(Vector3.forward); //auto.transform entfällt dann
}

 

GameObject.FindGameObjectsWithTag

Was macht es?

  • Findet alle GameObjects mit dem angegebenen Tag

Warum ist es schlecht?

  • Es ist stark davon auszugehen, dass Unity nicht so doof ist, die ganze Szene nach GameObjects mit einem bestimmten Tag zu durchsuchen, sondern dafür irgendwo ein Dictionary hat. Dennoch gibt diese Funktion eine Referenz auf ein Array zurück, das höchstwahrscheinlich nicht ohne weiteres gebildet werden kann.
    Kurz: Inperformant.

Was mache ich stattdessen?

Wieder erst einmal überlegen: Brauche ich überhaupt ein zentrales Script, das alles steuert? Können meine Objekte mit Tag xy nicht eigenständig die Aufgaben erledigen?

 

Wenn nein:

Eine statische generische Liste in einem Skript, das alle Objekte kriegen, die vorher den gesuchten Tag hatten.

Eine generische Liste ist eine Liste (kein Array, Listen können dynamisch wachsen) von Objekten, das nur Objekte eines bestimmten Typs auflisten kann/darf (Generizität).

(Für Fortgeschrittene: Ich benutze lieber Lists als HashSets weil ich keine Lust auf eine Wrapper-Klasse für eine Readonly-Kopie habe)

 

Originalcode:

function Update()
{
 var alleHaeuser : GameObject[] = GameObject.FindGameObjectsWithTag("Haus");
 for(var haus : GameObject in alleHaeuser)
 {
MachIrgendwasMit(haus);
 }
}

Dieses Skript würde an irgendein Objekt gehängt werden, das den überblick behalten soll.

 

Dieses Skript kriegen alle Objekte, die vorher den gesuchten Tag gehabt haben (was sie auch weiterhin dürfen):

//man beachte den zusätzlichen, nötigen Import!
import System.Collections.Generic;

private static var allHouses = List.< NameDesScripts >();//Die Leerzeichen in den Spitzen Klammern gehören da nicht hin - das Forum macht mir sonst meinen Post kaputt ^^

function Awake()
{
 //eintragen, wenn ich neu bin
 allHouses.Add(this);
}

function OnDestroy()
{
 //austragen, wenn ich zerstört werde
 allHouses.Remove(this);
}

static function GetAllHouses() : ReadOnlyCollection.< NameDesScripts >
{
 return allHouses.AsReadOnly();
}

 

Das ursprüngliche Script kann sich jetzt diese Objekte ganz einfach besorgen:

function Update()
{
 var alleHaeuser = NameDesScripts.GetAllHouses(); //Mit NameDesScripts ist natürlich das obere Script gemeint
 for(var haus : NameDesScripts in alleHaeuser)
 {
MachIrgendwasMit(haus);
//wenn wir im Originalcode wirklich das GameObject gebraucht wird, einfach haus.gameObject schreiben
 }
}

 

GetComponent in Update

Was macht es?

  • GetComponent gibt eine Komponente eines bestimmten Typs auf dem aktuellen/benannten GameObject zurück

Warum ist es schlecht?

  • Aus dem gleichen Grund, warum die bisher genannten Methoden inperformant sind, ist auch GetComponent inperformant.

Was mache ich stattdessen?

GetComponent kann man auch umgehen, indem man eine Eigenschaft erstellt und die Komponente rein zieht (siehe letzter Code zu GameObject.Find).

Gerade, wenn die Komponenten allerdings auf dem jeweils eigenen GameObject liegen, ist das aber unnötige Arbeit.

Stattdessen: Component Caching!

Originalcode:

function Update()
{
 var komponente : TypDerAnderenKomponente = GetComponent("TypDerAnderenKomponente");
 MachWasMit(komponente);
}

 

Stattdessen:

//private, weil die Referenz nur dieses Script etwas angeht
private var komponente : TypDerAnderenKomponente;

function Awake()
{
 komponente = GetComponent.< TypDerAnderenKomponente >();
 //man beachte die spitzen Klammern, die performanter sind als der String im Originalcode
 //auch hier gehören die Leerzeichen wieder nicht wirklich da hin 
}

function Update()
{
 MachWasMit(komponente);
}

 

Übrigens: Die Shortcuts für GetComponent, also transform, light, renderer und so weiter, sind nichts weiter als "versteckte" GetComponent-Aufrufe. Auch die Komponenten mit Shortcut dürfen daher gerne gecached werden.

 

 

Das war's auch schon

Diese drei sind häufige zu Performanceverlust lockende Wiederholungstäter.

Während des Schreibens sind mir einige Wege eingefallen, wie Unity Tech diese Methoden hätte implementiert haben können, sodass sie gar nicht so inperformant sind, wie man denken könnte. Der geschickte Einsatz von Dictionarys wäre da sehr gut möglich.

Trotzdem kann man mit diesen Tipps auf jeden Fall mindestens ein bisschen Performance gut machen, was zumindest auf mobilen Geräten einen Unterschied machen könnte.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Zu dem GetComponent Part würde ich noch unbedingt hinzufügen dass die Properties wie transform, camera, guiTexture, rigidbody:

 

bla.transform

 

nichts anderes als einen GetComponent Aufruf machen.

 

Auch möchte ich noch unbedingt hinzufügen dass alles was Sascha hier schreibt Sinn machtg, man es aber nicht sofort übertreiben soll. Wie es so schön heißt: "premature optimization is the root of all evil" erstmal den Code funktionierend bekommen, DANN optimieren.Und dabei unbedingt auf die Lesbarkeit achten.

 

 

Cool wäre es wenn du SendMessage auch noch auflisten könntest, das Teil ist quasi die Definition von Böse, wenns einen Maßstab dafür gäbe hätte sie einen Wert von 2 Kilohitler.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Die Maßeinheit Kilohitler gefällt :D

 

Aber mir stellen sich dann schon noch ein oder zwei Fragen. Am besten am Beispiel.

zb. habe ich eine Klasse Unit : MonoBehaviour in der so alles definiert wird, was eine Unit so kann.

Wenn ich eine "Unit" spawne, packe ich die Unit-Komponente in eine List<Unit> (oder ein Dict, was dynamisches eben) um schonmal das Suchen zu vermeiden. Das mache ich um schneller auf einzelne Methoden oder Variablen in Unit zugreifen will. Soweit, so klar, hoffe ich ;)

Nun möchte ich zb doch auf die Transform-Komponente zugreifen um diese Unit dann zb zu bewegen.

Also zb so


List<Unit> UnitList = new List<Unit>();


foreach (var u in UnitList )
{
u.transform.Translate(BlubbVec3); /// nur zur Veranschaulichung
// ist ja letztlich nichts anderes als GetComponent<Transform>()

/// anderes
u.MachNochWasTolles();
u.BlubbVar = 10;
// usw
}

 

Da ja .transform auch nur ein GetComponent<Transform>() ist, hätte ich das ja auch so machen können:

 


List<Transform> UnitList = new List<Transform>();

foreach (var u in UnitList )
{
u.Translate(BlubbVec3); /// nur zur Veranschaulichung
/// anderes
var _u = u.GetComponent<Unit>();
_u.MachNochWasTolles();
_u.BlubbVar = 10;
// usw
}

 

Angenommen das läuft in der Update, greife ich letztlich in beiden Varianten per GetComponent auf irgendwas zu, egal wie ich es drehe und wende. Wobei ich in Beispiel zwei ja noch eine temporäre Variable erzeuge. Ich kann mir beim besten Willen auch nicht vorstellen, dass hier zwei Listen, also auch zwei Schleifen, die Lösung sind nur um GetComponent zu vermeiden oder doch?

Was wäre nun die empfehlenswerte Variante? Ich persönlich tendiere zur Variante 1, lasse mich aber gerne eines besseren belehren... :) komme ja aus dem Design und nicht aus der Informatik :D

Link zu diesem Kommentar
Auf anderen Seiten teilen

Ich würde Variante 1 bevorzugen, später wenn du was optimieren willst kannst du in deiner Unit Klasse auch gern ein Field erstellen welches du mit der transform bewirfst:

 

public class Unit : MonoBehaviour
{
 public Transform myTransform;
 ...

 void Awake()
 {
myTransform = transform;
   ...
 }

 ...
}

 

Solange es aber keine Probleme mit dem direkten Verwenden von transform gibt würde ich da auch keine Sonderwege gehen.

 

Ob transform wirklich nur sowas hier ist, würde ich auch nochmal sicherheitshalber überprüfen, da es besonders bei der Transform Komponente die garantiert IMMEr existiert keinen Sinn macht jedesmal diese Komponente per GetComponent zu suchen, aber Unity neigt oft dazu sowas undenkbares zu machen,

Link zu diesem Kommentar
Auf anderen Seiten teilen

...

[code]
//eintragen, wenn ich neu bin
 allHouses.Add(this);

...

 

d.h. ich kann in combination mit dem kompletten rest dieser beispiels

//man beachte den zusätzlichen, nötigen Import! import System.Collections.Generic;
private static var allHouses : List< NameDesScripts > = List< NameDesScripts >();
//Die Leerzeichen in den Spitzen Klammern gehöran da nicht hin - das Forum macht mir sonst meinen Post kaputt ^^ function Awake() {  
//eintragen, wenn ich neu bin   allHouses.Add(this);
}
static function GetAllHouses() : ReadOnlyCollection< NameDesScripts > {
  return allHouses.AsReadOnly();
}
function OnDestroy()
{
 //austragen, wenn ich zerstört werde
 allHouses.Remove(this);
}


 

 

den schnipsel direkt z.b. auch beim instanzieren/destroy eines Gameobject aktiv nutzen ?

 

http://docs.unity3d.....OnDestroy.html

geil :)

bearbeitet von schlumpf2009
Link zu diesem Kommentar
Auf anderen Seiten teilen

Das mit den Shortcuts stimmt, habs hinzugefügt.

Ich mach das auch so oft, dass ich inzwischen feste Namen für die meisten Komponenten habe:

transform => me

rigidbody => body

renderer => r (warum auch immer)

wobei die schlauecoole Alternative ja die hier wäre:

new private Transform transform;

void Awake()
{
 transform = GetComponent<Transform>();
}

 

Was ich allerdings nicht so ohne weiteres einbauen kann ist SendMessage.

Ja, das Ding ist ein Ersatz für einen direkten Aufruf in sehr inperformant, aber es kann einige tolle Dinge.

Es passt halt zum komponentenbasierten System.

Wenn du, wie in anderen Engines üblich, über Vererbungshierarchien Objekte definierst, dann hast du mit Design Patterns das Problem gelöst, aber bei Komponenten... SendMessage mit DontRequireReceiver musst du erstmal toppen.

Insbesondere, weil SendMessage Coroutines automatisch mit StartCoroutine aufruft.

 

Wenn wir hier einen vernünftigen Ersatz zusammengeschustert kriegen, dann füge ich den gerne noch ein :)

Link zu diesem Kommentar
Auf anderen Seiten teilen

GetComponentsInChildren auf eine BasisKlasse oder ein Interface dann die Methode aufrufen die aufgerufen werden soll, noch besser wären Sender+Receiver ala Delegates.

 

SendMessage ist einfach nur ein sehr einfacher Weg ohne weiteres Wissen über andere Elemente Methoden aufzurufen. Alternativen sind ja da ;)

Link zu diesem Kommentar
Auf anderen Seiten teilen

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Gast
Auf dieses Thema antworten...

×   Du hast formatierten Text eingefügt.   Formatierung jetzt entfernen

  Only 75 emoji are allowed.

×   Dein Link wurde automatisch eingebettet.   Einbetten rückgängig machen und als Link darstellen

×   Dein vorheriger Inhalt wurde wiederhergestellt.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Lädt...
×
×
  • Neu erstellen...