Search Unity

How to add non-persistent UnityAction with a single parameter to a button's OnClick event?

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

  1. SweetBro

    SweetBro

    Joined:
    Jan 14, 2013
    Posts:
    69
    Title.

    As far as I can tell this is impossible since I am unallowed to pass a typed UnityAction to the AddListener method in OnClick, and I'm not allowed to replace OnClick with my own extended UnityEvent class.

    The reason why I'm doing this is because I'm creating a node-based dialog system for my game, and I want to procedurally create the choice buttons, clicking on the choice button would then call a static function and pass and int value that was assigned to it on Instantstiation to indicate which node to go to next.

    EDIT: Oh and I'm using C#, which is doubly wonderful since all the documentation related to events is written for JS.
     
  2. Senshi

    Senshi

    Joined:
    Oct 3, 2010
    Posts:
    557
    I posted this code sample yesterday which should allow you to do what you want.

    If you want to keep the buttons themselves and just remove the onClick handler, you can use UnityEvent.RemoveListener() to remove the delegate after sending your choice. =)
     
    SweetBro likes this.
  3. SweetBro

    SweetBro

    Joined:
    Jan 14, 2013
    Posts:
    69
    Thanks, I've gotten that's effectively how far I've gotten. How would I link the SomeListener or FloatEvent to the onClick handler?

    EDIT: Ef it, got frustrated and rewrote a bunch internal code to do this without passing parameters.
     
    Last edited: Sep 7, 2014
  4. Senshi

    Senshi

    Joined:
    Oct 3, 2010
    Posts:
    557
    I know you already worked around it, but in the interest of potentially cleaner code and future readers: I'm not entirely sure if I understand correctly, but (building on my previous example) you could always just invoke the action like so:

    Code (csharp):
    1. button.onClick.AddListener(SomethingThatInvokesTheEvent);
    2. //or:
    3. button.onClick.AddListener(delegate{onSomeFloatChange.Invoke(3.14f);});
    The disadvantage of the last one being that the value passed to Invoke() is captured, and thus not dynamic. I.e.:

    Code (csharp):
    1. float hp = 100f;
    2. button.onClick.AddListener(delegate{onSomeFloatChange.Invoke(hp);});
    will always send "100f" as the value, no matter what the current value of `hp` is.

    Is that what you had in mind, or am I reading into this wrongly? =)
     
    MrLucid72 likes this.
  5. SweetBro

    SweetBro

    Joined:
    Jan 14, 2013
    Posts:
    69
    Right, the issue is that you're passing the current value of hp rather than the value of hp when it's declared, in other words it behaves like a reference rather than a value. So example, if I'm creating the options in a loop and I'm passing the loop's iterator as the parameter, when the method gets called it will always equal to the max value of the iterator. Although, in retrospect this may be just an issue with delegates as whole which I partially attributed to the UnityEvent system when I was getting return values which weren't making sense, due to having what seems only to be a working understanding of delegates.
     
  6. Senshi

    Senshi

    Joined:
    Oct 3, 2010
    Posts:
    557
    If you actually want to capture a variable, you can simply use delegates for that, as their mentioned disadvantage is actually desirable here! =) I'm creating my buttons in a loop as well, and the following works as expected:

    Code (csharp):
    1. for(int i=0; i<20; i++){
    2.     Button button = Instantiate(buttonPrefab).GetComponent<Button>();
    3.     button.onClick.AddListener(delegate{callback(i);});
    4. }
    5.  
    6. void callback(int caller){
    7.     Debug.Log("Callback called by button ID " + caller);
    8. }
     
    rakkarage and SweetBro like this.
  7. Riolis

    Riolis

    Joined:
    Aug 23, 2014
    Posts:
    8
    I'm not quite sure how you did it, I'm using the almost the same code, but keep getting
    Callback called by button ID 20
    UnityEngine.Debug:Log(Object)
    for each button that I pressed.
     
  8. Senshi

    Senshi

    Joined:
    Oct 3, 2010
    Posts:
    557
    You're absolutely right. It seems I lied to you, my apologies!

    The problem is that i is actually captured, but it's a reference to i, rather than the value of i. The solution to this looks a bit ugly, but you need to make a local copy of the variable within your loop, like so:

    Code (csharp):
    1. for(int i=0; i<20; i++){
    2.     int local_i = i;
    3.     Button button = Instantiate(buttonPrefab).GetComponent<Button>();
    4.     button.onClick.AddListener(delegate{callback(local_i);});
    5. }
    6.  
    7. void callback(int caller){
    8.     Debug.Log("Callback called by button ID " + caller);
    9. }
    Sorry again for the confusion!
     
    rakkarage, nawash and Riolis like this.
  9. Riolis

    Riolis

    Joined:
    Aug 23, 2014
    Posts:
    8
    Thanks for the fix. After fiddling around, turns out what my problem is having that code in a coroutine. It works in a non coroutine method, but if you put the same exact code in coroutine, same error happens.

    Tho instead of Callback called by button ID 20, it pops out Callback called by button ID 19

    Do you have any idea why?
     
  10. Senshi

    Senshi

    Joined:
    Oct 3, 2010
    Posts:
    557
    That's rather peculiar... I wouldn't know this off the top of my head I'm afraid. I can't think of why putting this code in a coroutine would break it. Did you change anything else while it was in there?

    If you want to solve this still, feel free to post your code here, and perhaps a Debug.Log() of all local_i values as it loops through.
     
  11. Tim-C

    Tim-C

    Unity Technologies

    Joined:
    Feb 6, 2010
    Posts:
    2,225
    Needs to look like this:

    Code (csharp):
    1.  
    2. for(int i=0; i<20; i++){
    3.    Button button = Instantiate(buttonPrefab).GetComponent<Button>();
    4.     button.onClick.AddListener(delegate{int local_i = i; callback(local_i);})
    5. }
    6.  
     
    rakkarage and Riolis like this.
  12. Rom-

    Rom-

    Joined:
    Nov 26, 2008
    Posts:
    90
    Was there ever a solution for this problem?

    To be clear, the issue is that the delegate stores the local variables correctly when initialized by anything but a coroutine. However, when the listeners are initialized from within a coroutine, the local information is lost and it acts as a reference (every button has the last value of i).

    I.E. This works:
    Code (CSharp):
    1. void Start()
    2. {
    3.     for(int i=0; i<20; i++){
    4.         Button button = Instantiate(buttonPrefab).GetComponent<Button>();
    5.         button.onClick.AddListener(delegate{int local_i = i; callback(local_i);});
    6.     }
    7. }
    Each callback has a unique value of i.


    This does not work:
    Code (CSharp):
    1. IEnumerator InitializeButtons()
    2. {
    3.     for(int i=0; i<20; i++){
    4.         Button button = Instantiate(buttonPrefab).GetComponent<Button>();
    5.         button.onClick.AddListener(delegate{int local_i = i; callback(local_i);});
    6.         yield return null;
    7.     }
    8. }
    9.  
    10. void Start()
    11. {
    12.     StartCoroutine(InitializeButtons());
    13. }
    Each callback has the same value of i (19).

    Is there a fix other than reworking the initialization code to run outside of a coroutine?
     
    Last edited: Nov 18, 2014
  13. haim96

    haim96

    Joined:
    May 24, 2013
    Posts:
    107
    Hi!

    how do i apply delegate for the toggle component? i don't have OnClick for it. only onValueChanged.
    i'm adding toggles to panel with script and need to set each toggle the relevant index for "updateVehicleDisplay".

    so i tried:

    toggleScript.toggle.onValueChanged.AddListener(delegate {updateVehicleDisplay(i);});

    and i have:

    public void updateVehicleDisplay( int index){
    Debug.Log (vehicleList[index].name);
    }

    i don't get compile error, but nothing is happen...

    thanks!
     
  14. jashan

    jashan

    Joined:
    Mar 9, 2007
    Posts:
    3,307
    Doesn't work for me, when I'm having this in a method that is called by an OnClick - it's not a Coroutine (and I wouldn't know why this should make a difference). What does work for me is this (EDIT: No, it does not work :-/ ):

    Code (csharp):
    1. int counter = 0;
    2. for (int i = 0; i < addCount; i++) {
    3.     Button btn = (Button) Instantiate(buttonPrefab);
    4.     btn.transform.SetParent(buttonParent);
    5.     btn.onClick.AddListener(delegate { Callback(counter++); });
    6. }
    In other words: When I use a separate variable (not the one from the loop), it does work. When I use the loop-variable it doesn't work. When I instantiate a local variable inside the delegate with the loop-variable, it also doesn't work.

    Relevant documentation from the C# Programming Guide:

    Anonymous Methods (C# Programming Guide)

    Quote from there:

     
    Last edited: Jan 27, 2015
  15. jashan

    jashan

    Joined:
    Mar 9, 2007
    Posts:
    3,307
    After a little more research I found a way to make it work in the editor - but unfortunately this won't work for an actual build:

    Code (csharp):
    1. UnityEditor.Events.UnityEventTools.AddIntPersistentListener(
    2.     btn.onClick,
    3.     Callback,
    4.     i
    5. );

    Also, the source code for UnityEventTools isn't part of the open source UI package ... so I guess we're out of luck for now? :(
     
  16. jashan

    jashan

    Joined:
    Mar 9, 2007
    Posts:
    3,307
    No, like this:

    Code (csharp):
    1. for (int i=0; i<20; i++) {
    2.     Button button = Instantiate(buttonPrefab).GetComponent<Button>();
    3.     int local_i = i;
    4.     button.onClick.AddListener(delegate{callback(local_i);})
    5. }
    So: It needs to be a variable that is declared and assigned inside the loop but outside of the delegate-definition.
     
    Senshi likes this.
  17. haim96

    haim96

    Joined:
    May 24, 2013
    Posts:
    107
    i resolved this by adding temp var for i.
    thanks!
     
  18. cowlinator

    cowlinator

    Joined:
    Mar 15, 2012
    Posts:
    69
    +1
     
  19. nporaMep

    nporaMep

    Joined:
    Oct 31, 2014
    Posts:
    33
    I got this error also and here is what I found:
    If you have for(...) loop in Coroutine then you can't pass indexer from coroutine to a delegate.
    But - you can call a non IEnumerator function from a Coroutine which has that for(...) loop and it will work just fine.

    2 examples:
    NOT WORKING
    Code (CSharp):
    1. IEnumerator BeforeSavedCoroutine(CoroutineProgress progress, Action<TArrayElement[]> fieldSet) {
    2.         var elementsData = new TArrayElement[elementControllers.Count];
    3.  
    4.         for (int i = 0; i < elementControllers.Count; i++) {
    5.             int local_i = i;
    6.             if (elementControllers[i])
    7.                 elementControllers[i].OnBeforeSaved(progress, (val) => elementsData[local_i] = val);
    8.         }
    9.         while (!progress.IsDone)
    10.             yield return null;
    11.     }
    WORKING
    Code (CSharp):
    1. IEnumerator BeforeSavedCoroutine(CoroutineProgress progress, Action<TArrayElement[]> fieldSet) {
    2.         elementsData = new TArrayElement[elementControllers.Count];
    3.  
    4.         SaveChildren(progress);
    5.         while (!progress.IsDone)
    6.             yield return null;
    7.  
    8.         fieldSet(elementsData.Where(element => element != null).ToArray());
    9.     }
    10.     void SaveChildren(CoroutineProgress progress) {
    11.         for (int i = 0; i < elementControllers.Count; i++) {
    12.             int local_i = i;
    13.             if (elementControllers[i])
    14.                 elementControllers[i].OnBeforeSaved(progress, (val) => elementsData[local_i] = val);
    15.         }
    16.     }
     
  20. jonc113

    jonc113

    Joined:
    Mar 10, 2013
    Posts:
    22
    I ran into this same problem. Definitely a Unity Bug. Tried various permutations and like they say above, 2 things MUST be done or the value will be passed by reference:

    1) AddListener must not be in a Coroutine
    2) You must create a temp variable in the loop and not pass the loop variable

    I tried disobeying either rule and it doesn't work. Amazingly, I tried the temp variable in a coroutine (which should not exist outside the scope of the loop it was in) - and the event receives the final value of the supposedly destroyed temp variable!
     
    Marrt likes this.
  21. jacobgmartin

    jacobgmartin

    Joined:
    Sep 10, 2015
    Posts:
    37
    I concur. To add clarity to the final solution by jonc113:

    If you are in a Coroutine and you want to add a listener, you can call another *non coroutine* with the parameter of the button as an argument, and then safely do the lambda there.
     
    Marrt likes this.
  22. nporaMep

    nporaMep

    Joined:
    Oct 31, 2014
    Posts:
    33
  23. Gapa

    Gapa

    Joined:
    Dec 11, 2012
    Posts:
    27
    This should work
    Code (CSharp):
    1. private Button button;
    2.  
    3.         private void Awake()
    4.         {
    5.             UnityAction unityAction = null;
    6.             unityAction += () => Jamen1("UnityAction Jamen1");
    7.             unityAction += Jamen2;
    8.             unityAction += () =>
    9.             {
    10.                 print("UnityAction Jamen3");
    11.             };
    12.             unityAction?.Invoke();
    13.             // This clear the button.
    14.             button.onClick = new Button.ButtonClickedEvent();
    15.             // This add the UnityAction
    16.             button.onClick.AddListener(unityAction);
    17.         }
    18.  
    19.         void Jamen1(string text)
    20.         {
    21.             print(text);
    22.         }
    23.  
    24.         void Jamen2()
    25.         {
    26.             print("UnityAction Jamen2");
    27.         }
    Note: the ? is a null check.
    Code (CSharp):
    1. // Null Check
    2. unityAction?.Invoke();
    3.  
    4. // Don't Null Check
    5. unityAction.Invoke();
     
    Last edited: Jun 12, 2021