Search Unity

Referencing a Bolt FlowMachine Macro from a ScriptableObject

Discussion in 'Visual Scripting' started by dthurn, Aug 7, 2020.

  1. dthurn

    dthurn

    Joined:
    Feb 17, 2015
    Posts:
    77
    I have a bunch of ScriptableObjects in Unity to configure different behaviors for creatures in my game, which is great because it decouples behavior from the visual representation and lets me avoid having to make hundreds of different prefab variants.

    Now that it's a semi-official part of Unity, I'm interested in augmenting this approach using Bolt visual scripting to define more behavior logic. Bolt flow machine macros *are* serialized to .asset files on disk that behave pretty similarly to ScriptableObjects, but they are in fact serialized subclasses of MonoBehaviour. I'd really like to be able to configure my ScriptableObjects by plugging different FlowMachines into them, but I don't think there's any way to do this natively -- is there a good pattern I could use to solve this problem?
     
  2. MasterSubby

    MasterSubby

    Joined:
    Sep 5, 2012
    Posts:
    252
    Bolt Flow Macros are just straight up ScriptableObjects. Not directly serialized from the machine but live on their own. Shouldn't be an issue.

    So you want to use Flow Macros as a reference and trigger them as a part of your other ScriptableObjects?

    If so yes you can do that, and as long as it's in play mode while it happens, Coroutines will also work. All you need is a reference to the macro itself. Just use a FlowMacro field and assign it manually in the inspector.

    It would go something like this:

    Code (CSharp):
    1. var reference = GraphReference.New(macro);
    2.  
    3. reference.TriggerEventHandler("CustomEvent", new CustomEventArgs(null, "My Event", parameters), true, true)
    That's it. It's a method that was originally made for OnSceneGUI Editor use, but can be used to invoke any event unit. If it's a custom event it can disregard the object field.
     
    dthurn likes this.
  3. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    374
    @JasonJonesLASM Thanks for explaining that and the code snippet. If you don't mind, I'm wondering:

    1. In my mind, the macros represent a graph. If we already have a reference to that graph (say it's a FlowMacro asset),

    Then what is the point of
    GraphReference.New(macro)
    ?
    Does that get a runtime version, or instantiate something? Or something else?


    ---


    2. Is it possible to invoke the graph without using a custom event? Just run it with some inputs and get the outputs?
    For example, a FlowMacro that looked like this:

    (This is just an example graph with inputs/outputs -- it doesn't really do anything, just a graph for the sake of example)

    upload_2020-8-7_18-47-9.png

    Or is that not possible with Bolt?
     
  4. MasterSubby

    MasterSubby

    Joined:
    Sep 5, 2012
    Posts:
    252
    Yup, that's exactly what it does. It takes an IGraphRoot. That can be a macro or a machine, or something else you implement it with. If you choose the macro, your referencing the global assets graph. If the machine, the instance on that game object.

    Yes you can do this manually. Depends on your needs, but once you create a new Flow, you can either set the port directly:

    Code (CSharp):
    1. flow.SetValue(outputPort, value)
    Or if you are using your own input style unit, get it from another field or property.

    Code (CSharp):
    1. protected override void Definition ()
    2. {
    3.     myOutput = ValueOutput("myOutput", (flow) => { return someValue; });
    4. }
    Then invoke the output control:

    Code (CSharp):
    1. flow.Invoke(someControlOutput);
    2.  
    3. Or
    4.  
    5. flow.StartCoroutine(someOutputPort);
     
    Thimo_ and ModLunar like this.
  5. Thimo_

    Thimo_

    Joined:
    Aug 26, 2019
    Posts:
    59
    Hey,

    I have a question that is similar to this. I have a UnityEvent that gets triggered as a response to another event. (from Ryan Hipple's talk) I want to have a list in the inspector with conditions that should be met before my unityevent response may trigger.

    Is it possible to fill a list in the inspector with Unity Bolt macros that consist of boolean functions (and execute those macros in code)?

    This way I should be able to check those conditions before my Unityevent is triggered.

    Is unity bolt capable to let the user use a macro for multiple situations and at the same time set different parameters in the list in the inspector?
     
    Last edited: Aug 9, 2020
  6. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    374
    @JasonJonesLASM Wow, thanks so much! I absolutely needa test with this stuff.

    @Thimo_ I know exactly what you mean from Ryan Hipple's talk and his awesome ScriptableObject architecture,
    I wish I could answer, but I've only started Bolt a few days ago haha.
    I think it's possible, I just don't know the best way to ensure the FlowMacros would have a certain signature ("must return a boolean").
     
    Thimo_ likes this.
  7. MasterSubby

    MasterSubby

    Joined:
    Sep 5, 2012
    Posts:
    252
    There is no Functions in Bolt 1 so indeed there is no guarantees signatures. You'd have to implement this yourself. There are 2 ways.

    I've done this for something I'm working on, but it certainly was difficult to setup and 100% custom. I'd get more familiar with the Unit API and develop a custom Entry and Return.

    A start for those unfamiliar: https://lifeandstylemedia.com/docs/manual/entries/bolt/tutorials/CustomUnits_Introduction.php

    Essentially my Entry unit holds a list of Return Units. When a return unit is added to the graph, it gets the entry Unit from the current graph and adds it to the entry unit return list.

    The entry unit runs through all returns and invokes returnUnit.Define() anytime the returnType on the Entry unit changes. You can do this with a property during the setter.

    Now actually doing something with the return. Since a function is happens immediately, not like a coroutine, we can expect Flow Variables to exist during the duration. When we invoke our function we do flow.variables.Set("return_callback") to send the callback through the duration of the function until we reach Return. This is an Action<object>. Heres an example method doing this in code:

    Code (CSharp):
    1. public bool ConditionMet()
    2. {
    3.     var flow = Flow.New(reference);
    4.     bool value;
    5.     flow.variables.Set("return_callback", (obj) => { value = (bool)obj; });
    6.     flow.Invoke(entryOutput);
    7.     return value;
    8. }
    That's probably too much for most people, especially just getting started, so I'd usually recommend sticking to my original reply with built in custom events and manually invoking. You can just send a callback through as an output and allow it graph access and just invoke it manually without anything special. But you lose the graph specificities of the type, even though it still can return a bool.
     
    Last edited: Aug 9, 2020
    Thimo_ and ModLunar like this.
  8. Thimo_

    Thimo_

    Joined:
    Aug 26, 2019
    Posts:
    59
    @JasonJonesLASM ,

    Thank you for your explanation!

    I do have a few more questions if you don't mind. A Unit is one block in Bolt, right? You have a Unit that holds a reference to other Units (visual blocks).

    I don't completely understand what you are doing here. Do you have a small example?

    Do you have something visual of what you mean by this?

    I'm a little bit struggling to understand the link between your custom Units and the c# code to execute the conditions.

    How do I decide that I only want a flowmacro that has the conditionUnit in the graph?
    Is it possible to add variables from code into the graph?
     
  9. MasterSubby

    MasterSubby

    Joined:
    Sep 5, 2012
    Posts:
    252
    Correct, a Unit is a node, aka one block. An Entry unit is a custom unit I made, with a List of another type of unit I made, a Return unit.

    Something like this for the return unit, which holds the returnable type in one place, and redefines the ports on the return units:

    Code (CSharp):
    1. public List<ReturnUnit> returns = new List<ReturnUnit>();
    2.  
    3. private Type _type = typeof(object);
    4. public Type type
    5. {
    6.     get { return _type; }
    7.     set
    8.     {
    9.         _type = value;
    10.         for (int i = 0; i < returns.Count; i++)
    11.         {
    12.             returns[i].Define();
    13.         }
    14.     }
    15. }
    Meanwhile in the ReturnUnit we add them to this list like such:

    Code (CSharp):
    1. protected override void OnAfterAdd()
    2. {
    3.     entry = graph.units.OfType<EntryUnit>().Single();
    4.  
    5.     if (entry != null) entry.returns.Add(this);
    6. }
    Now the two should mostly do what is necessary to do this. My only images are on something I don't plan to show off for another month or 2, so I have no images to provide currently. It should get you close to the point of how a real Function would work, at least for what we have in Bolt 1.

    Really though, none of this is necessary unless you are looking for a native like solution, which I was. You can way more easily do this:

    Code (CSharp):
    1. reference.TriggerEventHandler("Custom", new GameobjectEventArgs(null, "MyEvent", returnMethodOwnerClass, anotherParam, andAnother), true, true);
    In the graph just get a method you want to use as a return. Can be a parameter of bool, whatever. What you do with the return data is your choice from the owner types method you chose as a return.
     
    Thimo_ likes this.
  10. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    374
    Hmmmm... so

    1. Is there no way to instantiate a graph myself without a FlowMachine or StateMachine's help?
    (aka instantiate and run a new instance at runtime)

    2. How do I get the running instance (called a "Flow", right?) on a FlowMachine? I can only seem to get its graph field, which isn't helpful here.
     
  11. MasterSubby

    MasterSubby

    Joined:
    Sep 5, 2012
    Posts:
    252
    I don't really have experience with the first question. I don't know how graphs start listening properly. Since all FlowMacros are ScriptableObjects, you can start by Instantiating a copy of it. I'd look into maybe calling macro.graph.StartListening() and maybe look at the source to how that process begins, but that method is definitely a part of the process in some form.

    I'm not sure what you mean on this last question. Flow is the data that occurs from one unit to the next, the white lines and data passed through them. GraphReference is the reference to the current instance. You can create the reference to the ongoing graph with GraphReference.New(macro, true). Instead of macro as the parameter, a machine is also optional as the root. From that you can get direct info about what object self is, the active machine, original asset, and other various things I've never bothered with.
     
    ModLunar likes this.
  12. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    374
    @JasonJonesLASM

    1. Ahh gotcha, that makes sense actually. It seems the API was not built for an easy way to instantiate and run graphs without a FlowMachine (or StateMachine). I tinkered deeper with that here.

    2. Ohhhh.. thanks for describing that difference!
    This is slightly confusing naming because OOP programmers like me think "oh god, it's the new keyword, it's creating a new object, etc....".

    For others coming back to this thread, this is what I gather (correct me if I'm wrong):
    a. GraphReference.New(...) with a macro OR machine parameter does NOT create a new graph instance, just a "new reference" to the FlowMachine's already-existing one.
    b. GraphReference.New(flowMachine, true) gets a reference to the FlowMachine's currently-running graph instance.
    c. GraphReference.New(flowMacro, true) gets the graph that lies inside of a FlowMacro asset.
     
    Last edited: Sep 28, 2020
  13. MasterSubby

    MasterSubby

    Joined:
    Sep 5, 2012
    Posts:
    252
    100% on the money. References can be the same graph, but instanced in different types of roots. You wouldn't want it to effect changes live in the original variables, from the machine changes, this is how it seperate which versions of things are actually running that flow. Which graph version or instance.
     
    ModLunar likes this.
  14. Navarth

    Navarth

    Joined:
    Dec 17, 2017
    Posts:
    21
    Apologies if this has been posted elsewhere @JasonJonesLASM - now that Bolt has been integrated as part of the Unity Visual Scripting package, I can't seem to find the TriggerEventHandler method mentioned above.

    I'd like to use Bolt/VS in my project as way of authoring scripted sequences of animations and effects, that live on their own as serialized data and are referenced in an existing MonoBehaviour or SO, then passed a few variables from the game state and called by C# scripting at runtime when the sequence needs to play.

    This seems quite possible, but I'm at a loss where to look for ways to run a graph from a ScriptGraphAsset reference in a C# script, or how to pass it the data (a dictionary of values) I need.