一、前言
時間飛逝叉讥,距離上次更新已經(jīng)有半年之久窘行!這幾個月里我只有三分之一的時間很忙,相反其他時間是比較閑的图仓,但是由于空閑時間非彻蘅“碎片化”,導(dǎo)致我一直沒有集中精力搞自己喜歡的“小游戲”了救崔。首先對我的讀者表示非常抱歉惶看!嗯捏顺,從本篇開始,我會陸陸續(xù)續(xù)更新一些新的文章纬黎,盡管更新的頻率可能會變得“佛系”幅骄,不過我肯定不會放棄 Godot 的,哈哈本今。 :sunglasses:
不知不覺拆座, Godot 3.1 正式版都已經(jīng)發(fā)布好幾個月了,現(xiàn)在最新的穩(wěn)定版本是 3.1.1 冠息,不知道大家有沒有感受到新版本中的一些新特性所帶來的開發(fā)樂趣呢挪凑?關(guān)于新特性這里我先不討論,在今天要介紹的這個小游戲制作過程中逛艰,我要告訴大家一個“很不幸”的消息:新版本中的 RigidBody2D fails with a bug! :joy: 對躏碳,你沒看錯,我遇到 Bug 了,而且還不算個小問題,它直接導(dǎo)致了我的游戲不能正常地“好好玩舜。”!
話又說回來脸甘,我所要講述的這個游戲是一個非常無聊的小游戲,僅用來作為示例演示而別無他意偏灿,我會在文章中指出新版本 Bug 出在哪丹诀,如何解決等。另外翁垂,游戲中包括的一些圖片文件铆遭、音樂素材、甚至不少源代碼都是來自或者參考了 Chris Bradfield 的一個名為 Space Rocks 的示例游戲沿猜,他的這個項目是開源的枚荣,地址在此: https://github.com/kidscancode/Godot-Game-Engine-Projects 。
我想通過本篇主要講述以下幾個小部分:
- 介紹 RigidBody2D 剛體節(jié)點的基本屬性
- 剛體節(jié)點的基本應(yīng)用以及注意點
- 游戲場景的結(jié)構(gòu)關(guān)系與核心代碼說明
- 最簡單的 FSM 有限狀態(tài)機介紹和應(yīng)用
- 新版本中存在的 Bug 以及解決方法
主要內(nèi)容: RigidBody2D 剛體節(jié)點的應(yīng)用以及簡單的 FSM 狀態(tài)機介紹
閱讀時間: 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/
二啼肩、正文
本篇目標(biāo)
- 了解剛體節(jié)點的基本屬性和作用
- 操控剛體節(jié)點的正確姿勢
- 剛體節(jié)點的碰撞檢測與響應(yīng)處理
- 簡單的 FSM 機制實現(xiàn)
- 版本更新帶來的代碼更新
游戲的主要場景
我之前已經(jīng)介紹過幾個小游戲了:
相比之前的游戲橄妆,本篇中我要介紹的這個太空飛船小游戲算比較簡單的一個,游戲中的元素類型少祈坠、操作也相對簡單害碾,但最重要的一點是,在本游戲制作中赦拘,我重點使用了 RigidBody2D 剛體節(jié)點慌随,這與之前討論的 KinematicBody2D 有著很大的區(qū)別,后續(xù)我們會討論,這里先預(yù)覽一下游戲中的所有場景結(jié)構(gòu)吧:
唯一一個要注意的地方我已經(jīng)在上圖中作了標(biāo)注: Rock.tscn 巖石場景中的子節(jié)點 CollisionShape2D 碰撞圖形沒有定義實質(zhì)的形狀阁猜。這是因為我們需要在游戲中動態(tài)生成不同尺寸的巖石丸逸,所以選擇在代碼中根據(jù)其大小創(chuàng)建對應(yīng)的碰撞圖形:
func _ready():
randomize()
# 設(shè)置位置和質(zhì)量(在Player.gd中設(shè)置位置是在_integrate_forces方法中)
self.position = _position
self.mass = _radius * density
# 設(shè)置圖片尺寸和爆炸粒子尺寸與傳遞的參數(shù)相匹配
_sprite.scale = Vector2(1, 1) * self.size * scaleFactor
_explosion.scale = Vector2(1, 1) * self.size * scaleFactor
# 給巖石一個碰撞體形狀,和傳遞的參數(shù)半徑相匹配
var shape = CircleShape2D.new()
var textureSize = _sprite.texture.get_size()
shape.radius = (textureSize.x + textureSize.y) / 2.0 * _radius * scaleFactor
_collisionShape.shape = shape
# 省略其他代碼……
我省略了一些代碼剃袍,有需要的話可以參考我的項目源碼黄刚,這里我就不全部貼出來了,其他的部分我也視情況作了一些注釋民效,相信大家一眼就能看懂憔维。 :smiley:
FSM 簡介與實現(xiàn)
FSM 即 Finite State Machine 有限狀態(tài)機的縮寫,相信很多游戲開發(fā)者都聽過或者在項目中使用過這種模型研铆。在 中文維基百科 中是這樣描述的:有限狀態(tài)自動機,簡稱狀態(tài)機州叠,是表示有限個狀態(tài)以及在這些狀態(tài)之間的轉(zhuǎn)移和動作等行為的數(shù)學(xué)模型棵红。
上圖來自 Chris Bradfield 的一本書[《 Godot Engine Game Development Projects 》],圖中每一個圓圈表示玩家的一種狀態(tài)咧栗,在某種情況下逆甜,比如鍵盤輸入、被攻擊致板、超時等原因交煞,玩家會從當(dāng)前狀態(tài)沿著箭頭切換到另一種狀態(tài)。如上圖斟或,舉個例子:玩家處于空閑狀態(tài)( IDLE )下素征,如果按下按鍵( key )則進(jìn)入跑步( RUN )狀態(tài),如果玩家速度為 0 ( speed=0 )則從跑步狀態(tài)切回空閑狀態(tài)萝挤。
關(guān)于狀態(tài)機我了解的并不多御毅,但是我在網(wǎng)上找到了一篇關(guān)于游戲設(shè)計模式之狀態(tài)模式的文章,內(nèi)容介紹非常詳盡怜珍,我已經(jīng)把它翻譯了出來端蛆,有興趣的朋友可以參考,當(dāng)做擴展閱讀吧酥泛,文章鏈接:【翻譯】游戲設(shè)計模式之狀態(tài)機今豆。 :smiley:
本游戲中,我參考了 Chris Bradfield 的《 Godot Engine Game Development Projects 》一書中 Space Rocks 小游戲的設(shè)計柔袁,下圖同樣來自此書:
可以看到呆躲,玩家即太空飛船具有以下四個狀態(tài):
-
INIT
即初始狀態(tài),這種狀態(tài)下飛船不可見捶索,也不會發(fā)生碰撞事件歼秽,等待游戲開始 -
ALIVE
即正常狀態(tài),初始狀態(tài)下點擊開始按鈕即進(jìn)入該狀態(tài)情组,飛船恢復(fù)正常并接受相關(guān)事件 -
INVULN
無敵狀態(tài)燥筷,這種情況是飛船被攻擊時進(jìn)入的狀態(tài)箩祥,一小段時間后自動恢復(fù)到正常狀態(tài) -
DEAD
死亡狀態(tài),生命值耗光后進(jìn)入該狀態(tài)肆氓,即游戲結(jié)束袍祖,隨后自動進(jìn)入INIT
狀態(tài)
結(jié)合狀態(tài)圖,代碼中實現(xiàn)起來非常簡單谢揪,相關(guān)地方我也做了注釋蕉陋,以下是主要代碼部分:
func _changeState(newState) -> void:
if _state == newState:
return
# 更改飛船的狀態(tài),注意設(shè)置飛船的可見性
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') # 無敵狀態(tài)動畫
_invulnerabilityTimer.start() # 無敵狀態(tài)計時器
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
一個方法實現(xiàn)了 FSM 凳鬓,并沒有所謂的高大上嘛,嗯……但是患民,這畢竟只是一個簡單缩举、非常簡單的小游戲,而且匹颤,使用這種思路避免了代碼中多個 bool
布爾類型和 if...else...
多層嵌套的混亂局面仅孩。
剛體的屬性及使用
在之前的文章中我已經(jīng)介紹過了 Godot 中的三種主要物理節(jié)點的功能特點和使用場景: KinematicBody2D/StaticBody2D/RigidBody2D ,其中 KinematicBody2D 是我們最重要的主角印蓖,關(guān)于它的介紹也擴展了不少辽慕,比如: Godot3游戲引擎入門之十二:Godot碰撞理論以及KinematicBody2D的兩個方法。但是赦肃,對于 RigidBody2D 剛體節(jié)點溅蛉,相反我僅做了使用場景的一個簡單介紹和比較,所以他宛,在本次小游戲中温艇,我們撇開 KinematicBody2D 轉(zhuǎn)而把精力集中到 RigidBody2D 上,重點介紹其使用和相關(guān)注意事項等堕汞。
其實在很多場景下 RigidBody2D 都是非常實用的勺爱,比如,想象一下讯检,用 Godot 做一個類似憤怒的小鳥游戲琐鲁,那么場景中肯定會有很多剛體節(jié)點,只要輕松一點人灼,各種物體相互碰撞到處亂飛围段,相反,你完全不用自己去編寫太多關(guān)于物理碰撞理論的代碼就實現(xiàn)了游戲的相關(guān)特性投放,是不是很爽奈泪?這就是剛體節(jié)點在游戲中的應(yīng)用場景之一。
1. 剛體的一些屬性
剛體和我們現(xiàn)實生活中的物體非常相似,所以一些這些物體的共有特性在 RigidBody2D 節(jié)點中也有所提現(xiàn)涝桅。首先拜姿,最重要的一點就是剛體和萬有引力那密不可分的關(guān)系,在 Godot 中設(shè)置重力( Gravity )對剛體的影響主要有兩種方式:一是在項目中設(shè)置全局引力值冯遂;二是在剛體屬性中設(shè)置引力的縮放系數(shù)蕊肥。
項目中的設(shè)置參考下圖,具體在 Project Settings -> General -> Physics -> 2d 中找到 Default Gravity 即默認(rèn)引力值配置蛤肌,在本游戲中壁却,由于處于外太空的所有物體都不受重力影響,所以可以在這里進(jìn)行全局配置裸准,把默認(rèn)引力值設(shè)置為 0
展东。
另一種方式則是設(shè)置剛體屬性中的 Gravity Scale
引力縮放系數(shù)值,它表示物體受重力的影響大小炒俱,本游戲中沒必要進(jìn)行設(shè)置盐肃。其他剛體的一些常見屬性有:
- Mass/Weight :質(zhì)量和重量,
G = mg
重力公式說明了重量和質(zhì)量向胡、引力三者的關(guān)系 - Contacts Reported/Contact Monitor/Can Sleep :是否響應(yīng)碰撞以及響應(yīng)碰撞體個數(shù)恼蓬、能否休眠
- Linear/Angular/Applied Forces :分別設(shè)置線性速度和阻力惊完、角速度和阻力僵芹、受力和扭矩力
- Friction/Bounce :碰撞材質(zhì)相關(guān)屬性,設(shè)置剛體的摩擦力和彈性系數(shù)等
最后一組屬性的設(shè)置之前小槐,你必須創(chuàng)建一個新的 PhysicsMaterial 即碰撞材質(zhì)拇派,這與老版本 Godot 中剛體屬性設(shè)置稍微不同。另外凿跳,剛體還有一些其他的屬性這里并沒有完全列出來件豌,比如 Mode 剛體模式或者 Custom Integrator 自定義碰撞響應(yīng)等,我們暫時不討論控嗜,在之后的文中如果用到再介紹吧茧彤。 :grin:
上圖是玩家和巖石節(jié)點的屬性,他們都是剛體節(jié)點疆栏,但是設(shè)置還是有差別的曾掂。可以看到壁顶,我給 Rock 巖石剛體覆蓋了默認(rèn)的材質(zhì)屬性珠洗,設(shè)置摩擦阻力為 0
并添加了一定的彈性力,這樣讓巖石在太空中碰撞起來后的響應(yīng)更有趣若专;而玩家 Player 即飛船剛體屬性配置中许蓖,最重要的是我勾選開啟了 Contact Monitor 屬性(默認(rèn)關(guān)閉),這對游戲的正常運行非常關(guān)鍵,否則我們無法檢測到宇宙飛船和其他任何敵人(巖石)之間的碰撞膊爪。
2. 剛體的碰撞測試
在我們之前的游戲中自阱,碰撞檢測一般是 Area2D 的專項,在我們這個游戲中也有 Area2D 節(jié)點的使用蚁飒,比如 Laser.tscn
子彈場景动壤。然而我們還需要響應(yīng)太空飛船和巖石之間的碰撞,他們都是剛體淮逻,如何響應(yīng)呢琼懊?前面我已經(jīng)說明了開啟碰撞檢測的屬性,除此之外爬早,我們還要在需要主動檢測碰撞的剛體中設(shè)置 Contacts Reported 屬性值哼丈,即碰撞體檢測數(shù)量,這里我們設(shè)置為 1
對于這個游戲已經(jīng)足夠筛严,那么碰撞響應(yīng)處理的代碼如下:
func _on_Player_body_entered(body):
if body.is_in_group('rock') && body.has_method('explode'):
# 與巖石碰撞醉旦,調(diào)用巖石的爆炸方法,傳遞飛船速度(也就是碰撞方向)
body.explode(self.linear_velocity)
# 計算傷害
_damage(body.size)
除了開啟碰撞桨啃,我們有時候還需要暫時關(guān)閉碰撞檢測功能车胡,比如飛船進(jìn)入無敵狀態(tài)的時候就不應(yīng)該和其他任何物體發(fā)生碰撞了,和之前的游戲一樣照瘾,我們的思路是:直接禁用飛船的碰撞圖形 CollisionShape2D 即可匈棘,代碼 _collisionShape.disabled = true
一行搞定。
當(dāng)你覺得一切就緒的時候析命,“詭異”的事情發(fā)生了:飛船在禁用了碰撞圖形后主卫,居然還能與其他碰撞體進(jìn)行正常的碰撞響應(yīng)!其實這在 Godot 3.1 之前的版本中是不會出現(xiàn)的鹃愤,一切正常簇搅,但是從 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 博主那里得到的解答,大致意思是:我們不能在物理模型碰撞檢測發(fā)生的過程中直接操作碰撞圖形软吐,相反應(yīng)該使用 set_deferred
方法瘩将,這就是告訴引擎,在物理碰撞處理完階段再進(jìn)行設(shè)置凹耙。修改 _collisionShape.disabled = true
如下即可:
# _collisionShape.disabled = true # 這在 Godot 3.1 版本中不能正常運行
_collisionShape.set_deferred('disabled', true) # 新版本適用姿现,禁用碰撞檢測
除了這一點需要注意之外,其他的和之前我們介紹的 KinematicBody2D 的處理幾乎一樣使兔。 :smile:
3. 使用代碼控制運動
實際上剛體的物理碰撞檢測和響應(yīng)都是交給引擎自動完成的建钥,所以我們很多時候沒必要插手剛體的運動,但是在本游戲中虐沥,我們的太空飛船并不適用熊经,我們?nèi)匀恍枰O(jiān)聽并控制它的一舉一動:不能飛出屏幕之外泽艘、設(shè)置其角速度和線速度、飛船的位置和角度重置等镐依。
self.position = Vector2.ZERO
self.rotation = 0.0
func _physics_process(delta):
self.position += velocity.rotated(self.rotation);
self.linear_velocity = velocity
# ......
以上代碼使我們常用的設(shè)置匹涮,但是不幸的是,這并不適用于 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):
# 計算前進(jìn)動力和旋轉(zhuǎn)扭矩务唐,并應(yīng)用給飛船剛體(沖量)
var force = Vector2(_thrustForceInput * thrustForce, 0).rotated(self.rotation)
var torque = _rotateDirection * rotateSpeed
state.apply_central_impulse(force)
state.apply_torque_impulse(torque)
# 設(shè)置飛船的位置雳攘,origin為飛船位置,xform.x為飛船主軸轉(zhuǎn)向枫笛,不要直接設(shè)置position
var xform = state.transform
if _needReset:
xform.origin = _resetPosition
xform.x = Vector2(1, 0)
_needReset = false
# 控制飛船在窗口邊緣的位置吨灭,形成一個閉合區(qū)間
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
# 更新狀態(tài)
state.transform = xform
通過 state
可以自由設(shè)置剛體的位置,比如上面的代碼主要是控制飛船在窗口邊緣的位置刑巧,另外喧兄,這里還有一個設(shè)置,由于玩家死亡后啊楚,我沒有刪除其引用而是將其隱身吠冤,那么飛船的位置是不固定的,游戲恢復(fù)重新開始后需要重置其位置為初始位置恭理,這里同樣地需要在 _integrate_forces
方法中進(jìn)行設(shè)置拯辙,如上代碼的注釋我已經(jīng)做了說明。
最后再啰嗦一句:對于剛創(chuàng)建(比如使用 instance
方法)的剛體物體蚯斯,直接設(shè)置其 position
位置屬性是沒問題的薄风,注意別混淆了饵较。 :grin:
新版本中剛體的問題
游戲開發(fā)過程也就是學(xué)習(xí)的過程拍嵌,也是填坑的過程,前面我們已經(jīng)了解到了 Godot 3.1 新版本中的一個細(xì)節(jié)問題了:如何正確設(shè)置剛體的碰撞圖形屬性循诉,需要使用 set_deferred
方法横辆。然而在本游戲的制作過程中,我還遇到了另一個 3.1.1 穩(wěn)定版本中尚未解決的 Bug 茄猫,而這個 Bug 居然在 Godot 3.0 中也是存在的:如果你一開始禁用剛體的碰撞圖形狈蚤,然后再經(jīng)過過一段時間再啟用,那么你的剛體變成了真正的直男——嗯划纽,只能前進(jìn)不能旋轉(zhuǎn)脆侮!如下代碼,第二行會失效:
state.apply_central_impulse(force) # 線性沖量勇劣,有效靖避。
state.apply_torque_impulse(torque) # 扭矩沖量潭枣,無效!
可以簡單地通過下面的代碼重現(xiàn)這個 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 開發(fā)版本中已經(jīng)得到了修復(fù)幻捏,關(guān)于開發(fā)版本的構(gòu)建可以到這里下載: Unofficial Godot Engine builds 盆犁,關(guān)于這個 Bug 我也在官方 Github 上開了一個 issue ,傳送門: https://github.com/godotengine/godot/issues/30551 篡九。不管怎樣谐岁,這個 Bug 肯定會在下一個穩(wěn)定版本中修復(fù)的,大家放心吧榛臼。
嗯伊佃,如果想測試本篇中的這個小游戲,我建議還是要下載 Godot 3.2 的開發(fā)版進(jìn)行項目導(dǎo)入和測試沛善。
三锭魔、總結(jié)
小游戲算是基本完成了,由于一些不可避免的問題路呜,使得我這個無聊的游戲開發(fā)了很長一段時間迷捧,不管怎樣,希望大家對 RigidBody2D 節(jié)點有一個新的認(rèn)識吧胀葱,而關(guān)于 RigidBody2D 剛體節(jié)點的一些其他應(yīng)用場景漠秋,我也打算會在后續(xù)文章中再做一個簡單的介紹,大家有什么意見和建議歡迎留言哦抵屿!嘿嘿庆锦!
本篇的 Demo 以及相關(guān)代碼已經(jīng)上傳到 Github ,地址: https://github.com/spkingr/Godot-Demos 轧葛,后續(xù)繼續(xù)更新搂抒,原創(chuàng)不易,希望大家喜歡尿扯! :smile:
我的博客地址: http://liuqingwen.me 求晶,我的博客即將同步至騰訊云+社區(qū),邀請大家一同入駐: https://cloud.tencent.com/developer/support-plan?invite_code=3sg12o13bvwgc 衷笋,歡迎關(guān)注我的微信公眾號: