Godot3游戏引擎入门之十四:RigidBody2D刚体节点的应用以及简单的FSM状态机介绍

Godot Cover

1、前言

时间飞逝,距离上次更新已经有半年之久!这几个月里我只有三分之一的时间很忙,相反其余时间是比较闲的,可是因为空闲时间很是“碎片化”,致使我一直没有集中精力搞本身喜欢的“小游戏”了。首先对个人读者表示很是抱歉!嗯,从本篇开始,我会陆陆续续更新一些新的文章,尽管更新的频率可能会变得“佛系”,不过我确定不会放弃 Godot 的,哈哈。 :sunglasses:html

不知不觉, Godot 3.1 正式版都已经发布好几个月了,如今最新的稳定版本是 3.1.1 ,不知道你们有没有感觉到新版本中的一些新特性所带来的开发乐趣呢?关于新特性这里我先不讨论,在今天要介绍的这个小游戏制做过程当中,我要告诉你们一个“很不幸”的消息:新版本中的 RigidBody2D fails with a bug! :joy: 对,你没看错,我遇到 Bug 了,并且还不算个小问题,它直接致使了个人游戏不能正常地“好好玩耍”!node

话又说回来,我所要讲述的这个游戏是一个很是无聊的小游戏,仅用来做为示例演示而别无他意,我会在文章中指出新版本 Bug 出在哪,如何解决等。另外,游戏中包括的一些图片文件、音乐素材、甚至很多源代码都是来自或者参考了 Chris Bradfield 的一个名为 Space Rocks 的示例游戏,他的这个项目是开源的,地址在此: github.com/kidscancode…python

result_1.gif

我想经过本篇主要讲述如下几个小部分:git

  1. 介绍 RigidBody2D 刚体节点的基本属性
  2. 刚体节点的基本应用以及注意点
  3. 游戏场景的结构关系与核心代码说明
  4. 最简单的 FSM 有限状态机介绍和应用
  5. 新版本中存在的 Bug 以及解决方法

主要内容: RigidBody2D 刚体节点的应用以及简单的 FSM 状态机介绍
阅读时间: 12 分钟
永久连接: user-gold-cdn.xitu.io/2019/7/23/1…
系列主页: liuqingwen.me/blog/introd…github

2、正文

本篇目标

  1. 了解刚体节点的基本属性和做用
  2. 操控刚体节点的正确姿式
  3. 刚体节点的碰撞检测与响应处理
  4. 简单的 FSM 机制实现
  5. 版本更新带来的代码更新

游戏的主要场景

我以前已经介绍过几个小游戏了:设计模式

相比以前的游戏,本篇中我要介绍的这个太空飞船小游戏算比较简单的一个,游戏中的元素类型少、操做也相对简单,但最重要的一点是,在本游戏制做中,我重点使用了 RigidBody2D 刚体节点,这与以前讨论的 KinematicBody2D 有着很大的区别,后续咱们会讨论,这里先预览一下游戏中的全部场景结构吧:微信

godot_14_scenes.jpg

惟一一个要注意的地方我已经在上图中做了标注: Rock.tscn 岩石场景中的子节点 CollisionShape2D 碰撞图形没有定义实质的形状。这是由于咱们须要在游戏中动态生成不一样尺寸的岩石,因此选择在代码中根据其大小建立对应的碰撞图形:app

func _ready():
    randomize()

    # 设置位置和质量(在Player.gd中设置位置是在_integrate_forces方法中)
    self.position = _position
    self.mass = _radius * density

    # 设置图片尺寸和爆炸粒子尺寸与传递的参数相匹配
    _sprite.scale = Vector2(1, 1) * self.size * scaleFactor
    _explosion.scale = Vector2(1, 1) * self.size * scaleFactor

    # 给岩石一个碰撞体形状,和传递的参数半径相匹配
    var shape = CircleShape2D.new()
    var textureSize = _sprite.texture.get_size()
    shape.radius = (textureSize.x + textureSize.y) / 2.0 * _radius * scaleFactor
    _collisionShape.shape = shape

    # 省略其余代码……
复制代码

我省略了一些代码,有须要的话能够参考个人项目源码,这里我就不所有贴出来了,其余的部分我也视状况做了一些注释,相信你们一眼就能看懂。 :smiley:dom

FSM 简介与实现

FSMFinite State Machine 有限状态机的缩写,相信不少游戏开发者都听过或者在项目中使用过这种模型。在 中文维基百科 中是这样描述的:有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动做等行为的数学模型ide

FSM_intro.jpg

上图来自 Chris Bradfield 的一本书[《 Godot Engine Game Development Projects 》],图中每个圆圈表示玩家的一种状态,在某种状况下,好比键盘输入、被攻击、超时等缘由,玩家会从当前状态沿着箭头切换到另外一种状态。如上图,举个例子:玩家处于空闲状态( IDLE )下,若是按下按键( key )则进入跑步( RUN )状态,若是玩家速度为 0 ( speed=0 )则从跑步状态切回空闲状态。

关于状态机我了解的并很少,可是我在网上找到了一篇关于游戏设计模式之状态模式的文章,内容介绍很是详尽,我已经把它翻译了出来,有兴趣的朋友能够参考,当作扩展阅读吧,文章连接:【翻译】游戏设计模式之状态机。 :smiley:

本游戏中,我参考了 Chris Bradfield《 Godot Engine Game Development Projects 》一书中 Space Rocks 小游戏的设计,下图一样来自此书:

FSM_Space_rocks.jpg

能够看到,玩家即太空飞船具备如下四个状态:

  • INIT 即初始状态,这种状态下飞船不可见,也不会发生碰撞事件,等待游戏开始
  • ALIVE 即正常状态,初始状态下点击开始按钮即进入该状态,飞船恢复正常并接受相关事件
  • INVULN 无敌状态,这种状况是飞船被攻击时进入的状态,一小段时间后自动恢复到正常状态
  • DEAD 死亡状态,生命值耗光后进入该状态,即游戏结束,随后自动进入 INIT 状态

结合状态图,代码中实现起来很是简单,相关地方我也作了注释,如下是主要代码部分:

func _changeState(newState) -> void:
    if _state == newState:
        return

    # 更改飞船的状态,注意设置飞船的可见性
    match newState:
        states.INIT:
            # _collisionShape.disabled = true # 这在 Godot 3.1 版本中不能正常运行
            _collisionShape.set_deferred('disabled', true) # 新版本适用,禁用碰撞检测
            _sprite.hide()
        states.ALIVE:
            _collisionShape.set_deferred('disabled', false)
            _sprite.show()                        # 显示
        states.INVULNERABLE:
            _collisionShape.set_deferred('disabled', true)
            _animationPlayer.play('invulnerable') # 无敌状态动画
            _invulnerabilityTimer.start()         # 无敌状态计时器
        states.DEAD:
            _collisionShape.set_deferred('disabled', true)
            self.linear_velocity = Vector2.ZERO   # 线速度归零
            self.angular_velocity = 0.0           # 角速度归零
            _sprite.hide()                        # 隐藏
            _exhaustParticles.emitting = false    # 中止粒子播放
            _engineAudio.stop()                   # 中止声音播放
    _state = newState
复制代码

一个方法实现了 FSM ,并无所谓的高大上嘛,嗯……可是,这毕竟只是一个简单、很是简单的小游戏,并且,使用这种思路避免了代码中多个 bool 布尔类型和 if...else... 多层嵌套的混乱局面。

刚体的属性及使用

在以前的文章中我已经介绍过了 Godot 中的三种主要物理节点的功能特色和使用场景: KinematicBody2D/StaticBody2D/RigidBody2D ,其中 KinematicBody2D 是咱们最重要的主角,关于它的介绍也扩展了很多,好比: Godot3游戏引擎入门之十二:Godot碰撞理论以及KinematicBody2D的两个方法。可是,对于 RigidBody2D 刚体节点,相反我仅作了使用场景的一个简单介绍和比较,因此,在本次小游戏中,咱们撇开 KinematicBody2D 转而把精力集中到 RigidBody2D 上,重点介绍其使用和相关注意事项等。

其实在不少场景下 RigidBody2D 都是很是实用的,好比,想象一下,用 Godot 作一个相似愤怒的小鸟游戏,那么场景中确定会有不少刚体节点,只要轻松一点,各类物体相互碰撞处处乱飞,相反,你彻底不用本身去编写太多关于物理碰撞理论的代码就实现了游戏的相关特性,是否是很爽?这就是刚体节点在游戏中的应用场景之一。

1. 刚体的一些属性

刚体和咱们现实生活中的物体很是类似,因此一些这些物体的共有特性在 RigidBody2D 节点中也有所提现。首先,最重要的一点就是刚体和万有引力那密不可分的关系,在 Godot 中设置重力( Gravity )对刚体的影响主要有两种方式:一是在项目中设置全局引力值;二是在刚体属性中设置引力的缩放系数。

项目中的设置参考下图,具体在 Project Settings -> General -> Physics -> 2d 中找到 Default Gravity 即默认引力值配置,在本游戏中,因为处于外太空的全部物体都不受重力影响,因此能够在这里进行全局配置,把默认引力值设置为 0

godot_14_default_gravity.jpg

另外一种方式则是设置刚体属性中的 Gravity Scale 引力缩放系数值,它表示物体受重力的影响大小,本游戏中不必进行设置。其余刚体的一些常见属性有:

  • Mass/Weight :质量和重量, G = mg 重力公式说明了重量和质量、引力三者的关系
  • Contacts Reported/Contact Monitor/Can Sleep :是否响应碰撞以及响应碰撞体个数、可否休眠
  • Linear/Angular/Applied Forces :分别设置线性速度和阻力、角速度和阻力、受力和扭矩力
  • Friction/Bounce :碰撞材质相关属性,设置刚体的摩擦力和弹性系数等

最后一组属性的设置以前,你必须建立一个新的 PhysicsMaterial 即碰撞材质,这与老版本 Godot 中刚体属性设置稍微不一样。另外,刚体还有一些其余的属性这里并无彻底列出来,好比 Mode 刚体模式或者 Custom Integrator 自定义碰撞响应等,咱们暂时不讨论,在以后的文中若是用到再介绍吧。 :grin:

godot_14_rigidbody2d_properties.jpg

上图是玩家和岩石节点的属性,他们都是刚体节点,可是设置仍是有差异的。能够看到,我给 Rock 岩石刚体覆盖了默认的材质属性,设置摩擦阻力为 0 并添加了必定的弹性力,这样让岩石在太空中碰撞起来后的响应更有趣;而玩家 Player 即飞船刚体属性配置中,最重要的是我勾选开启了 Contact Monitor 属性(默认关闭),这对游戏的正常运行很是关键,不然咱们没法检测到宇宙飞船和其余任何敌人(岩石)之间的碰撞。

2. 刚体的碰撞测试

在咱们以前的游戏中,碰撞检测通常是 Area2D 的专项,在咱们这个游戏中也有 Area2D 节点的使用,好比 Laser.tscn 子弹场景。然而咱们还须要响应太空飞船和岩石之间的碰撞,他们都是刚体,如何响应呢?前面我已经说明了开启碰撞检测的属性,除此以外,咱们还要在须要主动检测碰撞的刚体中设置 Contacts Reported 属性值,即碰撞体检测数量,这里咱们设置为 1 对于这个游戏已经足够,那么碰撞响应处理的代码以下:

func _on_Player_body_entered(body):
   if body.is_in_group('rock') && body.has_method('explode'):
      # 与岩石碰撞,调用岩石的爆炸方法,传递飞船速度(也就是碰撞方向)
      body.explode(self.linear_velocity)
      # 计算伤害
      _damage(body.size)
复制代码

除了开启碰撞,咱们有时候还须要暂时关闭碰撞检测功能,好比飞船进入无敌状态的时候就不该该和其余任何物体发生碰撞了,和以前的游戏同样,咱们的思路是:直接禁用飞船的碰撞图形 CollisionShape2D 便可,代码 _collisionShape.disabled = true 一行搞定。

当你以为一切就绪的时候,“诡异”的事情发生了:**飞船在禁用了碰撞图形后,竟然还能与其余碰撞体进行正常的碰撞响应!**其实这在 Godot 3.1 以前的版本中是不会出现的,一切正常,可是从 3.1 的版本开始:

In 3.1 Godot doesn't let you change the physics state during the physics processing stage. This change ($CollisionShape2D.set_deferred("disabled", true)) to the code tells it to disable the shape as soon as physics processing is complete.

这是我在遇到这个问题后从 KidsCanCode 博主那里获得的解答,大体意思是:*咱们不能在物理模型碰撞检测发生的过程当中直接操做碰撞图形,相反应该使用 set_deferred 方法,这就是告诉引擎,在物理碰撞处理完阶段再进行设置。*修改 _collisionShape.disabled = true 以下便可:

# _collisionShape.disabled = true # 这在 Godot 3.1 版本中不能正常运行
_collisionShape.set_deferred('disabled', true) # 新版本适用,禁用碰撞检测
复制代码

除了这一点须要注意以外,其余的和以前咱们介绍的 KinematicBody2D 的处理几乎同样。 :smile:

3. 使用代码控制运动

实际上刚体的物理碰撞检测和响应都是交给引擎自动完成的,因此咱们不少时候不必插手刚体的运动,可是在本游戏中,咱们的太空飞船并不适用,咱们仍然须要监听并控制它的一举一动:不能飞出屏幕以外、设置其角速度和线速度、飞船的位置和角度重置等。

self.position = Vector2.ZERO
self.rotation = 0.0

func _physics_process(delta):
    self.position += velocity.rotated(self.rotation);
    self.linear_velocity = velocity
    # ......
复制代码

以上代码使咱们经常使用的设置,可是不幸的是,这并不适用于 RigidBody2D ,这在 Godot 官方文档中有说明:

Note: You should not change a RigidBody2D’s position or linear_velocity every frame or even very often. If you need to directly affect the body’s state, use _integrate_forces, which allows you to directly access the physics state.

嗯,若是你想操做 RigidBody2D ,须要改用 _integrate_forces 方法:

func _integrate_forces(state):
    # 计算前进动力和旋转扭矩,并应用给飞船刚体(冲量)
    var force = Vector2(_thrustForceInput * thrustForce, 0).rotated(self.rotation)
    var torque = _rotateDirection * rotateSpeed
    state.apply_central_impulse(force)
    state.apply_torque_impulse(torque)

    # 设置飞船的位置,origin为飞船位置,xform.x为飞船主轴转向,不要直接设置position
    var xform = state.transform
    if _needReset:
        xform.origin = _resetPosition
        xform.x = Vector2(1, 0)
        _needReset = false

    # 控制飞船在窗口边缘的位置,造成一个闭合区间
    if xform.origin.x > _screenSize.x:
        xform.origin.x = 0
    elif xform.origin.x < 0:
        xform.origin.x = _screenSize.x
    if xform.origin.y > _screenSize.y:
        xform.origin.y = 0
    elif xform.origin.y < 0:
        xform.origin.y = _screenSize.y

    # 更新状态
    state.transform = xform
复制代码

经过 state 能够自由设置刚体的位置,好比上面的代码主要是控制飞船在窗口边缘的位置,另外,这里还有一个设置,因为玩家死亡后,我没有删除其引用而是将其隐身,那么飞船的位置是不固定的,游戏恢复从新开始后须要重置其位置为初始位置,这里一样地须要在 _integrate_forces 方法中进行设置,如上代码的注释我已经作了说明。

最后再啰嗦一句:对于刚建立(好比使用 instance 方法)的刚体物体,直接设置其 position 位置属性是没问题的,注意别混淆了。 :grin:

新版本中刚体的问题

游戏开发过程也就是学习的过程,也是填坑的过程,前面咱们已经了解到了 Godot 3.1 新版本中的一个细节问题了:如何正确设置刚体的碰撞图形属性,须要使用 set_deferred 方法。然而在本游戏的制做过程当中,我还遇到了另外一个 3.1.1 稳定版本中还没有解决的 Bug ,而这个 Bug 竟然在 Godot 3.0 中也是存在的:*若是你一开始禁用刚体的碰撞图形,而后再通过过一段时间再启用,那么你的刚体变成了真正的直男——嗯,只能前进不能旋转!*以下代码,第二行会失效:

state.apply_central_impulse(force) # 线性冲量,有效。
state.apply_torque_impulse(torque) # 扭矩冲量,无效!
复制代码

能够简单地经过下面的代码重现这个 Bug :

func _ready():
    _changeState(states.INIT)
    yield(self.get_tree().create_timer(3), 'timeout')
    _changeState(states.ALIVE)

func _integrate_forces(state):
    state.apply_central_impulse(force)
    state.apply_torque_impulse(torque)
    # 省略代码……
复制代码

bug_rotation_of_rigidbody2d_1.gif
bug_rotation_of_rigidbody2d_2.gif

不过这个 Bug 在 Godot 3.2 开发版本中已经获得了修复,关于开发版本的构建能够到这里下载: Unofficial Godot Engine builds ,关于这个 Bug 我也在官方 Github 上开了一个 issue ,传送门: github.com/godotengine… 。无论怎样,这个 Bug 确定会在下一个稳定版本中修复的,你们放心吧。

嗯,若是想测试本篇中的这个小游戏,我建议仍是要下载 Godot 3.2 的开发版进行项目导入和测试。

3、总结

小游戏算是基本完成了,因为一些不可避免的问题,使得我这个无聊的游戏开发了很长一段时间,无论怎样,但愿你们对 RigidBody2D 节点有一个新的认识吧,而关于 RigidBody2D 刚体节点的一些其余应用场景,我也打算会在后续文章中再作一个简单的介绍,你们有什么意见和建议欢迎留言哦!嘿嘿!

本篇的 Demo 以及相关代码已经上传到 Github ,地址: github.com/spkingr/God… ,后续继续更新,原创不易,但愿你们喜欢! :smile:

个人博客地址: liuqingwen.me ,个人博客即将同步至腾讯云+社区,邀请你们一同入驻: cloud.tencent.com/developer/s… ,欢迎关注个人微信公众号:

IT自学不成才
相关文章
相关标签/搜索