Search Unity

  1. Good news ✨ We have more Unite Now videos available for you to watch on-demand! Come check them out and ask our experts any questions!
    Dismiss Notice

How to AddListener() featuring an argument?

Discussion in 'UGUI & TextMesh Pro' started by AlwaysSunny, Sep 7, 2014.

  1. AlwaysSunny

    AlwaysSunny

    Joined:
    Sep 15, 2011
    Posts:
    260
    Code (csharp):
    1. // So, I can add a listener via code by method name with
    2. button.onClick.AddListener(SomeMethodName);
    3. // but can't include an argument?
    4. button.onClick.AddListener(SomeMethodName(SomeObject));
    I noticed this custom events solution in the FAQ's - is that the best way to achieve sending any kind of argument? Given my argument inherits from MB - in keeping with the rules of what arguments events can have - I feel I'm just missing an easier way. Can I create a UnityEngine.Events.UnityAction object which features my MB-derived-object argument instead?

    Thanks,

    Also, I can't paste from Mono into text box, I keep getting Chinese characters?
     
  2. Senshi

    Senshi

    Joined:
    Oct 3, 2010
    Posts:
    534
    You can wrap it inside a delegate, like so:

    Code (csharp):
    1. button.onClick.AddListener(delegate{SomeMethodName(SomeObject);});
    As for UnityActions which send along arguments, you can do something like:

    Code (csharp):
    1. public class FloatEvent : UnityEvent<float> {} //empty class; just needs to exist
    2.  
    3. public FloatEvent onSomeFloatChange = new FloatEvent();
    4.  
    5. void SomethingThatInvokesTheEvent(){
    6.     onSomeFloatChange.Invoke(3.14f);
    7. }
    8.  
    9. //Elsewhere:
    10. onSomeFloatChange.AddListener(SomeListener);
    11.  
    12. void SomeListener(float f){
    13.     Debug.Log("Listened to change on value " + f); //prints "Listened to change on value 3.14"
    14. }
     
  3. AlwaysSunny

    AlwaysSunny

    Joined:
    Sep 15, 2011
    Posts:
    260
    Hey hey, a quick and detailed response. With code! My sincere thanks.

    The delegate-wrapping method compiled, but threw a nigh indecipherable exception when I clicked the button. I'll paste it below in case its cause is easily recognized by another. I'll give the alternative method you provided a shot asap. I appreciate your time!


    System.NullReferenceException: Object reference not set to an instance of an object
    at UIContextMenuFlagship+<MakeButton>c__AnonStorey7.<>m__0 () [0x00000] in D:\Dev\UNITY\MyProjects\2014\StarGarden - Flat\Assets\NewUI\UIContextMenuFlagship.cs:42
    at UnityEngine.Events.InvokableCall.Invoke (System.Object[] args) [0x00010] in C:\BuildAgent\work\d63dfc6385190b60\Runtime\Export\UnityEvent.cs:110
    at UnityEngine.Events.InvokableCallList.Invoke (System.Object[] parameters) [0x00035] in C:\BuildAgent\work\d63dfc6385190b60\Runtime\Export\UnityEvent.cs:565
    at UnityEngine.Events.UnityEventBase.Invoke (System.Object[] parameters) [0x00006] in C:\BuildAgent\work\d63dfc6385190b60\Runtime\Export\UnityEvent.cs:714
    at UnityEngine.Events.UnityEvent.Invoke () [0x00000] in C:\BuildAgent\work\d63dfc6385190b60\Runtime\Export\UnityEvent_0.cs:53
    at UnityEngine.UI.Button.Press () [0x00017] in C:\BuildAgent\work\d63dfc6385190b60\Extensions\guisystem\guisystem\UI\Core\Button.cs:35
    at UnityEngine.UI.Button.OnPointerClick (UnityEngine.EventSystems.PointerEventData eventData) [0x00000] in C:\BuildAgent\work\d63dfc6385190b60\Extensions\guisystem\guisystem\UI\Core\Button.cs:41
    at UnityEngine.EventSystems.ExecuteEvents.Execute (IPointerClickHandler handler, UnityEngine.EventSystems.BaseEventData eventData) [0x00000] in C:\BuildAgent\work\d63dfc6385190b60\Extensions\guisystem\guisystem\EventSystem\ExecuteEvents.cs:52
    at UnityEngine.EventSystems.ExecuteEvents.Execute[IPointerClickHandler] (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.EventFunction`1 functor) [0x00087] in C:\BuildAgent\work\d63dfc6385190b60\Extensions\guisystem\guisystem\EventSystem\ExecuteEvents.cs:257
    UnityEngine.EventSystems.EventSystem:Update()
     
  4. Senshi

    Senshi

    Joined:
    Oct 3, 2010
    Posts:
    534
    Hmm. Looking at the top two lines, it appears you're attempting to access (a property of) a null object in UIContextMenuFlagship.cs on line 42. Perhaps you forgot to assign something? =)
     
  5. AlwaysSunny

    AlwaysSunny

    Joined:
    Sep 15, 2011
    Posts:
    260
    Yep. Please excuse me while I go kick myself. Works as expected, many thanks!
     
    Last edited: Sep 7, 2014
    IgorAherne and Senshi like this.
  6. Senshi

    Senshi

    Joined:
    Oct 3, 2010
    Posts:
    534
    Ha, you are excused! ;) Glad you got it work; good luck with your project! =)
     
  7. aleceiffel1066

    aleceiffel1066

    Joined:
    Aug 20, 2014
    Posts:
    5
    Trying to create a system that handles UI element generated events in a generic way, that limits the amount of coding or code attaching that the interface designer has to do within the Unity editor. Want them to be able to just drag a UI component from a component library (a prefab) into the scene, and modify its visual aspects to their heart's content. Meanwhile, it is already wired up to raise events that I can have my GameManager (or SceneManager, or LevelManager) code listen for. Those raised events should pass the UI element object that generated them, to the Manager code when the event is raised (as an argument of the event.raise method). Am I missing something in this thread or is there some additional information that could help me with this? It would be great if the designer didn't have to muck with any manually clacked in identifiers for the UI elements they were creating. Some of the out of the box methods require/allow a string argument, but one of the tutorial videos mentions that the Unity object could be one of the argument types in a UI element generated method call. I can't seem to find the Editor setting that passes the sender as an argument to any Unity or custom scripts.

    Many thanks
     
  8. Senshi

    Senshi

    Joined:
    Oct 3, 2010
    Posts:
    534
    @aleceiffel1066 If I understand correctly, you want to give an element prefab a trigger (like, say, onClick) from within the Editor, that sends along the element itself to a GameManager script, correct?

    The main problem you'll be facing with this approach is that you can't target a scene element inside a prefab. That is, if you throw a prefab into the scene, it will not have remembered its links to your in-scene GameManager.

    If that isn't a problem though, everything should be fairly straightforward. If you want to send along which Selectable sent the event for example, you should be able to just create the following function in your GameManager script:

    Code (csharp):
    1. public void SomeFunction(Selectable caller){
    2.     Debug.Log(caller);
    3. }
    Selecting that from the EventTrigger dropdown menu, you can then just drag the UI object into the parameter slot. =)
     
  9. aleceiffel1066

    aleceiffel1066

    Joined:
    Aug 20, 2014
    Posts:
    5
    Thank you @Senshi very much for the rapid reply, and my apologies for a delayed response.
    If I can describe this setup you will likely be able to offer advice for how to proceed - obviously if it suits your fancy :)

    EventRelay.cs
    Code (csharp):
    1.  
    2. usingUnityEngine;
    3. usingSystem.Collections;
    4.  
    5. publicclassEventRelay : BaseMonoBehaviour {
    6.  
    7. publicdelegatestringEventAction(EventMessageTypetype, Objectsender);
    8. publicstaticeventEventActionOnEventAction;
    9.  
    10. publicenumEventMessageType {
    11. GuiElementPicked,
    12. ObjectCollected,
    13. DefenseChosen,
    14. DefenseActivated,
    15. LoadGamePicked,
    16. SaveGamePicked,
    17. MainMenuScreenPicked,
    18. UpgradeShopPicked,
    19. SocialMenuPicked,
    20. WeaponChosen,
    21. WeaponFired,
    22. InventoryChecked,
    23. ItemAddedToInventory,
    24. ItemRemovedFromInventory,
    25. QuestionAnswered,
    26. QuestionPicked,
    27. DebugUIOnPicked,
    28. DebugUIOffPicked,
    29. PlayButtonPicked,
    30. EnemyAffected
    31. }
    32.  
    33. publicstaticstringRelayEvent(EventMessageTypemessageType, Objectsender) {
    34. returnOnEventAction(messageType, sender);
    35. }
    36. }
    37.  
    EventSender.cs (wired up using the Editor > Inspector)
    Code (csharp):
    1.  
    2. usingUnityEngine;
    3. usingSystem.Collections;
    4.  
    5. publicclassEventSender : BaseMonoBehaviour {
    6.  
    7. publicboolmouseIsOverThis = false;
    8. publicObjectsender;
    9.  
    10. //Updateiscalledonceperframe
    11. voidUpdate () {
    12. if(mouseIsOverThis) {
    13. if(Input.GetMouseButtonDown((int)MouseUtils.Button.Left)) {
    14. stringvalue = EventRelay.RelayEvent(
    15. EventRelay.EventMessageType.GuiElementPicked, sender);
    16. Debug.Log("GuiElementPicked " + value);
    17. }
    18. }
    19. }
    20. publicvoidOnMouseEnter(ObjecteventSource) {
    21. mouseIsOverThis = true;
    22. sender = eventSource;
    23. }
    24.  
    25. publicvoidOnMouseClick(ObjecteventSource) {
    26. mouseIsOverThis = true;
    27. sender = eventSource;
    28. }
    29.  
    30. publicvoidOnMouseExit(ObjecteventSource) {
    31. mouseIsOverThis = false;
    32. sender = eventSource;
    33. }
    34. }
    35.  
    EventListener.cs - added to an item that needs to handle an event
    Code (csharp):
    1.  
    2. usingUnityEngine;
    3. usingSystem.Collections;
    4. usingSystem.Collections.Generic;
    5.  
    6. publicclassEventListener : BaseMonoBehaviour {
    7.  
    8. publicList<EventRelay.EventMessageType> eventsHandled =
    9. newList<EventRelay.EventMessageType>();
    10.  
    11. voidOnEnable() {
    12. EventRelay.OnEventAction += HandleEvent;
    13. }
    14.  
    15. voidOnDisable() {
    16. EventRelay.OnEventAction -= HandleEvent;
    17. }
    18.  
    19. stringHandleEvent(EventRelay.EventMessageTypemessageType, Objectsender) {
    20. if(eventsHandled.Contains(messageType)) {
    21. Debug.Log("Handled event: " + messageType + " from sender: " + sender.ToString());
    22. returnthis.ToString();
    23. } else {
    24. //ignoreevent
    25. returnthis.ToString();
    26. }
    27. }
    28. }
    29.  

    The plan is to pass the generic GuiElementPicked event to the relay from the UI button (any UI button), along with the button (or other UI element) itself. So that my listener can evaluate which buttons were raising the events and the listener (on the GameManager or other object) could then invoke methods in the GameManager (TBD).

    Another complication that I haven't gotten to evaluate is that these UI elements, buttons and such - need to be in prefabs, and instantiated at runtime. Firstly via the action of a bootstrapping function in the GameManager (a persistent GameObject on the main scene). GuiLayouts (collections of controls, images, text fields, labels, etc) would be instantiated from a prefab library at run time. What problems do you see arising from this approach? I was planning on loading levels this way [possibly], and also NPCs, the Player, etc. Is this realistic in your opinion? Many thanks for the help. Feel free to contact me out-of-band if you prefer, or whatever suits. Thanks!
     
    Last edited: Sep 8, 2014
  10. Senshi

    Senshi

    Joined:
    Oct 3, 2010
    Posts:
    534
    Hey @aleceiffel1066, no problem! Unfortunately your code lost all formatting so it's a tad hard to read/ follow. Doubly unfortunately, I'm not sure how much time I'll have this week to further reply on these forums. That said:

    Let me see if I understand correctly what you want to see happen:
    - Be able to send a message to an EventListener script from any button, containing EventMessageType and the button from which the event originated
    - Use EventSender in conjunction with a Button, to handle the actual sending

    First off, I'm not sure what the EventRelay is for, specifically? At first glance I would assume you'd just want the button to send its action to the appropriate script/ call the correct method directly. Also, in EventSender.cs, where does `value` come from? =)

    Just in case I won't get a chance to respond tomorrow, here's how I might approach this situation:

    - Have EventSender inherit from the Button class and several interfaces (like IPointerEnterHandler eg), so you can easily override the appropriate methods there.
    - Inside those methods, call GameManager.ProcessEvent(MessageType.TYPE, this)

    If you want one central dispatcher, but multiple listeners (and don't like the idea of broadcasting), I would indeed use delegates like you are. I.e.: Have ProcessEvent() call Invoke() on the appropriate UnityEvent, sending the sender as a parameter (`onHover.Invoke(sender)`) You could also keep a List of which objects are listening to which events, and pass it around like that.

    Sorry if I completely misunderstood the question! I'm not entirely sure where you're experiecing difficulty, and the code is a bit hard to follow as-is due to the formatting.

    Also, it might be better to just make this its own topic as well. =)
     
    aleceiffel1066 likes this.
  11. aleceiffel1066

    aleceiffel1066

    Joined:
    Aug 20, 2014
    Posts:
    5
    Agreed on all counts, and thanks again. Appreciate your time so far, and completely understand that it might not be efficient to follow along on all this while it shakes out.
     
  12. Senshi

    Senshi

    Joined:
    Oct 3, 2010
    Posts:
    534
    Oh, it has nothing to do with efficiency; I'd be happy to help! Just that I have some other stuff going on this week that lure me away from the computer. ;)
     
  13. Anxo

    Anxo

    Joined:
    Jan 7, 2014
    Posts:
    11
    @$%#$%^# Awesome!
     
    nigel-moore and Senshi like this.
  14. warance

    warance

    Joined:
    May 4, 2014
    Posts:
    11
    Is the parameter passed into the delegate stored as a reference value?
    I have this simple code :

    Code (CSharp):
    1. void Start(){
    2.      int  i = 0;
    3.      foreach(var button in buttonCont)
    4.      {
    5.           button.GetComponent<Button>().onClick.AddListener(delegate { TestFn(i); });
    6.           ++i;
    7.      }
    8. }
    9.  
    10. void TestFn(int i)
    11. {
    12.      Debug.Log("testprint" + i);
    13. }
    ButtonCont have 2 buttons. The expected result is that when i clicked on button1, it prints "testprint1" and print "testprint2" when button2 is press. However both buttons print "testprint3".
     
    xxZap likes this.
  15. shaderop

    shaderop

    Joined:
    Nov 24, 2010
    Posts:
    942
    Not quite. It's actually being captured in a "closure" that contains all that is needed for the delegate/anonymous method to be called at a later time. It's as if the variable "i" was silently converted to a field in an object that was then silently passed to the delegates/anonymous methods when they were finally invoked.

    And BTW: I think using the delegate keyword as in your example would look archaic compared to how C# is written now. Something like this might be more in keeping with the times:

    Code (csharp):
    1. button.GetComponent<Button>().onClick.AddListener(() => TestFn(i));
    2.  
     
  16. Senshi

    Senshi

    Joined:
    Oct 3, 2010
    Posts:
    534
    @warance: @shaderop is correct in his explanation, but didn't mention a solution. ;) This should work:

    Code (CSharp):
    1. void Start(){
    2.      int  i = 0;
    3.      foreach(var button in buttonCont)
    4.      {
    5.          int _i = ++i;
    6.           button.GetComponent<Button>().onClick.AddListener(delegate { TestFn(_i); });
    7.      }
    8. }
    That way you are creating a seperate variable to be captured by the closure every time, so its value is never modified.

    @shaderop: RE archaic style - Eh, I'd say that's purely subjective in this case. One is a lambda, the other a delegate. The advantange of delegates is you can use it to wrap more than one function call (delegate{Foo(a); a += 10; Bar(a);}); the advantage of lambdas in this case is it's shorter and typically easier on the eyes. I'd advise anyone to just use whichever they find easiest/ most readable.
     
    ansyal2006, Lotramus, ltomov and 8 others like this.
  17. shaderop

    shaderop

    Joined:
    Nov 24, 2010
    Posts:
    942
    I don't think that is correct. You can definitely go to town in a lambda expression, e.g:
    Code (csharp):
    1. onClick.AddListener(() => {
    2.   Foo();
    3.   Bar();
    4.   AdInfinitum();
    5. });
    The is valid code that will compile. It's also quite common in the wild from what I have seen.

    I completely agree. It's just a "when in Rome" kind of deal, and most Romans seem to prefer lambdas, and would probably find them more readable.
     
  18. Senshi

    Senshi

    Joined:
    Oct 3, 2010
    Posts:
    534
    @shaderop Oops, you're absolutely right! Sorry about that! As for "when in Rome", I can't say I've really seen much preference, but this is slowly turning very anectodal. =P I do agree lambdas seem to preferred for Linq stuff and the like; for others I can't say. Anyway, I think we've derailed this thread enough. ;) Thanks for correcting me though!
     
    Tommy-Core likes this.
  19. M_oenen

    M_oenen

    Joined:
    Oct 21, 2015
    Posts:
    4
    foreach(int i=0; i<_len; i++)
    {
    button.transform.name = i.toString();
    button.GetComponent<Button>().onClick.AddListener(
    () => {
    TestFn( int.parse(button.transform.name ));
    }
    );
    }

    void TestFn(int i){
    print(Time.time+" "+i);
    }
     
    Sephis and Tactical_Beard like this.
  20. shoo

    shoo

    Joined:
    Nov 19, 2012
    Posts:
    62
    It doesn't works for me! It is always last i value from loop in my delegate/action. I even tryed int _r = Random.Range(1, 100);, and still it has same value for all actions/delegates. What could I do to fix this annoying feature?
     
    Last edited: Jan 24, 2017
  21. TheWirus

    TheWirus

    Joined:
    Dec 11, 2012
    Posts:
    1
    @shoo you probably made mistake makeing declaration of " int _i = ++i; " out of the loop.

    In @Senshi example He allocate "new instance" of _i property in each step of the loop. You probably took "declaration of allocation" out of the loop and it's allocated only once and here that's where your code reference to.

    Will always return 15 when clicking
    Code (CSharp):
    1.  
    2. int b; //allocate here
    3. for(int i = 0; i<15; i++) {
    4. b = i;
    5. listOfButtons[i].GetComponent<Button>().onClick.AddListener(() => Debug.Log(b) );
    6. }
    Will return specific index when clicking
    Code (CSharp):
    1.  
    2. int b;
    3. for(int i = 0; i<15; i++) {
    4. int b = i; //allocate new "instance" EACH Step of loop
    5. listOfButtons[i].GetComponent<Button>().onClick.AddListener(() => Debug.Log(b) );
    6. }
     
    Last edited: Mar 2, 2017
    Dawar, Oxygeniium and johnnieZombie like this.
  22. shoo

    shoo

    Joined:
    Nov 19, 2012
    Posts:
    62
    @bluksPL
    No, it would be too silly mistake for me.
    I faced such problem three times already, and I always had to use wrapper-function to create scope. So I just wondering why this works for you, guys.

    Tho I am usually use coroutines instead of functions, so maybe cycles inside coroutines doesn't work this way.
     
  23. johnnieZombie

    johnnieZombie

    Joined:
    Oct 23, 2012
    Posts:
    27
    I just spent a few hours trying to solve this. Thank you for the help everyone that contributed to this thread!

    So each loop iteration it is updating every addListener with the current i value?

    This was a frustrating bug!
     
  24. AscendDev

    AscendDev

    Joined:
    Apr 28, 2017
    Posts:
    51
    Just to throw my 2 cents in here. I have used a workaround for this using the EventSystem. Each of my buttons has a unique name, so I add an Onclick method at instantiation and then inside the OnClick method, I check the EventSystem with:

    GameObject ButtonClicked = EventSystem.current.currentSelectedGameObject;
     
    johnnieZombie likes this.
  25. ShawnFeatherly

    ShawnFeatherly

    Joined:
    Feb 22, 2013
    Posts:
    45
  26. Fattie

    Fattie

    Joined:
    Jul 5, 2012
    Posts:
    436
    Note these days it is very easy ...

    assuming all the arguments line up.


    Code (CSharp):
    1. public class HandleChangeLabel : MonoBehaviour {
    2.  
    3.     InputField uiInputField;
    4.     void Start () {
    5.  
    6.         uiInputField = GetComponent<InputField>();
    7.         uiInputField.onValueChanged.AddListener(TextChanged);
    8.     }
    9.     void TextChanged(string latestValue) {
    10.  
    11.         print("text field is now " + latestValue);
    12.     }
    13. }


    The script is sitting on a UI.InputField. InputField has an event onValueChanged , which understands it has a one-string argument.

    In fact nowadays you need only do this:

    Code (CSharp):
    1. uiInputField.onValueChanged.AddListener(TextChanged);
     
    jam-slc likes this.
  27. jam-slc

    jam-slc

    Joined:
    Oct 18, 2016
    Posts:
    9
    Fattie, thanks for your post, I have been wondering if there was a way to do events with arguments without delegates or lambda... However your example only shows how to listen to events already defined with an argument.

    I looked at the source for InputField to figure out how they implement it, and got it working the same way, by subclassing a custom UnityEvent:

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Events;
    3.  
    4. public class TestUnityEventWithArgs : MonoBehaviour {
    5.  
    6.     [System.Serializable]
    7.     public class OnTestEvent : UnityEvent<string> { };
    8.  
    9.     public OnTestEvent onTestEvent = new OnTestEvent();
    10.  
    11.     void Start () {
    12.         onTestEvent.AddListener(TestEventListening);
    13.  
    14.         onTestEvent.Invoke("Test string");
    15.     }
    16.  
    17.     void TestEventListening(string stringArgument) {
    18.         Debug.Log("Event with argument: " + stringArgument);
    19.     }
    20. }
     
    alex_unity141 likes this.
  28. zerovn2102

    zerovn2102

    Joined:
    Sep 25, 2018
    Posts:
    1

    Hey, how can I pass a value of the "f" parameter to the "SomethingThatInvokesTheEvent" method
     
  29. Fattie

    Fattie

    Joined:
    Jul 5, 2012
    Posts:
    436
    hi @zerovn2102

    It is totally explained here:

    https://stackoverflow.com/a/36249404/294884

    in a post on SO by some really intelligent guy ;)

    Be aware that this is now very easy in Unity. There is a huge amount of out of date example code on the internet which causes chaos.
     
  30. Fattie

    Fattie

    Joined:
    Jul 5, 2012
    Posts:
    436
    hi @jam-slc

    yes, that's correct. it's that simple.

    It is totally explained here:

    https://stackoverflow.com/a/36249404/294884

    in a post on SO by some really intelligent guy ;)

    Be aware that this is now very easy in Unity. There is a huge amount of out of date example code on the internet which causes chaos - ignore it
     
  31. Dawar

    Dawar

    Joined:
    Dec 7, 2014
    Posts:
    124
    Thanks Bro Its Working
     
unityunity