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自學不成才
相關文章
相關標籤/搜索