一、前言
時間飛逝,距離上次更新已經有半年之久!這幾個月裏我只有三分之一的時間很忙,相反其他時間是比較閒的,但是由於空閒時間非常“碎片化”,導致我一直沒有集中精力搞自己喜歡的“小遊戲”了。首先對我的讀者表示非常抱歉!嗯,從本篇開始,我會陸陸續續更新一些新的文章,儘管更新的頻率可能會變得“佛系”,不過我肯定不會放棄 Godot 的,哈哈。 😎
不知不覺, Godot 3.1 正式版都已經發布好幾個月了,現在最新的穩定版本是 3.1.1 ,不知道大家有沒有感受到新版本中的一些新特性所帶來的開發樂趣呢?關於新特性這裏我先不討論,在今天要介紹的這個小遊戲製作過程中,我要告訴大家一個“很不幸”的消息:新版本中的 RigidBody2D fails with a bug! 😂 對,你沒看錯,我遇到 Bug 了,而且還不算個小問題,它直接導致了我的遊戲不能正常地“好好玩耍”!
話又說回來,我所要講述的這個遊戲是一個非常無聊的小遊戲,僅用來作爲示例演示而別無他意,我會在文章中指出新版本 Bug 出在哪,如何解決等。另外,遊戲中包括的一些圖片文件、音樂素材、甚至不少源代碼都是來自或者參考了 Chris Bradfield 的一個名爲 Space Rocks 的示例遊戲,他的這個項目是開源的,地址在此: https://github.com/kidscancode/Godot-Game-Engine-Projects 。
我想通過本篇主要講述以下幾個小部分:
- 介紹 RigidBody2D 剛體節點的基本屬性
- 剛體節點的基本應用以及注意點
- 遊戲場景的結構關係與核心代碼說明
- 最簡單的 FSM 有限狀態機介紹和應用
- 新版本中存在的 Bug 以及解決方法
主要內容: RigidBody2D 剛體節點的應用以及簡單的 FSM 狀態機介紹
閱讀時間: 12 分鐘
永久鏈接: http://liuqingwen.me/blog/2019/07/20/introduction-of-godot-3-part-14-make-a-game-with-rigidbody2d-node-and-the-fsm-introduction
系列主頁: http://liuqingwen.me/blog/introduction-of-godot-series/
二、正文
本篇目標
- 瞭解剛體節點的基本屬性和作用
- 操控剛體節點的正確姿勢
- 剛體節點的碰撞檢測與響應處理
- 簡單的 FSM 機制實現
- 版本更新帶來的代碼更新
遊戲的主要場景
我之前已經介紹過幾個小遊戲了:
相比之前的遊戲,本篇中我要介紹的這個太空飛船小遊戲算比較簡單的一個,遊戲中的元素類型少、操作也相對簡單,但最重要的一點是,在本遊戲製作中,我重點使用了 RigidBody2D 剛體節點,這與之前討論的 KinematicBody2D 有着很大的區別,後續我們會討論,這裏先預覽一下游戲中的所有場景結構吧:
唯一一個要注意的地方我已經在上圖中作了標註: Rock.tscn 岩石場景中的子節點 CollisionShape2D 碰撞圖形沒有定義實質的形狀。這是因爲我們需要在遊戲中動態生成不同尺寸的岩石,所以選擇在代碼中根據其大小創建對應的碰撞圖形:
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
# 省略其他代碼……
我省略了一些代碼,有需要的話可以參考D我的項目源碼,這裏我就不全部貼出來了,其他的部分我也視情況作了一些註釋,相信大家一眼就能看懂。 😃
FSM 簡介與實現
FSM 即 Finite State Machine 有限狀態機的縮寫,相信很多遊戲開發者都聽過或者在項目中使用過這種模型。在 中文維基百科 中是這樣描述的:有限狀態自動機,簡稱狀態機,是表示有限個狀態以及在這些狀態之間的轉移和動作等行爲的數學模型。
上圖來自 Chris Bradfield 的一本書[《 Godot Engine Game Development Projects 》],圖中每一個圓圈表示玩家的一種狀態,在某種情況下,比如鍵盤輸入、被攻擊、超時等原因,玩家會從當前狀態沿着箭頭切換到另一種狀態。如上圖,舉個例子:玩家處於空閒狀態( IDLE )下,如果按下按鍵( key )則進入跑步( RUN )狀態,如果玩家速度爲 0 ( speed=0 )則從跑步狀態切回空閒狀態。
關於狀態機我瞭解的並不多,但是我在網上找到了一篇關於遊戲設計模式之狀態模式的文章,內容介紹非常詳盡,我已經把它翻譯了出來,有興趣的朋友可以參考,當做擴展閱讀吧,文章鏈接:【翻譯】遊戲設計模式之狀態機。 😃
本遊戲中,我參考了 Chris Bradfield 的《 Godot Engine Game Development Projects 》一書中 Space Rocks 小遊戲的設計,下圖同樣來自此書:
可以看到,玩家即太空飛船具有以下四個狀態:
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
。
另一種方式則是設置剛體屬性中的 Gravity Scale
引力縮放係數值,它表示物體受重力的影響大小,本遊戲中沒必要進行設置。其他剛體的一些常見屬性有:
- Mass/Weight :質量和重量,
G = mg
重力公式說明了重量和質量、引力三者的關係 - Contacts Reported/Contact Monitor/Can Sleep :是否響應碰撞以及響應碰撞體個數、能否休眠
- Linear/Angular/Applied Forces :分別設置線性速度和阻力、角速度和阻力、受力和扭矩力
- Friction/Bounce :碰撞材質相關屬性,設置剛體的摩擦力和彈性係數等
最後一組屬性的設置之前,你必須創建一個新的 PhysicsMaterial 即碰撞材質,這與老版本 Godot 中剛體屬性設置稍微不同。另外,剛體還有一些其他的屬性這裏並沒有完全列出來,比如 Mode 剛體模式或者 Custom Integrator 自定義碰撞響應等,我們暫時不討論,在之後的文中如果用到再介紹吧。 😁
上圖是玩家和岩石節點的屬性,他們都是剛體節點,但是設置還是有差別的。可以看到,我給 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 的處理幾乎一樣。 😄
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
orlinear_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
位置屬性是沒問題的,注意別混淆了。 😁
新版本中剛體的問題
遊戲開發過程也就是學習的過程,也是填坑的過程,前面我們已經瞭解到了 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 在 Godot 3.2 開發版本中已經得到了修復,關於開發版本的構建可以到這裏下載: Unofficial Godot Engine builds ,關於這個 Bug 我也在官方 Github 上開了一個 issue ,傳送門: https://github.com/godotengine/godot/issues/30551 。不管怎樣,這個 Bug 肯定會在下一個穩定版本中修復的,大家放心吧。
嗯,如果想測試本篇中的這個小遊戲,我建議還是要下載 Godot 3.2 的開發版進行項目導入和測試。
三、總結
小遊戲算是基本完成了,由於一些不可避免的問題,使得我這個無聊的遊戲開發了很長一段時間,不管怎樣,希望大家對 RigidBody2D 節點有一個新的認識吧,而關於 RigidBody2D 剛體節點的一些其他應用場景,我也打算會在後續文章中再做一個簡單的介紹,大家有什麼意見和建議歡迎留言哦!嘿嘿!
本篇的 Demo 以及相關代碼已經上傳到 Github ,地址: https://github.com/spkingr/Godot-Demos ,後續繼續更新,原創不易,希望大家喜歡! 😄
我的博客地址: http://liuqingwen.me ,我的博客即將同步至騰訊雲+社區,邀請大家一同入駐: https://cloud.tencent.com/developer/support-plan?invite_code=3sg12o13bvwgc ,歡迎關注我的微信公衆號: