文章目录
- 一、前言
- 二、插件下载
- 1、AssetStore下载
- 2、GitCode下载
- 三、官方教程
- 1、在线文档
- 2、离线文档
- 四、插件界面
- 1、打开编辑器
- 2、界面介绍
- 五、快速制作一棵行为树
- 1、创建物体
- 2、挂BehaviorTree脚本
- 3、添加Task节点
- 4、运行测试
- 5、导出BehaviorTree
- 6、手动引用BehaviorTree树资源
- 6.1、设置External Behavior
- 6.2、使用Behavior Tree Reference
- 7、动态加载行为树资源并设置External Behavior
- 六、复合节点:Composities
- 1、Sequence:顺序节点
- 1.1、节点介绍
- 1.2、新发口诀
- 1.3、案例1:通知开饭了
- 1.4、案例2:吃几口饭吃一口菜
- 2、Parallel:并行节点
- 2.1、节点介绍
- 2.2、新发口诀
- 2.3、案例:边跑步边听歌
- 3、Selector:选择节点
- 3.1、节点介绍
- 3.2、新发口诀
- 3.3、案例:诈骗举报概率
- 4、Parallel Selector:并行选择节点
- 4.1、节点介绍
- 4.2、新发口诀
- 4.3、案例:考研和找工作
- 5、Priority Selector:优先级选择节点
- 5.1、节点介绍
- 5.2、新发口诀
- 5.3、案例:先吵架还是先道歉
- 6、Random Selector:随机选择节点
- 6.1、节点介绍
- 6.2、新发口诀
- 6.3、案例:吃米饭、面条、饺子
- 7、Random Sequence:随机顺序节点
- 7.1、节点介绍
- 7.2、新发口诀
- 7.3、案例:番茄炒蛋
- 8、Parallel Complete:并行竞争节点
- 8.1、节点介绍
- 8.2、新发口诀
- 9、Selector Evaluator:评估选择节点
- 9.1、节点介绍
- 9.2、新发口诀
- 七、中断
- 1、Self中断:老妈喊你回家吃饭
- 2、Lower Priority中断:睡觉时地震
- 3、Both中断:两者都中断
- 八、Variables:行为树成员变量
- 1、添加变量
- 2、读取变量
- 3、修改变量
- 4、拓展变量类型
- 5、代码读写变量
- 九、行为树事件
- 1、Send Event节点
- 1.1、Target Game Object参数
- 1.2、Event Name参数
- 1.3、Group参数
- 1.4、Argument参数
- 2、Has Receive Event节点
- 2.1、Event Name参数
- 2.2、Stored Value参数
- 3、代码中注册和注销事件
- 十、拓展行为树节点
- 1、继承
- 2、案例:MoveTo
- 3、官方Sample包
- 十一、完毕
一、前言
嗨,大家好,我是新发。之前用过Behavior Designer
行为树插件,最近又需要用到,我发现网上的教程很多写得不够好,今天我就来写写Behavior Designer
行为树的教程吧,希望可以帮到大家。
二、插件下载
1、AssetStore下载
插件可以在AssetStore
下载到,
目前最新的版本是1.7.2
,
可以在官网查看每个版本的迭代内容:
https://opsive.com/news/category/release-notes/behavior-designer-release-notes/
2、GitCode下载
我放了1.7.1
版本的到GitCode
上,地址:https://gitcode.net/linxinfa/unitybehaviordesigner
下载下来后,放入你的工程即可,
其中我加了一个FixGUIStyle
脚本,
因为在Unity
专业版上,打开Behavior Designer
编辑器是这个鬼样,
注:如果你使用的是个人版,则不会有这个问题
它的GUIStyle
在Unity
专业版本有问题,插件编辑器是个dll
文件,我们无法修改编辑器源码,
然后我进行了反编译,找到了它的封装各种GUIStyle
的类:BehaviorDesignerUtility
,
通过反射,修改这个类的私有静态成员对象,代码如下:
using UnityEngine;
using UnityEditor;
using System.Reflection;
using BehaviorDesigner.Editor;
/// <summary>
/// 修复BehaviorDesigner编辑器的GUIStyle问题
/// </summary>
public class FixGUIStyle
{
[MenuItem("Tools/Behavior Designer/修复界面GUIStyle #b", false, 0)]
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
public static void ShowWindow()
{
var guiStyle = new GUIStyle();
guiStyle.alignment = TextAnchor.UpperCenter;
guiStyle.fontSize = 12;
guiStyle.wordWrap = true;
guiStyle.normal.textColor = Color.white;
guiStyle.active.textColor = Color.white;
guiStyle.hover.textColor = Color.white;
guiStyle.focused.textColor = Color.white;
Texture2D tex2D = new Texture2D(1, 1, TextureFormat.RGB24, false);
tex2D.SetPixel(1, 1, new Color(0.1f, 0.1f, 0.1f, 1f));
tex2D.hideFlags = HideFlags.HideAndDontSave;
tex2D.Apply();
guiStyle.normal.background = tex2D;
// 反射动态修改私有静态成员,修改GUIStyle
var t = typeof(BehaviorDesignerUtility);
var graphBackgroundGUIStyle = t.GetField("graphBackgroundGUIStyle", BindingFlags.Static | BindingFlags.NonPublic);
graphBackgroundGUIStyle.SetValue(null, guiStyle);
var taskCommentGUIStyle = t.GetField("taskCommentGUIStyle", BindingFlags.Static | BindingFlags.NonPublic);
taskCommentGUIStyle.SetValue(null, guiStyle);
var selectedBackgroundGUIStyle = t.GetField("selectedBackgroundGUIStyle", BindingFlags.Static | BindingFlags.NonPublic);
selectedBackgroundGUIStyle.SetValue(null, guiStyle);
var preferencesPaneGUIStyle = t.GetField("preferencesPaneGUIStyle", BindingFlags.Static | BindingFlags.NonPublic);
preferencesPaneGUIStyle.SetValue(null, guiStyle);
}
}
点击菜单Tools/Behavior Designer/修复界面GUIStyle
,或者按一下Shift+B
快捷键,即可修复GUIStyle
问题,
效果如下
三、官方教程
学习插件的时候,建议先看下官方文档。
1、在线文档
官方在线文档: https://opsive.com/support/documentation/behavior-designer/overview/
2、离线文档
插件的Behavior Designer
目录中有个Documentation.pdf
文档,不过可能很多同学都没去看这份文档,建议还是看一看,
四、插件界面
1、打开编辑器
点击菜单Tools/Behavior Designer/Editor
即可打开编辑器窗口,
如下
2、界面介绍
界面可以划分为3
个区域,如下
第1
个区域是行为树的组织区域,我们的行为树节点就是在这个区域上进行连线的;
第2
个区域顶部是四个标签页:Behavior
、Tasks
、Variables
、Inspector
,默认是Tasks
标签页,显示的是节点列表;
标签页 | 说明 |
Behavior | 对整个行为树的设置,比如行为树名称、是否激活时立即执行、执行完毕后是否重新执行等设置 |
Tasks | 任务节点列表,比如Composities(符合节点)、Decorators(装饰节点)、Actions(行为节点)等等 |
Variables | 行为树变量,可以设置全局变量,也可以设置行为树自身的变量 |
Inspector | 查看节点详细信息,设置节点参数 |
第3
个区域按钮功能如下
五、快速制作一棵行为树
在讲解节点之前,我先演示一遍如何快速制作一棵行为树,让大家对整个流程有个概念。
1、创建物体
首先在场景中创建一个物体,可以是任何物体,比如你想控制角色,那么这个物体就可以是你的角色模型,这里我创建一个空物体,
重命名为BehaviorTreeObj
,
2、挂BehaviorTree脚本
打开Behavior Designer
编辑器,先选中刚刚的BehaviorTreeObj
物体,然后在编辑器网格空白处右键鼠标,点击菜单Add Behavior Tree
,
这一步其实就是给物体挂上BehaviorTree
组件,我们也可以通过Add Component
手动添加BehaviorTree
组件,
3、添加Task节点
我以最简单的Log
节点为例,它的功能就是输出日志,在空白处点击鼠标左键,按键盘的空格键
即可弹出节点搜索框,搜索log
,可以看到Log
节点,点击即可创建出Log
任务节点,它会自动帮我添加一个Entry
入口节点,Entry
连向Log
,如下
我们给Log
节点设置Text
字段的值为Hello World
,并添加注释,如下
4、运行测试
运行Unity
,可以看到Log
节点被执行了,显示了一个绿色的勾,日志窗口也输出了HelloWorld
,
5、导出BehaviorTree
点击Export
按钮,将行为树导出保存为资源文件,
如下,这样子就可以复用行为树资源了,
6、手动引用BehaviorTree树资源
6.1、设置External Behavior
我们把上面导出的行为树资源赋值给BehaviorTree
组件的External Behavior
成员,如下
运行时,就会根据这个External Behavior
的行为树来执行了。
6.2、使用Behavior Tree Reference
另外,我们还可以通过Behavior Tree Reference
节点来引用行为树资源,
先创建一个Behavior Tree Reference
节点,点击Inspector
,设置它引用的外部行为树资源即可,
一般我们会做一个通用的行为树保存为资源文件,然后对特殊的怪物另外做行为树,并让其引用通用行为树资源,像这样子
7、动态加载行为树资源并设置External Behavior
我们也可以通过代码动态设置External Behavior
,我这里使用Addressables
插件来做资源加载,
注:关于
Addressables
的使用,可以参见我之前写的这篇文章:【游戏开发探究】Unity Addressables资源管理方式用起来太爽了,资源打包、加载、热更变得如此轻松(Addressable Asset System | 简称AA)
首先给行为树资源文件添加Addressable
管理,分配到一个Addressable
索引名,如下
注:勾上Addressable就会自动分配一个索引名了
接着我们可以使用代码加载行为树资源,动态赋值给BehaviorTree
组件的ExternalBehavior
对象,
创建一个AddBehaviorTree.cs
脚本,
代码如下
using System.Collections;
using UnityEngine;
using BehaviorDesigner.Runtime;
using UnityEngine.AddressableAssets;
public class AddBehaviorTree : MonoBehaviour
{
IEnumerator Start()
{
// 初始化Addressables
yield return Addressables.InitializeAsync();
// 添加BehaviorTree
var bt = gameObject.AddComponent<BehaviorTree>();
// 设置不立即执行
bt.StartWhenEnabled = false;
// 加载行为树资源
var loader = Addressables.LoadAssetAsync<ExternalBehaviorTree>("Assets/BtRes/Behavior.asset");
yield return loader;
// 设置ExternalBehavior
bt.ExternalBehavior = loader.Result;
// 执行行为树
bt.EnableBehavior();
}
}
我们创建一个空物体,重命名BtObj
,并给它挂上AddBehaviorTree
脚本,如下
我们运行Unity
,可以看到BtObj
物体动态挂了BehaviorTree
组件,并且动态设置了ExternalBehavior
对象,行为树也被执行了,输出了Hello World
,如下
我们可以点击BehaviorTree
组件上的Open
按钮,
可以看到行为树已经执行完毕,
如果我们想让行为树执行完毕后重复执行,可以设置RestartWhenComplete
为true
,
我们串一个Wait
节点,方便观察,
执行效果如下,可以看到行为树可以重复执行了,
上面我们用到了Squence顺序节点,它是一个复合节点,下文我会进行节点的详细介绍。
六、复合节点:Composities
行为树任务节点中最重要的节点就是复合节点了,它决定了行为树的执行流,也就是Tasks
列表中Composities
分组的这些节点,
我先简单做个日常生活的行为树给大家看看,如下
上面的行为树的执行过程是怎么样的呢?想要搞清楚就得先知道行为树的执行顺序:从左到右,并且深度优先。
另外,复合节点又控制着子节点的执行规则,是顺序执行还是并行执行,亦或者随机执行,是遇到成功就立即返回成功还是遇到失败就立即返回失败,亦或者遇到成功继续执行下一个,或者遇到失败继续执行下一个呢?
这些都是复合节点来控制的,这里头的逻辑需要大家好好琢磨,下面我带大家挨个过一遍复合节点,讲解过程中我会穿插顺带讲解一下用到的其他任务节点,并举一些实际的应用场景,方便大家学以致用,也希望大家能够触类旁通。
1、Sequence:顺序节点
1.1、节点介绍
顾名思义,顺序节点就是从左到右挨个顺序执行,当所有子节点都返回Success
时,它才返回Success
,
当某个子节点返回Failure
时,顺序节点就会立刻返回Failure
,
1.2、新发口诀
顺序节点,从左到右,为真继续,全真才真,一假即假,一假即停。
类似于与
逻辑。
1.3、案例1:通知开饭了
我们使用Has Received Event
来监听开饭的事件,当它检测收到事件时才会返回Success
,此时顺序节点就会去执行下一个子节点,即干饭的逻辑,达到响应事件的功能,如下
其中Has Received Event
节点要设置监听的事件名称,定义一个字符串即可,
抛出事件有两种方式,一种是代码调用行为树的SendEvent
接口,如下
public void SendEvent(string name);
public void SendEvent<T>(string name, T arg1);
public void SendEvent<T, U>(string name, T arg1, U arg2);
public void SendEvent<T, U, V>(string name, T arg1, U arg2, V arg3);
示例:
// bt是BehaviorTree对象
bt.SendEvent("EAT_EVENT");
另外一种是使用SendEvent
节点,
设置要发送的事件名称即可,
1.4、案例2:吃几口饭吃一口菜
想象一下你吃饭的过程,扒拉吃几口饭,然后吃一口菜,我们可以用顺序节点来组织,
2、Parallel:并行节点
2.1、节点介绍
并行节点,顾名思义,它会并行执行所有的子节点,所有的子节点都返回Success
,并行节点才会返回Success
,
只要有一个子节点返回Failure
,并行节点就会立刻返回Failure
。如下图,我们可以看到,等10
秒和等20
秒的两个节点还没执行完就被停止了,因为中间那个等1
秒的节点执行完毕并返回了Failure
给并行节点,整个并行以Failure
结束,那些被终止的Wait
也会直接返回Failure
,
2.2、新发口诀
并行节点,并行执行,全真才真,一假即假,一假即停,被停即假。
2.3、案例:边跑步边听歌
比如我们边跑步边听歌,同时还要监听是否有下雨,有下雨的话要中断跑步,如下
这个例子我用到了Selector
节点,并且启用了Self
中断,看不懂的同学不要着急,下文我会讲。
3、Selector:选择节点
3.1、节点介绍
选择节点与顺序节点类似,也是从左到右执行,不同的是,当有子节点返回Success
,选择节点就会立刻返回Success
,不会执行下一个子节点
如果子节点返回Failure
,会就执行下一个子节点,其实很好理解,就是从左到右遍历选一个Success
,有Success
就返回Success
,否则执行下一个子节点,
如果所有子节点都返回Failure
,选择节点才返回Failure
,
3.2、新发口诀
选择节点,从左到右,一真即真,一真即停,全假才假。
3.3、案例:诈骗举报概率
比如收到诈骗短信后,有99%
的概率会执行举报,有1%
的概率会上当受骗,如下
4、Parallel Selector:并行选择节点
4.1、节点介绍
顾名思义,就是并行执行选择节点,有一个Success
就立即返回Success
,并且会终止其他子节点,被终止的子节点会返回Failure
,如下
所有子节点都返回Failure
时,并行选择节点才返回Failure
,
4.2、新发口诀
并行选择节点,并行执行,一真即真,一真即停,全假才假。
4.3、案例:考研和找工作
现在人生道路有两条路:考研、找工作,同时进行,其中一件事情成了,就吃一顿好的庆祝一下,如下
5、Priority Selector:优先级选择节点
5.1、节点介绍
这个节点和Selector
类似,Selector
的执行顺序是从左到右,而Priority Selector
会先检查子节点的优先级(priority
)进行排序,优先级高的优先执行。
问题来了,我们如何设置子节点的优先级呢?节点的优先级默认是0
,如果要修改优先级,需要重写Task
的GetPriority
方法,
这里我封装一个带优先级参数的日志节点吧:PriorityLog
,创建一个PriorityLog
脚本,代码如下
using UnityEngine;
namespace BehaviorDesigner.Runtime.Tasks
{
[TaskDescription("带优先级参数的日志节点")]
[TaskIcon("{SkinColor}LogIcon.png")]
public class PriorityLog : Action
{
[Tooltip("Text to output to the log")]
public SharedString text;
[Tooltip("Is this text an error?")]
public SharedBool logError;
[Tooltip("Should the time be included in the log message?")]
public SharedBool logTime;
[Tooltip("优先级")]
public SharedFloat priority;
/// <summary>
/// 重写获取优先级的方法
/// </summary>
/// <returns></returns>
public override float GetPriority()
{
return priority.Value;
}
public override TaskStatus OnUpdate()
{
// Log the text and return success
if (logError.Value) {
Debug.LogError(logTime.Value ? string.Format("{0}: {1}", Time.time, text) : text);
} else {
Debug.Log(logTime.Value ? string.Format("{0}: {1}",Time.time, text) : text);
}
return TaskStatus.Success;
}
public override void OnReset()
{
// Reset the properties back to their original values
text = "";
logError = false;
logTime = false;
}
}
}
现在我们就可以在Tasks
列表中看到我们新封装的PriorityLog
节点了,
使用它连个简单的行为树,如下
我们可以选中节点,在Inspector
中设置节点的Priority
,
由于右边的节点优先级我们设置为了1
,比左边的节点优先级高,所以会优先执行右边的节点,因为右边的节点执行返回了Success
,此时Priority Selector
就会直接返回Success
,不会去执行左边的节点了,如下
5.2、新发口诀
优先级选择节点,优先级排序,一真即真,一真即停,全假才假。
5.3、案例:先吵架还是先道歉
我先给行为树创建一Foat
类型的变量:SorryPriority
,如下
以它作为道歉优先级变量,
接着制作如下的行为树,当按下A
键的时候,设置SorryPriority
变量,提高道歉的优先级,
其中Set Shared Float
节点的设置如下,让它去修改SorryPriority
变量为1
,
道歉节点的Priority
参数设置为SorryPriority
变量,如下
我们运行,如果我没有按下A
键,则道歉优先级是0
,2
秒情绪酝酿完毕后,会默认先执行吵架,
如果我在2
秒情绪酝酿期间按下了A
键,则会设置道歉优先级变量为1
,情绪酝酿完毕后就会优先执行道歉,就不会吵架了,
6、Random Selector:随机选择节点
6.1、节点介绍
这个很好理解,按选择节点的规则,只是不是从左到右执行,而是随机顺序执行。
如果有子节点返回Success
,则立即返回Success
,否则继续执行下一个节点,
只有所有的子节点都返回Failure
时,Random Selector
才返回Failure
,
6.2、新发口诀
随机选择节点,随机执行,一真即真,一真即停,全假才假。
6.3、案例:吃米饭、面条、饺子
中午吃什么好呢?随机选一个吧,好的,吃面条~
7、Random Sequence:随机顺序节点
7.1、节点介绍
这个也很好理解,就是顺序节点的规则,但不是从左到右执行,而是随机顺序执行。
当所有子节点都返回Success
时,它才返回Success
,
当某个子节点返回Failure
时,顺序节点就会立刻返回Failure
,
7.2、新发口诀
随机顺序节点,随机顺序,为真继续,全真才真,一假即假,一假即停。
7.3、案例:番茄炒蛋
我现在要做一道番茄炒蛋,有的人先炒番茄,有的人先炒鸡蛋,这里我们就可以使用随机顺序节点了,如果炒鸡蛋或者炒番茄失败,那么我们就吃不到番茄炒蛋了,只有两个都成功了,才能吃到番茄炒蛋,
8、Parallel Complete:并行竞争节点
8.1、节点介绍
这个节点其实就是并行执行所有的子节点,只要有一个子节点返回了结果,它就结束,并以这个子节点的结果为结果,
8.2、新发口诀
并行竞争节点,并行执行,最速之子,以子为果。
9、Selector Evaluator:评估选择节点
9.1、节点介绍
这个节点不是很好理解,它的逻辑是这样的,从左到右顺序执行,如果遇到子节点返回Success
,则立即结束,并返回Success
,否则继续执行下一个子节点。
有同学会问了,那它与Selector
节点有啥区别?
上面我用的是Log
节点,会立即返回结果,如果是用Wait
节点,则会有Running
状态,这个Selector Evaluator
节点是个固执的强迫症节点,它认为子节点应该给它返回Success
,如果子节点返回了Failure
,它会硬着头皮执行下一子节点,但是,一旦下一个子节点处于Running
状态,它就会像个渣男一样毫不犹豫地打断并返回第一个子节点,重新执行,只要有机会它就想要一个Success
结果,如果后续的子节点都是立即返回成功或失败,而没有Running
状态,则它会接受后续子节点的Success
,也就是说,它也不是非得第一个子节点不娶,后面的子节点只要立刻Success
,它就不打断去重新找第一个子节点了,相当地渣男啊!看,第二任只要考虑一下(Running
状态),它就会立即打断回去执行初恋,
如果第二任立即答应了,它就会接受第二任的结果,
如果第二任直接给个Failure
,那么这个渣男会去骚扰路人甲,如果路人甲给它Success
,它就会爱上路人甲,
如果路人甲立即给它Failure
,这个渣男才会死心,
9.2、新发口诀
评估选择节点,渣男一个,从左到右,遇真即真,遇假继续,Runing
时机打断,从头来过。
七、中断
复合节点有中断的权利,我们可以选中某个复合节点,然后点击Inspector
,设置它的中断模式,
默认为None
,即无中断,另外三个中断模式,下文我会挨个进行演示。
这里我先解释一下什么是中断,比如你正在睡觉,突然地震了,这个时候睡觉就要被中断,执行逃跑的逻辑,命要紧。
复合节点的中断控制是由谁来调度的呢?细心的同学应该会发现,执行行为树的时候,会自动创建一个Behavior Manager
对象,上面挂着BehaviorManager
脚本,我们的行为树的执行都是由这个BehaviorManager
来调度的,它就是最高司令,
感兴趣的同学可以反编译BehaviorDesigner.Runtime.dll
,查看BehaviorManager
的源码,
1、Self中断:老妈喊你回家吃饭
一个复合节点下可以有多个子节点,假设现在有两个子节点,假设此时正在执行第二个子节点(Running
状态),突然来了一个事件,需要中断第二个子节点,立即执行第一个子节点,这里就可以使用Self
中断,Self
其实是站在复合节点的角度的,就是中断复合节点自己。
假设你去朋友家打游戏,你决定打完游戏就回家,如果打游戏过程中,老妈打电话喊你回家吃饭,那么就中断打游戏,回家。
行为树如下
没有按下A
键的执行效果如下,打完游戏开开心心回家,
打游戏过程中,按下A
键,中断打游戏,回家,
2、Lower Priority中断:睡觉时地震
我们以睡觉的时候发生地震为例,演示一下Lower Priority
中断。
睡觉的时候如果发生地震,需要中断睡觉,执行逃命逻辑。
如下,睡觉节点与Sequence节点是平级关系,并且睡觉节点在右边,属于低优先级,当正在执行睡觉节点时,左边的Sequence节点想要中断右边的睡觉节点,设置Lower Priority
中断即可,
我们从中断的图标也可以看出,它指向右边,表示中断右边的节点的意思,
3、Both中断:两者都中断
Both
中断其实就是Self
和Lower Priority
都中断。如下
八、Variables:行为树成员变量
我们点击Variables
,可以给行为树添加成员变量,也可以查看已添加的成员变量,
1、添加变量
先起一个变量名,然后选择变量的数据类型,我们也可以添加自定义的数据类型,需要进行变量类型拓展,下文会讲解拓展变量类型的步骤,这里我们先选一个String
好了,最后点击Add
按钮即可,
我们可以修改变量名,给变量设置初始值,如下
2、读取变量
现在我们想通过Log
节点把myName
这个变量输出出来。
先选中Log
节点,切到Inspector
页,点击Text
值最右边的小点点,如下
此时会出现红色的感叹号
我们点击下拉框,选中myName
变量即可,
我们运行行为树,可以看到日志输出了变量值,如下
3、修改变量
我们在Log
节点左边串一个Set String
节点,
注:如果你的变量类型是
Bool
,则这里要使用Set Bool
节点,其他类型同理,选择对应的Set
节点,如果是自定义的数据类型,则要自己拓展一个Set
变量值的节点才行。
此时带有一个红色感叹号,我们需要给它设置要修改的变量,如下
我们运行行为树,可以看到输出的变量值是皮皮猫了,
4、拓展变量类型
实际项目中,我们大概率是要拓展行为树的变量类型的,比如我现在有个Blog
类,
/// <summary>
/// 博客
/// </summary>
public class Blog
{
/// <summary>
/// 作者
/// </summary>
public string author;
/// <summary>
/// 博客地址
/// </summary>
public string url;
public override string ToString()
{
return $"author: {author}, url: {url}";
}
}
我想让他出现行为树Variables
的变量类型列表中,我们需要给它包装一层SharedBlog
。
新建一个SharedBlog
脚本,
按照下面这个格式写即可:
namespace BehaviorDesigner.Runtime
{
[System.Serializable]
public class SharedT : SharedVariable<T>
{
public static implicit operator SharedT(T value)
{
return new SharedT { mValue = value };
}
}
}
SharedBlog
代码如下:
namespace BehaviorDesigner.Runtime
{
[System.Serializable]
public class SharedBlog : SharedVariable<Blog>
{
public static implicit operator SharedBlog(Blog value)
{
return new SharedBlog { mValue = value };
}
}
}
此时,我们就可以在变量类型列表中看到我们拓展的Blog
变量类型了,
我们给行为树添加一个Blog
变量吧,
5、代码读写变量
上面我们拓展了一个Blog
变量类型,并给行为树添加了一个Blog
变量,现在我们想在代码中对这个变量进行访问和修改,我们可以通过BehaviorTree
的GetVariable
方法和SetVariable
方法,
public SharedVariable GetVariable(string name);
public void SetVariable(string name, SharedVariable item);
例:
// 获取行为树对象
var bt = GetComponent<BehaviorTree>();
// 访问成员变量-----------------------------------------
var sharedBlog = (SharedBlog)bt.GetVariable("myBlog");
Debug.Log(sharedBlog.Value.ToString());
// 修改变量值-----------------------------------------
sharedBlog.Value.author = "Unity3D技术博主:林新发";
bt.SetVariable("myBlog", sharedBlog);
// 构建新的成员对象-----------------------------------------
SharedBlog newSharedBlog = new SharedBlog();
Blog newBlog = new Blog();
newBlog.author = "博客技术专家:林新发";
newBlog.url = "";
newSharedBlog.SetValue(newBlog);
// 赋值成员变量
bt.SetVariable("myBlog", newSharedBlog);
九、行为树事件
上文讲Sequence节点的时候已经举例讲了行为树事件,不过有些细节没有讲到,这里我进行补充。
与事件相关的节点是Send Event
和Has Received Event
,
另外,我们也可以通过代码来发出事件和监听事件,下面给大家演示一遍。
1、Send Event节点
我们首先看Send Event
节点,可以在Inspector
面板看到它的参数,
1.1、Target Game Object参数
行为树之间的事件是可以跨物体发送的,比如A
物体上的行为树给B
物体上的行为树发送事件。
默认为None
,事件会发给自身的行为树。
我们可以查看SendEvent
节点的源码,它调用了GetDefaultGameObject
方法去获取目标物体,
这个方法的源码如下:
意思就是如果为空,就返回自身的gameObject
,也就是行为树自身的物体。
如果我们想要设置这个SendEvent
节点的Target Game Object
参数,可以通过行为树变量来传递,比如在Variables
中定义一个targetObj
,
然后SendEvent
的Target Game Object
引用这个targetObj
变量,
而这个targetObj
变量的赋值,我们可以通过代码来赋值,例:
// go是目标物体GameObject对象,bt是自身行为树对象
SharedGameObject targetObj = new SharedGameObject();
targetObj.Value = go;
bt.SetVariable("targetObj", targetObj);
1.2、Event Name参数
Event Name
就是事件名称,自己定义一个字符串即可,建议为XXX_EVENT
。
1.3、Group参数
这个Group
参数又是干啥用的呢?因为一个物体可以挂多个BehaviorTree
组件,假设挂了3
个BehaviorTree
组件,我只想给其中的两个BehaviorTree
发送事件,这个时候就要用到Group
了。
我们可以在BehaviorTree
组件上看到有一个Group
参数,
我们可以给这些BehaviorTree
分组,默认Group
是0
,
如果SendEvent
的Group
也是0
,那么这个物体下所有Group
为0
的BehaviorTree
都会收到事件。
1.4、Argument参数
我们可以看到有三个Argument
参数,一般是用来做跨行为树发送事件时的数据传递用的。
比如A
打了B
,B
收到被打事件,但它并不知道是谁打它的,我们可以给它们定义一个playerId
成员变量,
注:这里你使用
GameObject
变量也可以,看具体需要~
发事件的时候把playerId
传递过去,B
在收到事件后会去读取这个参数,B
就知道是A
打了它了,
2、Has Receive Event节点
Has Receive Event
节点参数如下,
2.1、Event Name参数
Event Name
参数就是要监听的事件名称,与SendEvent
的Event Name
要对应上。
2.2、Stored Value参数
Stored Value
参数用来存储传递过来的数据的,与Send Event
的三个Argument
顺序对应。注意,这里是把传递过来的参数存储到Stored Value
设置的变量中,比如A
行为树传递了一个playerId
过来,表示打人者的Id
,B
行为树收到事件,把这个plaeyrId
存储到fromPlayerId
变量中,
然后我们可以在代码中通过B
行为树的GetVariable
方法读取到这个fromPlayerId
,
var fromPlayerId = (SharedInt)bt.GetVariable("fromPlayerId");
Debug.Log($"{fromPlayerId} 打了我");
3、代码中注册和注销事件
有时候我们需要在代码中自己注册事件的响应函数,可以调用行为树的RegisterEvent
方法,
public void RegisterEvent<T, U, V>(string name, Action<T, U, V> handler);
public void RegisterEvent<T, U>(string name, Action<T, U> handler);
public void RegisterEvent<T>(string name, Action<T> handler);
public void RegisterEvent(string name, System.Action handler);
对应的,要注销事件的响应函数,则调用UnregisterEvent
方法,
public void UnregisterEvent<T, U, V>(string name, Action<T, U, V> handler);
public void UnregisterEvent<T, U>(string name, Action<T, U> handler);
public void UnregisterEvent<T>(string name, Action<T> handler);
public void UnregisterEvent(string name, System.Action handler);
可以查阅Has Receive Event
节点的源码
十、拓展行为树节点
1、继承
实际项目开发过程中,我们一般都需要自己拓展一些行为树节点,我们知道,所有的节点都是继承Task
的,然后根据节点的功能分类,有细分了一些子类出来,比如复合节点的继承关系如下:
我们我们想要封装一个新的复合节点,也继承Composite
然后进行拓展即可。
同理,Action
类的节点也如此,
我们可以参考Log
节点、Wait
节点的写法,自行拓展自己的Action
节点。
2、案例:MoveTo
举个例子,现在要写一个移动主角到指定Transform
位置的Action
,首先创建一个MoveTo
脚本,
引入BehaviorDesigner
的命名空间,然后继承Action
,
using UnityEngine;
using BehaviorDesigner.Runtime;
using BehaviorDesigner.Runtime.Tasks;
// 移动主角到指定Transform位置
public class MoveTo: Action
{
}
接着定义一些必要的public
成员变量,这些变量会在Inspector
面板中显示,
using UnityEngine;
using BehaviorDesigner.Runtime;
using BehaviorDesigner.Runtime.Tasks;
// 移动主角到指定Transform位置
public class MoveTo : Action
{
public float speed = 0;
public SharedTransform target;
}
再接着,我们重写OnUpdate
方法,如下
public override TaskStatus OnUpdate()
{
if (Vector3.SqrMagnitude(transform.position - target.Value.position) < 0.1f) {
return TaskStatus.Success;
}
transform.position = Vector3.MoveTowards(transform.position, target.Value.position, speed * Time.deltaTime);
return TaskStatus.Running;
}
这样,我们自己封装的MoveTo
节点就完成了,
我们连个行为树,让Cub
来回移动,效果如下
3、官方Sample包
BehaviorDesigner
官方自己写了一些拓展包,我们可以把对应的包导入项目中使用,
这里分享给大家,
链接:https://pan.baidu.com/s/1mQUkxbeokOh6oyTmEG837w 提取码:oi74
十一、完毕
好了,就先写到这里吧,我是林新发:
一个在小公司默默奋斗的Unity
开发者,希望可以帮助更多想学Unity
的人,共勉~