A custom coroutine unit waits for the output to finish before continuing executing the coroutine

I’ve created a unit that use ControlInputCoroutine to create two outputs, animationStarted and animationFinished. They work as expected as long as only add normal units to the animationStarted output, but if i add a WaitForSeconds unit i run into a problem. The problem is that the Execute coroutine in my unit wait for the WaitForSeconds unit to finish until it continues to run, so if WaitForSceonds is set to ten, then it takes 10 seconds before the animationControllerValue.PlayOneShotAnimation(animationValue); line runs.

My guess the reason this happens is that when you yield return a ControlOutput that’s connected to coroutines they run the output synchronous, but is it possible to run the output asyncronous instead?

The code in question

using System.Collections;
using Unity.VisualScripting;

public class OneShotAnimationUnit : Unit
{
    [DoNotSerialize]
    public ControlInput inputTrigger;

    [DoNotSerialize]
    public ControlOutput animationStarted;
    [DoNotSerialize]
    public ControlOutput animationFinished;

    [DoNotSerialize]
    public ValueInput target;
    [DoNotSerialize]
    public ValueInput animation;

    protected override void Definition()
    {
        inputTrigger = ControlInputCoroutine("inputTrigger", Execute);

        animationStarted = ControlOutput("animationStarted");
        animationFinished = ControlOutput("animationFinished");

        target = ValueInput<AnimationController>("target");

        animation = ValueInput<int>("animation");


    }

    IEnumerator Execute(Flow flow)
    {
        var animationControllerValue = flow.GetValue<AnimationController>(target);
        var animationValue = flow.GetValue<int>(animation);

        yield return animationStarted;
        animationControllerValue.PlayOneShotAnimation(animationValue);
        while(animationControllerValue.IsOneShotAnimationRunning(animationValue))
        {
            yield return null;
        }
        yield return animationFinished;
    }
}

How it’s connected

If someone knows the answer to this I would still be glad to read it, but for now i solved it without using coroutines. Looking at the timer unit I instead used update events. Still, I think a coroutine solution would be a lot nicer, since this is a lot messier code.

My solution

using System.Collections;
using System;
using UnityEngine;
using Unity.VisualScripting;

public class OneShotAnimationUnit : Unit, IGraphElementWithData, IGraphEventListener
{
    public sealed class Data : IGraphElementData
    {
        public bool waitingForOneShotAnimationToFinish;

        public Delegate update;

        public bool isListening;

        public AnimationController animationController;

        public int animationValue;
    }

    [DoNotSerialize]
    public ControlInput inputTrigger;

    [DoNotSerialize]
    public ControlOutput animationStarted;
    [DoNotSerialize]
    public ControlOutput animationFinished;

    [DoNotSerialize]
    public ValueInput target;
    [DoNotSerialize]
    public ValueInput animation;

    protected override void Definition()
    {
        inputTrigger = ControlInput(nameof(inputTrigger), Start);

        animationStarted = ControlOutput("animationStarted");
        animationFinished = ControlOutput("animationFinished");

        target = ValueInput<AnimationController>("target");

        animation = ValueInput<int>("animation");


    }

    ControlOutput Start(Flow flow)
    {
        var data = flow.stack.GetElementData<Data>(this);

        data.animationController = flow.GetValue<AnimationController>(target);
        data.animationValue = flow.GetValue<int>(animation);

        data.animationController.PlayOneShotAnimation(data.animationValue);
        data.waitingForOneShotAnimationToFinish = true;
        return animationStarted;
    }

    private void TriggerUpdate(GraphReference reference)
    {
        using (var flow = Flow.New(reference))
        {
            Update(flow);
        }
    }

    public void Update(Flow flow)
    {
        var data = flow.stack.GetElementData<Data>(this);

        if (data.animationController == null || !data.waitingForOneShotAnimationToFinish)
            return;

        if (data.animationController.IsOneShotAnimationRunning(data.animationValue))
            return;

        flow.Invoke(animationFinished);
        data.waitingForOneShotAnimationToFinish = false;
    }


    public IGraphElementData CreateData()
    {
        return new Data();
    }

    public void StartListening(GraphStack stack)
    {
        var data = stack.GetElementData<Data>(this);

        if (data.isListening)
        {
            return;
        }

        var reference = stack.ToReference();
        var hook = new EventHook(EventHooks.Update, stack.machine);
        Action<EmptyEventArgs> update = args => TriggerUpdate(reference);
        EventBus.Register(hook, update);
        data.update = update;
        data.isListening = true;
    }

    public void StopListening(GraphStack stack)
    {
        var data = stack.GetElementData<Data>(this);

        if (!data.isListening)
        {
            return;
        }

        var hook = new EventHook(EventHooks.Update, stack.machine);
        EventBus.Unregister(hook, data.update);
        data.update = null;
        data.isListening = false;
    }

    public bool IsListening(GraphPointer pointer)
    {
        return pointer.GetElementData<Data>(this).isListening;
    }
}

Hi!

I faced a similar issue and found little documentation about it. After experimenting, I came up with this solution. While it was for a different use case, you might adapt it to your problem:

using System.Collections;
using Unity.VisualScripting;
using UnityEngine.SceneManagement;

[UnitTitle("Load Scene Async")]
[UnitCategory("Custom/Scene Management")]
public class LoadSceneAsyncNode : Unit
{
    [DoNotSerialize] private ValueInput _sceneName;
    [DoNotSerialize] public ControlInput InputTrigger;
    [DoNotSerialize] private ControlOutput _outputTrigger;
    [DoNotSerialize] private ValueOutput _progressOutput;

    private float _progress;

    protected override void Definition()
    {
        InputTrigger = ControlInputCoroutine("inputTrigger", LoadSceneAsyncCoroutine);
        _outputTrigger = ControlOutput("outputTrigger");
        _progressOutput = ValueOutput("progress01", flow => _progress);
        _sceneName = ValueInput<string>("Scene Name");
    }

    private IEnumerator LoadSceneAsyncCoroutine(Flow flow)
    {
        var sceneToLoad = flow.GetValue<string>(_sceneName);
        var task = SceneManager.LoadSceneAsync(sceneToLoad);

        task.allowSceneActivation = true;

        while (!task.isDone)
        {
            _progress = task.progress;
            yield return null;
        }

        flow.Run(_outputTrigger); // Trigger output after async operation completes
    }
}