Search Unity

Method with sequence of delegates (OnStart, OnUpdate, OnComplete)

Discussion in 'Scripting' started by YannGROS, Feb 20, 2020.

  1. YannGROS

    YannGROS

    Joined:
    Apr 9, 2018
    Posts:
    15
    Hello guys !

    I'm still pretty new to Unity and I'm learning day after day... I just dicovered DOTween and I really like the way to use it with the delegates (OnStart, OnUpdate, OnComplete...) and I would love to know how to do something similar.

    Code (CSharp):
    1. myTween.OnStart(myStartFunction).OnComplete(myCompleteFunction);
    So I managed to do something which is working, but I don't like the syntax :

    Code (CSharp):
    1. public delegate void MyCallback();
    2.  
    3. public void Foo(int p1, int p2, MyCallback onStart = null, MyCallback onUpdate = null, MyCallback onComplete = null)
    4.     {
    5.         if(onStart != null)
    6.             onStart.Invoke();
    7.  
    8.         for (int i = p1; i <= p2; i++)
    9.         {
    10.             if (onUpdate != null)
    11.                 onUpdate.Invoke();
    12.         }
    13.  
    14.         if (onComplete != null)
    15.             onComplete.Invoke();
    16.     }
    17.  
    18.     void Start()
    19.     {
    20.         Foo(0, 1000, () => Debug.Log("Started"), () => Debug.Log("+1"), () => Debug.Log("Finished"));
    21.     }
    I tried to look how is DOTween working, and I managed something like this :

    Code (CSharp):
    1. public delegate void MyCallback();
    2.  
    3. public class Sequence
    4.     {
    5.         public Sequence OnStart(MyCallback onStart = null)
    6.         {
    7.             if (onStart != null)
    8.                 onStart.Invoke();
    9.  
    10.             return new Sequence();
    11.         }
    12.  
    13.         public Sequence OnUpdate(MyCallback onUpdate = null)
    14.         {
    15.             if (onUpdate != null)
    16.                 onUpdate.Invoke();
    17.  
    18.             return new Sequence();
    19.         }
    20.  
    21.         public Sequence OnComplete(MyCallback onComplete = null)
    22.         {
    23.             if (onComplete != null)
    24.                 onComplete.Invoke();
    25.  
    26.             return new Sequence();
    27.         }
    28.     }
    29.  
    30.     public Sequence Foo(int p1, int p2)
    31.     {
    32.         for (int i = p1; i < p2; i++)
    33.         {
    34.             Debug.Log($"{i}");
    35.         }
    36.  
    37.         return new Sequence();
    38.     }
    39.  
    40.     void Start()
    41.     {
    42.         Foo(0, 1000).OnStart(() => Debug.Log($"Started")).OnComplete(() => Debug.Log($"Finished"));
    43.     }
    Foo() is called, then OnStart() and then OnComplete(), which is logical. But I'm stuck here... I don't know how to make the delegates invoke when they should be invoked.

    Thanks in advance for your help !
     
  2. kdgalla

    kdgalla

    Joined:
    Mar 15, 2013
    Posts:
    4,639
    Not sure what you mean since you already said that they are executing in the correct order. If you mean that you want Foo to control when the delegates are invoked, then go back to your first listing (The one that you said you didn't like). The whole point of DOTween's sequence architecture is to allow the user to call delegates in any order they want on the fly.

    Maybe I misunderstand what you are trying to do?
     
  3. unit_dev123

    unit_dev123

    Joined:
    Feb 10, 2020
    Posts:
    989
    method chaining can be useful, but make sure it serve purpose and not just because it reads better.
     
  4. kru

    kru

    Joined:
    Jan 19, 2013
    Posts:
    452
    The concept of chaining methods together is known as a fluent interface. There are a lot of good resources to show how best to do that.

    One question I have is why you're returning a new sequence with each fluent method, but I can't say that is entirely wrong with just the code snippets provided.

    I suspect that your main issue is that all of the start/update/complete methods get called immediately, all within the same frame. This is because the Foo() method is synchronous. There are several techniques to change the method to be asynchronous. The easiest route is probably to change Foo() to start a coroutine which calls the start/update/complete methods, yielding appropriately in between.
     
  5. YannGROS

    YannGROS

    Joined:
    Apr 9, 2018
    Posts:
    15
    I would like to get my OnStart delegate invoked before the Foo() method, the OnUpdate on each frame the Foo() method is running, and the OnComplete when Foo() is done.

    Correct order, not exactly, it's not what I want, but I understand why it's in this order : at the moment Foo().OnStart().OnComplete() is just going from left to right.

    Edit :

    Yeah, coroutine is the final goal, but I try to make it work synchronously first (Seems easier for a first step).
     
    Manuel_Jerome likes this.
  6. kdgalla

    kdgalla

    Joined:
    Mar 15, 2013
    Posts:
    4,639
    Like I said, the first listing you had does what you want. You said that you didn't like the syntax, but why is that?

    You may like the syntax for DOTween better, but it seems like it does not address your problem. This whole DOTween sequence is meant to be flexible and allow you to run whatever delegates you want in whatever order.

    I'm confused because your question seems like you are saying "Hey, I already have a solution to my problem but I want this different thing to be the solution instead. I've already discovered it doesn't work so how do I force it?"

    Edit: by the way, you may be interested in UnityEvents:
    https://docs.unity3d.com/ScriptReference/Events.UnityEvent.html

    Events are a bit more flexible than delegates. Event listeners are like delegates but you can assign multiple listeners to an event. UnityEvents are very convenient becuase you can assign event listeners in the inspector, rather than code.
     
    Last edited: Feb 20, 2020
  7. YannGROS

    YannGROS

    Joined:
    Apr 9, 2018
    Posts:
    15
    That's exactly the point.

    Haha, in a way, it could be seen it's like that.
    I wish I could do the same as DOTween because I want to implement a lot of methods with that "delegate sequence / method chaining". If I could avoid doing the first method a hundred times, that would be great (I know it would work, but it's not efficient).
    Also, I'm curious and I don't know how it's done in DOTween and I'd like to know how to do it (to improve, for curiosity...).

    Thanks, I'll check that !
     
  8. kdgalla

    kdgalla

    Joined:
    Mar 15, 2013
    Posts:
    4,639
    Oh, did you mean that you want to one sequence onStart, one sequence onUpdate and another sequence onComplete? So actually three different sequences?
     
  9. YannGROS

    YannGROS

    Joined:
    Apr 9, 2018
    Posts:
    15
    Yes, exactly ! For example :
    I'd like to call the Foo() method, but before calling it I need to Initialize, while Foo is running I'd like to get an UI effect for loading and once it's done, I want to update the UI. The syntax would be :

    Code (CSharp):
    1. Foo().OnStart(()=> Initialiaze ()).OnUpdate(() => LoadingUI()).OnComplete(()=> UpdateUI())
     
  10. Errorsatz

    Errorsatz

    Joined:
    Aug 8, 2012
    Posts:
    555
    That's a fluent interface. Look up the term for more information, but the basic gist is that you create a builder object that accumulates the parameters you want and then either:
    A) Executes them with a final call
    B) Executes them in response to future events

    That's an important point - if Foo is synchronous, you'll have to wait to call it until after all your callbacks are assigned, and since the callbacks are optional that means an extra call at the end:
    Code (CSharp):
    1. Foo().OnStart(Initialize).OnUpdate(LoadingUI).OnComplete(UpdateUI).Do();
    For DoTween, update and complete happen asynchronously, so you don't need that final call. However, consider whether you need all of these as callbacks. For a synchronous method, "before" and "after" can just literally be the previous/next lines in the code:
    Code (CSharp):
    1. Initialize();
    2. Foo(LoadingUI);
    3. UpdateUI();
    And even for asynchronous ones, Initialize can just be a separate call.

    Finally, there is a trade-off for using a fluent interface - the code that provides it is going to be more complicated to read. Read, not write, because that's the more important complexity. The benefit would hopefully be that the consuming code is therefore easier to read, but analyze whether the trade-off is worth it in a given case. It isn't always, and I say this as someone who really likes the fluent aesthetic.
     
  11. YannGROS

    YannGROS

    Joined:
    Apr 9, 2018
    Posts:
    15
    Thanks ! I understand how it works synchronously !
    Okay, that's where I need to learn more. In order to do what I'd like to do, I should go asynchronously obviously ?! Let's go for it then !
    How should I proceed ? Is it still fluent interface that we should use ?

    Thank you all for your answers. I really do appreciate it !

    Edit :

    I managed to make it work with the ".Do()" solution. So I tried to make it work without it. I've got this :
    Code (CSharp):
    1.    
    2.     public class Sequence
    3.     {
    4.         private MyCallback onStart = null;
    5.         private MyCallback onUpdate = null;
    6.         private MyCallback onComplete = null;
    7.  
    8.         private MyCallback _mainFunction = null;
    9.         public MyCallback mainFunction
    10.         {
    11.             get
    12.             {
    13.                 return _mainFunction;
    14.             }
    15.             set
    16.             {
    17.                 _mainFunction = value;
    18.                 Launch();
    19.             }
    20.         }
    21.  
    22.         public Sequence OnStart(MyCallback _onStart = null)
    23.         {
    24.             onStart = _onStart;
    25.             return this;
    26.         }
    27.  
    28.         public Sequence OnUpdate(MyCallback _onUpdate = null)
    29.         {
    30.             onUpdate = _onUpdate;
    31.             return this;
    32.         }
    33.  
    34.         public Sequence OnComplete(MyCallback _onComplete = null)
    35.         {
    36.             onComplete = _onComplete;
    37.             return this;
    38.         }
    39.  
    40.         public async void Launch()
    41.         {
    42.             await Task.Delay(1);
    43.  
    44.             if (onStart != null)
    45.                 onStart.Invoke();
    46.             if (mainFunction != null)
    47.                 mainFunction.Invoke();
    48.             if (onComplete != null)
    49.                 onComplete.Invoke();
    50.         }
    51.  
    52.         public void Foo(int p1, int p2)
    53.         {
    54.             for (int i = p1; i < p2; i++)
    55.             {
    56.                 Debug.Log(i);
    57.             }
    58.         }
    59.     }
    60.  
    61.     public Sequence Foo(int p1, int p2)
    62.     {
    63.         Sequence s = new Sequence();
    64.         s.mainFunction = () => s.Foo(p1, p2);
    65.         return s;
    66.     }
    67.  
    68.     void Start()
    69.     {
    70.         Foo(0, 5).OnStart(() => Debug.Log($"Started")).OnComplete(() => Debug.Log($"Completed"));
    71.     }
    This is what I want, which is great. But I don't know if that's the way to do it properly....
     
    Last edited: Feb 21, 2020
  12. Errorsatz

    Errorsatz

    Joined:
    Aug 8, 2012
    Posts:
    555
    My explanation was a bit unclear - I didn't mean the async keyword, just the general concept of the method occurring over a span of time instead of directly when it's called. For example, DoTween uses coroutines, IIRC.

    So the sequence you have is:
    1) Foo is called - at this point we don't know what callbacks will be assigned
    2) Callbacks are assigned
    3) Now the Sequence needs to know it can run, hence Do/Run/whatever.

    In DoTween, it's:
    1) The tween starts immediately, but it doesn't yet finish
    2) OnComplete callback is assigned
    3) (later, on another frame) The tween finishes and calls OnComplete

    Even in the latter case, there's no way to assign an (optional) OnStart without an "ok, you can actually start now" call after it. If OnStart and OnComplete were mandatory, you could track when they were both assigned and start then, but that negates much of the point.

    However - do you need OnStart to be a callback? It makes sense if that "start" could happen at a later point in time, but the way you have it now it runs immediately anyway - so you could just use:

    Code (CSharp):
    1. void Start()
    2. {
    3.     Debug.Log("Started");
    4.     Foo(0, 5).OnComplete(() => Debug.Log("Completed"));
    5. }
     
  13. YannGROS

    YannGROS

    Joined:
    Apr 9, 2018
    Posts:
    15
    I tried coroutines, but it must inherit from Monobehaviour, which is not what I want. I also tried the async without calling it right away, but without any success... So iif you've got more information on how to do that, that would be great !

    Yes I'd really do !