Godot行为树

Posted 张学徒

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Godot行为树相关的知识,希望对你有一定的参考价值。

行为树介绍

行为树是个节点树,父节点通过不断遍历子节点,根据不同类型的节点执行不同的分支。最终调用叶节点执行功能。行为树也不难理解,他就像代码逻辑一样,只是用节点的方式展现出来,而且比代码更直观。如果行为树中写有各种行为功能的节点的话,即便没有写过代码的,稍微学习一下,只用行为树也可以做出具有一定的智能行为的角色。

行为树从上到下,从左到右执行。

行为树采用节点描述行为逻辑。

主要有:选择节点、顺序节点、并行节点、修饰节点、随机节点、条件节点、行为节点。

一棵行为树表示一个AI逻辑。要执行这个 AI 逻辑,需要从根节点开始遍历整棵树,遍历执行的过程中,父节点根据自身的类型,确定需要如何执行、执行哪些子节点并继续执行,子节点执行完毕后,会将执行结果反馈给父节点。

如图,可大致看出角色的行为逻辑。而且添加更多行为时,只用在节点中再添加即可,可扩展性非常高。
在这里插入图片描述

执行结果

节点执行后有三种结果:

  • 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(计数):重复执行子节点多次。
    • …(可以自行设计其他更多符合自己需求的Decorator节点)
  • Leaf(叶节点):对叶节点进行重写,以进行逻辑判断和功能的执行。

    • Condition(条件节点):判断条件是否成立,只返回 SUCCEEDFAILED 这两种状态。
    • Action(行为节点):控制节点,执行各种功能。

其他类

  • Blackboard(黑板):存储节点树中的全局数据。
  • Root(行为树根节点):用来运行整个行为树。

节点脚本

开始进行节点的设计,我们先在 文件系统 中创建一个 src 文件夹,我们之后创建的脚本都放在这个文件夹里
在这里插入图片描述

通过上面的信息,我们可以添加任务结果枚举,如下的 enum{} 内容,添加 _task() 方法执行任务,并返回执行结果。剩下添加 rootactor 用于可能会操作用到的变量。

行为树节点的基类

脚本名: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 节点

脚本名: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
	
	# 如果都没有执行失败的,则回 SUCCEED
	return SUCCEED
Selector 节点

脚本名: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
	
	# 如果都没有成功执行的,则回 FAILED
	return FAILED
Parallel 节点

脚本名: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 节点

脚本名: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
Counter 节点

脚本名: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

脚本名:Blackboard.gd

## Blackboard 黑板,存储数据
extends Reference

var data = {}	# 存放数据

Leaf 节点

脚本名: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
Condition 节点

脚本名:Leaf_Condition.gd

## Condition 条件节点
extends "BT_Leaf.gd"


func _task():
	return SUCCEED if condition() else FAILED


# 重写这个方法
func condition() -> bool:
	return true
Action 节点

脚本名: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

最后设计用于运行整个节点数的根节点。

Root 节点

脚本名: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)

至此,行为树设计结束。


相关学习链接:

以上是关于Godot行为树的主要内容,如果未能解决你的问题,请参考以下文章

Godot FunctionTree 功能树 简单使用教程

Godot FunctionTree 功能树 简单使用教程

godot新手教程2[godot常用代码用法]

Godot实用代码1000例

Godot使用代码控制点击按钮

Godot组合键的实现