可视化编程插件Bolt的入门介绍及与PlayMaker的对比
Bolt是什么?
Bolt是一个比较新的Unity可视化编程插件,目前稳定版本为1.4,alpha测试版本为2.0。
官网地址:https://ludiq.io/bolt
Asset Store购买地址:https://assetstore.unity.com/packages/tools/visual-scripting/bolt-87491
Bolt在设计理念和使用上都很类似于UE4的蓝图(Blueprints),属于“流(flow)”式设计。简单地说,“流”式设计就是“按顺序依次执行每一步”,这其实才是最符合程序代码执行逻辑的设计,因为程序代码的执行逻辑就是“一行命令执行完再执行下一行命令”。
市面上用“流”式设计的可视化编程插件其实也蛮多的,早期的uScript Professional,现在的flowCanvas都属于此类。
我们可以这么来理解这些“流”式设计的可视化编程插件:
- 每一个“节点”代表一项命令;
- 执行完上一个“节点”所代表的命令之后再执行下一个“节点”所代表的命令;
- 数据(或变量)可以从一个“节点”输入给另一个“节点”。
在Bolt中,其基本的“节点”(被称为“单元(unit)”)是Unity的各种API命令。Bolt号称支持所有Unity内置命令(大概有23000多种),还允许手动添加其他的第三方插件的自定义类(class)。因此,Bolt自夸:“凡是可以用代码实现的功能,都可以用Bolt来实现”。
If it can be done with code, it can be done with Bolt.
这句话说得有一点夸张,但也代表了Bolt的特征。它本质上就是直接调用Unity的API命令,用和程序脚本差不多的执行逻辑来运行,这在本质上与c#脚本是一致的。比如下面这个Bolt的Graph,就完全等价于后面的那个c#脚本:
public class test : MonoBehaviour {
// Use this for initialization
void Start () {
Debug.Log ("Hello World!");
}
// Update is called once per frame
void Update () {
transform.Rotate (0f, Time.deltaTime * 30f, 0f);
}
}
可以看到,Start ()
和Update ()
函数变成了Event节点,Debug.Log()
、Transform.Rotate ()
、Time.deltaTime
也变成了普通节点。有专门的String节点用来获得一个String类型的值,也有Multiply节点来进行乘法计算。程序员如何“设计”一段代码,我们就可以如何“设计”一个Bolt的Graph。
当然,从实际的使用感受来看,这样的设计方式并不是很“人性化”,那些“节点”的功能对于初学者来说也不是那么“一目了然”,还需要用户具备一定的程序思维能力,才能将自己的需求转换成流式的节点运行逻辑。坦白来讲,Bolt的上手难度是要高于PlayMaker的。但是,一旦用户跨过了“新手期”,就可以大量将已有的“代码类”教程翻译成Bolt流图,获得快速提升。而不像PlayMaker,还需要将“流”式的代码改写成“状态机”,然后再才能予以实现。
本质上,PlayMaker中一个State中的Action也是以“流”的方式运行的(上方Action先执行,下方Action后执行),但一方面各种“Every Frame”和非“Every Frame”的Action混合在一起非常难以分辨,另一方面很难表达“分叉”式的逻辑(比如“if... else...”这样)。因此,对于稍复杂一些的“流”式逻辑,就很难在一个State中实现出来,而要改写成多个States之间的转移变化。
但实际上,“状态机”其实是为了解决在一些特定问题上“流"式逻辑太复杂而提出的一种归纳整理的方案,将复杂的”流“拆成几个相对简单的”状态“,而到了PlayMaker里面,往往不得不将本来很简单的”流“式逻辑改成甚至于更复杂的”状态机“方案,颇有点”脱了裤子放屁“的意思。
Bolt本身也有状态机(State Machine),但它的状态机就是在流(Flow Machine)之上的,这才是符合程序设计逻辑的做法。
Bolt的安装和设置
Bolt需要在Asset Store或者其官网上购买,购买后会获得一个.unitypackage
文件,用于导入到Unity项目中。
导入完成后会自动弹出设置窗口:
首先是欢迎界面,点击Next
按钮设置Naming:
这里建议使用Human Naming
,比较不那么“反人类”,但依然还是需要经常反查Unity的脚本帮助文件来了解哪些可用的Bolt单元/Unity函数。
Documentation栏是用来建立本项目的帮助文件的,点击Generate Documentation
可以建立编辑器内显示的帮助文档,如果出错也不要紧,忽略即可。Inspectors栏也可以直接点击Generate Inspectors
,让Bolt自动完成这一步工作。
如果希望在Bolt中控制第三方插件,则需要在Assemblies栏中加载对应的第三方插件的DLLs文件。
如果只是一些自定义的库(class)或者构造体(struct),则需要在Types栏中予以添加,然后点击Generate
让Bolt创建本项目所需要的codebase。这个codebase就是我们可以在Bolt中使用的全部单元的集合。
在使用过程中,我们也可以通过Bolt菜单中的相关命令,来添加新的Assemblies或者Types,或重建单元数据库。
-
Setup Wizard...
:重新设置本项目的Bolt -
Update Wizard...
:更新本项目的Bolt设置到最新(升级了Bolt版本之后使用) -
Configuration...
:打开偏好设置窗口 -
Unit Options Wizard...
:仅更新Assemblies和Types两项Bolt设置(需要添加新的插件支持或新的自定义类时使用) -
Build Unit Options
:直接重建codebase和unit database(在修改自定义脚本之后使用) -
Update Unit Options
:仅直接重建unit database(在修改自定义脚本之后使用)
在偏好设置窗口中,我们可以对Bolt进行一些设置,其中大部分选项都可以保持默认,但我个人比较建议在
Ludiq
一页中把Snap To Grid
选项勾上,这样强迫症患者就不需要对着参差不齐的流图而揪心了。
Bolt的基本概念
Machine(机)
Machine(机)是我们添加给游戏对象(Game Object)的Bolt组件,相当于一个完整的脚本。Bolt有两种Machine:Flow Machine(流机)和State Machine(状态机)。前者是用一个个Unit串成完整的流式逻辑,后者则是用流机来描述一个个不同的状态(states)以及状态与状态之间转换的条件(Transition)。
本文中不会多讲State Machine,在刚刚接触Bolt的时候,能用Flow Machine完成的交互设计都应该尽量用Flow Machine来完成,一方面是因为没必要,另一方面也是因为在Bolt中的State Machine设置起来其实还是蛮复杂的。
Embed VS. Macro
Machine可以内嵌(Embed)于Unity场景,也可以作为宏(Macro)保存在Asset文件夹里作为一个可以重复使用的游戏资源。
建议最好在刚创建完Machine就决定究竟是使用哪种方式,因为虽然可以从一种方式转换为另一种方式,但这个过程并不是100%无损的,复杂的Graph几乎不可能不在转换后出现各种各样的问题。
我个人的建议是,如果一个Machine所代表的功能会被用在不同的地方,就请尽量使用Macro,或者说,如果你准备“复制/粘贴”某个Machine中的Graph到一个新的Machine中去,那么就将这个Machine转换成Macro,然后在新Machine中调用。
Graph(图)
在Bolt中,Graph(图)是程序命令执行逻辑的可视化呈现。Flow Machine中呈现的是Flow Graph,State Machine中呈现的是State Graph。
Flow Graph呈现出来的是一堆彼此相连的units(单元),其中必定有一个起点,这个起点通常是一个Event类型的unit,比如常见的Start
和Update
。代表着“当……发生时,开始执行本Graph”。
比如前面示例中的左边的Graph,就是在说:“当本游戏对象被载入时,在console面板中输出一行字符串’Hello World!’”。
一个Machine中可以有多个Graphs,用来区分在不同事件发生时所触发的不同程序命令执行逻辑。通常还可以将同一事件所触发的多段逻辑分开成不同的Graph,以方便调试。比如“行走”和“跳跃”都会被Update
所触发,但我们可以在一个Flow Machine中用两个Update
打头的Graph来分别制作这两个交互行为。不过,如果这两个交互行为互相之间有所关联,或者需要有严格的前后执行顺序,那么最好还是做成一个单独的Graph,避免出现问题。
严格来说,Bolt其实是将Machine中的所有内容定义成一个Graph,但我个人认为这样反而不好,所以在这里解释成“一个graph代表一个完整的彼此相连的units执行流程结构”。希望不会对大家的理解造成误会。
Unit(单元)
单元的类型
Unit是Bolt中最基本的元素,一个unit代表一个操作命令,多个unit按照顺序组合成Graph,从而实现某一个特定的程序功能。默认情况下,Bolt有超过23000个不同的unit,我们可以把它们分成几个大的类别:
- 事件单元(events):决定“当……发生时”的各种unit,通常都会显示为绿色,被用在一个Flow Graph的起点
- 命令单元(actions):决定“做什么”的各种unit
- 数据单元(data):输入各种数据的unit,比如各种以“Literal”结尾的unit。这些单元可以无需调用变量而获得一个Unity数据,比如浮点数(float)、字符串(string)、光线(ray)、层遮罩(layer mask)等
- 计算单元(calculation):用来处理数据、计算数据的各种unit,比如加减乘除,各种数学函数等
- 变量单元(variable):用来调用或修改变量的unit,主要是
Set Variable
和Get Variable
两个 - 逻辑单元(logic):用来控制Graph的运行逻辑走向的单元,比如
Branch
(相当于“if... else...”条件语句)和Loop
(相当于“for...”循环语句)等等
新建/替换单元
在Graph窗口的空白处点击鼠标右键,选择Add Unit...
,就可以通过搜索关键字来创建新的unit。如果在一个已有的unit上点击鼠标右键,选择Replace...
,可以替换这个unit。
Fuzzy Finder(单元搜索器)
用来搜索unit的窗口叫“Fuzzy Finder”,目前还是比较好用的,搜索速度也比较快。但要注意的是,如果用多个关键字搜索,这多个关键字的顺序必须和结果中这些关键字的出现顺序保持一致才行,比如我搜“position” + “transform”就得不到“transform.position”,必须搜“transform” + ”position”才行。
Connections(连接) & Ports(接口)
- 三角形线框图标指向的(绿色的宽体箭头)叫Connections(连接),是用来连接不同units以决定其执行顺序的端口
- 圆形线框图标指向的(其他各种)叫Ports(接口),是用来传递数据的端口,根据数据类型的不同,有不同的普调样式,左边的是接受其他数据的接口,右边的是输出数据的接口
并不是每个unit都需要用被Connection相连接才能起效,有的unit根本就没有Connection端口。一般来说,执行计算任务的unit都不需要连接Connection,比如下图:
当然,这个图里面输出的数据最终还是需要被传递给某个连接了Connection的unit才会真的被执行。
Overloads(分身)
一个Unity命令可以有多种调用方式,叫做Overloads。Bolt中使用不同的unit来对应各个Overloads,我个人将其翻译成“分身”。
c#脚本中的Overloads 对应Bolt Unit的Overloads我个人不太喜欢这种将每个Overload都做成单独节点的做法,每次都要找半天,而且很容易看错。但貌似也没有更合适的办法了。
Variables(变量)
在Bolt中,有5种不同形式的变量:
- 图示变量(Graph):图示变量是图示例中的局部变量。它们具有最小的可调用范围,不能在其图示之外访问或修改。
- 对象变量(Object):对象变量属于游戏对象(GameObject)。他们在该游戏对象的所有图示上共享。
- 场景变量(Scene):场景变量在本场景的所有图示上共享。
- 应用变量(Application):即使场景改变,应用变量仍然存在。一旦应用程序退出,它们将被重置。
- 存储变量(Saved):即使应用程序退出,存储变量也会也会继续存在。他们可以被用作一个简单但功能强大的存储系统。它们被保存在Unity的玩家选项,这意味着它们不能像游戏对象和组件那样被引用。
个人认为,Bolt的变量形式被设计得很奇怪。很难将其与正常脚本中设置的变量对应起来理解。
- Graph Variable有点类似private variable(私有变量),但又必须提前定义好,远不如脚本中那么方便使用;
- Object Variable类似public variable(公开变量),可以被外部访问,但Bolt的Object Variable是通过在同一个Game Object上的一个叫做“Variables”脚本来定义的,相当于是在获取另一个组件的变量在用,而不是在本组件中定义可以公开访问的变量;
- Scene Variable类似global variable(全局变量),但其实是在场景中新建了一个叫“Scene Variables”的空游戏对象,然后通过“Variables”脚本来定义,相当于是在获取同一场景中另一个游戏对象的变量在用;
- Application Variable才好像是真的全局变量;
- Saved Variable实际上是在操作Unity的
PlayerPrefs
,算是一个简单实现“存档”功能的方法吧,但还是显得不伦不类的。
此外,Bolt能够用几乎所有Unity数据类型做为变量,比如:
- 浮点数(Float)
- 整数(Integar)
- 布尔型(Boolean):是/否
- 字符串(String)
- 字符(Char):字符串中的一个字符
- 枚举类型(Enums):预先设定好的有限的枚举选项,例如下拉菜单中的所有选项
- 向量(Vector):向量其实是一个struct,Unity中的向量包括二维向量(Vector2)、三维向量(Vector3)、四维向量(Vector4)
- 游戏对象(Game Object):Unity场景中的基本实体,每个游戏对象都有一个名称(name),一个位置和旋转的转换(transform),以及一个组件列表(components)
- 列表(List):列表是元素的有序集合。元素可以是任意类型的,但大多数情况下,列表中的所有元素必须是同一类型的。可以通过从0开始的索引位置(index)检索和分配列表中的每个元素
- 字典(Dictionary):字典是一个集合,其中元素有一个唯一的键,映射到它的值。可以通过字典的键来检索和分配字典的每个元素。比如:可以按名称(字符串键)创建年龄字典(整数值)
- 对象(Object):“对象”是一种特殊类型。各种Unity专有的数据类型都被归为Objects。
变量窗口可以通过菜单Window -> Variables打开:
我个人强烈建议将这个窗口dock到编辑器的UI里,虽然Graph窗口中会自动出现Variables窗口,但那需要Graph窗口足够大,而大多数时候,我们都不会将Graph窗口放大到那么大。
最左边是我自己dock的窗口,紧挨着的是自动弹出来的窗口,但自动弹窗并不是一直都在的最后,我们可以通过将variable拖动到Graph窗口中来创建Get Variable单元,以使用这个variable,也可以按住Alt
键拖动variable到Graph窗口来创建Set Variable单元,以设置这个variable。
Bolt的限制
Bolt所号称的“凡是可以用代码实现的功能,都可以用Bolt来实现”其实是有水份的。
虽然Bolt支持几乎所有的Unity函数命令,但并不等于我们就能完全无缝地将所有c#脚本代码翻译成Bolt流图。至少:
- Bolt目前还不支持Editor类的Unity函数命令。也就是说,没有办法用Bolt来做编辑器拓展。
- Bolt目前不能直接支持自定义类(class)或者构造体(Struct)。不是不能用,而是需要先用c#写出来然后再加载进Bolt,才能在Graph中创建相应的单元(unit)。
- Bolt目前还没有完美的办法实现自定义函数。Custom Event很类似了,但Custom Event不能定义输出结果是在是很淡疼的一件事情。而且Custom Event的本意是为了在c#脚本中调用Bolt的自定义事件。
这些缺失大多可以通过自己写一些脚本来“弥补”,但如果自己都能写脚本的,那还用Bolt干什么呢?况且,如果有专门的程序员帮我们写扩展来“弥补”缺失,那我们干嘛不用PlayMaker呢,反正缺什么Action就让人写就是了。
另外,Bolt的Graph的复杂度其实要比程序代码高很多,比如上面一条简单的transform.Rotate (0f, Time.deltaTime * 30f, 0f);
就要用3层结构4个单元来组合完成,可想而知一个复杂的程序代码翻译成流图之后会有多么复杂。而且,Bolt的运行效率(尤其是编辑器中的运行效率)比起代码来要低很多,项目小还好,一旦项目大起来,估计电脑差了连调试都调试不动呢。
因此,Bolt并不是一个“完美”的可视化编程插件,只是比现有的解决方案要好一些罢了。与其将其当做一个“生产力工具”,不如将其当做一个“学习工具”或者一个“原型工具”。在学习Bolt的过程中,尽量去了解正常的程序设计思维是如何进行的,去熟悉Unity的各种API命令,去掌握不同交互逻辑的实现手段和技巧,才是这个插件对于我们的最大价值。