Halting execution until a Unit reports "complete"

Hey guys! In C# I can write a simple method for a Character class…

MoveToPosition(float X, float Y, Action onReachPosition)
…which lets a programmer tell a character to go to (X, Y), not care at all how they actually get there, and upon that character reaching the position, invoke whatever callback they assigned to onReachPosition.

I’d like to accomplish a similar thing in Bolt where a designer can place a MoveToPosition Unit coded in C#, but instead of calling an onReachPosition callback, the Unit would block the flow of the graph until it has somehow reported that it has “completed”.

Essentially, a custom Wait Unit, such as Wait Until, Wait for Seconds, etc…

I’m having trouble deducing the best way to do so. If I add a callback to the method, the onReachPosition is - understandably - treated as an Unit input rather than any sort of flow output, but there doesn’t seem to be a way to actually assign a (Custom?) Event to the input parameter.

Meanwhile, implementing it as a coroutine like one of the Wait units simply executes the coroutine and passes through with no regard for whether or not it’s finished.

What’s the proper - and most designer friendly - way to create a C# Bolt Unit that “blocks” execution until it’s completed. Bonus points if the Unit can have multiple flow exits as can be seen on the Branch Unit.

Thanks!

You’ll have to poke around a bit to find a great way to do this. It sounds like you’re up for coding a Bolt Unit, but that isn’t generally well documented.

You can do this though.

The way to code a unit is to extend from the “Unit” class.

On that Unit, you can add fields of one of four types

ValueInput
ValueOutput
ControlInput
ControlOutput

These generally require the attribute [DoNotSerialize], since the ports aren’t actually something you want to store and Bolt uses a different serialization system - they are generated by the “Define” method, and may change based upon data you actually serialize.

Speaking of which, you then have to override this protected method called “Define” and then you can implement your unit by defining the ports, their callbacks, and optionally their relationships. You assign each of the fields you’ve created. You may be tempted to use new and do something like

this.character= new ValueInput("character") // WRONG

but the Unit class provides methods you should use instead

this.character = ValueInput("character")

The ControlInput method has a Coroutine version that you should use.

If you want your unit to highlight and warn of missing ports correctly, you should use the Succession(), Requirement, and Assigns() methods. Succession says that a control input will invoke a control output. Requirement says that a control output or value output requires a value input. And finally, Assigns says that invoking a control input will assign a value on a value output. (As an aside, values can be assigned using flow.SetValue, or they can invoke a method to retrieve their value. You don’t need any control ports on your custom unit.)

Here’s some code that might get you started, or - dare I say - finished? Either way it should be a good example if you approach other situations like this.

6743380--777280--upload_2021-1-19_21-58-53.png

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

public class ExampleUnit : Unit
{
    [DoNotSerialize]
    public ValueInput character { get; private set; }
 
    [DoNotSerialize]
    public ValueInput destination { get; private set; }
 
    [DoNotSerialize]
    [PortLabelHidden]
    public ControlInput enter { get; private set; }
 
    [DoNotSerialize]
    [PortLabelHidden]
    public ControlOutput exit { get; private set; }
 
    protected override void Definition()
    {
        enter = ControlInputCoroutine("enter", RunCoroutine);
        exit = ControlOutput("exit");

        character = ValueInput<Character>("character", null);
        character.NullMeansSelf();
  
        destination = ValueInput<Vector2>("destination", Vector2.zero);
    }

    private IEnumerator RunCoroutine(Flow flow)
    {
        var characterValue = flow.GetValue<Character>(character);
        var destinationValue = flow.GetValue<Vector2>(destination);

        bool complete = false;
        characterValue.MoveTo(destinationValue.x,
                              destinationValue.y,
                              () => complete = true);

        // you might have to add some more logic to
        // detect if the character is destroyed or any special cases
        yield return new WaitUntil(() => complete);
        yield return exit;
    }
}

You’ll have to rebuild all bolt units after doing this.

It might be a good next step for you to figure out how to earn your own bonus points. Self-care and all that.

(If you’re looking for a variable number of outputs, you can define an int field marked with [Serialize] or [SerializeAs] to store it, and use it during the Define to fill in a List<ControlOutput> field.)

Other more general approaches could include coding a unit that returns an Action that uses Flow.New when invoked, but that can get hairy (certain variables and value ports will be out of scope since they weren’t assigned on the same flow), or to create a unit for running a general coroutine from an IEnumerator valueInput.

2 Likes

Thank you that I found this, I’m so inspired by your post. I wanna do the similar thing but with a stop control input which will stop the coroutine, so how to stop “RunCoroutine” in this case? Having no idea where this coroutine is running on

flow.StopCoroutine(bool immediately)