Jump to content
Unity Insider Forum

OnClick event über Script steuern


Recommended Posts

Hallo zusammen,

ich habe eine Reihe von Buttons, die je nach voreistellungen des Spielers und auch durch zufall entweder da sind oder eben nicht. Problem ist es sind (derzeit) immer bis zu 6 Buttons, die letzen endes auf eine Liste zeigen mit insgesamt 6 Elementen. Also je Element ein Button. Sind nur 3 Buttons da, sind auch nur 3 Elemente in der Liste.

Da ich vorher nicht weiß welcher Button erscheint und welcher nicht kann ich die Buttons nicht vorher bei Unity einstellen welche Methode sie mit welchem Übergabewert ansteuern sollen. Im Internet habe ich auch was gefunden, das funktioniert allerding nur so halb:

for(int i = 0; 0 < list.count; i++)
{
  // btn ist der Button
  btn.onClick.AddListener(delegate { GetComponent<MenuManager>().ButtonAction(i); });
}

Das funktioniert auch auf den ersten Blick allerdings ist folgendes Problem dabei: Wenn es nur ein Button ist müsste i = 0 sein allerdings wird die ButtinAction mit 1 aufgerufen und bricht dann ab, weil es keinen entsprechenden Listeneintrag gibt und selbst wenn ich alle Buttons erscheinen lasse, und eigentlich alle Zahlen von 0 bis 5 übergeben werden müssten bekommen alle den übergabewert 6.

Unabhängig davon habe ich eh nicht ganz verstanden, was "delegate" macht, warum hier plötzlich geschweifte Klammern von nöten sind usw. das nehme ich mal einfach hin.

Warum allerdings trotz mehrerer schleifendurchläufe alle den selben Wert haben und dann auch noch eins höher als erwartet bleibt mir gerade ein Rätsel.

Kann mir da jemand helfen? Dankeschön.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Moin!

1. Die üblichere Schreibweise ist die Lambda-Schreibweise. Die gibt's in mehreren Sprachen und wird, wenn man sie erstmal kennt, meistens als angenehmer empfunden als da "delegate" hinzuschreiben. Ein Lambda-Ausdruck (genau wie mit "delegate") ist einfach eine Schreibweise für eine Methode ohne Namen. Man kann also als Beispiele Methoden nehmen und dann die entsprechenden Lambda-Ausdrücke zeigen:

a)

private void Foo()
{
  Debug.Log(5);
}

als Lambda-Ausdruck wäre

() => Debug.Log(5)

b)

private void Foo()
{
  Debug.Log(5);
  Debug.Log(10);
}

als Lambda-Ausdruck wäre

() =>
{
  Debug.Log(5);
  Debug.Log(10);
}

Hier braucht man geschweifte Klammern, weil mehr als eine Anweisung drinsteht.

c)

Jetzt mal mit Parametern:

private void Foo(int number)
{
  Debug.Log(number);
}

wäre

number => Debug.Log(number)

und

private void Foo(int number, string text)
{
  Debug.Log(number + text);
}

wäre

(number, text) => Debug.Log(number + text)

Man bemerke hier, dass keine Parametertypen angegeben werden - da steht also nix davon, dass "text" ein string ist.
Das liegt daran, dass Lambda-Ausdrücke zu Methodenreferenzen evaluieren. Diese werden als normale Werte behandelt - man kann sie also in eine Variable speichern oder eben als Parameter übergeben. Und in diesen Fällen ist vom Typ der Variable bzw. des Parameters her schon vorgegeben, dass da eine Referenz auf eine Methode kommen muss, die ein int- und einen string-Parameter hat.

Action<int, string> foo = (number, text) => Debug.Log(number + text);
// und jetzt kann man die Methode aufrufen
foo(5, " Stück");

Mit Lambda-Ausdrücken kann man einem Objekt mehr als nur simple Werte in die Hand drücken, sondern ganze Verhaltensweisen. Deshalb kannst du die Dinger benutzen, um Buttons zu sagen, was sie tun sollen.

btn.onClick.AddListener(() => GetComponent<MenuManager>().ButtonAction(i));

Soviel erstmal dazu.

2. Jetzt musst du (leider) das Konzept von Closures kennenlernen. Eine Closure ist wie ein Objekt, also ein Ding, das bestimmte Werte hat. Und jede anonyme Methode (also die, die du mit einem Lambda-Ausdruck erzeugst), hat so eine. Sie enthält die für die Ausführung der Methode relevanten Variablen aus der Umgebung, in der der Lambda-Ausdruck steht.

var number = 5;
Action foo = () => Debug.Log(number);
foo();

Hier wird eine int-Variable mit dem Wert 5 definiert, eine Methode die den Wert der Variable ausgibt, und dann wird diese Methode aufgerufen.

Es sollte also recht klar sein, dass die Zahl 5 ausgegeben wird. Die Variable "number" ist Teil der Closure der Methode, die von der Variable "foo" referenziert wird.

Jetzt kommt der Knackpunkt: Closures machen pass-by-reference. Das heißt, dass hier nicht die 5 in der Closure gespeichert wird, sondern eine Referenz auf die Variable "number". Wenn du folgendes tust:

var number = 5;
Action foo = () => Debug.Log(number);
number = 10;
foo();

dann wird tatsächlich 10 ausgegeben und nicht 5, weil beim Aufruf der Methode der aktuelle Wert der Variable angeschaut wird.

Wenn du also eine Schleife hast und in dieser Schleife Methoden erzeugst, die irgendetwas mit der Zählvariable i tun, dann wird am Ende jede dieser anonymen Methoden den aktuellen Wert von i nehmen - und das wird nun einmal der Wert am Ende des Schleifendurchlaufs sein, also in deinem Fall 1 bei einem Button bzw. 6 bei sechs Buttons.

Die Lösung des Problems ist jetzt ein bisschen stumpf: Du machst in der Schleife eine neue Variable und kopierst da den Wert rein; dann benutzt du diese Variable in deinem Lambda-Ausdruck statt i:

for(int i = 0; i < list.count; i++)
{
  var btn = list[i];
  
  var index = i;
  btn.onClick.AddListener(() => GetComponent<MenuManager>().ButtonAction(index));
}

Dann wird die Variable "index" angeschaut, wenn man auf einen Button klickt - und der Wert dieser Variable ändert sich nicht mehr, weil sie am Ende des jeweiligen Durchlaufs (eigentlich) out of scope geht.

Übrigens kannst du es dir (so als Schmakerl) sparen, jedes Mal GetComponent aufzurufen, indem du das vorher einmal machst:

var menuManager = GetComponent<MenuManager>();

for(int i = 0; i < list.count; i++)
{
  var btn = list[i];
  
  var index = i;
  btn.onClick.AddListener(() => menuManager.ButtonAction(index));
}

 

Link zu diesem Kommentar
Auf anderen Seiten teilen

Vielen Dank für diese, wenn auch ewtas

vor 14 Stunden schrieb Sascha:

stumpf

e, Lösung :D.
 

Für mich war da extrem viel neues drin

Aber das hat man davon, wenn man denkt eine unschuldige Frage zu stellen. Aber wo wir jetzt schon mal bei dem Thema sind, so ganz habe ich es noch nicht verstanden was mir diese "leeren Methoden" bringen.

vor 14 Stunden schrieb Sascha:
var number = 5;
Action foo = () => Debug.Log(number);
foo();

Zumal du hier die Methode foo Definierst und ihr die vorgehensweise Debug.Log mitgibst.
Klar sehe ich die Ersparnis von ein paar Zeile, da 

private void foo()
{
  Debug.Log(number);
}

schon ein wenig länger ist wobei es ja auch noch in einer Zeile gehen würde.

 

Wie Rufe ich denn Beispielsweise diese Methode auf? Wenn sie keinen Namen hat kann ich sie ja nicht ansprechen.

vor 14 Stunden schrieb Sascha:
number => Debug.Log(number)

Oder Dienen diese Lampda Ausdrücke nur die schreibweise zu vereinfachen und sie an Methoden an zu hängen?

Beim Ausprobieren habe ich auch noch folgende Probleme:

image.png.00a8f60264850488e4882a2e12527aa5.pngimage.png.969d416fb75de90a2b71376336d7cd8c.png

Oder ich habe das immernoch was nicht ganz verstanden. (Das glaube ich tatsächer eher)

 

Link zu diesem Kommentar
Auf anderen Seiten teilen

Der Lambda-Ausdruck ist ein Statement, das zu einem Wert evaluiert. Dieser Wert ist eine Referenz auf die Methode, die du erzeugst. Mit diesem Wert musst du bei Lambda-Ausdrücken etwas machen. Genauso wie du nicht

5 + 5;

in deinen Code schreiben kannst, sondern das Ergebnis irgendwie benutzen musst

var number = 5 + 5;

musst du das auch bei Lambda-Ausdrücken machen.

() => Debug.Log(number); // Geht nicht

Action foo = () => Debug.Log(number); // Geht

Damit ist hoffentlich auch die Frage

vor einer Stunde schrieb Singular:

Wie Rufe ich denn Beispielsweise diese Methode auf? Wenn sie keinen Namen hat kann ich sie ja nicht ansprechen.

geklärt: Die Methode hat keinen Namen, genau wie Objekte keinen Namen haben. Aber die Variable, die die Methode referenziert, hat einen Namen (hier foo), über den du die anonyme Methode aufrufen kannst.

vor einer Stunde schrieb Singular:

so ganz habe ich es noch nicht verstanden was mir diese "leeren Methoden" bringen.

Sie zu erzeugen passiert genau da, wo sie gebraucht werden. Man kann halt tatsächlich auch eine ganz normale Methode definieren und diese in einer Action-Variable referenzieren:

private void Start()
{
  Action foo = MyMethod;
  
  foo();
}

private void MyMethod()
{
  Debug.Log("hi");
}

Das kann auch oft die bessere Variante sein. Beim Button geht das genauso:

btn.onClick.AddListener(MyMethod);

und es spricht erstmal wirklich nicht viel dagegen. Wenn deine Methode einen guten Namen hat, ist das 1a.

Knifflig wird es dann aber, wenn du z.B. sechs verschiedene Verhaltensweisen haben willst, und wenn diese sich auch nur durch eine einzige Zahl unterscheiden. So wie bei dir :)

Die vordefinierte Methode hat nämlich im Gegensatz zur anonymen keine Closure. Naja, abgesehen vom Objekt, in dem sie steckt. Oder anders gesagt: Du kannst keine Parameterwerte an MyMethod binden, wenn diese Methode Parameter hätte.

Nehmen wir dein Beispiel:

private void Start()
{
  btn.onClick.AddListener(MyMethod);
}

private void MyMethod(int index)
{
  menuManager.ButtonAction(index);
}

hier würde das Programm erwarten, dass der Button deiner Methode beim Aufruf den Index übergibt. Tut er ja aber nicht. Und du kannst auch nicht schreiben

btn.onClick.AddListener(MyMethod(index));

weil du dann nicht MyMethod übergibst, sondern MyMethod aufrufst und dann das übergibst, was dabei zurückkommt.

Mit

() => menuManager.ButtonAction(index)

erzeugst du eine neue, parameterlose Methode, in die dein Index fest integriert ist. Wenn deine Schleife also sechsmal durchläuft, hast du sechs neue Methoden generiert, die wegen des sich ändernden Index tatsächlich auch alle unterschiedlich sind. Das Äquivalent dazu wäre MyMethod1, MyMethod2, MyMethod3 usw. zu definieren... merkste ;)

Du erzeugst also neue Methoden zur Laufzeit, damit z.B. so ein Unity-Button einfach stumpf eine Methode zum ausführen in die Hand kriegt. Diese Methode ist dann einzigartig und braucht beim Aufrufen keinen Parameter mehr, um anders zu sein als die der anderen Buttons.

Und zuallerletzt: "Action" ist in System definiert. Du musst also System.Action schreiben oder, was fast immer besser ist:

using System;

an den Anfang.

Link zu diesem Kommentar
Auf anderen Seiten teilen

vor 6 Stunden schrieb Sascha:

Du erzeugst also neue Methoden zur Laufzeit, damit z.B. so ein Unity-Button einfach stumpf eine Methode zum ausführen in die Hand kriegt. Diese Methode ist dann einzigartig und braucht beim Aufrufen keinen Parameter mehr, um anders zu sein als die der anderen Buttons.

Ahhh jetzt hats klick gemacht. "Zur Laufzeit eine neue Methode erzeugen" hatte ich vorher nicht verstanden. Wusste nicht mal dass das geht :D Perfekt danke dir.

vor 6 Stunden schrieb Sascha:

Und zuallerletzt: "Action" ist in System definiert. Du musst also System.Action schreiben oder, was fast immer besser ist:

using System;

Ja, das war das was mir gefehlt hat. Ich hatte keine using directiv mit drin für das Action. Danke dir.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Ich habe zu diesem Thema leider doch nochmal eine Frage:

Was mache ich hier Falsch, Ich würde behaupten, dass hier eigentlich alle hinterlegten Events gelöscht werden und das Fenster im Button leer ist:

private void Start()
	{
		TESTBUTTON.onClick.RemoveAllListeners();
	}

Es passiert aber gar nichts. Die Hinterlegten Events sind immernoch da. Ich bekomme aber auch keine Fehlermeldung.

Genauso geht es anders Herum aber auch nicht:

public void Start()
{
  TESTBUTTON.onClick.AddListener(() => OpenKontinent(0));  
}

//oder

public void Start()
{
  TESTBUTTON.onClick.AddListener(() => GetComponent<MenuManger>().OpenKontinent(0));  
}
  
  

Auch hier passiert nichts. Es wird nichts hinzugefügt oder weggenommen.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Okay "Added / Remove an non-persistent Listener to / from the UnityEvent"

Das wird es wohl sein. Wenn ich "Debug.Log(TESTBUTTON.onClick.GetPersistentEventCount());" hinzufüge bekomme ich auch 1 zurück. Was ist denn eine persistente Funktion?

Edit:
Okay das Hinzufügen klappt, doch. Es wird nur im Inspector nicht angezeigt. Ich denke dann weiß ich was ich zu tun habe... ich darf das nicht Insprector zuweisen sondern muss das über den Code machen, weil ich dann die Methoden, die ich hinterlegt habe auch wieder raus bekomme. Richtig?

Link zu diesem Kommentar
Auf anderen Seiten teilen

Archiviert

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

×
×
  • Neu erstellen...