前言
最近开始尝试做一些游戏DEMO,对于包含大量对话、3D场景各类触发等需要大量非程序配置的部分,通过传统配置表会非常复杂且不易读,以往会选择使用lua等脚本语言由策划配置,这次尝试使用可视化脚本配置,看看是否可以更方便直观的完成配置。
整个游戏使用可视化脚本开发不太现实,期望是底层和主要游戏逻辑使用C#编写,提供高级接口供可视化脚本调用。这样配置简单,逻辑上也比较可控。
比如玩家走进一个区域弹出对话框,可视化脚本中只需要监听玩家走进区域,并直接调用播放对话:
这么做肯定少不了编写大量的自定义事件和节点,但是除了官方教程外,网络上几乎没有其他关于Visual Scripting 自定义节点的教程,果不其然踩了不少坑。于是写一篇教程式的文章完善这方面的空白。
这篇文章是建立在你已经熟读官方教程的前提下编写的。
项目结构
创建一个EventHooks.cs
/EventNames.cs
文件,存放所有的自定义事件名:
c#
public static class EventHooks
{
// Bolt
public const string Custom = nameof(Custom);
// Global
public const string OnGUI = nameof(OnGUI);
}
创建一个动态调整参数数量的节点
Visual Scripting并不通过反射属性来构建节点,而是通过执行Definition方法逻辑来决定节点的构建形式。通过为节点增加一个count参数,就可以根据count值动态改变输出端口数量。
c#
public class MultiOutput : Unit
{
[SerializeAs(nameof(count))]
private int _count = 2;
[DoNotSerialize]
public ControlInput enter { get; private set; }
[DoNotSerialize]
[Inspectable, InspectorLabel("Count"), UnitHeaderInspectable("Count")]
public int count
{
get => _count;
set => _count = Mathf.Clamp(value, 1, 10);
}
[DoNotSerialize]
public ReadOnlyCollection<ControlOutput> multiOutputs { get; private set; }
protected override void Definition()
{
enter = ControlInputCoroutine(nameof(enter), flow => multiOutputs[0]);
var _multiOutputs = new List<ControlOutput>();
multiOutputs = _multiOutputs.AsReadOnly();
for (var i = 0; i < count; i++)
{
var output = ControlOutput(i.ToString());
Succession(enter, output);
_multiOutputs.Add(output);
}
}
}
EventUnit需要TArgs泛型用于参数类型,但我不想传参数怎么办
泛型类型使用EmptyEventArgs即可。
全局事件节点
官方教程中的EventUnit示例就是全局广播的,官方也提供了封装好的全局事件类GlobalEventUnit:
c#
public class OnGlobalEvent : GlobalEventUnit<EmptyEventArgs>
{
protected override string hookName => EventNames.OnGlobalEvent;
}
指定Target的事件节点
如果想要仅触发某个gameObject上的某一个事件,则需要使用封装好的GameObjectEventUnit:
c#
public class OnObjectEvent : GameObjectEventUnit<EmptyEventArgs>
{
public override Type MessageListenerType => null;
protected override string hookName => EventNames.OnObjectEvent;
}
运行时动态添加脚本监听的事件节点
实现MessageListener类(这个类会在运行时挂载到Graph所在的物体上):
c#
public class UnityOnPlayerEnterColliderMessageListener : MessageListener
{
private void OnTriggerEnter(Collider other)
{
if (Game.Instance.player.GetComponent<Collider>() == other)
{
EventBus.Trigger(EventNames.OnPlayerEnterCollider, gameObject);
}
}
}
然后在GameObjectEventUnit中指定MessageListenerType:
c#
public class OnPlayerEnterCollider : GameObjectEventUnit<EmptyEventArgs>
{
public override Type MessageListenerType => typeof(UnityOnPlayerEnterColliderMessageListener);
protected override string hookName => EventNames.OnPlayerEnterCollider;
}
为什么调用了EventBus.Trigger但是没有触发对应的事件
事件名、target、参数必须和定义严格匹配才会执行事件回调。比如:
没有使用GameObjectEventUnit,调用带target的EventBus.Trigger(EventNames.SomeEvent, gameObject)
不会有任何事件触发
GameObjectEventUnit<int>
指定了参数类型是int,调用不带参数的EventBus.Trigger(EventNames.SomeEvent, gameObject)
不会有任何事件触发
自定义事件节点只有放在Events分类下时才会显示
c# custom UnitCategory for events
放在其他分类下,不会在Fuzzy Finder中显示(但是可以搜索到并使用)
(除了这个帖子没有任何其他地方提到这个事情......查了好久
给自定义节点更换Icon
找到你想使用的图标对应的节点:
使用TypeIcon注解:
kotlin
[TypeIcon(typeof(SelectUnit))]
public class MyEvent : Unit
Icon只能使用官方图标库中的图标
参考资料
本文内容主要通过学习[email protected]/Runtime代码编写。