一、前言
在前面的游戲地圖基礎(chǔ)上,我們已經(jīng)實(shí)現(xiàn)了玩家的上下移動(dòng)控制,也有了相應(yīng)的碰撞體功能,一個(gè)小小的游戲世界已經(jīng)打造好,不過對(duì)于一個(gè)完整的游戲來說還是缺少點(diǎn)什么,沒有探索的樂趣就沒有吸引力,因此,這也就是我們本篇要實(shí)現(xiàn)的目標(biāo)——給游戲場(chǎng)景添加一些可愛的動(dòng)畫元素,比如金幣,來供玩家探索吧!
除此之外,我還會(huì)介紹 Godot 中兩個(gè)非常重要的概念或者實(shí)用技巧:子場(chǎng)景的創(chuàng)建和 Godot 中信號(hào)的使用。和之前的文章一樣,本篇也是基于上一篇文章: Godot3 游戲引擎入門之七:地圖添加碰撞體制作封閉的游戲世界。
主要內(nèi)容: 在游戲場(chǎng)景中添加互動(dòng)元素
閱讀時(shí)間: 10 分鐘
永久鏈接:http://liuqingwen.me/blog/2018/11/02/introduction-of-godot-3-part-8-add-collectable-elements-and-sub-scenes/
系列主頁:http://liuqingwen.me/blog/tags/Godot/
二、正文
本篇目標(biāo)
創(chuàng)建子場(chǎng)景,實(shí)例化,并添加多個(gè)子場(chǎng)景
介紹 Area2D 節(jié)點(diǎn)的功能和應(yīng)用
Godot 中的觀察者模式實(shí)現(xiàn):信號(hào)的使用
創(chuàng)建和使用包含函數(shù)調(diào)用的復(fù)雜動(dòng)畫
創(chuàng)建玩家子場(chǎng)景
為什么需要子場(chǎng)景呢?這其實(shí)有點(diǎn)類似程序中的面向?qū)ο笏枷?,如果你有使?Unity 開發(fā)游戲的經(jīng)驗(yàn),那么你對(duì) Unity 中深入人心的 Prefab 預(yù)制體概念肯定非常熟悉;同樣地在 Apple 中開發(fā) 2D 游戲,使用 SpriteKit 也會(huì)創(chuàng)建很多的子場(chǎng)景: SKScene ,然后在主游戲中加以重復(fù)利用。 Godot 中也有類似的概念,想象一下,當(dāng)你需要在場(chǎng)景中制作很多個(gè)功能類似的物體,比如多個(gè)相同的敵人,每個(gè)場(chǎng)景中數(shù)量還不一定一樣,如果每個(gè)場(chǎng)景中都去單獨(dú)制作一個(gè)個(gè)的敵人對(duì)象,那就顯得非常地不優(yōu)雅了,萬一設(shè)計(jì)不合理,全部都需要修改呢?這個(gè)時(shí)候,你就可以把它制作成一個(gè)預(yù)制件,使用預(yù)制件來克隆多個(gè)敵人,當(dāng)你需要修改某個(gè)功能的時(shí)候,你只需要修改這個(gè)預(yù)制件,那么所有的實(shí)例都能得到應(yīng)用,方便高效,還能提高游戲性能。這就是 Godot 中所謂的 Sub-Scene 子場(chǎng)景概念了。
說的很多,實(shí)際上做起來很簡(jiǎn)單。首先,我又得做下比較了: Godot 中的子場(chǎng)景可比 Unity 中的預(yù)制體功能強(qiáng)大多了!子場(chǎng)景可以嵌套,可以覆蓋,甚至還能單獨(dú)運(yùn)行,非常方便。其次,我們要了解到,什么情況下需要子場(chǎng)景:第一,獨(dú)立的節(jié)點(diǎn)可以制作成子場(chǎng)景,方便開發(fā)、調(diào)試、合作;第二,重復(fù)利用的元素可以制作成子場(chǎng)景。最后,我們來使用子場(chǎng)景來改進(jìn)一下我們當(dāng)前的游戲結(jié)構(gòu)。
在我們的游戲主場(chǎng)景中,玩家 Player 是一個(gè)五臟俱全的子節(jié)點(diǎn),這里我們完全可以把它當(dāng)做一個(gè)單獨(dú)的場(chǎng)景進(jìn)行開發(fā)利用,這樣的好處在于可以單獨(dú)修改 Player 節(jié)點(diǎn),提高效率,而且當(dāng)你有需求要在游戲的主場(chǎng)景中添加多個(gè)玩家的時(shí)候(這里不太可能,不過以后我們?cè)僬劧嗤婕揖钟蚓W(wǎng)連線游戲),你會(huì)發(fā)現(xiàn)特別地方便!制作子場(chǎng)景一般有兩種方式,這兩種方式都非常簡(jiǎn)單,靈活采用。
我們先講第一種方式:把場(chǎng)景中已有的節(jié)點(diǎn)轉(zhuǎn)化為子場(chǎng)景。在我們的游戲主場(chǎng)景中,選擇 Player 玩家節(jié)點(diǎn),右鍵彈出菜單中,選擇 Save Branch As Scene 即把該節(jié)點(diǎn)轉(zhuǎn)化為場(chǎng)景,然后選擇合適的位置,保存即可!現(xiàn)在 Player 節(jié)點(diǎn)變成了一個(gè)單獨(dú)的子節(jié)點(diǎn)了,右邊的 ? 電影小標(biāo)志說明該節(jié)點(diǎn)為一個(gè)子場(chǎng)景,你可以通過點(diǎn)擊這個(gè)標(biāo)志進(jìn)入 Player 子場(chǎng)景進(jìn)行編輯,非常簡(jiǎn)便、貼心。
前面說過,子場(chǎng)景類似預(yù)制體,可以進(jìn)行克隆創(chuàng)建出多個(gè)子場(chǎng)景的實(shí)例,接下來我們就通過制作金幣子場(chǎng)景對(duì)此進(jìn)行討論。
制作金幣場(chǎng)景
我們創(chuàng)建一些金幣來豐富游戲的場(chǎng)景,供玩家探索發(fā)現(xiàn)。先構(gòu)思一下金幣在游戲世界中的表現(xiàn):有一個(gè)金幣,它閃耀在世界的某個(gè)角落,如果有幸被玩家拾取,將會(huì)播放一段動(dòng)畫,然后消失于人間!嗯,是時(shí)候把我們的想象力轉(zhuǎn)化為實(shí)際操作了:我們來創(chuàng)建一個(gè)單獨(dú)的金幣子場(chǎng)景,包含有兩個(gè)動(dòng)畫,一個(gè)是閃耀,另一個(gè)是消失動(dòng)畫,還要有碰撞反饋,最好能自我消失! ?
這就是我要講的第二種子場(chǎng)景制作方式,首先我們點(diǎn)擊場(chǎng)景編輯器上方的 + 號(hào)按鈕,創(chuàng)建一個(gè)單獨(dú)的場(chǎng)景,選擇什么節(jié)點(diǎn)作為金幣場(chǎng)景根節(jié)點(diǎn)呢?這里我要介紹一個(gè)新的節(jié)點(diǎn): Area2D 區(qū)域節(jié)點(diǎn)。為什么要使用 Area2D 節(jié)點(diǎn)而非普通的 Node2D 或者之前我們多次接觸過的具有碰撞屬性的 StaticBody2D/KinematicBody2D 節(jié)點(diǎn)呢?原因在此:我們只需要一個(gè)能檢測(cè)碰撞,但不需要有任何物理反饋的節(jié)點(diǎn)。 Area2D 在此非常合適,它可以用來制作一個(gè)區(qū)域,檢測(cè)玩家進(jìn)出該區(qū)域,相比 PhysicsBody2D 下的物理碰撞屬性節(jié)點(diǎn),它沒有質(zhì)量、彈性等屬性,所以性能更高,另外有了 Area2D 作為根節(jié)點(diǎn),我們沒必要使用 Node2D 節(jié)點(diǎn)了。
選擇 Area2D 作為根節(jié)點(diǎn),改名為 Coin ,然后添加碰撞區(qū)域節(jié)點(diǎn)和圖片、動(dòng)畫節(jié)點(diǎn),調(diào)整相應(yīng)設(shè)置,按 Ctrl/Command + S 保存為 Coin.tscn 場(chǎng)景資源,場(chǎng)景結(jié)果如下圖:
接下來需要給金幣制作動(dòng)畫,按照前面的分析,需要兩個(gè)動(dòng)畫:一個(gè)是沒有被收集時(shí)的閃耀狀態(tài),一個(gè)是被收集后立刻消失的動(dòng)畫。第一個(gè)動(dòng)畫 rotate 非常簡(jiǎn)單,對(duì)于第二個(gè)消失動(dòng)畫 disappear 則稍微復(fù)雜點(diǎn),但是只要把動(dòng)畫思路弄清楚,然后分多個(gè)軌道單獨(dú)進(jìn)行設(shè)計(jì),調(diào)整,做出好看的效果也就非常簡(jiǎn)單了,動(dòng)畫分多個(gè)軌道:
碰撞體禁用屬性:玩家收集金幣后碰撞體不再有效,啟用 disabled 屬性
金幣位置屬性:金幣從下往上漂浮,即 position 位置屬性
透明度屬性:在顏色屬性里讓透明度變?yōu)?0 ,即 modulate 中的 alpha 值
縮放屬性:再添加一個(gè)縮放動(dòng)畫,在位置變化過程中不斷縮小,即 scale 的值
最后一個(gè),金幣需要回到第一幀,防止以某個(gè)側(cè)面圖片進(jìn)行消失,設(shè)置 frame 為 0 即可
記得做動(dòng)畫過程中不斷測(cè)試和調(diào)整播放時(shí)間。是不是感覺 Godot 中的 AnimationPlayer 簡(jiǎn)直是太強(qiáng)大了?嗯,甚至有點(diǎn)像 Adobe Animate ( Adobe Flash )動(dòng)畫工具啦!最后,提醒一點(diǎn):由于金幣會(huì)在玩家碰撞后立刻進(jìn)行消失動(dòng)畫,這個(gè)時(shí)候我們要保證玩家不會(huì)再和金幣繼續(xù)產(chǎn)生二次碰撞,所以一定要在消失動(dòng)畫的第一幀就禁用碰撞體,同時(shí)注意運(yùn)行游戲之前別因誤勾選而禁用了碰撞體,這點(diǎn)特別重要,如果不明白怎么回事,又發(fā)生了金幣不能被正常收集,那么你可以參考我之前的文章,使用 Godot 的碰撞體調(diào)試功能測(cè)試一下吧! ?
連接信號(hào)
我們的場(chǎng)景已經(jīng)準(zhǔn)備完畢,現(xiàn)在需要添加一些操作來實(shí)現(xiàn)游戲的運(yùn)行邏輯了。首先我們要做的是:當(dāng)金幣檢測(cè)到與玩家有碰撞響應(yīng)后立刻播放消失動(dòng)畫,表明已被收集。這個(gè)碰撞相當(dāng)于一個(gè)觸發(fā)器,而這個(gè)觸發(fā)器在 Godot 中就是以 Signal 信號(hào)的方式傳播出去的,我們收到信號(hào)之后立刻更改動(dòng)畫就可以了。那么,問題來了,這里涉及到一個(gè)非常重要的概念: Signal 信號(hào),這又是什么鬼?別急,且聽我慢慢解釋。 ?
編寫過程序的朋友應(yīng)該對(duì)程序設(shè)計(jì)模式中的觀察者模式或多或少有所了解,觀察者模式聽上去很專業(yè),高大上,實(shí)際上原理非常簡(jiǎn)單:有一個(gè)物體叫做事件源,也可叫被觀察者,另外有一個(gè)物體叫訂閱者,也叫觀察者,或者事件偵聽者,觀察者訂閱事件源的某個(gè)事件,當(dāng)事件源發(fā)生了這個(gè)事件后,它并不需要知道誰訂閱了它,只管把事件廣播出去即可,然后那些訂閱了這個(gè)事件的觀察者們就能立刻偵聽到這個(gè)事件,做出相應(yīng)的處理,這就是所謂的觀察者模式。
舉個(gè)例子,想象一下有這么幾個(gè)主角:某指揮中心、某急救中心和某狙擊手。他們之間的關(guān)系和事件,如下:
狙擊手作為被觀察者,可隨時(shí)發(fā)報(bào)
指揮中心作為觀察者,時(shí)刻等待信號(hào)到來
急救中心同樣訂閱了狙擊手的事件,作為觀察者
狙擊手發(fā)現(xiàn)敵人,發(fā)出信號(hào):“大量敵人出現(xiàn)”
指揮中心收到信號(hào),做出反應(yīng),立即派遣救援
急救中心并沒有訂閱這個(gè)事件,或者訂閱了也不處理
狙擊手被敵人干掉,發(fā)出信號(hào):“ Help me! ”
急救中心訂閱了該事件,馬上行動(dòng),開始救援
這就是觀察者模式,如果還不清楚的話,可以看下圖:
理解了觀察者模式,就理解了 Godot 中的信號(hào),回到金幣場(chǎng)景中,當(dāng) Area2D ( Coin ) 發(fā)生碰撞的時(shí)候,立刻發(fā)出“碰撞”信號(hào),所有的“感興趣的訂閱者”收到這個(gè)信號(hào)后作出各自相應(yīng)的處理,這個(gè)處理就是訂閱者們的“某個(gè)函數(shù)”。在 Godot 中訂閱事件或者信號(hào)叫 Connect 連接,信號(hào)發(fā)出后,連接了該信號(hào)的訂閱者的相應(yīng)函數(shù)會(huì)被調(diào)用,也就是成功處理了該事件,完成一個(gè)流程。如何使用 Signal 信號(hào)呢?原理簡(jiǎn)單,操作也不難:
按上圖中的操作步驟:先給 Area2D ( Coin )添加一個(gè)空腳本,然后點(diǎn)擊發(fā)出信號(hào)的節(jié)點(diǎn) Area2D ( Coin ),在 Node 面板的 Signals 下顯示了 Area2D 節(jié)點(diǎn)的所有信號(hào)種類,這里我們選擇 body_entered(PhysicsBody2D body) 也就是碰撞體進(jìn)入信號(hào),雙擊它或者單擊右下方的 Connect… 按鈕,在彈出框中選擇接收該信號(hào)的訂閱者(這里訂閱者仍然是金幣節(jié)點(diǎn)本身,自己處理自己發(fā)出的信號(hào)),設(shè)置處理信號(hào)的方法函數(shù),注意 Make Function 默認(rèn)開啟,如果關(guān)閉了則需要在腳本中手動(dòng)編寫該函數(shù)!連接后我們打開腳本文件,可以看到 Godot 自動(dòng)幫我們添加了一個(gè)方法,同時(shí)在 Area2D 的信號(hào)面板中也有了變化: body_entered(PhysicsBody2D body) 信號(hào)下有了新建方法的連接提示。啰嗦了點(diǎn),圖片能理解的朋友直接跳過吧!
暫時(shí)丟下代碼,我們轉(zhuǎn)到主場(chǎng)景中添加我們制作好的金幣子場(chǎng)景。在主場(chǎng)景中,點(diǎn)擊 ? 鏈接按鈕,然后選擇我們保存的金幣場(chǎng)景資源 Coin.tscn 文件,即可實(shí)例化一個(gè)金幣到主場(chǎng)景中,重復(fù)這個(gè)操作,多添加幾個(gè)金幣,放置到不同的位置,充分發(fā)揮你的想象吧!
工作基本完成,第二種子場(chǎng)景制作方式也介紹了,信號(hào)的原理、使用、添加也了解清楚了,最后就是邏輯處理啦。
邏輯代碼
回到金幣子場(chǎng)景,打開 GDScript 腳本,添加代碼:
extends Area2D
func _on_Coin_body_entered(body):
$AnimationPlayer.current_animation = 'disappear'
# 打印文字到控制臺(tái),作為測(cè)試用
print('Coin collected!')
1
2
3
4
5
6
代碼再簡(jiǎn)單不過!當(dāng)金幣被玩家收集后,也就是發(fā)生碰撞的時(shí)刻,金幣發(fā)出信號(hào),在代碼中處理信號(hào)讓金幣消失——運(yùn)行消失動(dòng)畫。運(yùn)行游戲,測(cè)試!
貌似一切 OK ,實(shí)際上這里潛伏了一個(gè)大問題:硬幣被收集后雖然表面上看不見,但實(shí)際上并沒從場(chǎng)景中消失!如果你開啟碰撞體調(diào)試就能清楚地看到這個(gè)問題的存在,這可能會(huì)引起一個(gè)運(yùn)行 Bug :如果金幣一直存在,游戲占用內(nèi)存越來越多不能及時(shí)釋放,以至于可能發(fā)生內(nèi)存溢出而導(dǎo)致游戲崩潰!如何處理呢?會(huì)不會(huì)添加很多邏輯?哈哈,完全沒必要,只需再添加一個(gè)簡(jiǎn)單的信號(hào)函數(shù)就可以輕松搞定!
我們已經(jīng)在上一節(jié)做到了金幣收集這個(gè)動(dòng)作,接下來要處理的事情是:當(dāng)金幣的消失動(dòng)畫運(yùn)行到最后一幀,要把它從游戲中真正的移除!這有涉及到信號(hào)的處理,當(dāng) AnimationPlayer 播放到最后一幀的時(shí)候也會(huì)發(fā)出一個(gè)信號(hào): animation_finished(String anim_name) 動(dòng)畫結(jié)束事件,和 Area2D 的碰撞事件類似,選擇 AnimationPlayer 節(jié)點(diǎn)下的相應(yīng)信號(hào),把這個(gè)信號(hào)連接到金幣根節(jié)點(diǎn) Coin 上,在方法處理中把該金幣從游戲場(chǎng)景中移除!
extends Area2D
func _on_Coin_body_entered(body):
$AnimationPlayer.current_animation = 'disappear'
print('Coin collected!')
func _on_AnimationPlayer_animation_finished(anim_name):
if anim_name == 'disappear':
# queue_free方法將出該節(jié)點(diǎn)
self.queue_free()
1
2
3
4
5
6
7
8
9
10
唯一要注意的地方在于代碼中的一個(gè)判斷條件: if anim_name == 'disappear' ,這是因?yàn)槠渌麆?dòng)畫播放結(jié)束的時(shí)候也會(huì)發(fā)出該信號(hào),而我們只想在消失動(dòng)畫結(jié)束時(shí)候做相應(yīng)處理。
大功告成,運(yùn)行查看效果!
Bonus: 函數(shù)動(dòng)畫
嗯,并沒有結(jié)束,學(xué)無止境!我們?cè)賹W(xué)習(xí)一個(gè) Godot 中動(dòng)畫節(jié)點(diǎn) AnimationPlayer 的新特性:函數(shù)調(diào)用關(guān)鍵幀!試想一下,如果我們可以在消失動(dòng)畫 disappear 的最后一幀自動(dòng)調(diào)用金幣根節(jié)點(diǎn)的 queue_free() 方法,那么不就可以實(shí)現(xiàn)場(chǎng)景中刪除金幣而無需連接信號(hào)、編寫方法、處理邏輯了嗎? Godot 3.1 就是這么強(qiáng)大,如你所愿!
首先,我們?yōu)榱瞬恢貜?fù)處理同一個(gè)事件,我們需要取消動(dòng)畫播放結(jié)束的信號(hào)。只需要在已連接好的信號(hào)下方,點(diǎn)擊 Disconnect 按鈕取消關(guān)聯(lián)即可。
其次,需要稍微修改消失動(dòng)畫。在動(dòng)畫面板中,插入一個(gè)新的軌道: Call Method Track 即方法調(diào)用軌道,然后選擇目標(biāo)為 Coin 根節(jié)點(diǎn);創(chuàng)建軌道后,在動(dòng)畫的最后插入一個(gè)新的關(guān)鍵幀,彈出 Select Method 方法選擇框;搜索 void queue_free() 方法,在 Node 類下,點(diǎn)擊確定,完成方法關(guān)鍵幀!大致步驟如下圖:
OK ,總算結(jié)束了,高高興興地去全世界收集金幣吧,騷年! ?
三、總結(jié)
本章文字偏多,內(nèi)容并不多,主要介紹了 Godot 中的兩個(gè)關(guān)鍵特性,希望大家能理解并應(yīng)用到自己的小游戲中。本篇代碼已經(jīng)上傳到 Github ,最后總結(jié)一下本次學(xué)習(xí)到的知識(shí)點(diǎn):
創(chuàng)建子場(chǎng)景并實(shí)例化子場(chǎng)景
連接訂閱事件信號(hào),處理信號(hào)
學(xué)習(xí)使用 Godot 3.1 動(dòng)畫中的方法調(diào)用特性
其他: Area2D 節(jié)點(diǎn)簡(jiǎn)介,碰撞處理,多軌道動(dòng)畫設(shè)計(jì)