前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Godot游戏开发实践之四:搬运Unity的Pluggable AI教程

Godot游戏开发实践之四:搬运Unity的Pluggable AI教程

原创
作者头像
IT自学不成才
修改于 2020-09-11 02:10:00
修改于 2020-09-11 02:10:00
1.1K0
举报
Godot游戏开发实践之四
Godot游戏开发实践之四

一、前言

在之前的几篇文章里我简单地介绍了 AI 寻路方式以及 Resource 的相关应用,那其实都是为这篇文章做铺垫的,本篇的内容是基于油管上一个比较老的 Unity AI 系列教程: Unity tutorial: Pluggable AI With Scriptable Objects ,教程详细介绍了 Unity 中如何实现可插拨式 AI 的功能,在我的一番苦苦研究下,硬生生地把它给搬运到了 Godot 中,搬运过程可谓是费了九牛二虎之力,这其中一部分原因是由于自己对 Godot API 的熟练程度不够,另一方面则是 Godot 本身的一些缺陷,这些我都会在本文中提出来。

Unity tutorial: Pluggable AI With Scriptable Objects
Unity tutorial: Pluggable AI With Scriptable Objects

因为 Unity 中的 ScriptObject 在 Godot 中相当于 Resource ,如果不是很熟悉,推荐大家阅读我的上一篇文章: Godot游戏开发实践之三:容易被忽视的Resource 。另外,搬用并等于照抄,本 Demo 实现的部分 AI 功能使用的是我自己的方式,这也在我之前的文章里有详细介绍: Godot游戏开发实践之二:AI之寻路新方式

说明:我不会很详细的讲述如何实现某些特定功能,所以推荐大家观看原 Unity 视频,如果上油管不方便,也请放心,视频教程我已经搬运到我的网盘,分享链接请关注我的公众号,回复 AI教程 即可(友情提示:套路……),哈哈。

主要内容: 无undefined阅读时间: 12 分钟undefined永久链接: http://liuqingwen.me/2020/09/08/godot-game-devLog-4-translate-pluggable-AI-tutorial-from-unity-to-godot/undefined系列主页: http://liuqingwen.me/introduction-of-godot-series/

二、正文

除了参考原视频教程,也可以克隆本 Demo 的源码,我已经上传到 Github ,供有兴趣的同学参考。

Godot Pluggable AI
Godot Pluggable AI

什么是可插拨AI

所谓可插拨其实和安装插件、热插拨等概念类似,就是可以随意添加或者删除某个功能,通过直接拖拽就能组成复杂的 AI 体系而无须手动重复编写代码,在 Unity 中使用的是 ScriptableObject 而 Godot 中即 Resource :

拖拽赋值
拖拽赋值

其实在编辑器方面, Unity 使用起来比 Godot 舒服多了。 :joy:

先说Godot的问题

搬运这个 AI 教程的时候,我反反复复、仔仔细细研究了很多次,在按步照搬的过程中出现了一个非常奇怪且头疼的问题:游戏无症状、无征兆地闪退

代码看上去没问题,按下 <kbd>F5</kbd> 运行游戏,窗口还没显示就马上停止运行,连错误提示都没有。曾经因为这个错误我一度想着放弃算了,但是转念一想, Godot 开发者岂能低头?! 所以我继续尝试,寻找错误原因,探索可行的解决方案,从至少能正常运行开始一步一步添加相关功能,最终发现了闪退的罪魁祸首: Circular reference to resource循环引用报错,这在我之前的文章中已经聊过,也有朋友遇到过类似的问题,错误信息大概是:

"scene/resources/resource_format_text.cpp:1387 - Circular reference to resource being saved found: 'res://src/Resources/States/???.tres' will be null next time it's loaded."

哪来的循环引用呢?熟悉游戏结构你就会感觉到这是很显然的:在我的游戏中有很多 Resource 资源类,比如 Action/Decision/State/Transitions 等,而这些资源相互之间或多或少发生了一些引用,就像 PatrolChaser 中引用了 ChaseChaser ,反过来 ChaseChaser 又引用 PatrolChaser 从而造成循环引用链,甚至还有更加复杂的、难以发觉的、千丝万缕的引用关系蕴含其中:

Alert Scanner -> Patrol Scanner -> Chase Scanner -> Alert Scanner -> Chase Scanner -> Alert Scanner -> ...

在编程语言里这些引用再正常不过,但是 Godot 3 还不能正常处理循环引用,这会在 4.0 中进行修复,我可不想等到明年春天了,最终解决方式是放弃部分插拨功能,对一些参数不采用推拽赋值的方式,取而代之的是在运行时判断对应资源是否为 null 再决定动态加载进行赋值,这就造成了需要额外的一个变量用来指向对应 Resource 文件的路径:

使用路径动态赋值
使用路径动态赋值

主要代码如下:

代码语言:txt
AI代码解释
复制
# trueState 和 falseState 可以为 null
# 如果为 null 则使用对应的文件路径进行动态加载
func _checkTransitions(controller : StateController) -> void:
    for transition in transitions:
    var decisionSucceeded : bool = transition.decision.decide(controller)
    if decisionSucceeded:
        var trueState = transition.trueState
        if trueState == null:  # 如果置空则动态加载一次
            trueState = load(transition.trueStateResource)
            transition.trueState = trueState
        controller.transitionToState(trueState)
    else:
        var falseState = transition.falseState
        if falseState == null:  # 如果置空则动态加载一次
            falseState = load(transition.falseStateResource)
            transition.falseState = falseState
        controller.transitionToState(falseState)

除此之外,还有一个不忍直视的问题是在编辑器中显示资源值的视图,一旦涉及多个参数、多种类型、多个级别的资源混合在一起,那么他们之间的层级关系在属性面板中变得极其难以辨别,感同身受一下这张慢动图所带来的崩溃心情吧:

复杂的变量关系属性图
复杂的变量关系属性图

嗯,此刻的我心中万马奔腾,无限次奔溃闪退并自动重启中……

AI结构分析

如果你看完了整个视频教程,你会发现这个 AI 系统的几个重要部件:

  • Action 表示动作,比如巡逻、射击等动作的控制实现
  • Decision 表示策略行为的决定,即状态之间进行切换的依据
  • State 表示状态,一个状态即一种 AI 行为,不同状态之间根据决定进行切换
  • Transition 包装了两个状态(正反状态),以及状态发生转换的决定

他们之间的关系图,以及主要的行为类:

AI关系图
AI关系图

Action 父类代码:

代码语言:txt
AI代码解释
复制
extends Resource
class_name AbstractAction, 'res://assets/icons/action-icon.svg'

export var debugDrawColor := Color.black # 颜色显示,Debug用
export var resourceName := 'Action'      # 名字,Debug用

# 动作的行为方法,每帧都会调用
func act(controller : StateController) -> void:
  pass

Decision 父类代码:

代码语言:txt
AI代码解释
复制
extends Resource
class_name AbstractDecision, 'res://assets/icons/decision-icon.svg'

export var debugDrawColor := Color.white # 颜色显示,Debug用
export var resourceName := 'Decision'    # 名字,Debug用

# 决定的方法,包装在 Transition 中,每帧都会调用
# 返回结果决定了切换到的状态
func decide(controller : StateController) -> bool:
  return false

State 状态类代码:

代码语言:txt
AI代码解释
复制
extends Resource
class_name State, 'res://assets/icons/state-icon.svg'

export(Array, Resource) var actions = []       # 当前状态下所有动作集合
export(Array, Resource) var transitions = []   # 所有的状态转换机制集合
export var debugStateColor := Color.green      # 颜色显示,Debug用

# 每帧执行状态更新
func updateState(controller : StateController) -> void:
  _doActions(controller)
  _checkTransitions(controller)

# 循环执行所有动作
func _doActions(controller : StateController) -> void:
  for action in actions:
    action.act(controller)

# 检查每一个转换机制,是否可以进行状态转换
func _checkTransitions(controller : StateController) -> void:
  # 代码参考上文
  # 省略……

控制器 Controller 和过渡机制 Transition 的代码就不贴了,控制器中代码都是一些基本状态和控制操作的实现。这里我把视频中介绍的所有 AI 类型例举如下:

代码语言:txt
AI代码解释
复制
Chase Chaser: {
  Actions: [ChaseAction, AttackAction],
  Transitions: {Decision: ActiveStateDecision, TrueState: Remain State, FalseState: Patrol Chaser}
}
Patrol Chaser: {
  Actions: [PatrolAction],
  Transitions: {Decision: LookDecision, TrueState: Chase Chaser, FalseState: Remain State}
}
Chase Scanner: {
  Actions: [ChaseAction, AttackAction],
  Transitions: {Decision: LookDecision, TrueState: Remain State, FalseState: Alert Scanner}
}
Patrol Scanner: {
  Actions: [PatrolAction],
  Transitions: {Decision: LookDecision, TrueState: Chase Scanner, FalseState: Remain State}
}
Alert Scanner: {
  Actions: [],
  Transitions: [{Decision: ScanDecision, TrueState: Patrol Scanner, FalseState: Remain State}, {Decision: LookDecision, TrueState: Chase Scanner, FalseState: Remain State}]
}

当然,这个 AI 系统绝不局限于此,你完全可以组合出更多 AI 状态,也可以添加你心目中所要实现的其他动作、决定、过渡和状态类,丰富这个强大的 AI 系统。

其他小功能简介

最后,游戏中使用的一些小技巧我也在本篇中简单介绍一下,包括:炸弹的范围伤害、相机自动跟踪、子弹高度模拟等。

炸弹范围伤害

炸弹范围伤害
炸弹范围伤害

从图中可以看出,我使用了指数级的衰减函数,也就是说距离炸弹爆炸中心越远,伤害衰减的越厉害,个人认为要符合现实一些,当然你完全可以使用简单的线性函数,伤害和距离成反比,这取决于你自己以及游戏机制的设计:

代码语言:txt
AI代码解释
复制
# 伤害最大范围
onready var damageRange : float = $CollisionShape2D.shape.radius

func _on_Explosion_body_entered(body: Node) -> void:
    if body.has_method('damaged'):
      var vector : Vector2 = body.global_position - self.global_position
      # 指数系数
      var ratio : float = 1.0 - pow(vector.length() / damageRange, 0.6)
      # 伤害和冲击力
      var damage := ceil(maxDamage * ratio)
      var force : Vector2 = maxForce * ratio * vector.normalized()
      body.damaged(damage, force)

相机自动跟踪

在本示例中我使用了相机自动跟踪的效果。

因为类似于多人游戏,使用相机进行跟踪是有必要的,这样可以保证所有的坦克、玩家都在当前视野中。实现起来不难,根据当前玩家数量以及玩家的位置计算最大边距以及中心点,然后移动并设置相机的缩放即可:

代码语言:txt
AI代码解释
复制
# 窗口大小
onready var _windowSize := self.get_viewport_rect().size
# 跟踪的目标
var targets := []

func _process(delta: float) -> void:
  if targets.size() <= 1:
    _camera.zoom = lerp(_camera.zoom, Vector2.ONE, 2.0 * delta)
    return
  
  var minPos := _windowSize  # 最小位置点
  var maxPos := Vector2.ZERO # 最大位置点
  for target in targets:
    if ! is_instance_valid(target):
      continue
    if target.global_position.x < minPos.x:
      minPos.x = target.global_position.x
    if target.global_position.x > maxPos.x:
      maxPos.x = target.global_position.x
    if target.global_position.y < minPos.y:
      minPos.y = target.global_position.y
    if target.global_position.y > maxPos.y:
      maxPos.y = target.global_position.y
  # 移动到中心点
  self.global_position = lerp(self.global_position, (maxPos + minPos) / 2, 2.0 * delta)
  
  # 计算缩放比例,相对于游戏主窗口
  var zoom = 2.0 * max((maxPos.x - minPos.x) / _windowSize.x, (maxPos.y - minPos.y) / _windowSize.y)
  zoom = clamp(zoom, 0.5, 1.0)
  _camera.zoom = lerp(_camera.zoom, Vector2.ONE * zoom, 2.0 * delta)

子弹高度模拟

原 Unity 视频中的 Tank 是一个 3D 游戏,所以子弹也就有射程(落地)和高度之分,如果在 2D 场景中不设置高度,炸弹只要碰上其他炸弹或者静态物体都会直接爆炸,那么游戏中的发射力(射程)也就毫无意义了,所以我使用代码简单地实现了子弹高度的模拟。

子弹高度模拟
子弹高度模拟

思路大概是这样的:给子弹添加一个阴影,阴影大小和透明度随子弹高度发生变化,飞行中的子弹在垂直方向上偏移一定位置表示高度,最后把碰撞体设置在阴影上。这里的变化都使用了线性比例,实现方式也相对简单,从上图也可以看出来:

代码语言:txt
AI代码解释
复制
export var missileBodyMaxOffset := 60.0            # 最高时子弹视觉偏移
export(float, 1.0, 10.0) var shadowMaxScale := 1.5 # 阴影最大缩放,即子弹离地最低点
export(float, 0.0, 1.0) var shadowMinScale := 0.5  # 阴影最小缩放,即子弹离地最高点
export(float, 0.0, 1.0) var shadowMinAlpha := 0.25 # 阴影最小透明度,最高点
export(float, 1.0, 2.0) var shadowMaxAlpha := 2.5  # 阴影最大透明度,最低点

func init(force : float, maxSpeed : int, resistance : int, dir : Vector2) -> void:
  _direction = dir
  _fullSpeed = maxSpeed
  _moveResistance = resistance # 阻力,即重力加速度
  
  # 计算能达到的最大高度
  _maxFlyHeight = 0.5 * maxSpeed * maxSpeed / resistance
  # 计算线性关系系数a和b:y=ax+b
  _paramScaleA = (shadowMinScale - shadowMaxScale) / _maxFlyHeight
  _paramScaleB = shadowMaxScale
  _paramAlphaA = (shadowMaxAlpha - shadowMinAlpha) / (shadowMinScale - shadowMaxScale)
  _paramAlphaB = shadowMaxAlpha - shadowMinScale * _paramAlphaA
  
  # 保证炸弹总是往上方偏移,不然看起来奇怪
  var angle = fmod(dir.angle(), 2 * PI)
  if angle > PI * 0.5 || angle < - PI * 0.5:
    missileBodyMaxOffset = -missileBodyMaxOffset
  
  # 水平和垂直初始速度一样,模拟45度发射导弹
  _velocityX = force * maxSpeed
  _velocityY = force * maxSpeed

# 计算水平和垂直位移
func _physics_process(delta: float) -> void:
  self.position += _direction * _velocityX * delta
  _velocityY -= _moveResistance * delta
  currentHeight += _velocityY * delta
  _adjustHeight(currentHeight)
  if currentHeight <= 0.0:
    explode()

# 根据高度调整阴影大小、透明度、子弹垂直偏移
func _adjustHeight(height : float) -> void:
  _body.position.y = - height / _maxFlyHeight * missileBodyMaxOffset
  var shadowScale = _paramScaleA * height + _paramScaleB
  _shadow.scale = Vector2.ONE * shadowScale
  var shadowAlpha = _paramAlphaA * shadowScale + _paramAlphaB
  _shadow.modulate.a = shadowAlpha

嗯,我就想弱弱问一句:现实生活中物体越高其阴影是越大还是越小呢?……

三、总结

这种 AI 系统具有比较强的扩展性和易用性,有点复杂问题简单模块化的思维,用起来应该会相当爽,当然我也没有具体项目案例,另外也有一些不足之,个人经验主要概括为这两点:

  1. Pluggable AI 确实比较强大,使用非常方便,因为是可插拨,即使配置复杂的 AI 都只要轻轻一拖一拽一松手就完成了
  2. 但是这种方式也有令人不爽的地方,比如耦合还是比较厉害的,代码中需要访问、修改很多玩家相关数据,依然需要一番精心的设计

好在 Unity 中具有更加成熟的碰撞检测相关 API ,比如 SphereCast 还有 Navigator 都是极好用的 AI 辅助工具, Godot 中就只能手动实现了。 :joy:

最后,务必关注我的公众号,回复 AI教程 我会送上本套视频以及非常棒的一套 AStar 讲解视频(毫无疑问也是在 Unity 中实现,但是原理通用)。本篇的 Demo 以及相关代码已经上传到 Github ,地址: https://github.com/spkingr/Godot-Pluggable-AI , 后续继续更新,原创不易,希望大家喜欢! :smile:

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Godot游戏开发实践之二:AI之寻路新方式
AI 一直是游戏开发中一个热门词汇,当然这不是人工智能的那个 AI ,而是指有着人类思想的 NPC 或者聪明的敌人等等。根据游戏的类型和复杂程度, AI 的实现可以很简单,也可以非常复杂。作为新手,本文不会讨论所谓高级 AI 的实现方式,那太不现实,不过我们可以先从最简单、最常用也是最实用的 AI 寻路探索开始入手,进而丰富我们的小游戏!
IT自学不成才
2020/08/02
2.3K0
Godot3游戏引擎入门之十一:Godot中的粒子系统与射击游戏(下)
2018-12-25 by Liuqingwen | Tags: Godot | Hits
IT自学不成才
2019/01/08
1.6K0
Godot游戏开发实践之三:容易被忽视的Resource
首先,特大喜讯,奔走相告, Godot 爱好者们又有新的窝了——我们国人自建的 Godot 论坛: Godot中文社区已经正式开放,这里有一手的开发资源,最新的科技动向,开发上有啥问题可以随时发帖,欢迎大家随时到论坛来讨论、交流和学习游戏开发的最新技术。 :grin:
IT自学不成才
2020/08/19
1.9K0
Godot游戏开发实践之一:使用High Level Multiplayer API制作多人游戏(下)
继续接着上篇介绍局域网多人游戏的开发: Godot游戏开发实践之一:使用High Level Multiplayer API制作多人游戏(上) ,本篇主要讲解代码分析与开发总结。
IT自学不成才
2020/08/02
1.7K0
Godot游戏开发实践之一:使用High Level Multiplayer API制作多人游戏(下)
Godot游戏开发实践之一:使用High Level Multiplayer API制作多人游戏(上)
距离上一次发文已经稳稳超过一年了,去年一直在做 #¥@#*!%……%#&…%&^# 然后待在家里了!偶尔写写 BUG ,一直默默关注着 Godot ,这不已经 3.2.2 版本了,距离“神秘”的 4.0 版本又近了一步。接下来我还是会不断探索,努力提高自己,努力提高别人,哈哈。有时间多和大家交流探讨 Godot 游戏开发中的一些技能、技巧、技术吧。 :sunglasses:
IT自学不成才
2020/08/02
2K0
Godot游戏开发实践之一:使用High Level Multiplayer API制作多人游戏(上)
Godot3游戏引擎入门之三:移动我们的主角
2018-09-18 by Liuqingwen | Tags: Godot | Hits
IT自学不成才
2019/01/08
1.4K0
Godot3游戏引擎入门之四:给主角添加动画(下)
2018-09-27 by Liuqingwen | Tags: Godot | Hits
IT自学不成才
2019/01/08
1.1K0
Godot3游戏引擎入门之五:上下左右移动动画(上)
2018-10-10 by Liuqingwen | Tags: Godot | Hits
IT自学不成才
2019/01/08
2K0
Godot3游戏引擎入门之四:给主角添加动画(上)
2018-09-25 by Liuqingwen | Tags: Godot | Hits
IT自学不成才
2019/01/08
1K0
Godot进行2D游戏开发入门-安装与介绍
https://docs.godotengine.org/zh_CN/latest/about/introduction.html
码客说
2023/08/08
1.4K0
Godot进行2D游戏开发入门-安装与介绍
Godot3游戏引擎入门之十:介绍一些常用的节点并开发一个小游戏(中)
2018-12-05 by Liuqingwen | Tags: Godot | Hits
IT自学不成才
2019/01/08
7840
游戏开发中的物理之使用KinematicBody2D
Godot提供了多个碰撞对象以提供碰撞检测和响应。试图确定要为您的项目使用哪个选项可能会造成混淆。如果您了解每个问题的工作原理和优点和缺点,则可以避免这些问题并简化开发。在本教程中,我们将研究 KinematicBody2D节点,并显示一些使用它的示例。
海拥
2021/08/23
9180
Godot3游戏引擎入门之十:介绍一些常用的节点并开发一个小游戏(下)
2018-12-06 by Liuqingwen | Tags: Godot | Hits
IT自学不成才
2019/01/08
9100
Godot3游戏引擎入门之十:介绍一些常用的节点并开发一个小游戏(上)
2018-11-30 by Liuqingwen | Tags: Godot | Hits
IT自学不成才
2019/01/08
1.3K0
Godot3游戏引擎入门之七:地图添加碰撞体制作封闭的游戏世界
2018-10-22 by Liuqingwen | Tags: Godot | Hits
IT自学不成才
2019/01/08
1.6K0
游戏开发中的物理之射线投射
游戏开发中最常见的任务之一是投射光线(或自定义形状的物体)并检查其撞击。这样就可以进行复杂的行为,AI等。本教程将说明如何在2D和3D中执行此操作。
海拥
2021/08/23
9130
Unity3D OpenVR 虚拟现实 保龄球打砖块游戏开发
据说水哥买了 Valve Index 设备,既然这个设备这么贵,不开发点有(zhi)趣(zhang)游戏就感觉对不起这个设备。本文将来开始着手开发一个可玩性不大,观赏性极强的保龄球打砖块游戏。这仅仅只是一个入门级的游戏,代码量和制作步骤都超级少,适合入门
林德熙
2021/05/18
1.5K0
Unity3D OpenVR 虚拟现实 保龄球打砖块游戏开发
一看就懂 - 从零开始的游戏开发
0x00 写在最前面 对于开发而言,了解一下如何从零开始做游戏是一个非常有趣且有益的过程(并不)。这里我先以大家对游戏开发一无所知作为前提,以一个简单的游戏开发作为🌰,跟大家一起从零开始做一个游戏,浅入浅出地了解一下游戏的开发 此外,诸君如果有游戏制作方面的经验,也希望能不吝赐教,毕竟互相交流学习,进步更快~ 这次的分享,主要有几个点: Entity Component System 思想,以及它在游戏开发中能起的作用(important!) 一个简单的 MOBA 游戏,是如何一步步开发出来的 Entity
Tecvan
2022/01/04
1.2K0
一看就懂 - 从零开始的游戏开发
【学习笔记】Unity3D官方游戏教程:Survival Shooter tutorial
2017-06-25 by Liuqingwen | Tags: Unity3D | Hits
IT自学不成才
2019/01/08
2.9K0
三年全职 Rust 游戏开发,真要放弃 Rust 吗?
在网上看到了一个两年前的评论,这件事好像也印证了他的说法,他是不是会偷笑自己的「神预言」 呢?
张汉东
2024/05/07
3.5K0
三年全职 Rust 游戏开发,真要放弃 Rust 吗?
推荐阅读
相关推荐
Godot游戏开发实践之二:AI之寻路新方式
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档