筆者作為一個unity新手,很希望能盡快用unity做出自己的游戲。為了督促自己抓緊學習,同時也可以熟悉unity使用過程中的一些技巧,並發現、探討一些問題,便決定通過撰寫博客來達到以上目的,所以就將有以下這個系列的幾篇博文。
以筆者目前掌握的技能來看,制作2d游戲是一個比較合適的選擇,於是便選擇了官方教程中的2d rogue like作為自己用來學習的項目。如果各位看官在閱讀途中發現了什么問題,還希望及時指出。
附上教程地址:http://unity3d.com/cn/learn/tutorials/projects/2d-roguelike-tutorial
==========以下是正文=========================================
每一個游戲在運行過程中,都會有很多的數值需要被計算以及記錄,因此為了方便管理,可以通過一個“游戲管理器”來統一管理這些信息。而這次筆者將盡力介紹一下在這個教程中的“游戲管理器”都做了什么,是如何做的。
首先是GameManager的代碼結構(有部分和官方略有區別,不過整體是一樣的)
可以看出,GameManager提供的主要功能有:
- 控制游戲進程,其中包括控制關卡的開始延遲、每一回合間的延遲、控制是否允許玩家操作等、判斷游戲是否結束等;
- 初始化游戲信息,其中包括了生成地圖等;
- 記錄游戲當中的一些數值,包括玩家的生命值、當前進行到的關卡級別等;
- 持有敵人等部分對象的引用;
- 控制游戲的顯示狀態、比如是應該顯示地圖還是顯示正在加載等輔助信息;
- 控制游戲的UI。
同時由於“游戲管理器”控制的都是整個游戲中一些共有的信息,所以我們不會同時需要兩個游戲管理器,因此游戲管理器將是一個單例對象,所以它還需要自己管理一些跟單例操作有關的信息。
接下來說明一下每個方法的具體作用。
當cs腳本被加載之后,將首先執行Awake()方法,具體內容如下:
由於Awake()方法將在所有其它方法之前被執行,因此一般用這個方法來進行一些必要變量的初始化等等。而在本例中,方法首先判斷了GameManager的單例是否有值,如果沒有則將其指向當前對象;如果有值則將之前值的游戲對象(gameObject)銷毀;接下來用DontDestoryInLoad()方法將當前的GameManager標記為再下一個場景創建時不會被銷毀,通過這個方法可以保留在上一關中記錄的數值;接下來再為enemies(敵方單位)數組以及boardScript(地圖管理器)分配內存;最后執行InitGame()方法初始化當前關的內容。
在這些里面值得注意的有,首先由於unity在每加載新的一關時,會把上一關創建的對象全部銷毀,但是由於我們在GameManager中存放了很多數據需要在下一關使用,所以不能讓自己被直接銷毀,所以需要用DontDestoryOnLoad()方法來保持自己。而BoardManager是用來管理地圖生成並且存放地圖數據的類,具體內容將會單獨作為一篇文章(估計就下一篇吧┗|*`0′*|┛)。
接下來看InitGame()方法。
首先將doingSetup標記修改為true,通過修改這個標記,並在Update函數中進行判斷可以使關卡的邏輯更新暫停,避免在游戲初始化的過程中就已經開始運行的情況;接下來是更新ui,使得關卡在初始化的過程中會被“第x天”的信息給覆蓋,而不是直接顯示下一關的地圖,這樣如果在需要初始化的內容比較多的情況下還是非常有用的,並且可以給人一種比較明顯的關卡過渡的感覺;接下來便是清除管理器之前持有的敵方單位的引用以及最后生成地圖。
在這里值得注意的是用到了Invoke()方法。Unity中的Invoke()方法是用來在調用該方法的指定時間長度之后調用另一個方法的。該方法有兩個參數,第一個是需要被調用的方法名,而第二個則是等待的時間長度。在本例里,則是要在“levelStartDelay”秒后調用“HideLevelImage”方法。內容如下:
即:將覆蓋在關卡地圖上的信息去掉,並將doingSetup標記置為false,這樣在Update函數中便會開始執行游戲的邏輯。可以看到其中調用了GameObject.SetActive()方法,該方法的內容是將某個GameObject標記為Active或者Inactive,而Unity只會顯示被標記為Active的對象。需要注意的是,如果某個對象是另一個GameObject的子對象,且其父級對象未被標記為Active的話,即便其自身被標記為Active也依舊不會在畫面中出現,但是可以通過GameObject.activeSelf屬性獲取到其是否被激活。
OnLevelWasLoaded()方法用來在場景切換結束后進行一些工作,也是Unity提供的API之一。
在這里的作用就是把關卡級數+1, 之后再初始化游戲。值得注意的是,筆者查到好像在開始游戲的第一個場景里(點擊play按鈕之后進入的第一個場景被加載后),該方法並不會被觸發,只有在之后的場景被加載完成后才會被觸發,因此不用擔心沒有進行第一關就直接進入了第二關。(即便會被觸發,解決方法也是很簡單的)。
至於Start()方法,筆者之前是打算在這里面初始化一些游戲的數據的,但是由於他的執行順序在Awake()和OnLevelWasLoaded()之后,所以會導致很多在前兩個方法里面需要的對象還沒有初始化便需要被調用,因此便把那些工作放在了Awake()里面,不過這種方式是否算是正確的使用方式可能還需要進一步探索,既然是初學就先以實現功能為主吧(實力偷懶\(^o^)/~)。
GameOver()方法是用來終止整個游戲的,內容如下:
主要作用是隱藏游戲的地圖界面並顯示最后的分數。可以看到其中將GameManager的enabled屬性設定為了false;這樣將會停止GameManager的Update()執行,通過這種方式將使整個游戲的邏輯停止更新。
AddEnemyToList()方法,
很簡單,由創建敵方單位的腳本調用,作用就是讓GameManager持有某個敵方單位的引用便於操作。
Update()方法也很簡單,如果同時滿足“非玩家回合、敵方單位不在移動中、游戲沒有在進行設定操作”這三個條件,則開啟協程MoveEnemies()。
具體協程是什么、是如何實現的,本篇暫且不詳細講解(其實讓我講我也說不清楚),總之只需要知道現在它就是用來使用單線程的方式達到多線程的效果就行了,具體在本例中的MoveEnemies()方法,就是讓地方單位可以根據玩家的位置進行移動,同時又不會因為計算使整個游戲在某段時間完全失去響應。
在Unity中,通過協程方式執行的方法主要有兩個特征:
- 返回值是IEnumerator類型且沒有參數 (在這里要更正一下,有參數的方法也是可以用作協程的)
- 方法中會出現yield關鍵字
具體攜程可以用來干的事情也許會有很多種,不過比較廣泛的用法是進行一些需要延時的操作但是在延時的過程中又不至於讓程序失去響應。由於Unity的邏輯處理是單線程的,所以如果不通過這種方法來實現延時操作的話,可能會導致線程卡在某個地方導致體驗下降。
在方法中,首先標記了enemiesMoving為true,這樣就不會使Update()方法再次開啟一個協程導致處理混亂。通過yeild return new WaitForSecond()方法,則可以使方法執行到這一句之后暫停一段時間,之后繼續從這個地方開始執行。而當在等待的過程中,Unity會優先處理一些其它的工作,待滿足回歸條件(在這里也就是等待的時間結束)之后,就會繼續執行下面的語句。當所有的敵方單位移動結束后,方法將會把playersTurn標記為true,將enemiesMoving標記為false,使系統進入玩家回合。
以上就是整個GameManager的結構。由於第一次寫此類文章,結構有些散亂且由於部分內容打算再下一篇文章繼續說明,所以導致可能有些方法的內容沒有介紹清楚,還請多多包涵。