找回密码
 立即注册
查看: 450|回复: 0

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

[复制链接]
发表于 2023-1-8 14:46 | 显示全部楼层 |阅读模式
Visual Scripting(以下简称VS)是unity的可视化编程方案,它的前身是第三方插件Bolt(Bolt的文档和社区帖子基本可以直接套用到VS)。本文不涉及VS的使用,而是介绍我在使用过程中写的一些代码,包括但不限于标题。
接下来介绍我用代码做的四件事。
参数传递

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

[ContextMenu("Get Set Value")]
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;

[UnitTitle("On my Custom Event")]//The Custom Scripting Event node to receive the Event. Add "On" to the node title as an Event naming convention.
[UnitCategory("Events\\MyEvents")]//Set the path to find the node in the fuzzy finder as Events > My Events.
public class MyCustomEvent : EventUnit<int>
{
    [DoNotSerialize]// 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
[UnitTitle("Send My Custom Event")]
[UnitCategory("Events\\MyEvents")]//Setting the path to find the node in the fuzzy finder as Events > My Events.
public class SendMyEvent : Unit
{
    [DoNotSerialize]// Mandatory attribute, to make sure we don’t serialize data that should never be serialized.
[PortLabelHidden]// Hide the port label, as we normally hide the label for default Input and Output triggers.
public ControlInput inputTrigger { get; private set; }
    [DoNotSerialize]
public ValueInput myValue;
    [DoNotSerialize]
    [PortLabelHidden]// 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;


[SerializationVersion("A")]
[SpecialUnit]
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>
[Serialize]
    [Inspectable]
    [InspectorExpandTooltip]
public bool coroutine { get; set; } = false;

    [DoNotSerialize] public ControlOutput branchA { get; private set; }
    [DoNotSerialize] public ControlOutput branchB { get; private set; }

    [DoNotSerialize] public int branchIndex;

    [DoNotSerialize] 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;
}

[UnitTitle("On my Custom Branch Event")]//The Custom Scripting Event node to receive the Event. Add "On" to the node title as an Event naming convention.
[UnitCategory("Events\\MyEvents")]//Set the path to find the node in the fuzzy finder as Events > My Events.
public class MyBranchEvent : BranchEventUnit<BranchOutString>
{

    [DoNotSerialize]// 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。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-12-22 09:04 , Processed in 0.094074 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表