How to AddListener() featuring an argument?

// So, I can add a listener via code by method name with
button.onClick.AddListener(SomeMethodName);
// but can't include an argument?
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?

4 Likes

You can wrap it inside a delegate, like so:

button.onClick.AddListener(delegate{SomeMethodName(SomeObject);});

As for UnityActions which send along arguments, you can do something like:

public class FloatEvent : UnityEvent<float> {} //empty class; just needs to exist

public FloatEvent onSomeFloatChange = new FloatEvent();

void SomethingThatInvokesTheEvent(){
    onSomeFloatChange.Invoke(3.14f);
}

//Elsewhere:
onSomeFloatChange.AddListener(SomeListener);

void SomeListener(float f){
    Debug.Log("Listened to change on value " + f); //prints "Listened to change on value 3.14"
}
66 Likes

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+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()

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? =)

Yep. Please excuse me while I go kick myself. Works as expected, many thanks!

2 Likes

Ha, you are excused! :wink: Glad you got it work; good luck with your project! =)

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

@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:

public void SomeFunction(Selectable caller){
    Debug.Log(caller);
}

Selecting that from the EventTrigger dropdown menu, you can then just drag the UI object into the parameter slot. =)

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 :slight_smile:

EventRelay.cs

usingUnityEngine;
usingSystem.Collections;

publicclassEventRelay : BaseMonoBehaviour {

publicdelegatestringEventAction(EventMessageTypetype, Objectsender);
publicstaticeventEventActionOnEventAction;

publicenumEventMessageType {
GuiElementPicked,
ObjectCollected,
DefenseChosen,
DefenseActivated,
LoadGamePicked,
SaveGamePicked,
MainMenuScreenPicked,
UpgradeShopPicked,
SocialMenuPicked,
WeaponChosen,
WeaponFired,
InventoryChecked,
ItemAddedToInventory,
ItemRemovedFromInventory,
QuestionAnswered,
QuestionPicked,
DebugUIOnPicked,
DebugUIOffPicked,
PlayButtonPicked,
EnemyAffected
}

publicstaticstringRelayEvent(EventMessageTypemessageType, Objectsender) {
returnOnEventAction(messageType, sender);
}
}

EventSender.cs (wired up using the Editor > Inspector)

usingUnityEngine;
usingSystem.Collections;

publicclassEventSender : BaseMonoBehaviour {

publicboolmouseIsOverThis = false;
publicObjectsender;

//Updateiscalledonceperframe
voidUpdate () {
if(mouseIsOverThis) {
if(Input.GetMouseButtonDown((int)MouseUtils.Button.Left)) {
stringvalue = EventRelay.RelayEvent(
EventRelay.EventMessageType.GuiElementPicked, sender);
Debug.Log("GuiElementPicked " + value);
}
}
}
publicvoidOnMouseEnter(ObjecteventSource) {
mouseIsOverThis = true;
sender = eventSource;
}

publicvoidOnMouseClick(ObjecteventSource) {
mouseIsOverThis = true;
sender = eventSource;
}

publicvoidOnMouseExit(ObjecteventSource) {
mouseIsOverThis = false;
sender = eventSource;
}
}

EventListener.cs - added to an item that needs to handle an event

usingUnityEngine;
usingSystem.Collections;
usingSystem.Collections.Generic;

publicclassEventListener : BaseMonoBehaviour {

publicList<EventRelay.EventMessageType> eventsHandled =
newList<EventRelay.EventMessageType>();

voidOnEnable() {
EventRelay.OnEventAction += HandleEvent;
}

voidOnDisable() {
EventRelay.OnEventAction -= HandleEvent;
}

stringHandleEvent(EventRelay.EventMessageTypemessageType, Objectsender) {
if(eventsHandled.Contains(messageType)) {
Debug.Log("Handled event: " + messageType + " from sender: " + sender.ToString());
returnthis.ToString();
} else {
//ignoreevent
returnthis.ToString();
}
}
}

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!

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. =)

1 Like

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.

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. :wink:

@%#%^# Awesome!

5 Likes

Is the parameter passed into the delegate stored as a reference value?
I have this simple code :

void Start(){
     int  i = 0;
     foreach(var button in buttonCont)
     {
          button.GetComponent<Button>().onClick.AddListener(delegate { TestFn(i); });
          ++i;
     }
}

void TestFn(int i)
{
     Debug.Log("testprint" + i);
}

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”.

1 Like

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:

button.GetComponent<Button>().onClick.AddListener(() => TestFn(i));
9 Likes

@warance : @shaderop is correct in his explanation, but didn’t mention a solution. :wink: This should work:

void Start(){
     int  i = 0;
     foreach(var button in buttonCont)
     {
         int _i = ++i;
          button.GetComponent<Button>().onClick.AddListener(delegate { TestFn(_i); });
     }
}

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.

15 Likes

I don’t think that is correct. You can definitely go to town in a lambda expression, e.g:

onClick.AddListener(() => {
  Foo();
  Bar();
  AdInfinitum(); 
});

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.

6 Likes

@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. :wink: Thanks for correcting me though!

2 Likes

foreach(int i=0; i<_len; i++)
{
button.transform.name = i.toString();
button.GetComponent().onClick.AddListener(
() => {
TestFn( int.parse(button.transform.name ));
}
);
}

void TestFn(int i){
print(Time.time+" "+i);
}

2 Likes

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?