【游戏开发教程】BehaviorDesigner插件制作AI行为树(Unity
文章目录
* – + 一、前言 + 二、插件下载 + * 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下载到,地址:https://assetstore.unity.com/packages/tools/visual-scripting/behavior-designer-behavior-trees-for-everyone-15277
目前最新的版本是 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;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; 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() { yield return Addressables.InitializeAsync(); var bt = gameObject.AddComponent(); bt.StartWhenEnabled = false; var loader = Addressables.LoadAssetAsync(“Assets/BtRes/Behavior.asset”); yield return loader; 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(string name, T arg1);public void SendEvent(string name, T arg1, U arg2);public void SendEvent(string name, T arg1, U arg2, V arg3);
示例:
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; public override float GetPriority() { return priority.Value; } public override TaskStatus OnUpdate() { 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() { 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类,
public class Blog{ public string author; public string url; public override string ToString() { return $”author: {author}, url: {url}”; }}
我想让他出现行为树 Variables的变量类型列表中,我们需要给它包装一层 SharedBlog。新建一个 SharedBlog脚本,
按照下面这个格式写即可:namespace BehaviorDesigner.Runtime{ [System.Serializable] public class SharedT : SharedVariable { public static implicit operator SharedT(T value) { return new SharedT { mValue = value }; } }}
SharedBlog代码如下:
namespace BehaviorDesigner.Runtime{ [System.Serializable] public class SharedBlog : SharedVariable { 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();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 = “https://blog.csdn.net/linxinfa”;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变量的赋值,我们可以通过代码来赋值,例: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(string name, Action handler);public void RegisterEvent(string name, Action handler);public void RegisterEvent(string name, Action handler);public void RegisterEvent(string name, System.Action handler);
对应的,要注销事件的响应函数,则调用 UnregisterEvent方法,
public void UnregisterEvent(string name, Action handler);public void UnregisterEvent(string name, Action handler);public void UnregisterEvent(string name, Action 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;public class MoveTo: Action{}
接着定义一些必要的 public成员变量,这些变量会在 Inspector面板中显示,
using UnityEngine;using BehaviorDesigner.Runtime;using BehaviorDesigner.Runtime.Tasks;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; 十一、完毕
好了,就先写到这里吧,我是林新发:https://blog.csdn.net/linxinfa一个在小公司默默奋斗的 Unity开发者,希望可以帮助更多想学 Unity的人,共勉~
Original: https://blog.csdn.net/linxinfa/article/details/124483690Author: 林新发Title: 【游戏开发教程】BehaviorDesigner插件制作AI行为树(Unity | 保姆级教程 | 动态图演示 | Unity2021最新版)
相关阅读Title: opencv-python3 | cv2.findContours()检测图像中物体轮廓cv2.findContours检测物体轮廓什么是物体轮廓cv2.findContourscv2.drawContours代码示例什么是物体轮廓
轮廓可以简单地理解为连接所有连续点(沿物体边界)的曲线,这些点通常具有相同的颜色或强度。 轮廓在图像分析中具有重要意义,是物体形状分析和对象检测和识别的有用工具,是理解图像语义信息的重要依据。
cv2.findContours
通常,为了提高物体轮廓检测的准确率,首先要将彩色图像或者灰度图像处理成二值图像(黑白图像)或者使用Canny边缘检测算法对原图像进行一次滤波处理,这样可以在不丢失轮廓信息的前提下降低图像语义信息的复杂度,更有助于我们准确地分析物体轮廓。因此,在opencv里边,寻找轮廓的过程更像是在黑色背景中寻找白色物体。下边是一段使用opencv-python里的cv2.findConttours()检测物体轮廓的代码。
import numpy as npimport cv2im = cv2.imread(‘test.jpg’)imgray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)ret, thresh = cv2.threshold(imgray, 127, 255, 0)contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
从上边的代码中,我们可以看到cv2.findContours()有三个参数:thresh、cv2.RETR_TREE、cv2.CHAIN_APPROX_SIMPLE。
参数说明:
thresh:图像数据(二值图像或经过Canny算法处理之后的图像)cv2.RETR_TREE:轮廓检索方式,还有cv2.RETR_LIST、cv2.RETR_EXTERNAL、cv2.RETR_CCOMPcv2.CHAIN_APPROX_SIMPLE:轮廓的估计方法,除此之外还有 cv2.CHAIN_APPROX_NONE
第二个参数指定的不同轮廓检索方法有什么区别呢?
轮廓检索方法作用cv2.RETR_LIST这是最简单的一种寻找方式,它不建立轮廓间的子属关系,也就是所有轮廓都属于同一层级cv2.RETR_TREE完整建立轮廓的层级从属关系cv2.RETR_EXTERNAL只寻找最高层级的轮廓cv2.RETR_CCOMP把所有的轮廓只分为2个层级,不是外层的就是里层的
详情请参考 cv2.findContours()的轮廓层级关系.
前边说了物体轮廓是具有相同灰度值的形状的边界。它是以形状边界上的点的坐标(x,y)储存的,但是cnts里边是储存了边界上所有点的坐标吗?还是只储存了个别点的坐标?这是由第三个参数轮廓的估计方法指定的。如果传递 cv2.CHAIN_APPROX_NONE,则存储所有边界点。 但实际上我们需要所有的点吗? 例如,您找到了一条直线的轮廓。 你需要线上的所有点来代表那条线吗? 不,我们只需要那条线的两个端点。 这就是 cv.CHAIN_APPROX_SIMPLE 所做的。 它去除所有冗余点并压缩轮廓,从而节省内存。如图1所示。
图1. 不同轮廓估计方法的效果图
cv2.findContours()返回了两个变量:contours, hierarchy。
输出变量说明:
contours:一个包含了图像中所有轮廓的list对象。其中每一个独立的轮廓信息以边界点坐标(x,y)的形式储存在numpy数组中。hierarchy:一个包含4个值的数组:[Next, Previous, First Child, Parent]。Next:与当前轮廓处于同一层级的下一条轮廓Previous:与当前轮廓处于同一层级的上一条轮廓First Child:当前轮廓的第一条子轮廓Parent:当前轮廓的父轮廓因为一般不使用hierarchy,所以这里不讨论轮廓的层级关系,想深入研究的朋友请移步:cv2.findContours()的轮廓层级关系.; cv2.drawContours
计算得到图像中物体轮廓之后,我们需要将轮廓在图像中绘制出来才能更直观地体验到。这时候需要用到cv2.drawContours()方法。它的第一个参数是图像,第二个参数是储存轮廓信息的python 列表,第三个参数是轮廓的索引(在绘制单个轮廓时很有用。要绘制所有轮廓,传递 -1),其余参数是颜色、厚度 等等。
绘制检索到的所有轮廓
cv.drawContours(img, contours, -1, (0,255,0), 3)
绘制检索到的所有轮廓中的第四个
cv.drawContours(img, contours, 3, (0,255,0), 3)
但是更多时候我们使用下边这种方法绘制单独的某一个轮廓。
第二种方法绘制检索到的所有轮廓中的第四个
cnt = contours[4]cv.drawContours(img, [cnt], 0, (0,255,0), 3)代码示例import cv2import imutilsimport numpy as npimg_dir = r’C:\Users\Lei\Desktop\8.jpg’img = cv2.imread(img_dir)img = imutils.resize(img, height=500)gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)gray = cv2.GaussianBlur(gray, (5, 5), 0)binary = cv2.Canny(gray, 30, 120)contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)cv2.imshow(‘origin’, img)if cv2.waitKey(0) & 0xFF == ord(‘q’): cv2.destroyWindow(‘origin’)cv2.imshow(‘binary’, binary)if cv2.waitKey(0) & 0xFF == ord(‘q’): cv2.destroyWindow(‘binary’)draw_img = img.copy()for i in range(len(contours)): area = cv2.contourArea(contours[i]) if area < 800: continue rect = cv2.minAreaRect(contours[i]) box = cv2.boxPoints(rect) box = np.int0(box) cv2.drawContours(draw_img, [box], 0, (0, 0, 255), 5)cv2.imshow('origin with contours', draw_img)if cv2.waitKey(0) & 0xFF == ord('q'): cv2.destroyWindow('origin with contours')
代码中首先对读取的RGB图像(图2)转灰度图,然后进行高斯滤波去噪,再使用Canny算子进行边缘检测得到黑白图像(图3)。对黑白图像进行轮廓检索,检索到的轮廓再根据cv2.contourArea()计算得到的面积大小进行一次筛选,去掉因噪声引起的检测。根据检测到的轮廓信息,使用cv2.minAreaRect()得到包含轮廓信息的最小矩形框rect,再使用cv2.boxPoints()计算出rect的四个顶点。最后,使用cv2.drawContours()绘制出rect(图4)。
图2. RGB原图像图3. Canny算子得到的黑白图像图4. 使用矩形框显示轮廓检测结果
Original: https://blog.csdn.net/Just_do_myself/article/details/124215020Author: 一颗磐石Title: opencv-python3 | cv2.findContours()检测图像中物体轮廓
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/269384/
转载文章受原作者版权保护。转载请注明原作者出处!