【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(&#34;get &#34; + t);
// 赋值
graph.Set(varName, testInt);
t = graph.Get<int>(varName);
Debug.Log(&#34;set &#34; + t);
}
在graph中创建对应的变量
得到结果
触发自定义事件
接下来我们希望在C# 中控制VS执行一些graph里的逻辑。VS的逻辑都是靠事件驱动的,事件节点有个绿色箭头的输出,它所连接的节点才会在事件触发后被执行。新建一个graph会默认有Start和Update。创建自定义事件有两种方式:
方式1:使用VS自带的CustomEvent节点
这是最简单的方法。
触发代码:
public void TriggerEvent1()
{
CustomEvent.Trigger(gameObject, &#34;TestEvent&#34;, 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 &#34;On&#34; 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($&#34;Missing event hook for &#39;{this}&#39;.&#34;);
}
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&#39;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&#39;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&#39;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&#39;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 &#34;On&#34; 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]