Search Unity

Question Halting execution until a Unit reports "complete"

Discussion in 'Visual Scripting' started by PJTTG, Jan 13, 2021.

  1. PJTTG

    PJTTG

    Joined:
    Oct 4, 2018
    Posts:
    9
    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!
     
  2. mintman

    mintman

    Joined:
    Nov 28, 2013
    Posts:
    4
    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

    Code (CSharp):
    1. this.character= new ValueInput("character") // WRONG
    but the Unit class provides methods you should use instead

    Code (CSharp):
    1. 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.


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

    Code (CSharp):
    1. using System.Collections;
    2. using Unity.VisualScripting;
    3. using UnityEngine;
    4.  
    5. public class ExampleUnit : Unit
    6. {
    7.     [DoNotSerialize]
    8.     public ValueInput character { get; private set; }
    9.  
    10.     [DoNotSerialize]
    11.     public ValueInput destination { get; private set; }
    12.  
    13.     [DoNotSerialize]
    14.     [PortLabelHidden]
    15.     public ControlInput enter { get; private set; }
    16.  
    17.     [DoNotSerialize]
    18.     [PortLabelHidden]
    19.     public ControlOutput exit { get; private set; }
    20.  
    21.     protected override void Definition()
    22.     {
    23.         enter = ControlInputCoroutine("enter", RunCoroutine);
    24.         exit = ControlOutput("exit");
    25.  
    26.         character = ValueInput<Character>("character", null);
    27.         character.NullMeansSelf();
    28.  
    29.         destination = ValueInput<Vector2>("destination", Vector2.zero);
    30.     }
    31.  
    32.     private IEnumerator RunCoroutine(Flow flow)
    33.     {
    34.         var characterValue = flow.GetValue<Character>(character);
    35.         var destinationValue = flow.GetValue<Vector2>(destination);
    36.  
    37.         bool complete = false;
    38.         characterValue.MoveTo(destinationValue.x,
    39.                               destinationValue.y,
    40.                               () => complete = true);
    41.  
    42.         // you might have to add some more logic to
    43.         // detect if the character is destroyed or any special cases
    44.         yield return new WaitUntil(() => complete);
    45.         yield return exit;
    46.     }
    47. }
    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.
     
    Last edited: Jan 20, 2021
    JSymss and Havokki like this.
  3. rockrong

    rockrong

    Joined:
    Apr 2, 2019
    Posts:
    8
    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

     
  4. Trindenberg

    Trindenberg

    Joined:
    Dec 3, 2017
    Posts:
    398
    flow.StopCoroutine(bool immediately)