Search Unity

why wont onClick.AddListener() accept a field?

Discussion in 'Scripting' started by Epic-Username, Sep 28, 2015.

  1. Epic-Username

    Epic-Username

    Joined:
    May 24, 2015
    Posts:
    339
    I have tried using onClick.AddListener(Method("...")); but when i try to add a field to the method it gives me errors even though it requires a field, why?

    Errors:
    error CS1502: The best overloaded method match for `UnityEngine.Events.UnityEvent.AddListener(UnityEngine.Events.UnityAction)' has some invalid arguments

    error CS1503: Argument `#1' cannot convert `void' expression to type `UnityEngine.Events.UnityAction'
    And i know for a fact that it requires a string, not left void!. which is what the error is telling me.
     
  2. blizzy

    blizzy

    Joined:
    Apr 27, 2014
    Posts:
    775
    Please post the code you are trying to use.
     
  3. Epic-Username

    Epic-Username

    Joined:
    May 24, 2015
    Posts:
    339
    this is basically the part its detecting the error:
    Code (CSharp):
    1.  
    2.     private Button PB;
    3.  
    4.     void Start () {
    5.             PB = GameObject.Find("PButton").GetComponent<Button>();
    6.             PB.onClick.AddListener(SP("..."));
    7.     }
    8.  
    9.     public void SP(string PN){
    10.        
    11.     }
    This isn't my whole code but i cut out the unnecessary parts and only left the parts that are related to the error so it would be easier to read.
     
  4. blizzy

    blizzy

    Joined:
    Apr 27, 2014
    Posts:
    775
    You are trying to pass the result of your call to SP() (which is void) to AddListener().
     
  5. Epic-Username

    Epic-Username

    Joined:
    May 24, 2015
    Posts:
    339
    The problem is its not allowing me to pass variables, in this case a string, into a method but i don't understand why its not letting me. And I'm not trying to pass the result to the button, i want the button to pass the result to the gameObject it finds when clicked, not me pass it to the button.
     
  6. blizzy

    blizzy

    Joined:
    Apr 27, 2014
    Posts:
    775
    It's what your code does currently. First it is calling SP(), then it's passing the result of SP() to AddListener(). You can't pass void, which is the result of SP().
     
  7. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,539
  8. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,334
    as @blizzy is saying, what you are doing now is calling the method SP, and sending in the result of that to the listener.

    The listener needs a method, not the return value of the method. There are two ways of doing what you're trying to achieve; either create a method that calls the method you want to call with the correct parameters:

    Code (CSharp):
    1. private Button PB;
    2.  
    3. private void Start() {
    4.     PB = GameObject.Find("PButton").GetComponent<Button>();
    5.     PB.onClick.AddListener(Foo);
    6. }
    7.  
    8. public void Foo() {
    9.     SP("...");
    10. }
    11.  
    12. public void SP(string PN) {}
    Or use a lambda expression to inline that:

    Code (CSharp):
    1. private Button PB;
    2.  
    3. private void Start() {
    4.     PB = GameObject.Find("PButton").GetComponent<Button>();
    5.     PB.onClick.AddListener(() => SP("..."));
    6. }
    7.  
    8. public void SP(string PN) {}
     
  9. Epic-Username

    Epic-Username

    Joined:
    May 24, 2015
    Posts:
    339
    I cant create another method to call the method because the method requires an input which is only generated in the start for loop i created and the () => Method("...") is not getting put on the button and throwing me a IndexOutOfRangeException when i click the button, even though there's no array or anything like that in the method.
     
  10. A.Killingbeck

    A.Killingbeck

    Joined:
    Feb 21, 2014
    Posts:
    483
    IndexOutOfRangeException has nothing to do with the lambda expression, post your current code again AND the code in the function you're trying to call.
     
  11. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,334
    Yeah, post the code. Sounds like you've run into one of C# weirdnesses, but I can't tell for sure without seeing some code.
     
  12. Epic-Username

    Epic-Username

    Joined:
    May 24, 2015
    Posts:
    339
    Code (CSharp):
    1.     public GameObject[] Parts;
    2.     private Button[] partButtons;
    3.  
    4.     public string SelectedPart;
    5.  
    6.     void Start () {
    7.         partButtons = new Button[Parts.Length];
    8.         for (int i = 0; i < partButtons.Length; i++) {
    9.             partButtons[i] = GameObject.Find(Parts[i].name.ToString() + "_Button").GetComponent<Button>();
    10.             partButtons[i].onClick.RemoveAllListeners();
    11.             partButtons[i].onClick.AddListener(() => SelectPart(Parts[i].ToString()));
    12.         }
    13.     }
    14.  
    15.     public void SelectPart(string partName){
    16.         SelectedPart = partName;
    17.     }
    I'm trying to create a list of buttons and when clicked it sets the string "SelectedPart" to the input from that method. Also don't worry about the array values there right.
     
  13. blizzy

    blizzy

    Joined:
    Apr 27, 2014
    Posts:
    775
    Read the page Baste posted. When SelectPart() is called, the variable i will always be equal to partButtons.Length.

    Also, it might be better to pass a GameObject instead of a string (and maybe just use a string at the latest second, for example for displaying text.)
     
  14. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,539
    Ah yes - gotta love closures.

    Move
    Code (csharp):
    1.  
    2. Parts[i].ToString()
    3.  
    out of the lambda.
     
    Kiwasi likes this.
  15. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,334
    That behaviour is changed in C# 5, according to the link I posted.

    One day, folks, one day.
     
    Kiwasi, blizzy and KelsoMRK like this.
  16. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,539
    According to the article it only changes it for foreach loops - so you don't have to jerk around with the enumeration pointer - but not changed in for loops (I typed "for for loops" there and thought better of it).

    Still can be confusing stuff - so I would say set up as much as you can outside of the closure so you can be absolutely sure of what's being passed in. Closing over loop variables is risky business in that regard.
     
    Kiwasi likes this.
  17. Epic-Username

    Epic-Username

    Joined:
    May 24, 2015
    Posts:
    339
    If i do that then there's no point in trying to call it because i need it to take a string as an argument.
     
  18. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    You don't need to capture the whole string if you don't want to. You do need to capture the i value.

    As in:

    Code (CSharp):
    1.     public GameObject[] Parts;
    2.     private Button[] partButtons;
    3.  
    4.     public string SelectedPart;
    5.  
    6.     void Start () {
    7.         partButtons = new Button[Parts.Length];
    8.         for (int i = 0; i < partButtons.Length; i++) {
    9.             int a = i;
    10.             partButtons[i] = GameObject.Find(Parts[i].name.ToString() + "_Button").GetComponent<Button>();
    11.             partButtons[i].onClick.RemoveAllListeners();
    12.             partButtons[i].onClick.AddListener(() => SelectPart(Parts[a].ToString()));
    13.         }
    14.     }
    15.  
    16.     public void SelectPart(string partName){
    17.         SelectedPart = partName;
    18.     }
    But if there is a possibility that the parts will move about, then you really do want to capture the whole string.

    Code (CSharp):
    1.     public GameObject[] Parts;
    2.     private Button[] partButtons;
    3.  
    4.     public string SelectedPart;
    5.  
    6.     void Start () {
    7.         partButtons = new Button[Parts.Length];
    8.         for (int i = 0; i < partButtons.Length; i++) {
    9.             string a = Parts[a].ToString();
    10.             partButtons[i] = GameObject.Find(Parts[i].name.ToString() + "_Button").GetComponent<Button>();
    11.             partButtons[i].onClick.RemoveAllListeners();
    12.             partButtons[i].onClick.AddListener(() => SelectPart(a));
    13.         }
    14.     }
    15.  
    16.     public void SelectPart(string partName){
    17.         SelectedPart = partName;
    18.     }
    This is indeed C# weirdness. There is a reason for it, to do with some complex things about the stack and how memory is managed. But the rule to remember is always capture the index before using it in a listener.
     
    phatality123, blizzy and KelsoMRK like this.
  19. Epic-Username

    Epic-Username

    Joined:
    May 24, 2015
    Posts:
    339
    Thanks for the help, this seems to work correctly.
     
  20. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,539
    It basically has to do with the fact that loop variables aren't considered part of the loop scope. Compounded by foreach loops that have an enumerator reference that changes.

    And yes - what @BoredMormon suggested is exactly what I meant - but I guess I didn't articulate it well enough :)
     
  21. Deleted User

    Deleted User

    Guest

    Thank you guys for your posts in this thread.

    Especially Blizzy for being dry, and direct, and accurate, and Baste for posting an academic example. Also thanks Mormon for the solve, though it's not for me.

    I'm implementing my first delegate-event system, and this helped me get a better understanding of delegates (and comically realize I'm just building an event system on top of another event system for no apparent reason, but hey).

    Edit: I was running into the exact same issue with closures, which while I'm sure make sense perfectly to a more experienced programmer who knows the back end better, was totally opaque to me. I observed that a similar expression outside the for loop (inside an object constructor being called inside the for loop) behaved as expected, but that int i always returned the for loop's breaking value, which was very confusing to say the least.

    I wish they had decided on including a compiler warning for this! Seems useless behavior.
     
    Last edited by a moderator: Jan 3, 2017