Godot 4 插件 - Utility AI 研究

今天看到一个视频教学

Godot4 | 实现简单AI | Utility AI 插件_哔哩哔哩_bilibili

就看了一下。吸引我的不是插件,是AI这两个字母。这AI与Godot怎么结合?感觉还是离线使用,值得一看。

视频时间不长,15分钟左右,看得我云山雾罩,不过演示项目能直接下载(AI Demo.zip官方版下载丨最新版下载丨绿色版下载丨APP下载-123云盘)

下载下来,能运行,是个小游戏,不过逻辑没大看明白,可能以后看明白后会觉得很简单,但初接触,里面的弯弯绕那么多,一时不好理。

看介绍里还有一个插件自带Demo(godot-utility-ai-examples.zip官方版下载丨最新版下载丨绿色版下载丨APP下载-123云盘),感觉会简单一些。下载打开一看,果然简单很多。

插件自带Demo

因为Demo就一个场景AgentExample,且子节点就两个,这样就清爽了。

 不过运行一下,感觉没啥吸引力,就几个数字在那里变来变去。怎么能与AI挂上钩?

肯定是我理解的问题,再看一下

主场景的脚本很简单

func _ready():
	var needs: AgentNeeds = $Agent.needs
	needs.food_changed.connect(%FoodBar._on_needs_changed)
	needs.fun_changed.connect(%FunBar._on_needs_changed)
	needs.energy_changed.connect(%EnergyBar._on_needs_changed)

	$Agent.state_changed.connect(%StateLabel._on_state_changed)

就是把几个进度条的显示与needs的相应信号绑定到一起了,每个显示的处理逻辑都是一样的

func _on_needs_changed(p_value: float) -> void:
	value = p_value

这好象没啥,数据正常显示。

哦,数据怎么来的?这个needs变量是AgentNeeds类型,从agent_needs.gd来看,这是一个Resource。

# Copyright (c) 2023 John Pennycook
# SPDX-License-Identifier: 0BSD
class_name AgentNeeds
extends Resource


signal food_changed(value)
signal fun_changed(value)
signal energy_changed(value)


@export var food := 0.5 : set = _set_food
@export var fun := 0.5 : set = _set_fun
@export var energy := 0.5 : set = _set_energy


func _set_food(p_food: float) -> void:
	food = clamp(p_food, 0.0, 1.0)
	food_changed.emit(food)


func _set_fun(p_fun: float) -> void:
	fun = clamp(p_fun, 0.0, 1.0)
	fun_changed.emit(fun)


func _set_energy(p_energy: float) -> void:
	energy = clamp(p_energy, 0.0, 1.0)
	energy_changed.emit(energy)

Godot有点意思,在资源里还带有逻辑。这不闹嘛,还是脚本。在理解的领域,把资源与脚本画一个约等于符号。

这个资源有三个属性,对应三个写方法,然后会触发三个相应的信号。仅此而已。这还是没有看到数据的起源。

再看一下脚本情况,还剩下一个agent.gd,是绑定到Agent节点的脚本。难道这里还有入口?

哦,看到Agent节点下还有一个Timer节点,那想必应该一定是这个Timer节点在不断做啥事。打开脚本看下,果然

# Copyright (c) 2023 John Pennycook
# SPDX-License-Identifier: 0BSD
class_name Agent
extends Node2D


signal state_changed(state)


enum State {
	NONE,
	EATING,
	SLEEPING,
	WATCHING_TV,
}


@export var needs: AgentNeeds
var state: State = State.EATING


var _time_until_next_decision: int = 1


@onready var _options: Array[UtilityAIOption] = [
	UtilityAIOption.new(
		preload("res://examples/agents/eat.tres"), needs, eat
	),
	UtilityAIOption.new(
		preload("res://examples/agents/sleep.tres"), needs, sleep
	),
	UtilityAIOption.new(
		preload("res://examples/agents/watch_tv.tres"), needs, watch_tv
	),
]


func eat():
	state = State.EATING
	_time_until_next_decision = 5
	state_changed.emit(state)


func sleep():
	state = State.SLEEPING
	_time_until_next_decision = 10
	state_changed.emit(state)


func watch_tv():
	state = State.WATCHING_TV
	_time_until_next_decision = 1
	state_changed.emit(state)


func _on_timer_timeout():
	# Adjust the agent's needs based on their state.
	# In a real project, this would be managed by something more sophisticated!
	if state == State.EATING:
		needs.food += 0.05
	else:
		needs.food -= 0.025

	if state == State.SLEEPING:
		needs.energy += 0.05
	else:
		needs.energy -= 0.025

	if state == State.WATCHING_TV:
		needs.fun += 0.05
	else:
		needs.fun -= 0.025

	# Check if the agent should change state.
	# Utility helps the agent decide what to do next, but the rules of the game
	# govern when those decisions should happen. In this example, each action
	# takes a certain amount of time to complete, but the agent will abandon
	# eating or sleeping when the associated needs bar is full.
	if (
		(state == State.SLEEPING and needs.energy == 1)
		or (state == State.EATING and needs.food == 1)
	):
		_time_until_next_decision = 0

	if _time_until_next_decision > 0:
		_time_until_next_decision -= 1
		return

	# Choose the action with the highest utility, and change state.
	var decision := UtilityAI.choose_highest(_options)
	decision.action.call()

在Timer的时钟事件中,根据当前的状态,修改相应属性值,这样界面上的数据就不断变化。

看代码时,发现还有个_time_until_next_decision变量,看名字其作用就是下决定的时间。真实逻辑是

	if _time_until_next_decision > 0:
		_time_until_next_decision -= 1
		return

	# Choose the action with the highest utility, and change state.
	var decision := UtilityAI.choose_highest(_options)
	decision.action.call()

即,_time_until_next_decision <= 0的情况下,会进行decision计算,否则不计算,保持现状。大概应该是这个意思。

但decision计算是要干啥?UtilityAI.choose_highest(_options),应该是在几个选项中选最优先的项,或者说是最紧要的项,最重要的项。可以看到_options的定义

@onready var _options: Array[UtilityAIOption] = [
	UtilityAIOption.new(
		preload("res://examples/agents/eat.tres"), needs, eat
	),
	UtilityAIOption.new(
		preload("res://examples/agents/sleep.tres"), needs, sleep
	),
	UtilityAIOption.new(
		preload("res://examples/agents/watch_tv.tres"), needs, watch_tv
	),
]

就三项,对于就eat、sleep、watch_tv三个逻辑,这些逻辑最终都会发出信号state_changed,该信号绑定到主场景脚本中的%StateLabel._on_state_changed,简单显示一下内容

func _on_state_changed(state: Agent.State) -> void:
	match state:
		Agent.State.EATING:
			text = "Eat"
		Agent.State.SLEEPING:
			text = "Sleep"
		Agent.State.WATCHING_TV:
			text = "Watch TV"

这下,基本弄明白了,核心就是定义_options选项,然后用UtilityAI.choose_highest(_options)取得目标选项,触发相应逻辑。

好象明白了,又好象没明白,仔细再琢磨一下,才发现,UtilityAI.choose_highest(_options)这个最重要的函数,它是怎么工作的?它凭啥能选出最紧要、重要的选项,这个过程,程序员能设计些什么?

这个答案肯定不能在UtilityAI的代码中去找,因为UtilityAI肯定是通用的处理方式,刚才这些选项是业务相关的,应该是程序员处理的事

回过头再看下_options的定义,里面有几个UtilityAIOption,带有一个tres参数。跟进查看源码,UtilityAIOption一共有三个参数:behavior、context、action

func _init(
	p_behavior: UtilityAIBehavior = null,
	p_context: Variant = null,
	p_action: Variant = null
):
	behavior = p_behavior
	context = p_context
	action = p_action

而UtilityAI.choose_highest(_options)是一个类函数

static func choose_highest(
	options: Array[UtilityAIOption], tolerance: float = 0.0
) -> UtilityAIOption:
	# Calculate the scores for every option.
	var scores := {}
	for option in options:
		scores[option] = option.evaluate()

	# Identify the highest-scoring options by sorting them.
	options.sort_custom(func(a, b): return scores[a] < scores[b])

	# Choose randomly between all options within the specified tolerance.
	var high_score: float = scores[options[len(options) - 1]]
	var within_tolerance := func(o): return (
		absf(high_score - scores[o]) <= tolerance
	)
	return options.filter(within_tolerance).pick_random()

它分别通过各选项的option.evaluate()计算出各选项的实时值。然后从低到高排序,如果有容许误差(tolerance),则过滤筛选,可能结果不止一个,则pick_random随机选一个。

所以,还得看各选项option.evaluate()是如何工作的。

func evaluate() -> float:
	return behavior.evaluate(context)
func evaluate(context: Variant) -> float:
	var scores: Array[float] = []
	for consideration in considerations:
		var score := consideration.evaluate(context)
		scores.append(score)
	return _aggregate(scores)

各个behavior根据context进行计算,其各个考虑因子consideration(UtilityAIConsideration)分别计算得到结果,成为一个数列scores: Array[float],再根据aggregation类型确定最终结果的生成逻辑

func _aggregate(scores: Array[float]) -> float:
	match aggregation:
		AggregationType.PRODUCT:
			return scores.reduce(func(accum, x): return accum * x)

		AggregationType.AVERAGE:
			return scores.reduce(func(accum, x): return accum + x) / len(scores)

		AggregationType.MAXIMUM:
			return scores.max()

		AggregationType.MINIMUM:
			return scores.min()

	push_error("Unrecognized AggregationType: %d" % [aggregation])
	return 0

这里用到Array.reduce函数,以前没用过这个函数,所以不太清楚这些代码的结果。但问下ChatGPT,了解了:

所以,最终的问题是:behavior中的各consideration是啥,怎么来的?

回到_options的定义

@onready var _options: Array[UtilityAIOption] = [
	UtilityAIOption.new(
		preload("res://examples/agents/eat.tres"), needs, eat
	),
	UtilityAIOption.new(
		preload("res://examples/agents/sleep.tres"), needs, sleep
	),
	UtilityAIOption.new(
		preload("res://examples/agents/watch_tv.tres"), needs, watch_tv
	),
]

应该从这三个tres中找答案。比如eat.tres

这就对上了,原来在这里定义了各要素:Aggregation为Product,表示最终结果连乘。不过只有一个Consideration,所以连不连的也就一样了。

sleep.tres、watch_tv.tres也同样理解。

这里面还有一点,就是各Consideration的定义,它是用图表示出来的,看起来很直观,其实不太好定量理解,这个既然是算法逻辑,那还是精确一些好理解,但画成图形,尤其是还有一大堆参数可调,就感觉不好控制了。不过目前暂看图形曲线,能看到IO大概关系,参数什么的暂不关心。

到此,整个流程清晰了:

1. Agent的Timer周期性(1s)处理:

1.1 每秒根据状态,调整needs的food、energy、fun三个属性,从而触发needs的三个信号。这三个信号绑定到界面的三个进度条,从而三个进度条显示相应属性值大小

1.2 决策时刻(秒)减1。如果<=0,则进行决策,决策结果会影响状态。而决策过程就是UtilityAI.choose_highest(_options),即各选项自行根据输入计算得到自己的输出,然后由UtilityAI筛选出目标选项。确定后,触发目标选项的action(分别动态赋值为agent.gd中的eat、sleep、watch_tv函数),更新相应状态并触发信号,由主场景的_on_state_changed函数显示相应的状态信息。

B站AI Demo

现在回来看B站的Demo项目。现在回来,直接看重点:agent的tres

一共有三个tres:attack、chase、run_away,那应该会有三个状态,结果是4个

enum State {
	IDLE,
	CHASE,
	RUN_AWAY,
	ATTACK,
}

这也不能说是理解错误,反而是十分正确与准确。

attack.tres是Product模式,一个Consideration,嗯,很好理解

chase.tres是Product模式,三个Considerations,run_away.tres是Product模式,四个Considerations,同样好理解。这些就是在各选项的实时计算时的依据。

下来就是看各选项的定义,肯定会与这三个tres有关

@onready var _options: Array[UtilityAIOption] = [
	UtilityAIOption.new(
		preload("res://Enemy/agent/attack.tres"), needs, attack
	),
	UtilityAIOption.new(
		preload("res://Enemy/agent/chase.tres"), needs, chase
	),
	UtilityAIOption.new(
		preload("res://Enemy/agent/run_away.tres"), needs, run_away
	)
]

 果真如此。这里的needs为输入,第三个参数将在相应的选项被选中后调用。

func idle():
	state = State.IDLE
	state_changed.emit(state)


func chase():
	state = State.CHASE
	state_changed.emit(state)


func run_away():
	state = State.RUN_AWAY
	state_changed.emit(state)


func attack():
	state = State.ATTACK
	state_changed.emit(state)

一看就是熟悉的味道。不过翻遍了代码,也没看到state_changed的绑定处理函数。难道是没有用这个信号?原来视频里提醒过了:信号没有使用。那好吧,这就是只改变内部的状态,外部不需要显示或处理这个信号。

同样,不用猜,还会有一个Timer来处理。该Timer的时钟周期为0.4s:

func _on_timer_timeout() -> void:
	var needs_info = get_parent().get_ai_needs()
	
	for key in needs_info.keys():
		needs.set(key, needs_info[key])
	
	var decision := UtilityAI.choose_highest(_options)
	decision.action.call()

与自带Demo的区别在于,这里的_options中的needs输入,是从父场景中取得的get_parent().get_ai_needs(), 相当于父场景提供实时输入数据

func get_ai_needs() -> Dictionary:
	return {
		"my_hp": hp / enemy_hp,
		"player_hp": _player_node.hp / _player_node.max_hp,
		"partners": 1.0 if _partners > 3 else _partners / 3,
		"could_hit_player": _could_hit_player,
		"could_run_away": _could_run_away,
	}

这个UtilityAI的任务好象就完成了:时钟中获取实时数据,判断目标选项,调用目标选项的action,其中完成内部的状态改变。

这是什么AI?感觉就是一个简单的逻辑

再看了一下Demo项目,感觉内容比较多,主要是碰撞相关内容处理、动画效果展示,还有就是路径规划。呃,路径规划_make_path,是不是AI的工作呢,看看源码,原来是NavigationAgent2D的功劳,与AI无关。

@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D

func _make_path() -> void:
	match $Agent.state:
		1:
			nav_agent.target_position = _player_node.global_position
		2:
			var _partner_nodes = get_tree().get_nodes_in_group("enemy")
			if len(_partner_nodes) == 1:
				_could_run_away = 0.0
			else:
				var _partner = [null, INF]
				for _pt in _partner_nodes:
					if _pt == self:
						continue
					
					var _partner_distance = global_position.distance_to(_pt.global_position)
					if _partner_distance < _partner[1]:
						_partner[0] = _pt
						_partner[1] = _partner_distance
					
					nav_agent.target_position = _partner[0].global_position
					_could_run_away = 1.0

但,好吧,说是AI就是AI吧,毕竟那些输出都是计算机算出来的

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/49803.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C#——多线程之Task

C#——多线程之Task 前言一、Task是什么&#xff1f;二、各应用场景以及实例分析1.异步执行代码2.等待异步操作完成3.并行执行多个任务4.处理异常5.取消异步操作 三、一些其他问题1.WhenAll与WhenAny的区别 总结 前言 在代码编写过程中&#xff0c;经常会用到多线程的知识&…

初步了解C++模板

一、函数模板 如果我们要写一个交换两个变量值的函数Swap&#xff0c;那么我们得对每一种类型都写一个&#xff0c;以便适用不同类型的参数&#xff0c;但是有了模板之后&#xff0c;可以简化操作 template<class T> void Swap(T& x, T& y) {T tmp x;x y;y …

第J2周:ResNet50V2算法实战与解析

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f366; 参考文章&#xff1a;365天深度学习训练营-第J2周&#xff1a;ResNet50V2算法实战与解析&#x1f356; 原作者&#xff1a;K同学啊|接辅导、项目定制 目录 一、论文解读1. ResNetV2结构与…

Linux下查找python路径

本地目前装了几个版本的python&#xff0c;这里记录下查找python路径的方法。 1&#xff1a;whereis命令 whereis python2&#xff1a;which命令 which python与whereis相似&#xff0c;但which会返回第一个找到的执行文件的位置。 3&#xff1a;find命令 find命令可以搜索系…

CenOS设置启动级别

背景知识 init一共分为7个级别&#xff0c;这7个级别的所代表的含义如下 0&#xff1a;停机或者关机&#xff08;千万不能将initdefault设置为0&#xff09;1&#xff1a;单用户模式&#xff0c;只root用户进行维护2&#xff1a;多用户模式&#xff0c;不能使用NFS(Net File S…

【Docker】Docker相关基础命令

目录 一、Docker服务相关命令 1、启动docker服务 2、停止docker服务 3、重启docker服务 4、查看docker服务状态 5、开机自启动docker服务 二、Images镜像相关命令 1、查看镜像 2、拉取镜像 3、搜索镜像 4、删除镜像 三、Container容器相关命令 1、创建容器 2、查…

【C++】开源:Linux端ALSA音频处理库

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍Linux端ALSA音频处理库。 无专精则不能成&#xff0c;无涉猎则不能通。。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&#xff0c…

Windows数据类型LPSTR学习

Windows在C语言的基础之上又定义了一些Windows下的数据类型&#xff1b;下面学习一下LPSTR&#xff1b; LPSTR和LPWSTR是Win32和VC所使用的一种字符串数据类型。LPSTR被定义成是一个指向以NULL(‘\0’)结尾的32位ANSI字符数组指针&#xff0c;而LPWSTR是一个指向以NULL结尾的64…

代码版本管理工具 git

1. 去B站看视频学习&#xff0c;只看前39集&#xff1a; 01-Git概述&#xff08;Git历史&#xff09;_哔哩哔哩_bilibili 2.学习Linux系统文本编辑器的使用 vi编辑器操作指令分享 (baidu.com) (13条消息) nano编辑器的使用_SudekiMing的博客-CSDN博客 windows下载安装Git官…

电路原理分析1

d2的作用是提供一个1.25v的电平 r3、r4的作用都是限流 c1是滤波 运放的4、8脚是常规的外围 这个运放是一个运算放大电路 具体计算是这样的&#xff1a; 按照虚短原则&#xff0c;输入的信号Uinu1,输出的信号Uoutu3 按照虚断原则&#xff0c;i1i2i5i5 u1/r2i1i5&#xff…

Longhorn vs Rook vs OpenEBS vs Portworx vs IOMesh:细说 5 款 K8s 持久化存储产品优劣势

云原生时代下&#xff0c;越来越多的企业开始使用 Kubernetes&#xff08;K8s&#xff09;承载数据库、消息中间件等“生产级”有状态工作负载。由于这些应用对数据持久保存、性能、容量扩展和快速交付具有较高的要求&#xff0c;企业往往需要采用专为 Kubernetes 环境设计的持…

wxwidgets Ribbon使用简单实例

// RibbonSample.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include <wx/wx.h> #include "wx/wxprec.h" #include "wx/app.h" #include "wx/frame.h" #include "wx/textctrl.h" #include "…

教雅川学缠论02-K线

传统行情上的K线是下图中这样子的 而在缠论中K线是下面这样子的&#xff0c;它没有上影线和下影线 下图是武汉控股2023年7月的日K线 接下来我们将它转换成缠论K线&#xff08;画图累死我了&#xff09; K线理解了我们才能进行下一步&#xff0c;目前位置应该很好理解的

压缩算法的原理丨基因型vcf文件为什么压缩后发生了什么?

压缩算法的本质 最近碰到一个神奇的现象&#xff0c;一份大小为16GB的xx.vcf.gz文件&#xff0c;解压之后体积变为600GB的vcf文件&#xff0c;为什么一份文件经过压缩后体积缩小了这么多&#xff1f; (work) [bio notes 21:29:40 ~/work/20230726/data]$ ls -lh总用量 620GB-…

OnnxRuntime TensorRT OpenCV::DNN性能对比(YoloV8)实测

1. 前言 之前把ORT的一套推理环境框架搭好了,在项目中也运行得非常愉快,实现了cpu/gpu,fp32/fp16的推理运算,同onnx通用模型在不同推理框架下的性能差异对比贴一下,记录一下自己对各种推理框架的学习状况 YoloV8模型大小 模型名称参数量NANO3.2M...... 2. CPU篇 CPU推理框架性…

机器人状态估计:robot_localization 功能包高级参数详解

机器人状态估计&#xff1a;robot_localization 功能包高级参数详解 前言功能包简介相关参数高级参数 前言 移动机器人的状态估计需要用到很多传感器&#xff0c;因为对单一的传感器来讲&#xff0c;都存在各自的优缺点&#xff0c;所以需要一种多传感器融合技术&#xff0c;将…

扫地机语音提示芯片,智能家居语音交互首选方案,WT588F02B-8S

智能家居已经成为现代家庭不可或缺的一部分&#xff0c;而语音交互技术正是智能家居的核心。在智能家居设备中&#xff0c;扫地机无疑是最受欢迎的产品之一。然而&#xff0c;要实现一个更智能的扫地机&#xff0c;需要一颗语音提示芯片&#xff0c;以提供高质量的语音交互体验…

【MySQL】表的内外连接

目录 一、内连接二、外连接2.1 左外连接2.2 右外连接 三、OJ题 表的连接分为内连和外连 一、内连接 内连接实际上就是利用where子句对两种表形成的笛卡儿积进行筛选&#xff0c;我们前面学习的查询都是内连接&#xff0c;也是在开发过程中使用的最多的连接查询。 语法&#x…

Feign API模块导入的两种方式

说明&#xff1a;在微服务框架中&#xff0c;会把其他微服务用到的FeignClient统一放到一个模块里面&#xff0c;称为FeignAPI&#xff0c;其他微服务需要使用FeignClient&#xff0c;可以引入FeignAPI的Maven坐标。 但是只引入FeignAPI的坐标还不行&#xff0c;FeignAPI中的B…

自动化测试:让软件测试更高效更愉快!

谈谈那些实习测试工程师应该掌握的基础知识&#xff08;一&#xff09;_什么时候才能变强的博客-CSDN博客https://blog.csdn.net/qq_17496235/article/details/131839453谈谈那些实习测试工程师应该掌握的基础知识&#xff08;二&#xff09;_什么时候才能变强的博客-CSDN博客h…