行为树是个节点树,父节点通过不断遍历子节点,根据不同类型的节点执行不同的分支。最终调用叶节点执行功能。行为树也不难理解,他就像代码逻辑一样,只是用节点的方式展现出来,而且比代码更直观。如果行为树中写有各种行为功能的节点的话,即便没有写过代码的,稍微学习一下,只用行为树也可以做出具有一定的智能行为的角色。
行为树从上到下,从左到右执行。
行为树采用节点描述行为逻辑。
主要有:选择节点、顺序节点、并行节点、修饰节点、随机节点、条件节点、行为节点。
一棵行为树表示一个AI逻辑。要执行这个 AI 逻辑,需要从根节点开始遍历整棵树,遍历执行的过程中,父节点根据自身的类型,确定需要如何执行、执行哪些子节点并继续执行,子节点执行完毕后,会将执行结果反馈给父节点。
- 行为树 Behavior Tree 原理 一
如图,可大致看出角色的行为逻辑。而且添加更多行为时,只用在节点中再添加即可,可扩展性非常高。
节点执行后有三种结果:
SUCCEED
(执行成功)FAILED
(执行失败)RUNNING
(正在执行)Composite(组合节点)
组合节点用来控制树的遍历方式,每种组合节点的遍历方式都不相同。一般有以下几个节点。
如果结果为 RUNNING
,则下一帧仍从这个节点开始运行。
Sequence
(顺序节点):按照节点顺序执行,如果有一个结果为 FAILED
,则中断执行,返回 FAILED
,类似于“逻辑与(And)”。Selector
(选择节点):按照节点顺序执行,如果有一个结果为 SUCCEED
,则中断执行,返回 SUCCEED
,类似于“逻辑或(Or)”。Parallel
(并行节点):子节点中有一个结果为 FAILED
,则中断执行返回 FAILED
。RUNNING
,则执行完返回 RUNNING
。SUCCEED
,结果返回 SUCCEED
。Decorator(修饰节点):修饰节点(Decorator)修饰节点不能独立存在,其作用为对子节点进行修饰,以得到我们所希望的结果.
修饰节点有很多种,其中有一些是用于决定是否允许子节点运行的,也叫过滤器,例如 Until Success
, Until Fail
等,首先确定需要的结果,循环执行子节点,直到节点返回的结果和需要的结果相同时向父节点返回需要的结果,否则返回 RUNNING
。
Inverter
(反转):任务执行结果如果为 SUCCEED
,则结果转为 FAILED
;任务执行结果如果为 FAILED
,则结果转为 SUCCEED
;结果为 RUNNING
则不变。UntilSuccess
(直到成功):一直执行,返回 RUNNING
,直到结果为 SUCCEED
。UntilFail
(直到失败):一直执行,返回 RUNNING
,直到结果为 FAILED
。Counter
(计数):重复执行子节点多次。Leaf(叶节点):对叶节点进行重写,以进行逻辑判断和功能的执行。
Condition
(条件节点):判断条件是否成立,只返回 SUCCEED
,FAILED
这两种状态。Action
(行为节点):控制节点,执行各种功能。开始进行节点的设计,我们先在 文件系统
中创建一个 src
文件夹,我们之后创建的脚本都放在这个文件夹里。
通过上面的信息,我们可以添加任务结果枚举,如下的 enum{}
内容,添加 _task()
方法执行任务,并返回执行结果。剩下添加 root
、actor
用于可能会操作用到的变量。
脚本名:BT_Node.gd
## BTNode 行为树的基类节点
extends Node
## 任务执行结果
enum {
SUCCEED, # 执行成功
FAILED, # 执行败
RUNNING, # 正在执行
}
var root # 节点的根节点
var actor # 控制的节点
var task_idx = 0 # 当前执行的 task 的 index(执行的第几个节点)
## 节点的任务,返回执行结果
func _task() -> int:
return SUCCEED
组合节点,控制树的遍历方式。
脚本名:Composite_Sequence.gd
## Sequence 执行成功则继续执行,执行一次失败则返回失败
extends "BT_Node.gd"
var result = SUCCEED
func _task():
while task_idx < get_child_count():
result = get_child(task_idx)._task()
# 执行成功继续执行下一个,直到失败或束
if result == SUCCEED:
task_idx += 1
else:
break
if task_idx >= get_child_count() || result == FAILED:
task_idx = 0
if result == FAILED:
return FAILED
# 如果都没有执行失败的,则回 SUCCEED
return SUCCEED
脚本名:Composite_Selector.gd
## Selector 执行失败则继续执行,执行一次成功则返回成功
extends "BT_Node.gd"
var result = FAILED
func _task():
while task_idx < get_child_count():
result = get_child(task_idx)._task()
# 执行失败继续执行下一个,直到成功败或结束
if result == FAILED:
task_idx += 1
else:
break
if task_idx >= get_child_count() || result == SUCCEED:
task_idx = 0
if result == SUCCEED:
return SUCCEED
# 如果都没有成功执行的,则回 FAILED
return FAILED
脚本名:Composite_Parallel.gd
## Paraller 并行节点,全部节点都执行一遍
extends "BT_Node.gd"
var result = SUCCEED
func _task():
var is_running = false
# 运行全部子节点,有一个为失败,则返回 FAILED
for task_idx in get_child_count():
var node = get_child(task_idx)
result = get_child(task_idx)._task()
if result == FAILED:
return FAILED
elif result == RUNNING:
is_running = true
# 如果有运行的节点则返回 RUNNING
if is_running:
return RUNNING
# 如果全部都是成功状态,则返回 SUCCEE
return SUCCEED
改变子节点任务执行的结果。以下做两个可能会用到的两个节点。
脚本名:Decorator_Inverter.gd
## Inverter 取反
extends "BT_Node.gd"
var result
func _task():
result = get_child(0)._task()
# 如果 成功,则返回 失败
if result == SUCCEED:
return FAILED
# 如果 失败,则返回 成功
elif result == FAILED:
return SUCCEED
else:
return RUNNING
脚本名:Decorator_Counter.gd
在这里,这个脚本中其实可以只写 run_task()
方法中的代码,可以少写一半代码。我是额外写了一个 _run_loop()
方法,供更多不同情况的需求。
## Counter 计数器,运行指定次数
extends "BT_Node.gd"
## 执行类型
enum RunType {
TaskCount, # 任务执行次数(多帧的时间执行完最大次数)
LoopCount, # 循环次数(一帧时间执行完最大次数)
}
export (RunType) var run_type = RunType.TaskCount
export var max_count = 3 # 执行最大次数
var run_func : FuncRef
var count = 0 # 执行节点的次数
func _ready():
if run_type == RunType.LoopCount:
run_func = funcref(self, "_run_loop")
elif run_type == RunType.TaskCount:
run_func = funcref(self, "_run_task")
func _task():
return run_func.call_func()
func _run_loop():
count = 0
while count < max_count:
# 计数
count += 1
if get_child(0)._task() == FAILED:
return FAILED
return SUCCEED
func _run_task():
var result = get_child(0)._task()
count += 1
if result == FAILED:
count = 0
return FAILED
if count < max_count:
return RUNNING
else:
count = 0
return SUCCEED
在设计 Leaf 节点之前,Leaf 需要用到 Blackboard 进行存储数据,所以我们设计一下 Blackboard。
脚本名:Blackboard.gd
## Blackboard 黑板,存储数据
extends Reference
var data = {} # 存放数据
脚本名:BT_Leaf.gd
叶节点,用户重写相关的方法,实现各种功能。
## BT_Leaf 行为树叶节点,用于重写行为树
extends "BT_Node.gd"
var blackboard = null # 黑板(记录设置数据,用于 Action 节点中)
#==================================================
# Set/Get
#==================================================
## 设置黑板
func set_blackboard(value):
blackboard = value
## 设置数据
func set_data(property: String, value):
blackboard.data[property] = value
## 获取数据
func get_data(property: String):
return blackboard.data[property]
#==================================================
# 自定义方法
#==================================================
func _task():
return SUCCEED
脚本名:Leaf_Condition.gd
## Condition 条件节点
extends "BT_Leaf.gd"
func _task():
return SUCCEED if condition() else FAILED
# 重写这个方法
func condition() -> bool:
return true
脚本名:Leaf_Action.gd
## Action 控制节点,执行功能
extends "BT_Leaf.gd"
func _task():
return action(get_viewport().get_physics_process_delta_time())
# 重写这个方法
func action(delta: float) -> int:
return SUCCEED
最后设计用于运行整个节点数的根节点。
脚本名:BT_Root.gd
用来执行驱动整个行为树节点
## Root 根节点
extends "BT_Node.gd"
const Blackboard = preload("Blackboard.gd")
var blackboard # 全局行为树黑板
##==================================================
# 内置方法
##==================================================
func _ready():
_init_data()
_init_node(self)
func _physics_process(delta):
get_child(0)._task()
##==================================================
# 自定义方法
##==================================================
## 初始化当前数据
func _init_data():
root = self
actor = get_parent()
blackboard = Blackboard.new()
## 初始化节点
func _init_node(node: Node):
node.actor = self.actor
node.root = self.root
if node.has_method("set_blackboard"):
node.blackboard = self.blackboard
# 不断向子节点迭代,对节点树中的所有节点进行初始化设置
for child in node.get_children():
_init_node(child)
至此,行为树设计结束。
相关学习链接: