xiaozongpeng 发表于 2023-1-8 14:46

【Unity】Visual Scripting与C#脚本交互

Visual Scripting(以下简称VS)是unity的可视化编程方案,它的前身是第三方插件Bolt(Bolt的文档和社区帖子基本可以直接套用到VS)。本文不涉及VS的使用,而是介绍我在使用过程中写的一些代码,包括但不限于标题。
接下来介绍我用代码做的四件事。
参数传递

想要C#与VS交互,第一件事是变量传递给VS,以及从VS获取回来。
先放代码:
public ScriptMachine machine;


public void GetSetValue()
{
string varName = "testInt";
int testInt = 5;
var graphRef = GraphReference.New(machine, true);
var graph = Variables.Graph(graphRef);

// 获取变量
var t = graph.Get<int>(varName);
Debug.Log("get " + t);

// 赋值
graph.Set(varName, testInt);
    t = graph.Get<int>(varName);
Debug.Log("set " + t);
}
在graph中创建对应的变量


得到结果


触发自定义事件

接下来我们希望在C# 中控制VS执行一些graph里的逻辑。VS的逻辑都是靠事件驱动的,事件节点有个绿色箭头的输出,它所连接的节点才会在事件触发后被执行。新建一个graph会默认有Start和Update。创建自定义事件有两种方式:
方式1:使用VS自带的CustomEvent节点

这是最简单的方法。


触发代码:
public void TriggerEvent1()
{
CustomEvent.Trigger(gameObject, "TestEvent", 10, 15);
}
console输出25
这种方式虽然快捷,但是问题是“Arg.0”这种参数名字不方便读,如果是常用的固定逻辑每次只能复制,很麻烦。
方式2:继承事件节点,自定义事件

此方法解决了方式1的问题,但是需要写自定义节点的代码。
可以从官方文档把例子copy过来起步:
Create a Custom Scripting Event node | Visual Scripting | 1.7.8 (unity3d.com)
我做了一点点修改,贴上来:
using Unity.VisualScripting;
using UnityEngine;

//The Custom Scripting Event node to receive the Event. Add "On" to the node title as an Event naming convention.
//Set the path to find the node in the fuzzy finder as Events > My Events.
public class MyCustomEvent : EventUnit<int>
{
    // No need to serialize ports.
public ValueOutput result { get; private set; }// The Event output data to return when the Event is triggered.
protected override bool register => true;

// Add an EventHook with the name of the Event to the list of Visual Scripting Events.
public override EventHook GetHook(GraphReference reference)
    {
return new EventHook(nameof(MyCustomEvent));
    }

protected override void Definition()
    {
base.Definition();
// Setting the value on our port.
result = ValueOutput<int>(nameof(result));
    }
// Setting the value on our port.
protected override void AssignArguments(Flow flow, int data)
    {
      flow.SetValue(result, data);
    }

}
如果无法添加这个脚本,需要Project Settings -> Visual Scripting -> Regenerate Nodes


对应的触发事件代码:
public void TriggerEvent2()
{
var eventHook = new EventHook(nameof(MyCustomEvent));
EventBus.Trigger(eventHook, 3);
}
除了脚本触发,还可以在graph内触发。这是敲空格键触发事件的例子:


其中的“Send My Custom Event”需要自己写,还是可以参照文档:
Create a Custom Scripting Event Sender node | Visual Scripting | 1.7.8 (unity3d.com)
using Unity.VisualScripting;
using UnityEngine;

//Custom node to send the Event

//Setting the path to find the node in the fuzzy finder as Events > My Events.
public class SendMyEvent : Unit
{
    // Mandatory attribute, to make sure we don’t serialize data that should never be serialized.
// Hide the port label, as we normally hide the label for default Input and Output triggers.
public ControlInput inputTrigger { get; private set; }
   
public ValueInput myValue;
   
    // Hide the port label, as we normally hide the label for default Input and Output triggers.
public ControlOutput outputTrigger { get; private set; }

protected override void Definition()
    {

inputTrigger = ControlInput(nameof(inputTrigger), Trigger);
myValue = ValueInput<int>(nameof(myValue),1);
outputTrigger = ControlOutput(nameof(outputTrigger));
Succession(inputTrigger, outputTrigger);
    }

//Send the Event MyCustomEvent with the integer value from the ValueInput port myValueA.
private ControlOutput Trigger(Flow flow)
    {
EventBus.Trigger(nameof(MyCustomEvent), flow.GetValue<int>(myValue));
return outputTrigger;
    }
}
编辑时触发事件

想要在编辑时(edit mode)执行逻辑,有两种方法
方法1:使用社区扩展

RealityStop/Bolt.Addons.Community: A community-driven project for extending Unity Bolt (github.com)
其中的Manual Event是在节点上有一个触发按钮,点击就可以触发事件。忘记截图了
方法2:自己写

在自定义事件的代码(见触发自定义事件-方法2)中添加一个函数:
public void Execute(GraphReference graphReference, int data)
{
Flow flow = Flow.New(graphReference);
    flow.SetValue(result, data);
    flow.Run(trigger);
}
这个函数创建了一个Flow,去Run了trigger。Flow是graph中绿色箭头的类型,而trigger就是这个绿色箭头的实例。


触发事件部分:
public void ExecuteInEditMode()
{
var graphRef = GraphReference.New(machine, true);
MyCustomEvent eventInstance = null;
foreach (var unit in (graphRef.graph as FlowGraph).units)
    {
if (unit is MyCustomEvent)
      {
            eventInstance = unit as MyCustomEvent;
      }
    }
    eventInstance.Execute(graphRef, 40);
}
带分支的事件

和@血色双眸 的交流中了解到一个需求(他也有写VS的文章噢),希望事件节点有分支:


他写了协程版本,就是要勾选这里:


我尝试写了一个不勾选协程的版本(但是问题是勾选协程应该没用……)
看过刚才的代码我们知道,这需要两个Flow。自定义事件所继承的abstract class EventUnit<TArgs>只有一个trigger,所以我快速尝试了一把,直接把这个EventUnit<TArgs>复制出来然后改。可能还有更好的方法,欢迎在评论区告诉我(合十.jpg)
using System;
using System.Collections.Generic;
using UnityEngine;
using Unity.VisualScripting;




public abstract class BranchEventUnit<TArgs> : Unit, IEventUnit, IGraphElementWithData, IGraphEventHandler<TArgs>
{
public class Data : IGraphElementData
{
public EventHook hook;

public Delegate handler;

public bool isListening;

public HashSet<Flow> activeCoroutines = new HashSet<Flow>();
    }

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

/// <summary>
    /// Run this event in a coroutine, enabling asynchronous flow like wait nodes.
    /// </summary>

   
   
public bool coroutine { get; set; } = false;

    public ControlOutput branchA { get; private set; }
    public ControlOutput branchB { get; private set; }

    public int branchIndex;

    protected abstract bool register { get; }

protected override void Definition()
    {
isControlRoot = true;

branchA = ControlOutput(nameof(branchA));
branchB = ControlOutput(nameof(branchB));
    }

public virtual EventHook GetHook(GraphReference reference)
    {
throw new InvalidImplementationException($"Missing event hook for '{this}'.");
    }

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

if (data.isListening)
      {
return;
      }

if (register)
      {
var reference = stack.ToReference();
var hook = GetHook(reference);
Action<TArgs> handler = args => Trigger(reference, args);
EventBus.Register(hook, handler);

            data.hook = hook;
            data.handler = handler;
      }

      data.isListening = true;
    }

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

if (!data.isListening)
      {
return;
      }

// The coroutine's flow will dispose at the next frame, letting us
      // keep the current flow for clean up operations if needed
foreach (var activeCoroutine in data.activeCoroutines)
      {
            activeCoroutine.StopCoroutine(false);
      }

if (register)
      {
EventBus.Unregister(data.hook, data.handler);
            data.handler = null;
      }

      data.isListening = false;
    }

public override void Uninstantiate(GraphReference instance)
    {
// Here, we're relying on the fact that OnDestroy calls Uninstantiate.
      // We need to force-dispose any remaining coroutine to avoid
      // memory leaks, because OnDestroy on the runner will not keep
      // executing MoveNext() until our soft-destroy call at the end of Flow.Coroutine
      // or even dispose the coroutine's enumerator (!).
var data = instance.GetElementData<Data>(this);
var coroutines = data.activeCoroutines.ToHashSetPooled();

#if UNITY_EDITOR
new FrameDelayedCallback(() => StopAllCoroutines(coroutines), 1);
#else
StopAllCoroutines(coroutines);
#endif

      base.Uninstantiate(instance);
    }

static void StopAllCoroutines(HashSet<Flow> activeCoroutines)
    {
// The coroutine's flow will dispose instantly, thus modifying
      // the activeCoroutines registry while we enumerate over it
      // foreach (var activeCoroutine in activeCoroutines)
      // {
      //   activeCoroutine.StopCoroutineImmediate();
      // }
      //
      // activeCoroutines.Free();
}

public bool IsListening(GraphPointer pointer)
    {
if (!pointer.hasData)
      {
return false;
      }

return pointer.GetElementData<Data>(this).isListening;
    }

public void Trigger(GraphReference reference, TArgs args)
    {
var flow = Flow.New(reference);

if (!ShouldTrigger(flow, args))
      {
            flow.Dispose();
return;
      }

AssignArguments(flow, args);

Run(flow);
    }

protected virtual bool ShouldTrigger(Flow flow, TArgs args)
    {
return true;
    }

protected virtual void AssignArguments(Flow flow, TArgs args)
    {
    }

private void Run(Flow flow)
    {
if (flow.enableDebug)
      {
var editorData = flow.stack.GetElementDebugData<IUnitDebugData>(this);

            editorData.lastInvokeFrame = EditorTimeBinding.frame;
            editorData.lastInvokeTime = EditorTimeBinding.time;
      }

if (coroutine)
      {
            flow.StartCoroutine(GetBranch(), flow.stack.GetElementData<Data>(this).activeCoroutines);
      }
else
{
            flow.Run(GetBranch());
      }
    }

protected static bool CompareNames(Flow flow, ValueInput namePort, string calledName)
    {
Ensure.That(nameof(calledName)).IsNotNull(calledName);

return calledName.Trim()
            .Equals(flow.GetValue<string>(namePort)?.Trim(), StringComparison.OrdinalIgnoreCase);
    }

private ControlOutput GetBranch()
    {
return branchIndex == 0 ? branchA : branchB;
    }
}
复制过来后做了几件事:

[*]协程部分的代码是internal的,没法用,我直接暴力删除了
[*]输出的Flow是ControlOutput类型的,我加了ControlOutput branchA和ControlOutput branchB两个变量,以及int branchIndex来指定执行哪个分支。当然也可以写得复杂一点支持任意个分支
[*]在void Run(Flow flow)函数中根据branchIndex执行分支
接下来继承一个int类型的事件:
using Unity.VisualScripting;
using UnityEngine;

public struct BranchOutString
{
public int branch;
public string result;
}

//The Custom Scripting Event node to receive the Event. Add "On" to the node title as an Event naming convention.
//Set the path to find the node in the fuzzy finder as Events > My Events.
public class MyBranchEvent : BranchEventUnit<BranchOutString>
{

    // No need to serialize ports.
public ValueOutput result { get; private set; }// The Event output data to return when the Event is triggered.

protected override bool register => true;

protected override void Definition()
    {
base.Definition();
// Setting the value on our port.
result = ValueOutput<string>(nameof(result));
    }

// Add an EventHook with the name of the Event to the list of Visual Scripting Events.
public override EventHook GetHook(GraphReference reference)
    {
return new EventHook(nameof(MyBranchEvent));
    }

// Setting the value on our port.
protected override void AssignArguments(Flow flow, BranchOutString data)
    {
branchIndex = data.branch;
      flow.SetValue(result, data.result);
    }

}
因为只是简单改动代码,所以AssignArguments函数这里还是和原来一样,只能传进来一个参数。那么就需要在这个参数里塞入branchIndex和数据。因此,这个类的泛型是struct BranchOutString。
页: [1]
查看完整版本: 【Unity】Visual Scripting与C#脚本交互