简介

About Visual Scripting | Visual Scripting | 1.8.0

教程

安装

  1. 可通过Window->Package Manager,搜索Visual Scripting。

Unity3D的可视化编程工具:VisualScripting - 图1

  1. 在Editor -> Project Settings -> Visual Scripting -> Initialize Visual Scripting

Unity3D的可视化编程工具:VisualScripting - 图2

首次在项目中使用可视化脚本时,必须选择初始化可视化脚本。初始化 Visual Scripting 以解析 Visual Scripting 节点库的所有程序集和类型。初始化可视化脚本后,重新生成节点库。请参阅下面的重新生成节点。

Unity3D的可视化编程工具:VisualScripting - 图3

  1. 在Editor->Preferences->Visual Scripting中,可以调整视图参数。

详细参数可查看手册

窗口元素

Graph Editor 图表编辑器,可以在其中创建、排列和链接节点

Unity3D的可视化编程工具:VisualScripting - 图4

fuzzy finder 模糊查找器,可以使用它来查找节点并将它们添加到图表中

Unity3D的可视化编程工具:VisualScripting - 图5

Graph toolbar 图表工具栏,可以在图表编辑器中更改特定于视图的设置并执行一些布局操作

Unity3D的可视化编程工具:VisualScripting - 图6

Graph Inspector 图表视窗,可以在其中查看关节点的详细信息并为图形配置其他设置

Unity3D的可视化编程工具:VisualScripting - 图7

Blackboard 黑板,可以在其中定义和编辑变量

Unity3D的可视化编程工具:VisualScripting - 图8

图表类型

在Assets中右键Create->Visual Scripting

Unity3D的可视化编程工具:VisualScripting - 图9

可视化脚本有两种类型:脚本图和状态图

Script Graph 脚本图

脚本图表控制并连接特定的操作和值。脚本图中的操作按特定顺序发生。动作可以在每一帧发生,或者在特定事件发生时发生。

可视化脚本通过节点表示脚本图中的操作。将节点与边连接在一起,告诉您的应用程序要执行什么操作以及按什么顺序执行。

脚本图可以访问大量节点,这些节点对应于 Unity 编辑器中的不同特性和功能。通过模糊查找器访问这些节点。

脚本图定义了应用程序运行时游戏对象执行的操作的细节。

Unity3D的可视化编程工具:VisualScripting - 图10

State Graph 状态图

状态图具有状态,并给出应用程序通过称为转换的连接在状态之间移动时的逻辑。使用状态图设计 AI 行为或定义场景和关卡结构。

状态图中的状态和转换告诉您的应用程序何时根据事件或在满足条件后更改其行为。

Unity3D的可视化编程工具:VisualScripting - 图11

组件

Script Machine

Unity3D的可视化编程工具:VisualScripting - 图12

使用ScripGraph.asset文件

State Machine

Unity3D的可视化编程工具:VisualScripting - 图13

使用StateGraph.asset文件

添加自定义数据

添加数据类型或程序集

创建好脚本后,可在Editor->Project Settings->Visual Scripting->Generate Nodes->Type Options中添加脚本,再点击Regenerate Nodes生成中间脚本

需要注意,当脚本类重构类名或方法名后,在Graph界面中不会刷新,需要通过特性[RenamedFrom(“”)]标记。

Unity3D的可视化编程工具:VisualScripting - 图14

Unity3D的可视化编程工具:VisualScripting - 图15

自定义节点

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using Unity.VisualScripting;
  5. public class MyNode : Unit
  6. {
  7. #region 定义流程的输入和输出
  8. [DoNotSerialize] //标记不需要序列化端口
  9. public ControlInput inputTrigger;//定义输入端口
  10. [DoNotSerialize] //标记不需要序列化端口
  11. public ControlOutput outputTrigger;//定义输出端口
  12. #endregion
  13. #region 添加端口(ports)以允许该节点发送和接收数据或触发脚本图中的其他节点
  14. [DoNotSerialize]
  15. public ValueInput myValueA;//输入的数据类型
  16. [DoNotSerialize]
  17. public ValueInput myValueB;//输入的数据类型
  18. [DoNotSerialize]
  19. public ValueOutput result;//输出的数据类型
  20. private string resultValue;
  21. #endregion
  22. #region 添加逻辑(logic)以告知 Visual Scripting 节点如何处理从其端口接收的任何数据。
  23. #endregion
  24. #region 在向节点添加端口和添加逻辑后,关系有助于可视化脚本在脚本图中正确显示自定义 C# 节点。
  25. #endregion
  26. //添加节点时,对此节点的规范(初始化)
  27. protected override void Definition()
  28. {
  29. //定义输入
  30. inputTrigger = ControlInput("InputTrigger", (flow) =>
  31. {
  32. resultValue = flow.GetValue<string>(myValueA) + flow.GetValue<string>(myValueB) + "!!!";
  33. return outputTrigger;
  34. });
  35. //定义输出
  36. outputTrigger = ControlOutput("outputTrigger");
  37. //定义数据的输入和输出
  38. myValueA = ValueInput<string>("myValueA", "Hello");
  39. myValueB = ValueInput<string>("myValueB", string.Empty);
  40. result = ValueOutput<string>("result", (flow) => { return resultValue; });
  41. //定义节点关系 $RelationType$($Port1$, $Port2$)$RelationType$$Port1$$Port2$
  42. Requirement(myValueA, inputTrigger);//指定在运行此节点前需要myValueA
  43. Requirement(myValueB, inputTrigger);//指定在运行此节点前需要myValueB
  44. Succession(inputTrigger, outputTrigger);//输出端口与输入端口链接
  45. Assignment(inputTrigger, result);//指定在触发 inputTrigger 时将数据写入结果字符串输出。
  46. }
  47. }

Unity3D的可视化编程工具:VisualScripting - 图16

自定义节点Inspector面板

  1. using System;
  2. using Unity.VisualScripting;
  3. using UnityEngine;
  4. [Descriptor(typeof(MyNode))]
  5. public class MyNodeDescriptor : UnitDescriptor<MyNode>
  6. {
  7. public MyNodeDescriptor(MyNode unit) : base(unit) { }
  8. protected override void DefinedPort(IUnitPort port, UnitPortDescription description)
  9. {
  10. base.DefinedPort(port, description);
  11. switch (port.key)
  12. {
  13. case "inputTrigger":
  14. description.summary = "Trigger the concatenation of two strings, myValueA and myValueB, and return the result string on the Result port.";
  15. break;
  16. case "myValueA":
  17. description.summary = "First string value.";
  18. break;
  19. case "myValueB":
  20. description.summary = "Second string value.";
  21. break;
  22. case "outputTrigger":
  23. description.summary = "Execute the next action in the Script Graph after concatenating myValueA and myValueB.";
  24. break;
  25. case "result":
  26. description.summary = "The result string obtained from concatenating myValueA and myValueB.";
  27. break;
  28. }
  29. }
  30. }

自定义事件

创建监听事件的节点

using Unity.VisualScripting;
using UnityEngine;

//为自定义脚本事件注册一个字符串名称以将其挂钩到事件。可以将此类保存在单独的文件中,并将多个 Events 作为公共静态字符串添加到该文件中。
public static class EventNames
{
    public static string MyCustomEvent = "MyCustomEvent";
}

[UnitCategory("Events\\MyEvents")]
public class MyCustomEvent : EventUnit<int>
{
    [DoNotSerialize]
    public ValueOutput result { get; private set; }
    protected override bool register => true;

    // 将具有事件名称的 EventHook 添加到 VisualScripting 事件列表中
    public override EventHook GetHook(GraphReference reference)
    {
        return new EventHook(EventNames.MyCustomEvent);
    }
    protected override void Definition()
    {
        base.Definition();

        result = ValueOutput<int>(nameof(result));
    }

    protected override void AssignArguments(Flow flow, int data)
    {
        flow.SetValue(result, data);
    }
}

创建触发事件的节点

using Unity.VisualScripting;
using UnityEngine;

//定义一个发送事件的节点
[UnitCategory("Events\\MyEvents")]
public class SendMyEvent : Unit
{
    [DoNotSerialize]
    [PortLabelHidden]
    public ControlInput inputTrigger { get; private set; }
    [DoNotSerialize]
    public ValueInput myValue;
    [DoNotSerialize]
    [PortLabelHidden]
    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);
    }

    //发送事件
    private ControlOutput Trigger(Flow flow)
    {
        EventBus.Trigger(EventNames.MyCustomEvent, flow.GetValue<int>(myValue));
        return outputTrigger;
    }
}

Unity3D的可视化编程工具:VisualScripting - 图17

从脚本中触发事件

EventBus.Trigger(EventNames.MyCustomEvent, 2);

using Unity.VisualScripting;
using UnityEngine;

    public class CodeTriggerCustomEvent : MonoBehaviour
    {
      void Update()
      {
         if (Input.anyKeyDown)
         {
            //Trigger the previously created Custom Scripting Event MyCustomEvent with the integer value 2.
            EventBus.Trigger(EventNames.MyCustomEvent, 2);
         }
      }
   }

在脚本中监听事件和注销事件

static EventBus.Register<TArgs>(EventHook hook, Action<TArgs> handler)

static EventBus.Unregister(EventHook hook, Delegate handler)

using Unity.VisualScripting;
using UnityEngine;
public class EventReceiver : MonoBehaviour
{
    private EventHook mAIUsePropEventHook;
    private System.Action<ItemType> mAIUsePropEventAction;

    private EventHook mAIShootEventHook;
    private System.Action<RoundState> mAIShootEventAction;

    private void Awake()
    {
        mAIUsePropEventHook = new EventHook(MachineEventNames.AIUsePropEvent, gameObject);
        mAIUsePropEventAction = EventAIUseProp;
        EventBus.Register<ItemType>(MachineEventNames.AIUsePropEvent, mAIUsePropEventAction);

        mAIShootEventHook = new EventHook(MachineEventNames.AIShootEvent, gameObject);
        mAIShootEventAction = EventAIShoot;
        EventBus.Register<RoundState>(MachineEventNames.AIShootEvent, mAIShootEventAction);
    }

    private void OnDestroy()
    {
        EventBus.Unregister(mAIUsePropEventHook, mAIUsePropEventAction);
        mAIUsePropEventAction = null;
        mAIUsePropEventHook = null;

        EventBus.Unregister(mAIShootEventHook, mAIShootEventAction);
        mAIShootEventAction = null;
        mAIShootEventHook = null;
    }

    private void EventAIUseProp(ItemType usePropType)
    {
    }

    private void EventAIShoot(RoundState target)
    {
    }
}

CustomEvent

static CustomEvent.Trigger(GameObject target, string name, params object[] args)

Unity3D的可视化编程工具:VisualScripting - 图18

CustomEvent的实现还是用EventBus.Trigger(EventHooks.Custom, target, new CustomEventArgs(name, args));

public static void Trigger(GameObject target, string name, params object[] args)
{
    EventBus.Trigger(EventHooks.Custom, target, new CustomEventArgs(name, args));
}

父级子级图和Input节点、Output节点的关系

因为图标中可以嵌入图标,嵌入关系是通过Input节点和Output节点来实现控制流程。

父级图表:

Unity3D的可视化编程工具:VisualScripting - 图19

子级图表:

Unity3D的可视化编程工具:VisualScripting - 图20

C#与VisualScripting的数据交互

C#->VisualScripting

对Variables赋值和取值

Unity3D的可视化编程工具:VisualScripting - 图21

VariableDeclarations.Set([InspectorVariableName(ActionDirection.Set)] string variable, object value)

<font style="color:rgb(38, 38, 38);">VariableDeclarations.Get<T>([InspectorVariableName(ActionDirection.Get)] string variable)</font>

using Unity.VisualScripting;
using UnityEngine;

/// <summary>
/// 测试交互
/// </summary>
public class TestInteraction : MonoBehaviour
{
    public Variables TargetVariables;
    public ScriptMachine TargetScriptMachine;

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            //从c#给VisualScripting赋值
            TargetVariables.declarations.Set("TestVariableA", 10);
            //在c#获取VisualScripting的值
            Debug.Log($"获取值:{TargetVariables.declarations.Get<Vector3>("TargetPos")}");
        }
    }
}

对Graph Inspector的赋值和取值

Unity3D的可视化编程工具:VisualScripting - 图22

using Unity.VisualScripting;
using UnityEngine;

/// <summary>
/// 测试交互
/// </summary>
public class TestInteraction : MonoBehaviour
{
    public Variables TargetVariables;
    public ScriptMachine TargetScriptMachine;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.C))
        {
            Debug.Log(TargetScriptMachine.HasDescriptor());
            Debug.Log(TargetScriptMachine.graph.valueInputDefinitions.Count);

            //赋值
            TargetScriptMachine.graph.valueInputDefinitions[0].defaultValue = 10;
        }
    }
}