0
Answered

Bolt 1.4.0 event units

Guavaman 5 months ago • updated 4 days ago 11

I'm trying to convert some custom event units from Bolt 1.3.0 to 1.4.0. The new changes to the API broke the old ones. I've mostly got it working (I think), but I cannot find a way to get the underlying object from the ValueInput anymore. This is important because I'm trying to subscribe to an event in an object instance, not a static event. You can see the code below.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Ludiq;
using global::Bolt;
// Just a test class that exposes an event.
public class Test : MonoBehaviour {
    public event Action<TestArgs> SuperEvent;
    void Update() {
        if(Input.GetKeyDown(KeyCode.Alpha1)) {
            Debug.Log("Triggering Test.SuperEvent");
            SuperEvent(new TestArgs() { value = true });
        }
    }
}
// Some args for the event.
public class TestArgs {
    public bool value;
}
// The Bolt event unit
[UnitCategory("Events")]
[UnitShortTitle("OnSuperEvent")]
[UnitSurtitle("SuperEvent")]
[UnitTitle("SuperEvent")]
public sealed class TESTEVENT : GlobalEventUnit<TestArgs> {
    [DoNotSerialize, PortLabel("SuperEvent")]
    public ValueInput input_SuperEvent { get; private set; }
    [DoNotSerialize, PortLabelHidden]
    public ValueOutput output { get; private set; }
    protected override string hookName { get {  return "Test.SuperEvent"; } }
    private uint _uniqueId;
    private string _hookName;
    private TestArgs _temp_eventArgs;
    private Test _temp_TEST;
    private Action<TestArgs> __eventDelegate;
    protected override void Definition() {
        base.Definition();
        this.input_SuperEvent = base.ValueInput<TESTEVENT>("input_SuperEvent");
        this.output = base.ValueOutput<TestArgs>("output", _GetTempEventArgs);
        this.__eventDelegate = _OnBoltEventReceived;
    }
   
    public override void StartListening(GraphStack graphStack) {
        this._temp_TEST = input_SuperEvent.GetValue<Test>(); // <-- This no longer works and I cannot find any way to get it anymore
        if(this._temp_TEST != null) this._temp_TEST.SuperEvent += _OnEventReceived;
        EventBus.Register<TestArgs>(new EventHook(hookName, this), __eventDelegate);
        base.StartListening(graphStack);
    }
    public override void StopListening(GraphStack graphStack) {
        if(this._temp_TEST != null) this._temp_TEST.SuperEvent -= _OnEventReceived;
        EventBus.Unregister(hookName, __eventDelegate);
        base.StopListening(graphStack);
    }
    private TestArgs _GetTempEventArgs(Flow flow) {
        return _temp_eventArgs;
    }
    private void _OnEventReceived(TestArgs args) {
        this._temp_eventArgs = args;
        // This will trigger the event in Bolt and continue flow
        EventBus.Trigger<TestArgs>(new EventHook(hookName, this), args);
    }
    // This is called by EventBus.Trigger
    private void _OnBoltEventReceived(TestArgs args) {
        // nothing to do in here
        Debug.Log("OnBoltEventReceived");
    }
}
Bolt Version:
Unity Version:
.NET Version:
GOOD, I'M SATISFIED
Satisfaction mark by Guavaman 5 months ago
Answered

Don't have much time to explain it sorry, but here's a code that should work!

As I said, the approach changed a lot in 1.3. In the future, we're looking into making hooking into C# events like this an automatic process auto-detected by Bolt.

https://gist.github.com/lazlo-bonin/ee2dd7f48e7562e65c383fb10f9c49f6

No need to explain, I can follow it. It should be all I need. Thanks a lot! And I totally understand being swamped by support. :P

I'm getting a GraphPointerException every time I run it from the line var data = stack.GetElementData<Data>(this);

GraphPointerException: Graph element data type mismatch. Found Bolt.EventUnit`1+Data[TestArgs], expected TestEventUnit+Data.[ GameObject ]

Ludiq.GraphPointer.GetElementData[Data] (IGraphElementWithData element)
TestEventUnit.StartListening (Ludiq.GraphStack stack) (at Assets/TestComponent.cs:59)
Bolt.FlowGraph.StartListening (Ludiq.GraphStack stack)
Bolt.FlowMachine.OnEnable ()

Geavaman, would be happy to see ur final result

+2

Okay, I found the answer. There was a missing method override in Lazlo's example:

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

This creates the data object which is used to hold the ValueInput target.

The final example:

using System;
using Bolt;
using Ludiq;
using UnityEngine;
public class TestComponent : MonoBehaviour
{
    public event Action<TestArgs> testEvent;
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Alpha1))
        {
            Debug.Log("Triggering Test.testEvent");
            if (testEvent != null)
            {
                testEvent(new TestArgs { testArg = true });
            }
        }
    }
}
public class TestArgs
{
    public bool testArg;
}
[UnitCategory("Events")]
public sealed class TestEventUnit : EventUnit<TestArgs>
{
    public new class Data : EventUnit<TestArgs>.Data
    {
        public TestComponent testComponent;
    }
    [DoNotSerialize]
    private ValueInput _testComponent;
    [DoNotSerialize]
    private ValueOutput _testArg;
    [DoNotSerialize]
    public ValueInput testComponent { get { return _testComponent; } }
    [DoNotSerialize]
    public ValueOutput testArg { get { return _testArg; } }
    protected override bool register { get { return false; } }
    protected override void Definition()
    {
        base.Definition();
        _testComponent = ValueInput<TestComponent>("_testComponent");
        _testArg = ValueOutput<TestArgs>("_testArg");
    }
    public override IGraphElementData CreateData() {
        return new Data();
    }
    public override void StartListening(GraphStack stack)
    {
        var data = stack.GetElementData<Data>(this);
        if (data.isListening)
        {
            return;
        }
        var reference = stack.ToReference();
        var testComponent = Flow.FetchValue<TestComponent>(this.testComponent, reference);
        if (testComponent == null)
        {
            return;
        }
        
        Action<TestArgs> handler = args => Trigger(reference, args);
        testComponent.testEvent += handler;
        data.testComponent = testComponent;
        data.handler = handler;
        data.isListening = true;
    }
    public override void StopListening(GraphStack stack)
    {
        var data = stack.GetElementData<Data>(this);
        if (!data.isListening)
        {
            return;
        }
        if (data.testComponent != null)
        {
            data.testComponent.testEvent -= (Action<TestArgs>)data.handler;
        }
        data.isListening = false;
    }
    protected override void AssignArguments(Flow flow, TestArgs args)
    {
        base.AssignArguments(flow, args);
        flow.SetValue(testArg, args);
    }
}

D'oh! My bad. Glad you got it working! Let me know when the Rewired integration is updated to 1.4, I know a lot of Bolt users are counting on it :)

Hi,

It's already out on the Unity Asset Store today. Thanks again for your help!

I've received reports that a new update to Bolt has broken the Rewired integration. Upon investigating, I've found that the previous code, while it still compiles, triggers a MissingMethodException in EventUnit.cs

Exception:

System.MissingMethodException: Method not found: 'Bolt.Flow.SetValue'.
at Bolt.EventUnit`1[Rewired.InputActionEventData].Trigger (Ludiq.GraphReference reference, InputActionEventData args) [0x00018] in C:\Users\lazlo\Projects\Bolt1\Package\Bolt.Flow\Runtime\Framework\Events\EventUnit.cs:121
at Rewired.Integration.Bolt.Units.StaticEventUnitBase`1+<>c__DisplayClass9_0[Rewired.InputActionEventData].b__0 (InputActionEventData args) [0x00000] in :0
at ZDmUewXFlcwHowUcNkMxkwjgpHg.PPEWftyFTqDHeNdKrHROJxFKoeL (.QybpWQNrrVAXgLLadtCwdKLyFIA , UpdateLoopType ) [0x00000] in :0

Tracing this to my code:

public abstract class StaticEventUnitBase<TEventArgs> : EventUnit<TEventArgs> {

        public new class Data<TEventArgs> : EventUnit<TEventArgs>.Data {
        }

        /// <summary>
        /// The event data.
        /// </summary>
        [DoNotSerialize, PortLabelHidden]
        public ValueOutput output { get; private set; }

        protected override bool register { get { return false; } }

        protected override void Definition() {
            base.Definition();
            this.output = base.ValueOutput<TEventArgs>("output");
        }

        public override IGraphElementData CreateData() {
            return new Data<TEventArgs>();
        }

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

            if (data.isListening) {
                return;
            }

            var reference = stack.ToReference();

            // THIS IS THE LINE THAT TRIGGERS THE EXCEPTION
            // The call to Trigger(reference, args) in turn calls Bolt.Flow.SetValue which
            // apparently no longer exists.
            Action<TEventArgs> handler = args => Trigger(reference, args);
            SubscribeEvent(handler, reference);
            data.handler = handler;
            data.isListening = true;
        }

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

            if (!data.isListening) {
                return;
            }

            UnsubscribeEvent((Action<TEventArgs>)data.handler);

            data.isListening = false;
        }

        protected override void AssignArguments(Flow flow, TEventArgs args) {
            base.AssignArguments(flow, args);
            flow.SetValue(output, args);
        }

        protected abstract void SubscribeEvent(Action<TEventArgs> handler, GraphReference reference);
        protected abstract void UnsubscribeEvent(Action<TEventArgs> handler);
    }


The exact line of code causing this is in the StartListening method:

// THIS IS THE LINE THAT TRIGGERS THE EXCEPTION
// The call to Trigger(reference, args) in turn calls Bolt.Flow.SetValue which
// apparently no longer exists.
Action<TEventArgs> handler = args => Trigger(reference, args);
In order to get the Bolt integration functioning again with Bolt 1.4.0f11, something needs to be changed, but I do not know what. Thanks!

After more investigation, the signature of the Bolt.Flow class has changed at some point between 1.4.0 and 1.4.0f11:

Old:
public void SetValue(ValueOutput output, object value);

New:
public void SetValue(IUnitValuePort port, object value)

Even though this method is never called by Bolt.EventUnit<>.Trigger, the mere use of the Flow class within this method is causing the MissingMethodException because the Rewired_Bolt_Runtime.dll was compiled against the older Bolt.Flow.Runtime.dll which had a different signature for the Flow class compared to the current version. I have never seen something like this happen when using managed dependencies. I was not aware the referring library stored method signatures for classes it references and would throw an exception if something in that signature didn't match even if it wasn't called... I can see that the only way to handle this situation is to recompile Rewired_Bolt_Runtime.dll with every single new release of Bolt.

Hi Guavaman, sorry about your troubles here. I had to make that API change to support input caching and I didn't think it would cause any problem since ValueOutput inherits from IUnitValuePort.

This kind of stuff is precisely why I don't mark the API as stable or public yet, in case we need to change internals across versions. 

Bolt 2 will be distributed as source files, so this means your approach for integration would change. I'm guessing you would have to release your integration as source files that expect both Rewired and Bolt in the project. In that case, the change from f10 to f11 would have been seamless, though!

Thank you so much for maintaining the integration up to date, Rewired is a fantastic asset :)

Hi Lazlo,

I understand. It's tough making systems that have to interface with so many other systems.

That's probably a good idea to just change my integration to use source code files instead of a DLL. That would have survived this update since it would be recompiled by Unity. I'll look into doing that.

Thanks! Great job on Bolt!