Jump to content
Unity Insider Forum

Collisions, Tags und Names. Wie vermeidet man Strings?


Torigas

Recommended Posts

Hallo Ihr lieben,

 

hier mal eine Anfängerfrage von mir and die erfahreneren Unity Entwickler hier.

 

Wie vermeide ich Tags und Names zum Beispiel im Collision Handling?

Wenn ich also meinen Spieler nur springen lassen will, wenn er in bestimmten Arealen ist kann ich ja folgendes machen:

....
bool canjump = false;
[serializeField]
string jumpTag = "jumpplane";
void OnTriggerEnter(Collider other) {
    if(Collider.CompareTag(jumpTag))
{
canjump = true;
}
   }
....

Okay, potentiell kann ich mit einem Custom Inspector den String loswerden:

https://docs.unity3d.com/ScriptReference/EditorGUI.TagField.html

 

Aber geht das noch schöner?

Wie würdet Ihr so ein Problem lösen?

 

Vielen Dank!

Link zu diesem Kommentar
Auf anderen Seiten teilen

Einfach Komponenten draufschmeißen.

Ich finde es für meinen Teil auch absolut okay, leere MonoBehaviours zu haben.

if(other.GetComponent<JumpVolume>())

 

public class JumpVolume : MonoBehaviour { }

 

Fun Fact: Je länger solche Klassen in der Codebase sind, desto höher wird die Wahrscheinlichkeit, dass da doch noch sinnvoller Code drin landet.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Einfach Komponenten draufschmeißen.

Ich finde es für meinen Teil auch absolut okay, leere MonoBehaviours zu haben.

if(other.GetComponent<JumpVolume>())

 

public class JumpVolume : MonoBehaviour { }

 

Fun Fact: Je länger solche Klassen in der Codebase sind, desto höher wird die Wahrscheinlichkeit, dass da doch noch sinnvoller Code drin landet.

Das hab ich auch überlegt - allerdings sind "GetComponent" Aufrufe schlecht für die Performance. Man soll diese vor allem nicht in Update durchführen. Jetzt kommen bei vielen Objekten natürlich oft "OnTriggerEnter" vor, ergo wäre mein Instinkt erstmal kein GetComponent zu nutzen.

http://chaoscultgames.com/2014/03/unity3d-mythbusting-performance/

https://snowhydra.wordpress.com/2015/06/01/unity-performance-testing-getcomponent-fields-tags/

 

Das sind natürlich nur "kleine" Unterschiede, allerdings hatte ich gehofft es gäbe eine Lösung welche schöner und zumindest gleich schnell wäre. Oder gibt es sonst keine Lösung? Sind Tags vielleicht doch nicht ganz so schlecht?

Link zu diesem Kommentar
Auf anderen Seiten teilen

Oder du siehst die "springbaren Areale" (GameObjekts + Collider) auf ein Jump-Ability-Script, welches ein Array enthält mit allen Collidern auf denen gesprungen werden darf.

Die Idee mag ich. Kann man natürlich auch weniger mechanisch über ein Skript machen, welches automatisch beim Szenenstart alle "Jump-Areas" sucht. Dann müsste man sich nur noch Gedanken machen über den schnellen Check ob das Objekt mit dem kollidiert wird ein Teil der Datenstruktur ist oder nicht.

Dann stellt sich die Frage, ob so ein Check nicht unperformanter wäre als "GetComponent". Denke mit einem Dictionary könnte man das relativ schnell imlementieren.

 

Spannende Ansätze!

Link zu diesem Kommentar
Auf anderen Seiten teilen

Die Collider per Hand in ein Array zu ziehen finde ich überaus unschön, weil es schwierig ist und/oder zusätzlichen Editorcode braucht, um da den Überblick zu behalten.

Die Grundidee, alle bestimmten Collider in einer Sammlung zu halten ist allerdings nicht uninteressant.

Eine Mischung wäre vermutlich einen Versuch wert:

[RequireComponent(typeof(Collider))]
public class JumpVolume : MonoBehaviour
{
 private static HashSet<Collider> allVolumes = new HashSet<Collider>();

 void Awake()
 {
   allVolumes.Add(GetComponent<Collider>());
 }

 public static bool IsJumpVolume(Collider c)
 {
   return allVolumes.Contains(c);
 }
}

if(JumpVolume.IsJumpVolume(other))

 

Allerdings bin ich bezüglich der Performance skeptisch gegenüber der Bedenken.

Wo alle sich einig sind ist, dass GetComponent länger dauert als ein normaler C#-Referenzzugriff.

Daher, keine Frage, ist es besser, wenn möglich die von GetComponent zu erwartenden Referenzen abzuspeichern.

 

Daraus darf man aber nicht den Schluss ziehen, dass GetComponent grundsätzlich langsamer wäre als andere Dinge.

Z.B. würde ich wirklich nicht davon ausgehen, dass GetComponent per se langsamer wäre als HashSet.Contains. Und mindestens das bräuchte man, um GetComponent zu ersetzen.

Müsste man natürlich alles mal testen, aber diese grundsätzliche Angst vor GetComponent als Performancefresser finde ich etwas übertrieben.

Link zu diesem Kommentar
Auf anderen Seiten teilen

GetComponent() vs HashSet:

 

HashSet:

pro: flexibler (GO sind einfach austauschbar ohne hinzufügen von Komponenten)

kontra: wird inperformant bei hoher Stückzahl (Zugriffszeit O(n) bei Hash)

 

GetComponent():

Ich gehe mal davon aus, das GetComponent "ähnlich" (O(n)) schnell wie ein Hashzugriff ist, wenn man einen kontreten Typ benutzt, also nicht:

public Component GetComponent(string type);

sondern

public Component GetComponent(Type type);

pro: geringe Anzahl verschiedener Komponenten am GO

kontra: unflexibel , Zugriffszeit 2*O(n) nur spekulativ

Link zu diesem Kommentar
Auf anderen Seiten teilen

Man kann die beiden Sachen doch gar nicht auf diese Weise vergleichen.

GetComponent tut merkwürdige Dinge und ist zur Hälfte in C++ implementiert.

Dafür muss es nur innerhalb der erwartbar kurzen Liste von Komponenten eines bestimmten GameObjects die richtige raussuchen.

HashSet.Contains dagegen muss aus einem Set von eventuell sehr vielen Elementen das richtige finden, wobei sich das ja auf ein Mal GetHashCode und den einen oder anderen Referenzzugriff beschränkt.

 

Ich glaube, jedwede theoretische Überlegung hierzu ist gänzlich unbrauchbar im Vergleich zu einigen vernünftigen Benchmarks.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Auf jeden Fall schöne Ideen mit dem HashSet und so.

 

Die JumpArea war nunmehr vor allem als Beispiel gemeint. Es gibt in verschiedenen Tutorials immer wieder den Drang Tags oder gar Namen zu nutzen.

Meint Ihr, dass man den Ansatz mit Hashsets verallgemeinern könnte?

 

Oder würdet ihr sagen, dass es den Aufwand nicht wert ist und man Zwecks Lesbarkeit/Wartbarkeit doch die GetComponent<T> oder doch die Tag Variante bevorzugen sollte? Bei der Frage gehts mir auch ein wenig um "Schönen Code".

Link zu diesem Kommentar
Auf anderen Seiten teilen

Also ich benutze bei sowas immer GetComponent und fahre total super damit. Hab erst kürzlich ein kleines Tower Defense System entwickelt (das ich vllt in der Zukunft mal zu einem RTS weiter entwickle, sofern ich irgendwann checke wie man Starcraft 2 ähnliche Maps erstellt :D) und da "gette" ich eigentlich jede Sekunde bestimmt etliche Male oder so irgendwelche Components und läuft alles super smooth.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Meint Ihr, dass man den Ansatz mit Hashsets verallgemeinern könnte?

Darüber würde ich mir erst Gedanken machen, wenn es sich als vorteilhaft gegenüber GetComponent erweisen sollte :)

Ich schau mal, ob ich heute ein paar Benchmarks zusammen kriege.

Link zu diesem Kommentar
Auf anderen Seiten teilen

So, ich habe eben dazu etwas geschrieben und Ergebnisse dazu.

Der Bencher, den ich dafür geschrieben habe, gibt's hier: https://gist.github....2976f54181e9bf4

 

Die Komponente auf den Collidern:

 

using UnityEngine;
using System.Collections.Generic;

[RequireComponent(typeof(Collider))]
public class SpecificVolume : MonoBehaviour
{
public static HashSet<Collider> allVolumes = new HashSet<Collider>();

void Awake()
{
	allVolumes.Add(GetComponent<Collider>());
}

public static bool IsSpecificVolume(Collider c)
{
	return allVolumes.Contains(c);
}
}

 

 

Der Benchmark-Code:

 

var array = new List<Collider>(SpecificVolume.allVolumes).ToArray();
var randomCollider = new Func<Collider>(() => array[unityEngine.Random.Range(0,array.Length)]);

// Benchmark GetComponent
{
var time = Bencher.Bench(() =>
{
	var collider = randomCollider();
	collider.GetComponent<SpecificVolume>();
});
Debug.Log("GetComponent: " + time);
}

// Benchmark HashSet.Contains
{
var time = Bencher.Bench(() =>
{
	var collider = randomCollider();
	SpecificVolume.IsSpecificVolume(collider);
});
Debug.Log("HashSet.Contains: " + time);
}

 

 

In der Szene sind 1000 GameObjects mit

  • Transform
  • BoxCollider
  • SpecificVolume (Script)

Ergebnisse der Benchmarks:

GetComponent: 164.4ns

HashSet.Contains: 233.9ns

 

HashSet.Contains braucht also etwa 1,4-mal so lange.

Ich habe auch mal die Anzahl der Komponenten auf jedem GameObject auf 6 erhöht, hat am Ergebnis nicht wirklich etwas geändert.

 

Sofern nicht jemandem eine Wahnsinnsidee kommt, wie man die HashSet-Idee noch wesentlich performanter bauen kann, ist das erst einmal aus dem Rennen.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Mhh, ist es nicht so, daß wenn du 1000 GOs mit angehangenem Skript erzeugst, dann durchsucht jeder Aufruf von "IsSpecificVolume()" 1000 Hashs. Um einen Vergleich der Performance zwischen "Contains()" und "GetComponent()" zu schaffen, müsstest du 1000 verschiedene Komponenten (also verschiedene Typen script1, script2 .. script1000 ) an jedes GO hängen, oder habe ich nun einen Denkfehler? Das der HashSet mit zunehmender Anzahl an Objekten immer langsamer wird hatte ich ja oben schon gesagt ( O(n) ). Dafür das "GetComponent" nicht suchen muss (da ja nur eine Komponente dranhängt) finde ich die Performance von "GetComponent" eher schwach.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Das der HashSet mit zunehmender Anzahl an Objekten immer langsamer wird hatte ich ja oben schon gesagt ( O(n) ).

Wo haste das eigentlich her?

Der Zugriff auf ein HashSet ist (edit: fast immer nahezu) O(1).

 

Dafür das "GetComponent" nicht suchen muss (da ja nur eine Komponente dranhängt) finde ich die Performance von "GetComponent" eher schwach.

Da sind drei Komponenten auf jedem GameObject.

 

Mal von all dem abgesehen, haben GameObjects keine 1000 Komponenten. 1000 zu checkende Volumen in einer Szene ist dagegen realistisch. Die Zahlen sind hier nicht zufällig gewählt.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Stimmt der "HashSet" ist schneller als ich dachte, da mich der Name irregeführt hat (ist kein Set aus Hashes) und es intern ein Hashtable ist und der hat O(1), das erklärt auch, warum der HashSet nur um 40% langsamer ist obwohl er mit 1000 Einträgen umgehen muss.

 

Nicht unbedingt, wenn man in einer Szene 5-10 Areas mit bestimmten Eigenschaften hat, dann sind das eben 5-10 zu prüfende Volumen und die einem Skript als Array oder eben einem HastSet zu übergeben kann durchaus Sinn machen und Vorteile haben, kommt ganz auf den Anwendungsfall an. Beispielsweise muss man nicht in der Szene suchen, wo denn nun überall die Komponenten dranhängen und wo nicht, sondern man kann es sofort im Inspektor bei dem betreffenden Skript sehen, welche Volumen die entsprechenden Eigenschaften haben. Weiterhin hat man jederzeit sehr leicht Zugriff auf alle Volumen, eine Operation die diese Volumen betreffen, kann somit sehr schnell erfolgen. Aber stimmt, ein Hashset mit 1000 Objekten zu erzeugen und die bei jedem Zugriff auf ein entsprechendes Volumen zu durchsuchen macht nicht wirklich Sinn, da ist "GetComponent" eine gute Variante (wie wir jetzt wissen). Die Performance von "GetComponent" ist also zumindest nicht schlechter (eher besser), als ein Zugriff auf einen Hashtable (entsprechend HashSet in C#).

 

Man mal bitte noch einen Test "Tag" vs "GetComponent" ;)

Aber ich vermute mal ein Tag-Vergleich sollte schneller sein, allerdings kann man ein Objekt eben nur 1x taggen und Komponenten kann man beliebig viele verpassen.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Nicht unbedingt, wenn man in einer Szene 5-10 Areas mit bestimmten Eigenschaften hat, dann sind das eben 5-10 zu prüfende Volumen [...]

Das mag sein. HashSet skaliert ja aber nicht wirklich. Beim Test jetzt gerade kommt es mit nur 10 GOs auf dieselbe Zeit.

 

[...] man kann es sofort im Inspektor bei dem betreffenden Skript sehen, welche Volumen die entsprechenden Eigenschaften haben.

Also, das finde ich sehr optimistisch. Wenn du 20-30 Volumen hast und die alle in einer Liste hast, in der nur deren Name steht, dann würde ich nicht behaupten, dass das einen großartigen Mehrwert bedeutet - eher das Gegenteil.

Unity zeigt die Objekte in einem dreidimensionalen Raum an, wenn man das möchte. Das wäre mir wesentlich lieber.

 

Aber zu Tag gegen GetComponent (10 GOs):

  • CompareTag: 208.6ns
  • GetComponent: 147.7ns

Ich muss aber meine Ergebnisse einmal korrigieren.

Die erste Zeile, die einen zufälligen Collider besorgt, kostet ja auch Zeit:

  • randomCollider: 56.7ns

Das ziehe ich von den Ergebnissen ab und erhalte (10 GOs):

  • CompareTag: 151.9ns
  • GetComponent: 91.0ns
  • HashSet.Contains: 177.2ns

Bedeutet:

HashSet.Contains braucht +~95%

CompareTag braucht +~67%

Link zu diesem Kommentar
Auf anderen Seiten teilen

Ein überraschendes aber sehr spannendes Ergebnis. Vielen Dank nochmal!

Dann heißt es zukünftig wohl erstmal "GetComponent yay - Tags Nay!"

 

Ich dachte wirklich, dass Tags schneller wären. Wie kann das denn sein? Ob er wohl erst GetComponent ausführt bevor er Zugriff auf das Tag bekommt?

Okay. Ich würde noch einen kleinen Vergleich in die Rechnung einbeziehen. Bei GetComponent kann ja auch null raus kommen. Das dürfte allerdings nicht viel ändern.

Man kann ja nicht davon ausgehen, dass das Script auf allen Objekten, mit denen kollidiert wird, drauf ist.

var collider = randomCollider();
var myscript = collider.GetComponent<SpecificVolume>();
if(myscript != null){}else{}

Link zu diesem Kommentar
Auf anderen Seiten teilen

Ja, Danke für die Tests @Sascha, sehr interessant.

Ich denke "HashSet.Contains" verbraucht die meiste Zeit innerhalb der Hashfunktion (Erzeugung des Hashs) und da fällt die Anzahl der Elemente im Hashtable dann nicht wirklich ins Gewicht.

Könnte es sein, daß die Performance von "CompareTag" besser wird bei sehr kurzen Tags, also zum Beispiel von nur einem Zeichen?

 

Was ist wenn du den Tag so vergleichst:

string.CompareOrdinal(GameObject.tag,"tagstring")

Link zu diesem Kommentar
Auf anderen Seiten teilen

Okay. Ich würde noch einen kleinen Vergleich in die Rechnung einbeziehen. Bei GetComponent kann ja auch null raus kommen. Das dürfte allerdings nicht viel ändern.

Man kann ja nicht davon ausgehen, dass das Script auf allen Objekten, mit denen kollidiert wird, drauf ist.

var collider = randomCollider();
var myscript = collider.GetComponent<SpecificVolume>();
if(myscript != null){}else{}

Naja, die Abfrage habe ich bei allen Tests weggelassen.

 

Könnte es sein, daß die Performance von "CompareTag" besser wird bei sehr kurzen Tags, also zum Beispiel von nur einem Zeichen?

 

Was ist wenn du den Tag so vergleichst:

string.CompareOrdinal(GameObject.tag,"tagstring")

In beiden Fällen bewegen wir uns in Richtungen, wo Performance nicht mehr der einzige Maßstab ist.

Kurze Tagnamen sind weniger sprechend und irgendwann wird das Projekt sehr undurchsichtig.

Mit CompareOrdinal zu vergleichen ist weniger sicher als CompareTag. CompareTag sagt einem ja wenigstens, wenn ein abgefragter Tag nicht einmal existiert. Ein manueller String-Vergleich würde Projekte sehr schwer zu debuggen machen.

 

GetComponent dagegen bietet Sicherheit nicht einmal nur zur Laufzeit, sondern zur Compile Time. Selbst wenn es in meinen Benchmarks nicht so gut abgeschnitten hätte, würde ich es wegen des positiven Einflusses auf die Codequalität vorziehen.

Link zu diesem Kommentar
Auf anderen Seiten teilen

ÃŽch würde immer noch eine Tag-Kennung vorziehen und "GetComponent" nur verwenden, wenn ein GO mehrere Eigenschaften besitzen soll. Grundsätzlich glaube ich, daß die Tags genau für solche Szenarien da sind, was mich nur "stört" ist die "schlechte" Performance. Ich denke es wäre besser gewesen, Tags als numerische Label anzubieten, ähnlich wie bei "Layernamen" nur aber mit einer höheren Anzahl an Bits.

 

By the way:

"compareTag(aString)" soll um 27% schneller sein als "gameObject.tag == "aString"

Link zu diesem Kommentar
Auf anderen Seiten teilen

Ich kann mir gut vorstellen, dass string-== ziemlich käsig implementiert ist und CompareTag etwas kluges macht.

Jedenfalls kann ich das ungefähr bestätigen. Bei mir sind es gerade sogar 79%.

 

Unity-Tags sind aus mehreren Gründen unpraktisch. Keine Korrektheitsprüfung zur Compile Time, nur ein Tag pro Objekt, keine zusätzlichen Eigenschaften...

Link zu diesem Kommentar
Auf anderen Seiten teilen

Naja, die Abfrage habe ich bei allen Tests weggelassen.

 

Klar. Das wird aber beim HashSet beim Insert berücksichtigt, dass das nicht "null" sein sollte. Davon kann man ausgehen.

Beim GetComponent muss man hingegen sicherstellen, dass das Ergebnis nicht "null" ist. Sollte aber auch keinen großen Unterschied machen.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Ah, es geht dir um den null-Vergleich... ich hatte deine Aussage auf die if-Abfrage bezogen, die ja um das Contains auch noch drum müsste.

Aber da hast du Recht... ich habe gerade noch die Benchmarks erweitert und bin über das Ergebnis etwas verwundert:

 

var collider = randomCollider();

57ns

 

var collider = randomCollider();
var check = collider.CompareTag("SpecificVolume");

208ns

 

var collider = randomCollider();
var check = collider.tag == "SpecificVolume";

331ns

 

var collider = randomCollider();
var check = collider.GetComponent<SpecificVolume>();

146ns

 

Variablentyp beachten:

var collider = randomCollider();
bool check = collider.GetComponent<SpecificVolume>();

205ns

 

var collider = randomCollider();
var check = SpecificVolume.IsSpecificVolume(collider);

238ns

 

CompareTag und GetComponent unterscheiden sich also nur um wenige Nanosekunden (GetComponent ~2% schneller), wenn man das Ergebnis von GetComponent noch auf bool castet. Performancetechnisch ist also kein echter Unterschied zu erwarten.

HashSet.Contains ist immerhin noch ~20% langsamer.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Archiviert

Dieses Thema ist jetzt archiviert und für weitere Antworten gesperrt.

×
×
  • Neu erstellen...