Unity官方教程 2D Roguelike(4):角色移動

2D Roguelike 最終效果

前言

Unity官方教程 2D Roguelike(3):移動邏輯中,我們完成了基本的移動邏輯,并且可以控制大胡子進(jìn)行關(guān)卡內(nèi)移動。這一節(jié)我們主要完成的內(nèi)容是:

  • 擊破障礙墻開辟道路
  • 實現(xiàn)被敵人攻擊掉血的邏輯
  • 通過拾取食物和飲料增加生命
  • 移動到Exit進(jìn)入到下一關(guān)

本節(jié)你將學(xué)會什么?

  • 設(shè)置動畫狀態(tài)轉(zhuǎn)換
  • 代碼控制觸發(fā)動畫狀態(tài)
  • 控制臺debug打印信息
  • 延時調(diào)用方法
  • 代碼控制場景加載

在擊打障礙墻和被敵人攻擊的時候,我們期望大胡子除了扣血掉血之外,還有明顯的動作變化讓我們辨別。所以接下來我們先實現(xiàn)角色動畫的轉(zhuǎn)換功能。

一、實現(xiàn)角色動畫轉(zhuǎn)換

角色動畫的轉(zhuǎn)換功能,指的是兩種情況:

  1. 角色遇到障礙墻并且進(jìn)行攻擊,這時候角色的動畫從idle切換到chop,并且在chop動畫結(jié)束之后切換回idle。
  2. 角色遇到敵人并且被敵人攻擊,這時候角色的動畫從idle切換到hit,并且在hit動畫結(jié)束之后切換回idle。
角色動畫轉(zhuǎn)換關(guān)系

轉(zhuǎn)換邏輯主要是通過動畫控制器實現(xiàn)。

第1步:增加動畫轉(zhuǎn)換的觸發(fā)參數(shù)

我們雙擊Animation Controllers文件夾下的Player打開Animator面板。

Player動畫控制器

在Animator面板,我們可以看到一共存在3個動畫狀態(tài):PlayerIdle、PlayerHit、PlayerChop,其中橙色的為默認(rèn)狀態(tài),也就是當(dāng)游戲運行的時候角色會播放此動畫。

Animator

在設(shè)置動畫狀態(tài)之間的轉(zhuǎn)換關(guān)系之前,我們需要添加一些參數(shù)作為轉(zhuǎn)換的觸發(fā)條件。首先選中左上角的Parameters按鈕,點擊下方輸入框的右側(cè)+號,選擇Trigger,新增一個參數(shù)playerChop。

參數(shù)playerChop

使用同樣的方法再增加一個命名為playerHit的參數(shù)。Trigger類型的參數(shù)類似于布爾值,當(dāng)選中激活的時候也就是為真,條件滿足,則進(jìn)行狀態(tài)間的切換,然后立刻重置。

第2步:設(shè)置動畫狀態(tài)轉(zhuǎn)換

我們來看看如何設(shè)置待機動畫和劈砍動畫的轉(zhuǎn)換。右鍵PlayerIdle,選擇Make Transition,再點擊PlayerChop,可以看到兩個狀態(tài)之間建立起了連接,箭頭方向代表是從PlayerIdle狀態(tài)轉(zhuǎn)換為PlayerChop。

Make Transition

當(dāng)然,劈砍動畫讀完之后是需要切換回待機的,所以我們還需要右鍵PlayerChop,再次選擇Make Transition,創(chuàng)建一條返回到PlayerIdle的線。

Make Transition

選中高亮從PlayerIdle出發(fā)到PlayerChop的線,我們會發(fā)現(xiàn)連線也是有屬性設(shè)置的,可以設(shè)置比如是否有打斷時間、過度持續(xù)時長、觸發(fā)轉(zhuǎn)換的條件等。

我們只需要設(shè)置以下幾項:

  • Has Exit Time:接受打斷時間,勾選后要等當(dāng)前動畫播放到指定的地方,才會接下一個狀態(tài);不勾選的話,就允許動畫隨時被打斷切換到下一個狀態(tài)。在這里,因為我們需要在遇到障礙墻的時候立刻劈砍,不需要等 待機動畫 播放完畢,所以我們選擇不勾選。
  • Transition Duration(s):過度動畫持續(xù)時間,一般3D動畫的話會用上來實現(xiàn)動畫狀態(tài)之間混合。在這里我們是使用基于Sprite的動畫,所以不需要過度,設(shè)置為0即可。
  • Conditions:進(jìn)行轉(zhuǎn)換的觸發(fā)條件,在這里我們選擇playerChop。
從idle到chop的transition

現(xiàn)在需要配置從PlayerChop到PlayerIdle的轉(zhuǎn)換。選中高亮從PlayerChop到PlayerIdle的白線:

  • Has Exit Time:和前面相反,我們在從劈砍動畫轉(zhuǎn)換回待機動畫的時候,必須保證劈砍動畫播放完畢了才可以轉(zhuǎn)為待機動畫。所以這里我們需要勾中此項。
  • Exit Time:當(dāng)Has Exit Time為選中狀態(tài)的時候,此項可進(jìn)行配置。在此例中我們修改為1,這樣確保在進(jìn)行轉(zhuǎn)換之前劈砍動作是執(zhí)行完畢了的。
  • Transition Duration(s):同上,為0即可。
  • Conditions:因為我們勾選了 Has Exit Time選項,也就意味著在Exit Time結(jié)束,劈砍動畫播放完畢之后會自動轉(zhuǎn)換為下一個狀態(tài),因此不需要額外設(shè)定觸發(fā)條件。
從chop到idle的transition

如此就完成了PlayerIdle和PlayerChop之間的互相轉(zhuǎn)換的設(shè)置。用同樣的步驟,我們連接PlayerIdle和PlayerHit兩種狀態(tài)并且進(jìn)行設(shè)置,當(dāng)然,從待機到受傷的Conditions是選擇playerHit條件的。

PlayerIdle和PlayerHit的transition

OK,我們可以來測試一下動畫轉(zhuǎn)換是否成功。
左鍵點擊Animator面板標(biāo)簽欄不松開,拖動到下面Console右側(cè),然后運行游戲,分別選中playerChop和playerHit觸發(fā)器,注意查看大胡子的動作。

測試動畫轉(zhuǎn)換

可以看到大胡子在選擇對應(yīng)觸發(fā)器的情況下做出了正確的反應(yīng),配置正確!

二、劈砍障礙墻

前面提過,遇到障礙墻的時候是在OnCantMove()內(nèi)執(zhí)行敲墻相關(guān)邏輯。那我們就在Player腳本里增加以下代碼:

敲墻相關(guān)代碼
  • 首先是新增公共成員wallDamage,指的是每次劈砍對障礙墻造成的傷害值,在這里我們設(shè)置默認(rèn)值1,可以在編輯器里的inspector窗口修改。
  • 新增一個私有成員animator,掛載在Player物體上的Animator組件,并且在Start()方法內(nèi)進(jìn)行初始化賦值。因為對Start()進(jìn)行了重寫,所以需要添加修飾關(guān)鍵詞override,然后通過base.Start()調(diào)用了父類MovingObject的Start()方法。
  • OnCantMove()方法內(nèi),把傳入來的組件參數(shù)轉(zhuǎn)化為我們想要的Wall類型,賦值給hitWall,然后調(diào)用Wall類的DamagWall方法來實現(xiàn)真正的攻擊障礙墻操作(如替換磚圖片、扣障礙墻生命等)。為了讓我們?nèi)庋圩R別出角色在進(jìn)行攻擊,我們需要調(diào)用animator的SetTrigger()方法來激活playerChop觸發(fā)器,這樣就會播放對應(yīng)的chop動畫,看到大胡子在劈砍障礙墻。
成功敲墻

可以注意到,大胡子的確是成功把障礙墻砍倒了,但是我只按了一次向右方向鍵,它卻執(zhí)行了4次敲墻操作讓墻消失了(Wall類設(shè)置墻生命是4)?;仡欀拔覀?yōu)榱私鉀Q單次輸入?yún)s多次移動而臨時添加的修正代碼,其實也很好理解,是由于基類里AttempMove()方法內(nèi)最后一行,我們讓開關(guān)為true了,執(zhí)行了多次Update()。后續(xù)寫怪物邏輯的時候會同步修正這個問題。

playerTurn為true

三、拾取食物和飲料

拾取食物和飲料是為了補充角色的生命(或者稱呼為血量、食物、體力、點數(shù)都可以),這個生命有以下邏輯:

  • 走一步扣1點生命(按一次移動方向鍵就扣1點,不管實際上是否成功移動)。
  • 被攻擊,減少定量生命。
  • 拾取食物和飲料,增加定量生命。
  • 小于等于0時則游戲結(jié)束。

那么,我們首先需要有生命這個成員屬性,才能在編寫拾取食物邏輯時調(diào)用。

第1步:增加角色生命屬性

角色的生命屬性貫穿于整個游戲過程,包括進(jìn)入新的關(guān)卡也會把這個屬性繼承過去,因此理應(yīng)是在GameController腳本中聲明設(shè)定。

playerFoodPoints

在GameController腳本中增加圖中所示代碼,新增一個公共成員playerFoodPoints,代表角色的生命值,初始值是100。
然后切換到Player腳本,增加如下代碼。

生命值food
  • 新增私有成員變量food,作為角色的生命值。
  • Start()方法,獲取游戲管理器里角色生命值playerFoodPoints,并且賦值給food。
  • 當(dāng)物體被銷毀時會自動調(diào)用OnDisable()方法,把當(dāng)前角色生命值返回給游戲管理器。

第2步:拾取食物和飲料

前面制作素材的預(yù)制件的時候曾提過,把Food、Soda等的碰撞器Is Trigger選項選中,是為了可以在腳本代碼里使用OnTriggerEnter2D等方法執(zhí)行檢測到碰撞之后的操作。那我們就把這個方法加入到Player腳本。

OnTriggerEnter2D()

代碼簡析:

  • 新增兩個公共成員變量pointsPerFood、pointsPerSoda,分別指的是拾取食物和飲料之后能給角色增加多少生命值,并且賦予了初始值。
  • OnTriggerEnter2D方法,檢測物理發(fā)生碰撞的時候調(diào)用執(zhí)行里面的代碼,參數(shù)other變量表示Player碰到的其他2D碰撞器,用if語句判斷碰到的這個對象是否被標(biāo)記了Tag為“Food”或者“Soda”,如果是的話則把對應(yīng)的成員變量pointsPerFood或pointsPerSoda加給food,并且調(diào)用SetActive把這個對象設(shè)置為未激活狀態(tài)(不顯示),也就是false。
    通過上述代碼,我們可以實現(xiàn)在角色移動到食物或者飲料格子的時候,角色生命增加,食物或者飲料消失。
    讓我們來測試下。
拾取食物和飲料

很順利,但是我們不知道生命是否真的增加了,那么可以試試增加Debug.Log代碼來打印信息。

使用Debug.Log打印出來的信息會出現(xiàn)在Unity的Console控制臺,測試程序的時候會經(jīng)常利用這個方法來定位問題所在。

在OnTriggerEnter2D()方法內(nèi)增加代碼,把food值打印出來。

增加log打印

再運行游戲,并且切換到Console窗口。

有日志的吃東西

可以看到,在吃了一堆紅果子之后,food值打印出來是110,原來的100加上新增10點生命,數(shù)目是正確的!后面的蘇打水同理。
測試完畢,記得把這兩句debug代碼刪掉啦~

四、被敵人攻擊

在Player腳本里增加一個LoseFood()方法,代碼如下:

LoseFood()
  • 方法為public是因為攻擊的主體是Enemy,所以會在Enemy的腳本里調(diào)用這個方法來實現(xiàn)對角色的扣除生命操作。
  • 傳入的參數(shù)loss是指敵人單次攻擊對角色造成的傷害數(shù)值,當(dāng)角色被攻擊的時候,會播放hit動畫,生命會被扣除,同時檢測如果生命小于等于0,則角色死亡,游戲結(jié)束。
    游戲管理者控制游戲進(jìn)程,因此我們把游戲結(jié)束邏輯代碼寫在了GameController腳本里并在LoseFood()里調(diào)用。
GameOver

五、移動到Exit進(jìn)入下一關(guān)

角色移動到關(guān)卡右上角的Exit格子的時候,是進(jìn)入下一關(guān)卡,也就是說重新加載場景生成下一關(guān)卡的地圖。

Restart
  • 我們需要在頭部引入UnityEngine.SceneManagement,這樣后面代碼才可以調(diào)用方法SceneManager.LoadScene()。
  • 在OnTriggerEnter2D里新增判斷,當(dāng)tag為Exit的時候,就通過Invoke方法來延時調(diào)用Restart方法執(zhí)行加載場景操作。

Invoke()可以實現(xiàn)根據(jù)時間調(diào)用指定的方法,延時加載場景,這樣視覺上過渡更加平滑,不突兀。

  • Restart(),調(diào)用SceneManager.LoadScene()方法來重新加載指定的場景,0代表最后加載的場景,在我們這個例子里就是唯一的場景Main。

enabled = false 可以禁用腳本,避免角色在加載新場景之前就再次移動了。

重新加載場景

這時候我們有個疑問:為什么重新加載了沒有生成新的關(guān)卡?(無視粉紅色,那是我個人配置的背景色...)


為什么呢?

真相只有一個,“罪魁禍?zhǔn)住本褪撬?strong>DontDestroyOnLoad()!

DontDestroyOnLoad()

GameController作為游戲管理器,從頭到尾只能有一個并且直到游戲結(jié)束才需要銷毀,所以在之前講單例模式的時候調(diào)用了DontDestroyOnLoad()方法來保證重新加載場景的時候不銷毀實例。
那不銷毀的情況下為什么會導(dǎo)致不生成新關(guān)卡呢?這和Awake()的特點有關(guān)系:

在游戲?qū)ο蟊讳N毀之前的整個生命周期之中,Awake()只執(zhí)行一次!

所以重新加載場景之后,Awake()不執(zhí)行,自然就不能調(diào)用InitGame()方法來生成隨機關(guān)卡了。

注釋DontDestroyOnLoad()

我們試試把代碼注釋掉再運行游戲。

重新生成新關(guān)卡

的確是有效了,但是這樣做會導(dǎo)致游戲管理器每次進(jìn)入新關(guān)卡就被摧毀,需要保留繼承的數(shù)據(jù)比如人物生命、回合開關(guān)等都會被還原為初始值。因此,我們是建議使用另外更好的辦法來實現(xiàn)重新加載場景時生成下一關(guān)的,后續(xù)會補充。
測試完畢,記得把兩條杠去掉啦~

六、本篇收尾

我們講過,角色意圖移動的時候就會扣1點生命,現(xiàn)查漏補缺,把相關(guān)邏輯代碼加上:

重寫AttempMove方法
  • 在Player的Update()里,是每次獲取了輸入就執(zhí)行AttempMove(),因此我們把相關(guān)的扣血邏輯寫在AttempMove()方法內(nèi)。
  • 首先就是扣除生命1點,然后通過base調(diào)用父類的方法進(jìn)行判斷和移動,最后如果生命小于低于0則調(diào)用GameOver()方法結(jié)束游戲。

判斷游戲是否結(jié)束這段代碼之前也在LoseFood()方法內(nèi)用過,所以我們可以把這段代碼提煉出來作為一個新的方法,這樣以后再有需要的地方就可以直接調(diào)用,實現(xiàn)代碼復(fù)用。

CheckIfGameOver

感興趣的童鞋還可以用Debug打印下food值,看是不是每走一步扣1點生命。
角色移動的邏輯基本都實現(xiàn)了,完成了這個小游戲最核心的機制,接下來就是一馬平川啦!

上一章傳送門:移動邏輯
下一章傳送門:敵人移動

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容