Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice
  4. Dismiss Notice

Question Concettenation of Actions

Discussion in 'Scripting' started by BulbasaurLvl5, May 6, 2021.

  1. BulbasaurLvl5

    BulbasaurLvl5

    Joined:
    May 10, 2020
    Posts:
    42
    Hey,

    so yesterday everything worked and today...

    So here is my Problem:

    class1 is there to detect if an itemslot is clicked

    Code (CSharp):
    1. public class ItemSlotBehaviour : MonoBehaviour, IPointerClickHandler
    2. {
    3.     public event Action<Item> OnShiftLeftClickEvent;
    4.  
    5.     public void OnPointerClick(PointerEventData eventData)
    6.     {
    7.         if(eventData != null && eventData.button == PointerEventData.InputButton.Left)// && Input.GetKey(KeyCode.LeftControl))
    8.         {
    9.             Debug.Log(OnShiftLeftClickEvent);
    10.             if (Item !=null && OnShiftLeftClickEvent != null)
    11.             {
    12.                 Debug.Log("ItemSLot.PointerClick");
    13.                 OnShiftLeftClickEvent(Item);
    14.             }
    15.         }
    16.     }
    the second class just know about the slots and is an intermediate for the action:

    Code (CSharp):
    1. public class StashBehaviour : MonoBehaviour
    2. {
    3.     //[SerializeField] List<Item> items = new List<Item>();
    4.     public List<Item> items = new List<Item>();
    5.     [SerializeField] Transform itemSlotsParent;
    6.     [SerializeField] ItemSlotBehaviour[] itemSlots;
    7.  
    8.     public event Action<Item> OnItemInStashShiftClickedEvent;
    9.  
    10.     private void Awake()
    11.     {
    12.         foreach(ItemSlotBehaviour isb in itemSlots)
    13.         {
    14.             //Debug.Log(isb.name);
    15.             isb.OnShiftLeftClickEvent += OnItemInStashShiftClickedEvent;
    16.             isb.OnShiftLeftClickEvent += TestAction;
    17.         }
    18.     }
    19.  
    20.     public void TestAction(Item item)
    21.     {
    22.         Debug.Log(this.name + item);
    23.     }
    So far everything works, TestAction(item) is executed just fine, tells me the item etc and Debug.Log(OnShiftLeftClickEvent); from the first class tells me there is indeed an action

    The third class now should do something

    Code (CSharp):
    1. public class InventoryBehaviour : MonoBehaviour
    2. {
    3.     [SerializeField] EquipmentBehaviour equipment;
    4.     [SerializeField] StashBehaviour stash;
    5.  
    6.     private void Awake()
    7.     {
    8.         stash.OnItemInStashShiftClickedEvent += EquipFromStash;
    9.         equipment.OnEquipmentShiftClickedEvent += UnequipFromEquipment;
    10.         stash.OnItemInStashShiftClickedEvent += TestAction;
    11.         stash.TestAction(stash.items[0]);
    12.     }
    13.  
    14.     public void TestAction(Item item)
    15.     {
    16.         Debug.Log(this.name + item);
    17.     }
    And here for some reason i cannot add a function to the second class's action, i.e. stash.OnItemInStashShiftClickedEvent += TestAction; does not add the function. If i remove TestAction from the second class (stashbehave) the first debuglog correctly tells me that the action is null. However i can execute stash.TestAction(stash.items[0]); just fine, which tells me that stash is correctly set and, thus, the action should be correctly applied.
    Also stash.OnItemInStashShiftClickedEvent += TestAction; does not show any error and it is all the same as for StashBehaviour... I can not see where things do not work.

    thanks in advance
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,780
    I feel like what you are doing is certainly 100% reasonable, so there must be some key step missing or setup missing, and I don't see it from the code above. I'm sure that adding multiple listeners to an event did not magically stop working, so I have to conclude your code flow isn't quite what you think it is.

    To help gain more insight into your problem, I recommend liberally sprinkling (EVEN MORE!) Debug.Log() statements through your code to display information in realtime.

    Doing this should help you answer these types of questions:

    - is this code even running? which parts are running? how often does it run?
    - what are the values of the variables involved? Are they initialized?

    Knowing this information will help you reason about the behavior you are seeing.

    You could also just display various important quantities in UI Text elements to watch them change as you playtest.

    If you are running a mobile device you can also view the console output. Google for how on your particular mobile target.

    Here's an example of putting in a laser-focused Debug.Log() and how that can save you a TON of time wallowing around speculating what might be going wrong:

    https://forum.unity.com/threads/coroutine-missing-hint-and-error.1103197/#post-7100494
     
  3. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,538
    You have a race condition here and the offending line is this one:

    Code (CSharp):
    1. isb.OnShiftLeftClickEvent += OnItemInStashShiftClickedEvent;
    Note that delegates are immutable classes. So you can not modify the content of a delegate (only through reflection). When you "add" a method or delegate to a multicast delegate, you essentially create a new delegate with the Delegate.Combine method.

    That means when the offending line is executed, you combine the delegate in OnShiftLeftClickEvent with the the delegate that is currently stored in OnItemInStashShiftClickedEvent into a new delegate which will replace the old one in OnShiftLeftClickEvent. That means if you later change / replace the delegate "OnItemInStashShiftClickedEvent" it won't affect what is stored in your "OnShiftLeftClickEvent".

    What you can do is use a closure or an explicit calling method like this:

    Code (CSharp):
    1. public event Action<Item> OnItemInStashShiftClickedEvent;
    2. private void InvokeItemInStashShiftClickedEvent(Item item))
    3. {
    4.     OnItemInStashShiftClickedEvent(item);
    5. }
    6.  
    Now in your subscription code, you subscribe this method and not the delegate from the event field. So now you would do

    Code (CSharp):
    1. isb.OnShiftLeftClickEvent += InvokeItemInStashShiftClickedEvent;
    This should work as you had intended.
     
  4. BulbasaurLvl5

    BulbasaurLvl5

    Joined:
    May 10, 2020
    Posts:
    42
    Hey,

    thanks for taking your time to look over the code. I did sprinkle Debug.logs around and that is the reason i came to the conclusion that the thirds class function is not added to the seconds class action. I did try to add further logs, but things like class3{debug.log(class2.action)} are forbidden

    i rewatched the tutorial (min 7 - min9) and he literally did the same thing. I also added new actions to the classes with minimal code and it is the same result. Other than that i do not have any idea how i could miss some set up, as the the only set up involved is the [Serializefiled] stash, which i checked about 10times by now

    ok, give a moment to understand this[/QUOTE]
     
  5. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,380
    [edit]Looks like while I was typing this up Bunny83 basically said the same thing I'm saying here[/edit]

    OK... I'm not fully understanding what exactly you're trying to describe.

    BUT

    I do notice a huge issue in your code:
    Code (csharp):
    1. public class StashBehaviour : MonoBehaviour
    2. {
    3.     //[SerializeField] List<Item> items = new List<Item>();
    4.     public List<Item> items = new List<Item>();
    5.     [SerializeField] Transform itemSlotsParent;
    6.     [SerializeField] ItemSlotBehaviour[] itemSlots;
    7.  
    8.     public event Action<Item> OnItemInStashShiftClickedEvent;
    9.  
    10.     private void Awake()
    11.     {
    12.         foreach(ItemSlotBehaviour isb in itemSlots)
    13.         {
    14.             //Debug.Log(isb.name);
    15.             isb.OnShiftLeftClickEvent += OnItemInStashShiftClickedEvent; //RIGHT HERE
    16.             isb.OnShiftLeftClickEvent += TestAction;
    17.         }
    18.     }
    This line of code is an issue. You're adding your event OnItemInStashShiftClickedEvent to isb.OnShiftLeftClickEvent during Awake.

    Here's the thing the "event" keyword is effectively just an access modifier (there's a little more to it... but you can just think of it as one). It basically says "only the owning class can call this delegate or set it, and anyone else can add/remove delegates to its multicast list (as long as the public/private/internal is respected)".

    So when you say:
    Code (csharp):
    1. isb.OnShiftLeftClickEvent += OnItemInStashShiftClickedEvent;
    I get the feeling you believe this would forward OnShiftLeftClickEvent to OnItemInStashShiftClickedEvent. If more listeners are added to OnItemInStashShiftClickedEvent, then OnShiftLeftClickEvent would continue forwarding.

    But that's NOT what will happen.

    When you say:
    Code (csharp):
    1. isb.OnShiftLeftClickEvent += OnItemInStashShiftClickedEvent;
    Really what you're doing is adding the current multicast list that is added to OnItemInStashShiftClickedEvent at time of += will be added.

    This is an issue since you're doing this in Awake. So over in InventoryBehaviour:
    Code (csharp):
    1. public class InventoryBehaviour : MonoBehaviour
    2. {
    3.     [SerializeField] EquipmentBehaviour equipment;
    4.     [SerializeField] StashBehaviour stash;
    5.  
    6.     private void Awake()
    7.     {
    8.         stash.OnItemInStashShiftClickedEvent += EquipFromStash; //HERE
    9.         equipment.OnEquipmentShiftClickedEvent += UnequipFromEquipment;
    10.         stash.OnItemInStashShiftClickedEvent += TestAction; //AND HERE
    11.         stash.TestAction(stash.items[0]);
    12.     }
    You're adding these to StashBehaviour. But that doesn't mean that when ItemSlotBehaviour.OnShiftLeftClickEvent raises these will necessarily raise.

    It hinges on WHEN Awake is called. If InventoryBehaviour has Awake called first... it will be forwarded on. But if Awake is called after it will not. And the order of Awake isn't guaranteed (unless you explicitly set the execution order and both objects exist at load).

    To put it more simply try this:

    Code (csharp):
    1. public class Test : MonoBehaviour
    2. {
    3.  
    4.     void Start()
    5.     {
    6.             var a = new Foo();
    7.             var b = new Bar();
    8.             b.Blargh += () => Debug.Log("DID IT EARLY");
    9.             b.Register(a);
    10.             b.Blargh += () => Debug.Log("DID IT LATE");
    11.  
    12.             a.DoBlargh();
    13.     }
    14.  
    15.     class Foo
    16.     {
    17.         public event Action Blargh;
    18.  
    19.         public void DoBlargh()
    20.         {
    21.             Blargh?.Invoke();
    22.         }
    23.     }
    24.  
    25.     class Bar
    26.     {
    27.         public event Action Blargh;
    28.  
    29.         public Bar()
    30.         {
    31.  
    32.         }
    33.  
    34.         public void Register(Foo foo)
    35.         {
    36.             foo.Blargh += Blargh;
    37.         }
    38.     }
    39.  
    40. }
    If you ran this you will notice that "DID IT EARLY" will display, but "DID IT LATE" will NOT display. Because of the order that you added the events.
     
    Bunny83 likes this.
  6. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,380
    I will point out... tutorials aren't necessarily bullet proof. They can often contain bugs. It's one of the big reasons I'm not a fan of tutorials... especially ones from unofficial sources.

    This even goes for us here on the forums. We can only offer the knowledge we have and we too can make mistakes. It's why I suggest never copying pasting code from other people especially if you don't understand what the code they're handing you does. Especially if you're attempting to learn.
     
    Bunny83 and Kurt-Dekker like this.
  7. BulbasaurLvl5

    BulbasaurLvl5

    Joined:
    May 10, 2020
    Posts:
    42
    Hey, i also do not totally understand what your saying :) but i think i can boil it down to this? If i understand that correctly i already had the same thought and did put the last function addition into start (which should be called after awake) and also in update (because i was desperate), with no change in results

    €: but yeah the idea i this: create action -> add action2 -> add function to action2, thus, function is called when action1 is called
     
  8. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,380
    Changing the order to Start is not going to fix what is effectively code smell.

    This:
    Code (csharp):
    1.     public event Action<Item> OnItemInStashShiftClickedEvent;
    2.  
    3.     private void Awake()
    4.     {
    5.         foreach(ItemSlotBehaviour isb in itemSlots)
    6.         {
    7.             //Debug.Log(isb.name);
    8.             isb.OnShiftLeftClickEvent += OnItemInStashShiftClickedEvent; //RIGHT HERE
    This should never be done. It's just bad code on the part of who wrote the tutorial.

    If you want forwarding of event's Bunny83 is offers up a way to forward events.

    ...

    As to if this is your problem... no idea. I don't fully understand what your problem is. You say:
    What do you mean by "I cannot add a function to the second class's action"? Are you getting an error? Is it not behaving the way you expect? What is happening? I believe you're saying it's not behaving the way you expect and the code Bunny83 and I pointed to is a potential cause of it because that specific line of code is unintuitive and generally just bad code on the part of the tutorial writer.

    Instead of using arbitrary language (and this is why I gave example code at the end of my post to try to demonstrate what it is I was explaining)... describe what you expect as results, and what you get as results.

    "I Expect the message log to say 'a,b,c'; but instead I get 'a,c'."
     
  9. BulbasaurLvl5

    BulbasaurLvl5

    Joined:
    May 10, 2020
    Posts:
    42
    Bunnys solution did the job, but i do not understand why. If you guys would bear with a bit longer i would be again thankful.

    I'll try to explain with some pseudo code

    What i want is one class that has an action
    Code (CSharp):
    1. class1
    2. {
    3. action1
    4. }
    a second class with another action, that "adds" to the first action
    Code (CSharp):
    1. class2
    2. {
    3. action2
    4.  
    5. class1.action1 += action2 //you guys pointed out that this is the problem
    6. }
    to finally add a function from a third class
    Code (CSharp):
    1. class3
    2. {
    3. function3
    4.  
    5. class2.action2 += function3
    6. }
    what i did expact was: i call action1 and through "chain execution" function3 is executed. What did happen was, that unity told me that action1 is null.

    Bunnys solution is to wrap action2 into another function
    Code (CSharp):
    1. class2
    2. {
    3. action2
    4. functionInvokeAction2
    5.  
    6. class1.action1 += functionInvokeAction2 //this seems to solve the race condition
    7.  
    8. }
    However the part that i do not understand is this:

    The part i do not understand is "...When you "add" a method or delegate...", i would expect that i does not matter if i add a delegate (before) or a function (now) but apparently it does.

    If it would make a difference, this part would make sense to me:

    My take-home-message would be: Do not add a delegate to another delegate

    € my guess would be: the "race" is if class3 manages to add function3, before the delegates are merged. If it is to late, than the delegates are already merged and a function added to action2 will not be handed to action1 (which is a new delegate by now). Thus, by moving function3 assignment to start, should make the chain not work, which happened. Then also moving class1.action1 += action2 into start should "solve" the issue, because the delegates will only be merged after function3 is added, which i just tried and also "solved" the issue

    €€ awake should be executed in order of the parent objects in the editor right? Then, everything should have worked out, because the first thing that should be done is class3 execution. maybe that is what you meant with execution order is not set
     
    Last edited: May 6, 2021