萍乡网站建设哪家公司好,外贸常用社交网站有哪些,大连是哪个省,重庆建设教育培训管理系统网站今天看到一个视频教学
Godot4 | 实现简单AI | Utility AI 插件_哔哩哔哩_bilibili
就看了一下。吸引我的不是插件#xff0c;是AI这两个字母。这AI与Godot怎么结合#xff1f;感觉还是离线使用#xff0c;值得一看。
视频时间不长#xff0c;15分钟左右#xff0c;看得…今天看到一个视频教学
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.needsneeds.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 Resourcesignal 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_energyfunc _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 Node2Dsignal state_changed(state)enum State {NONE,EATING,SLEEPING,WATCHING_TV,
}export var needs: AgentNeeds
var state: State State.EATINGvar _time_until_next_decision: int 1onready 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 5state_changed.emit(state)func sleep():state State.SLEEPING_time_until_next_decision 10state_changed.emit(state)func watch_tv():state State.WATCHING_TV_time_until_next_decision 1state_changed.emit(state)func _on_timer_timeout():# Adjust the agents needs based on their state.# In a real project, this would be managed by something more sophisticated!if state State.EATING:needs.food 0.05else:needs.food - 0.025if state State.SLEEPING:needs.energy 0.05else:needs.energy - 0.025if state State.WATCHING_TV:needs.fun 0.05else: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 0if _time_until_next_decision 0:_time_until_next_decision - 1return# 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 - 1return# 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 EatAgent.State.SLEEPING:text SleepAgent.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_behaviorcontext p_contextaction 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进行计算其各个考虑因子considerationUtilityAIConsideration分别计算得到结果成为一个数列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
一共有三个tresattack、chase、run_away那应该会有三个状态结果是4个
enum State {IDLE,CHASE,RUN_AWAY,ATTACK,
}
这也不能说是理解错误反而是十分正确与准确。
attack.tres是Product模式一个Consideration嗯很好理解 chase.tres是Product模式三个Considerationsrun_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.IDLEstate_changed.emit(state)func chase():state State.CHASEstate_changed.emit(state)func run_away():state State.RUN_AWAYstate_changed.emit(state)func attack():state State.ATTACKstate_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 $NavigationAgent2Dfunc _make_path() - void:match $Agent.state:1:nav_agent.target_position _player_node.global_position2:var _partner_nodes get_tree().get_nodes_in_group(enemy)if len(_partner_nodes) 1:_could_run_away 0.0else:var _partner [null, INF]for _pt in _partner_nodes:if _pt self:continuevar _partner_distance global_position.distance_to(_pt.global_position)if _partner_distance _partner[1]:_partner[0] _pt_partner[1] _partner_distancenav_agent.target_position _partner[0].global_position_could_run_away 1.0
但好吧说是AI就是AI吧毕竟那些输出都是计算机算出来的