Search Unity

Discussion Effects, Coroutines, Templates, and UI Documents

Discussion in 'UI Toolkit' started by sunwugong, Nov 30, 2022.

  1. sunwugong

    sunwugong

    Joined:
    Sep 30, 2016
    Posts:
    4
    Hey Community!

    I have been experimenting with the UI Toolkit and I find myself still struggling to wrap my head around patterns that make sense to me for some things. I fear this may in part be because UI Toolkit is distinctly non-Unity in many ways and I am yet to push my thinking out of that box.

    Most recently I ran into this issue around templates and using coroutines to give them effects. I have a script that is meant to manage some coroutine driven effect on my template. It is designed such that each template should get a copy of it. Being a coroutine this requires some monobehavior from which to run the coroutine. A monobehavior implies a game object on which it must be parked. In this case I could have many copies of my template in a given UXML and thus need many copies of my script. As templates are not game objects, I can neither wrap the effect running script up on my templates 1:1 like I might a prefab that will be instantiated as a game object, nor can I put my coroutine running monobehaviors on the template. To complicate things, I have been trying to stay true to the design intent of UI Toolkit by having only a single game object holding my UI document.

    I "solved" this problem in two different ways, neither of which I love.

    In the first solution my template effect running script is NOT a monobehavior but still contains a public IEnumerator. I have a higher level UI management monobehavior script on my UI Document game object that constructs a copy of the script for each template instance as they are created. Each of these also have a set of Actions and public methods through which the higher level UI monobehavior can subscribe to a button hit event on the non-mono template script level then the higher level mono StartsCoroutine on the public IEnumerator in the relevant template script instance.

    The second solution is similar but the template level scripts ARE monobehaviors. When the higher level UI script instantiates the templates it adds a component of one of these scripts to itself for for each. This allows the template level scripts to fully manage their own button hits and coroutine effect execution internally, but has the down side of adding a whole lot of apparently duplicate monobehavior instances (but for the template they reference) to the UI Document game object (one per template instance). I am not sure if this is a problem, for performance or whatever, but it certainly is ugly imho.

    Two other things I considered is ditching the template level script all together and just wrapping all that functionality into the higher level UI script, or having a single more manager-like monobehavior that is created to manage this behavior on all the templates (not one each). I like both of these less.

    Thoughts? Better approaches? Thank you for the insights!
     
  2. ontrigger

    ontrigger

    Joined:
    Oct 31, 2016
    Posts:
    24
    I think your problem isn't necessarily in the foreign nature of UI Toolkit, but rather in your use of coroutines. From what I understand, you need each template to have its own update logic separate from others (like clicking on a list element, causing it to animate). In that case, why can't you just use a normal update loop? Everything that can be done in coroutines can be done better in an update loop.

    Extract your animation logic to an Update method and call it from the manager's Update method. That way you won't need to attach a behavior to each template.
     
  3. sunwugong

    sunwugong

    Joined:
    Sep 30, 2016
    Posts:
    4
    Re: Coroutines -> Update, thanks for the viewpoint! I will say though that seems like a slightly different flavor of the same issue. I think of Update loops and Coroutines as doing very similar things, just one is better for near continuous things and another for more discrete long running processes/effects. But using Update would have the same problem: Update needs a monobehavior which needs a game object and VisualElements are not game objects so I can't modularly package my effects within UI Toolkit Templates like I might be able to on Prefabs. Your suggestion to move things to the higher level manager script Update is very similar to one of my already working solutions where I let the manager run the coroutines. Your solution would totally work, but I am not sure it improves on my manager coroutine solution in terms of packaging the effects on the Template level more effectively.
     
  4. ontrigger

    ontrigger

    Joined:
    Oct 31, 2016
    Posts:
    24
    Once again, you do not need to attach a monobehavior to each template instance. All you need to do is add an Update method on the template itself and then call that from the manager behavior. That way the actual update logic is in your template and not the manager.

    Code (CSharp):
    1.  
    2. public interface IUpdatable
    3. {
    4.     void Update(float dt);
    5. }
    6.  
    7. private List<IUpdatable> myTemplatesThatImplementIUpdatable = new() {...};
    8.  
    9. private void Update()
    10. {
    11.     foreach (var updatable in myTemplatesThatImplementIUpdatable)
    12.     {
    13.         updatable.Update(Time.deltaTime);
    14.     }
    15. }
    This strategy is basically how the entire unity update loop works.
     
  5. sunwugong

    sunwugong

    Joined:
    Sep 30, 2016
    Posts:
    4
    I follow what you mean with your Update suggestion, but I don't like it. As mentioned I see this as no improvement over letting the higher level manager class run all the coroutines of the templates. It is still offloading heavy lifting from the templates themselves to the manager and expanding the responsibility of the manager more than I wish. On some level this is an "is there another way?" post.

    Your packaging of an non-monobehavior Update interface that can be slapped on non monobehavior classes that sit under an monobehavior to kind of extend update into them is interesting, though. It's just not what I am looking for. What I am saying is I WANT to put a monobehavior on a Template because then my manager can be completely dumb to any details within templates that require monobehavior functionality like Update or Coroutines.

    Using Prefabs as an example, a Prefab version of a Template could have a monobehavior template level class component packaged right on itself allowing the manager to be solely responsible for instantiating and maybe positioning the right set of prefabs but then allowing each prefab to run it's own internal effect methods, whether coroutine or update based.

    So, in summary, neat Update approach, but it is kind of more of the same in broad strokes as compared to my current solutions, at least in a manager degree of responsibility sense.
     
    Last edited: Nov 30, 2022
  6. Neutron

    Neutron

    Joined:
    Apr 25, 2011
    Posts:
    45
    You can run co-routines on any monobehaviour, doesn't need to be the one the script is attached too, nor does a script need to be attached to a monobahaviour to run a co-routine. Just pass your template a reference to any mono-behaviour when you create it, and it can use that reference to start co-routines itself without any need for the logic to bleed into outside classes. Monobehaviour could be your UI manager, or literally any monobehaviour in your scene.
     
  7. ontrigger

    ontrigger

    Joined:
    Oct 31, 2016
    Posts:
    24
    The manager's entire responsibility is described in 3 lines of code. You can't get much simpler than that.

    You've just described the solution to your problem. Simply put your monobehavior in a prefab, add the VisualTreeAsset of your template as a serialized field and assign it in the inspector. In your manager, instantiate the prefab and initialize your template-level behavior. Inside the behavior simply create an instance of your template and add it to the ui document. The resulting code should look like this:
    Code (CSharp):
    1. public class MyTemplateManager : MonoBehaviour
    2.     {
    3.         public GameObject templateViewPrefab;
    4.      
    5.         private UIDocument document;
    6.      
    7.         private void OnEnable()
    8.         {
    9.             document = GetComponent<UIDocument>();
    10.          
    11.             Instantiate(templateViewPrefab)
    12.                 .GetComponent<MyTemplateView>()
    13.                 .Init(document.rootVisualElement);
    14.         }
    15.     }
    16.  
    17.     public class MyTemplateView : MonoBehaviour
    18.     {
    19.         public VisualTreeAsset myTemplate;
    20.  
    21.         private VisualElement root;
    22.      
    23.         public void Init(VisualElement parent)
    24.         {
    25.             root = myTemplate.Instantiate();
    26.             parent.Add(root);
    27.         }
    28.     }
    Obviously, the templatemanager will deal with more than 1 template, this is just a usage example
     
  8. sunwugong

    sunwugong

    Joined:
    Sep 30, 2016
    Posts:
    4
    @Neutron, yeah I know. That is what I have been doing, letting a different monobehavior run IEnumerators on a non-mono scripts. I was just lamenting the need to do so. At this point I actually took one of my original solutions and combined it with some of @ontrigger's notions and have been making my top level UIDocument monobehavior script extend a MonoBehaviourWorker extension of MonoBehaviour I wrote which exposes methods to allow other non-mono scripts to register their own wrappers of update behavior and exposes a public method to which IEnumerators can be passed to be started as coroutines.

    @ontrigger as to your code, this is again something I am trying to avoid. It feels to me like the purpose of the new UI system in part is to greatly reduce the Scene / GameObject footprint of the UI in the inspector. You have one GO on which the UIDocument is placed, maybe have a couple other component scripts on it, and that's it. Your solution has me not doing this, but rather instantiating a GO for my template view. You are only doing one, but in my test case I have ~20 of my templates so that would be iterating through a return of a UI Query and making 20 new GOs in the scene to support those templates. If I am going to do that some of the utility of UI Toolkit as compared to UGUI seems reduced to me.

    Anyway, I appreciate everyone's thoughts so far. It still seems true to me that it it harder to truly package some behavior entirely on the template layer without involving any other scripts and objects like possible with prefabs. I was hoping I might be disabused of this notion but it seems to hold. Using the MonoBehaviourWorker approach I mentioned above does streamline out some of my gripes, though, and allows me to keep my template classes non monobehaviour so it is not too bad.
     
  9. dlorre

    dlorre

    Joined:
    Apr 12, 2020
    Posts:
    699
    I think you don't have to stick to one Monobehaviour, i've had a peek at Dragon Crashers code and they definitely have more than one. I have issues myself because I made classes to handle my ui panels that are not Monobehaviour and that blocks me sometimes, for example to display a running time, so I need to rely on another script to do that job and it makes things a bit more complicated than I would. However you can make use of the event system to trigger coroutines and updates so it's not that bad.
     
  10. dlorre

    dlorre

    Joined:
    Apr 12, 2020
    Posts:
    699
    I just realized that you can do this with Unity 2023.1:

    Code (csharp):
    1.  
    2.         private async Task displayTimeAsync(CancellationToken cancellationToken)
    3.         {
    4.             var ts = TimeSpan.FromSeconds(1);
    5.             while (!cancellationToken.IsCancellationRequested)
    6.             {
    7.                 timeLabel.text = formatTime(VideoManager.Instance.Time);
    8.                 await Task.Delay(ts, cancellationToken);
    9.             }
    10.         }
    11.  
    There is no need to call Update or even have a Monobehaviour. There is more:

    https://docs.unity3d.com/2023.1/Documentation/ScriptReference/Awaitable.html

    Here's the other version:

    Code (csharp):
    1.  
    2.         private async Awaitable displayTimeAsync(CancellationToken cancellationToken)
    3.         {
    4.             while (!cancellationToken.IsCancellationRequested)
    5.             {
    6.                 timeLabel.text = formatTime(VideoManager.Instance.Time);
    7.                 await Awaitable.WaitForSecondsAsync(1, cancellationToken);
    8.             }
    9.         }
    10.  
    Though in this case it does not make much difference I guess.
     
    Last edited: Dec 9, 2022