前言
在Unity官方教程 2D Roguelike(3):移動邏輯中,我們完成了基本的移動邏輯,并且可以控制大胡子進行關卡內移動。這一節我們主要完成的內容是:
- 擊破障礙墻開辟道路
- 實現被敵人攻擊掉血的邏輯
- 通過拾取食物和飲料增加生命
- 移動到Exit進入到下一關
本節你將學會什么?
- 設置動畫狀態轉換
- 代碼控制觸發動畫狀態
- 控制臺debug打印信息
- 延時調用方法
- 代碼控制場景加載
在擊打障礙墻和被敵人攻擊的時候,我們期望大胡子除了扣血掉血之外,還有明顯的動作變化讓我們辨別。所以接下來我們先實現角色動畫的轉換功能。
一、實現角色動畫轉換
角色動畫的轉換功能,指的是兩種情況:
- 角色遇到障礙墻并且進行攻擊,這時候角色的動畫從idle切換到chop,并且在chop動畫結束之后切換回idle。
- 角色遇到敵人并且被敵人攻擊,這時候角色的動畫從idle切換到hit,并且在hit動畫結束之后切換回idle。
轉換邏輯主要是通過動畫控制器實現。
第1步:增加動畫轉換的觸發參數
我們雙擊Animation Controllers文件夾下的Player打開Animator面板。
在Animator面板,我們可以看到一共存在3個動畫狀態:PlayerIdle、PlayerHit、PlayerChop,其中橙色的為默認狀態,也就是當游戲運行的時候角色會播放此動畫。
在設置動畫狀態之間的轉換關系之前,我們需要添加一些參數作為轉換的觸發條件。首先選中左上角的Parameters按鈕,點擊下方輸入框的右側+號,選擇Trigger,新增一個參數playerChop。
使用同樣的方法再增加一個命名為playerHit的參數。Trigger類型的參數類似于布爾值,當選中激活的時候也就是為真,條件滿足,則進行狀態間的切換,然后立刻重置。
第2步:設置動畫狀態轉換
我們來看看如何設置待機動畫和劈砍動畫的轉換。右鍵PlayerIdle,選擇Make Transition,再點擊PlayerChop,可以看到兩個狀態之間建立起了連接,箭頭方向代表是從PlayerIdle狀態轉換為PlayerChop。
當然,劈砍動畫讀完之后是需要切換回待機的,所以我們還需要右鍵PlayerChop,再次選擇Make Transition,創建一條返回到PlayerIdle的線。
選中高亮從PlayerIdle出發到PlayerChop的線,我們會發現連線也是有屬性設置的,可以設置比如是否有打斷時間、過度持續時長、觸發轉換的條件等。
我們只需要設置以下幾項:
- Has Exit Time:接受打斷時間,勾選后要等當前動畫播放到指定的地方,才會接下一個狀態;不勾選的話,就允許動畫隨時被打斷切換到下一個狀態。在這里,因為我們需要在遇到障礙墻的時候立刻劈砍,不需要等 待機動畫 播放完畢,所以我們選擇不勾選。
- Transition Duration(s):過度動畫持續時間,一般3D動畫的話會用上來實現動畫狀態之間混合。在這里我們是使用基于Sprite的動畫,所以不需要過度,設置為0即可。
- Conditions:進行轉換的觸發條件,在這里我們選擇playerChop。
現在需要配置從PlayerChop到PlayerIdle的轉換。選中高亮從PlayerChop到PlayerIdle的白線:
- Has Exit Time:和前面相反,我們在從劈砍動畫轉換回待機動畫的時候,必須保證劈砍動畫播放完畢了才可以轉為待機動畫。所以這里我們需要勾中此項。
- Exit Time:當Has Exit Time為選中狀態的時候,此項可進行配置。在此例中我們修改為1,這樣確保在進行轉換之前劈砍動作是執行完畢了的。
- Transition Duration(s):同上,為0即可。
- Conditions:因為我們勾選了 Has Exit Time選項,也就意味著在Exit Time結束,劈砍動畫播放完畢之后會自動轉換為下一個狀態,因此不需要額外設定觸發條件。
如此就完成了PlayerIdle和PlayerChop之間的互相轉換的設置。用同樣的步驟,我們連接PlayerIdle和PlayerHit兩種狀態并且進行設置,當然,從待機到受傷的Conditions是選擇playerHit條件的。
OK,我們可以來測試一下動畫轉換是否成功。
左鍵點擊Animator面板標簽欄不松開,拖動到下面Console右側,然后運行游戲,分別選中playerChop和playerHit觸發器,注意查看大胡子的動作。
可以看到大胡子在選擇對應觸發器的情況下做出了正確的反應,配置正確!
二、劈砍障礙墻
前面提過,遇到障礙墻的時候是在OnCantMove()內執行敲墻相關邏輯。那我們就在Player腳本里增加以下代碼:
- 首先是新增公共成員wallDamage,指的是每次劈砍對障礙墻造成的傷害值,在這里我們設置默認值1,可以在編輯器里的inspector窗口修改。
- 新增一個私有成員animator,掛載在Player物體上的Animator組件,并且在Start()方法內進行初始化賦值。因為對Start()進行了重寫,所以需要添加修飾關鍵詞override,然后通過base.Start()調用了父類MovingObject的Start()方法。
- 在OnCantMove()方法內,把傳入來的組件參數轉化為我們想要的Wall類型,賦值給hitWall,然后調用Wall類的DamagWall方法來實現真正的攻擊障礙墻操作(如替換磚圖片、扣障礙墻生命等)。為了讓我們肉眼識別出角色在進行攻擊,我們需要調用animator的SetTrigger()方法來激活playerChop觸發器,這樣就會播放對應的chop動畫,看到大胡子在劈砍障礙墻。
可以注意到,大胡子的確是成功把障礙墻砍倒了,但是我只按了一次向右方向鍵,它卻執行了4次敲墻操作讓墻消失了(Wall類設置墻生命是4)。回顧之前我們為了解決單次輸入卻多次移動而臨時添加的修正代碼,其實也很好理解,是由于基類里AttempMove()方法內最后一行,我們讓開關為true了,執行了多次Update()。后續寫怪物邏輯的時候會同步修正這個問題。
三、拾取食物和飲料
拾取食物和飲料是為了補充角色的生命(或者稱呼為血量、食物、體力、點數都可以),這個生命有以下邏輯:
- 走一步扣1點生命(按一次移動方向鍵就扣1點,不管實際上是否成功移動)。
- 被攻擊,減少定量生命。
- 拾取食物和飲料,增加定量生命。
- 小于等于0時則游戲結束。
那么,我們首先需要有生命這個成員屬性,才能在編寫拾取食物邏輯時調用。
第1步:增加角色生命屬性
角色的生命屬性貫穿于整個游戲過程,包括進入新的關卡也會把這個屬性繼承過去,因此理應是在GameController腳本中聲明設定。
在GameController腳本中增加圖中所示代碼,新增一個公共成員playerFoodPoints,代表角色的生命值,初始值是100。
然后切換到Player腳本,增加如下代碼。
- 新增私有成員變量food,作為角色的生命值。
- Start()方法,獲取游戲管理器里角色生命值playerFoodPoints,并且賦值給food。
- 當物體被銷毀時會自動調用OnDisable()方法,把當前角色生命值返回給游戲管理器。
第2步:拾取食物和飲料
前面制作素材的預制件的時候曾提過,把Food、Soda等的碰撞器Is Trigger選項選中,是為了可以在腳本代碼里使用OnTriggerEnter2D等方法執行檢測到碰撞之后的操作。那我們就把這個方法加入到Player腳本。
代碼簡析:
- 新增兩個公共成員變量pointsPerFood、pointsPerSoda,分別指的是拾取食物和飲料之后能給角色增加多少生命值,并且賦予了初始值。
-
OnTriggerEnter2D方法,檢測物理發生碰撞的時候調用執行里面的代碼,參數other變量表示Player碰到的其他2D碰撞器,用if語句判斷碰到的這個對象是否被標記了Tag為“Food”或者“Soda”,如果是的話則把對應的成員變量pointsPerFood或pointsPerSoda加給food,并且調用SetActive把這個對象設置為未激活狀態(不顯示),也就是false。
通過上述代碼,我們可以實現在角色移動到食物或者飲料格子的時候,角色生命增加,食物或者飲料消失。
讓我們來測試下。
很順利,但是我們不知道生命是否真的增加了,那么可以試試增加Debug.Log代碼來打印信息。
使用Debug.Log打印出來的信息會出現在Unity的Console控制臺,測試程序的時候會經常利用這個方法來定位問題所在。
在OnTriggerEnter2D()方法內增加代碼,把food值打印出來。
再運行游戲,并且切換到Console窗口。
可以看到,在吃了一堆紅果子之后,food值打印出來是110,原來的100加上新增10點生命,數目是正確的!后面的蘇打水同理。
測試完畢,記得把這兩句debug代碼刪掉啦~
四、被敵人攻擊
在Player腳本里增加一個LoseFood()方法,代碼如下:
- 方法為public是因為攻擊的主體是Enemy,所以會在Enemy的腳本里調用這個方法來實現對角色的扣除生命操作。
- 傳入的參數loss是指敵人單次攻擊對角色造成的傷害數值,當角色被攻擊的時候,會播放hit動畫,生命會被扣除,同時檢測如果生命小于等于0,則角色死亡,游戲結束。
游戲管理者控制游戲進程,因此我們把游戲結束邏輯代碼寫在了GameController腳本里并在LoseFood()里調用。
五、移動到Exit進入下一關
角色移動到關卡右上角的Exit格子的時候,是進入下一關卡,也就是說重新加載場景生成下一關卡的地圖。
- 我們需要在頭部引入UnityEngine.SceneManagement,這樣后面代碼才可以調用方法SceneManager.LoadScene()。
- 在OnTriggerEnter2D里新增判斷,當tag為Exit的時候,就通過Invoke方法來延時調用Restart方法執行加載場景操作。
Invoke()可以實現根據時間調用指定的方法,延時加載場景,這樣視覺上過渡更加平滑,不突兀。
- Restart(),調用SceneManager.LoadScene()方法來重新加載指定的場景,0代表最后加載的場景,在我們這個例子里就是唯一的場景Main。
enabled = false 可以禁用腳本,避免角色在加載新場景之前就再次移動了。
這時候我們有個疑問:為什么重新加載了沒有生成新的關卡?(無視粉紅色,那是我個人配置的背景色...)
真相只有一個,“罪魁禍首”就是它:DontDestroyOnLoad()!
GameController作為游戲管理器,從頭到尾只能有一個并且直到游戲結束才需要銷毀,所以在之前講單例模式的時候調用了DontDestroyOnLoad()方法來保證重新加載場景的時候不銷毀實例。
那不銷毀的情況下為什么會導致不生成新關卡呢?這和Awake()的特點有關系:
在游戲對象被銷毀之前的整個生命周期之中,Awake()只執行一次!
所以重新加載場景之后,Awake()不執行,自然就不能調用InitGame()方法來生成隨機關卡了。
我們試試把代碼注釋掉再運行游戲。
的確是有效了,但是這樣做會導致游戲管理器每次進入新關卡就被摧毀,需要保留繼承的數據比如人物生命、回合開關等都會被還原為初始值。因此,我們是建議使用另外更好的辦法來實現重新加載場景時生成下一關的,后續會補充。
測試完畢,記得把兩條杠去掉啦~
六、本篇收尾
我們講過,角色意圖移動的時候就會扣1點生命,現查漏補缺,把相關邏輯代碼加上:
- 在Player的Update()里,是每次獲取了輸入就執行AttempMove(),因此我們把相關的扣血邏輯寫在AttempMove()方法內。
- 首先就是扣除生命1點,然后通過base調用父類的方法進行判斷和移動,最后如果生命小于低于0則調用GameOver()方法結束游戲。
判斷游戲是否結束這段代碼之前也在LoseFood()方法內用過,所以我們可以把這段代碼提煉出來作為一個新的方法,這樣以后再有需要的地方就可以直接調用,實現代碼復用。
感興趣的童鞋還可以用Debug打印下food值,看是不是每走一步扣1點生命。
角色移動的邏輯基本都實現了,完成了這個小游戲最核心的機制,接下來就是一馬平川啦!