Unity学习笔记2 简易2D横版RPG游戏制作(二)
十二、敵人受攻擊時的閃爍和Player的生命值的修正
上一篇中,我們利用Controller2D中的IEnumerator TakenDamage接口,使得我們的Player受到攻擊時會進行閃爍,我們同樣地也希望在我們的敵人身上可以實現相同的效果。所以我們現在需要復制Controller2D腳本里面的兩個內容到我們的Enemy2D腳本里面去:
第一個內容:
//顯示角色當前正受到攻擊float takenDamage = 0.2f;
第二個內容:
public IEnumerator TakenDamage(){renderer.enabled = false;yield return new WaitForSeconds(takenDamage);renderer.enabled = true;yield return new WaitForSeconds(takenDamage);renderer.enabled = false;yield return new WaitForSeconds(takenDamage);renderer.enabled = true;yield return new WaitForSeconds(takenDamage);renderer.enabled = false;yield return new WaitForSeconds(takenDamage);renderer.enabled = true;yield return new WaitForSeconds(takenDamage);}
接下來我們需要對Bullet腳本進行處理,使其在碰撞到我們的敵人時,向敵人發送一個閃爍的信號:(修改內容如下)
void OnTriggerEnter(Collider other){if (other.gameObject.tag == "Enemy") {Destroy(gameObject);other.gameObject.SendMessage("EnemyDamaged",damageValue,SendMessageOptions.DontRequireReceiver);other.gameObject.SendMessage("TakenDamage",SendMessageOptions.DontRequireReceiver);}if (other.gameObject.tag == "LevelObjects") {Destroy(gameObject);}}
其實就是增加了一句:
other.gameObject.SendMessage("TakenDamage",SendMessageOptions.DontRequireReceiver);
解決了敵人的閃爍問題之后,我們接下來要處理另外一個問題了。這個問題就是,我們的角色每消滅一些敵人就會增加經驗,而經驗的增加會自動補充HP,這個有點不對頭和不合理,我們升級的時候增加的應該是最大HP,在每個等級下,我們應該設置一個當前的最大HP,不能無限制地增加HP才對,所以呢,我們需要將原來的GameManager腳本里面的playersHealth變量改名為curHealth,并且增加一個maxHealth變量,同時設置為3,將原本的playersHealth++修改為maxHealth++,這樣的話,消滅敵人只會增加你的最大HP,而不會增加你當前的HP,至于升級之后是不是要讓HP全滿呢,這個功能很容易實現,我暫時不想加進去。
接下來我們就需要考慮下一個問題了,那就是角色的戰斗力必須隨著等級的提升而得到提升才比較合理。不能每次攻擊都只扣敵人一滴血啊。而且敵人只有3到6滴血的設定不太合理,其實反正敵人的血是不顯示出來的,完全可以設置一些大一點的數值,比如100,500之類的,這個就到后面有需要再改吧。現在暫時先不動。
十三、補血藥的設置
在這一講里面,首先要處理一個問題:我們的Player應該是有戰斗力的,而不是每次只向敵人發送1滴的傷害值。利用某某++的方法可以很容易實現升級時增加戰斗力,這個就不說了。關鍵是怎么弄成用我們的戰斗力去減敵人的血值。首先,我們在GameManager腳本里面添加這么一行:
static public int bulletDamage = 1; 然后把我們的curHealth,也就是當前HP的值也改成static public。這里順便補充一下,static在c#中的作用。
靜態分配的,有兩種情況:1. 用在類里的屬性、方法前面,這樣的靜態屬性與方法不需要創建實例就能訪問,通過類名或對象名都能訪問它,靜態屬性、方法只有“一份”:即如果一個類新建有N個對象,這N 個對象只有同一個靜態屬性與方法; 2. 方法內部的靜態變量:方法內部的靜態變量,執行完靜態變量值不消失,再次執行此對象的方法時,值仍存在,它不是在棧中分配的,是在靜態區分析的, 這是與局部變量最大的區別; 如果這個說得不具體的話,那么可以看一下下面這個,紅黑聯盟里面講的,非常形象,保證一看馬上就明白了:(我也是在看視頻教程的過程中碰到了static不懂,然后看下面這個理解透徹的)
提起static,一般理解為靜態、全局。
何為static?我理解的static屬于程序的直屬單位,而非static就是非直屬單位。
舉一個非常常見的例子,中國有4個直轄市,北京、上海、天津、重慶,這些相當于static,而廣州、南京、杭州等就是非static,中央可以直接管理北京、上海、天津、重慶,而廣州、南京、杭州應由各省政府管理,Main方法可以直接調用static,而調用非static需要實例化。
class City() { //4個直轄市static 靜態全局類型 public static void Beijing(){} public static void ShangHai(){} public static void Tianjin(){} public static void Chongqing(){} //其他城市 非靜態 public void Guangzhou(){} public void Nanjing(){} } void Main() { //調用static類型的方法 City.Beijing();//調用北京 City.Shanghai();//調用上海 //調用非static類型的方法 //沒有直接調用權利,必須先實例化 City chengShi=new City(); chengShi.Guangzhou();//調用廣州 } 講的形象就達到目的了,為剛開始學習 編程 的同學加把勁兒。
原鏈接:http://www.2cto.com/kf/201209/152853.html
好啦,然后我們繼續。接著我們打開我們的Bullet腳本,把里面的內容修改成這樣:
using UnityEngine; using System.Collections;public class Bullet : MonoBehaviour {//用于碰撞時摧毀兩個物體void OnTriggerEnter(Collider other){if (other.gameObject.tag == "Enemy") {Destroy(gameObject);other.gameObject.SendMessage("EnemyDamaged",GameManager.bulletDamage,SendMessageOptions.DontRequireReceiver);other.gameObject.SendMessage("TakenDamage",SendMessageOptions.DontRequireReceiver);}if (other.gameObject.tag == "LevelObjects") {Destroy(gameObject);}}void FixedUpdate(){Destroy (gameObject, 1.25f);} } 其實上面的內容只是將
other.gameObject.SendMessage("EnemyDamaged",GameManager.bulletDamage,SendMessageOptions.DontRequireReceiver); 這一行進行了處理,我們原本的那個damageValue變量已經被刪掉,然后替換成我們GameManager里面的bulletDamage,因為我們的這個bulletDamage已經修改為static,所以現在可以這樣調用了。以后如果想要升級增加角色的殺傷力的話,就容易多了。
接下來我們想要在運行游戲中按C的時候順便顯示我們的戰斗力,這個太簡單了,只需要對GameManager里面做一點點修改:
if (playerStats) {statsDisplay.text = "等級:" + level + "\n經驗:" + curEXP + "/" + maxEXP + "\n攻擊力:" + bulletDamage;} 這一部分大家可以隨便修改,你想顯示什么就修改什么。至于在畫面中的位置,可以調整GUIStats的transform,這個就不再贅述。
我稍微做了點修改。
接下來我們需要在場景中新增一個quad,去掉Mesh Collider,加上Box Collider,然后Box?Collider的size全部改成1,transform里面的position的z值別忘了設成0,名字隨便起,是用來做成補血藥的,我命名為HealthPotion,然后為它增加一個同名的tag。為了區分,我順便弄了一個同名的material扔上去,將顏色調成粉紅色。(呵呵……感覺是毒藥而不是血藥啊……)
接著我們打開上次弄的那個StickToPlatform腳本,把里面的東西復制到我們的Controller2D上面,其實因為兩個腳本都是綁定在Player身上,就沒必要弄兩個腳本了,直接合并成一個就成了。
然后我們再增加一段小代碼來使得我們的Player碰到帶有HealthPotion的物體時,將這個物體摧毀并且curHealth值加1。
void OnTriggerStay(Collider other){if (other.tag == "Platform") {this.transform.parent = other.transform;}}void OnTriggerExit(Collider other){if (other.tag == "Platform") {this.transform.parent = null;}}void OnTriggerEnter(Collider other){if (other.tag == "HealthPotion") {GameManager.curHealth++;Destroy(other.gameObject);}}
很簡單吧,現在補血藥就已經做好了。想要做出補多少血的補血藥都已經不是什么問題了。想要做出補藍、增加戰斗力什么的補血藥,也不是什么問題了哈哈。
有個地方需要大家注意一下,那就是curHealth必須加上static,否則會出現An object reference is required to access non-static member這樣的報錯顯示。在全局靜態函數里面是不可以使用非全局靜態變量的。
吃藥前(注意左上角兩個紅心)
吃藥后,哈哈,療程短見效快,一粒補一滴血~
關于Box Collider的范圍的問題,可以參考上一篇講到的Player的Character Controller組件的問題,所以我們可以順便把藥物的Box Collider的size的X值改為1.3。
十四、游戲暫停和游戲存檔
游戲存檔看似簡單,不過也是個比較蛋疼的問題。我們打開GameManager腳本:
首先,我們增加一個布爾值:
//用于暫停的布爾值bool pauseMenu;
然后我們增添以下代碼:
if (pauseMenu) {if(GUI.Button(new Rect(Screen.width*.25f,Screen.height*.4f,Screen.width*.5f,Screen.height*.1f),"保存游戲")){print ("已保存");PlayerPrefs.SetInt("Player Level",level);PlayerPrefs.SetInt("Player EXP",curEXP);}if(GUI.Button(new Rect(Screen.width*.25f,Screen.height*.6f,Screen.width*.5f,Screen.height*.1f),"顯示保存的數據")){print ("顯示保存的數據");print("當前等級:"+ PlayerPrefs.GetInt("Player Level"));print("當前經驗:"+ PlayerPrefs.GetInt("Player EXP"));}} 然后保存腳本。
現在我們在運行游戲的過程中按P鍵,就可以調出一個這樣的畫面:
當我們點擊保存游戲時,就會保存相應的數據。我們可以先殺掉一只小怪,然后保存,然后重新運行,然后點擊“顯示保存的數據”,這個時候就會看到:
但是這個時候有兩個問題,第一個問題是,雖然在這里我們可以看到保存的數據,但是實際上在游戲里面,如果我們按C鍵查看角色的屬性時,發現經驗值并沒有保存下來。第二個問題是,我們的存檔還不知道怎么刪除……
接下來馬上解決這個問題:
十五、載入游戲和刪除存檔
好了,我們繼續修改我們的GameManager腳本,首先我們要增加一個void Awake()函數,這個東西和void Start()有什么不同呢?我順便摘錄了一段網上搬過來的筆記:
這是博客園的一篇介紹的原鏈接:http://www.cnblogs.com/xpvincent/p/3178042.html
Unity3D初學者經常把Awake和Start混淆。
簡單說明一下,Awake在MonoBehavior創建后就立刻調用,Start將在MonoBehavior創建后在該幀Update之前,在該Monobehavior.enabled == true的情況下執行。
[javascript]?view plaincopy ?我們通常書寫的腳本,并不會定義[ExecuteInEditMode]這個Attribute,所以Awake和Start都只有在Runtime中才會執行。
例1:
[javascript]?view plaincopy ? 以上代碼,在Awake中我們調用了enabled = false; 禁止了這個MonoBehavior的update。由于Start, Update, PostUpdate等屬于runtime行為的一部分,這段代碼將使Start不會被調用到。
在游戲過程中,若有另外一組代碼有如下調用:
這個時候,若該MonoBehavior之前并沒有觸發過Start函數,將會在這段代碼執行后觸發。
例2:
player.cs
other.cs
[javascript]?view plaincopy ? 以上代碼中,我們在player Awake的時候去為handAnchor賦值。如果我們將這步操作放在Start里,那么在other.cs中,當執行GetWeapon的時候就會出現handAnchor是null reference.
總結:我們盡量將其他Object的reference設置等事情放在Awake處理。然后將這些reference的Object的賦值設置放在Start()中來完成。
當MonoBehavior有定義[ExecuteInEditMode]時
當我們為MonoBehavior定義了[ExecuteInEditMode]后,我們還需要關心Awake和Start在編輯器中的執行狀況。
??? 當該MonoBehavior在編輯器中被賦于給GameObject的時候,Awake, Start 將被執行。
??? 當Play按鈕被按下游戲開始以后,Awake, Start 將被執行。
??? 當Play按鈕停止后,Awake, Start將再次被執行。
??? 當在編輯器中打開包含有該MonoBehavior的場景的時候,Awake, Start將被執行。
值得注意的是,不要用這種方式來設定一些臨時變量的存儲(private, protected)。因為一旦我們觸發Unity3D的代碼編譯,這些變量所存儲的內容將被清為默認值。
下面再來看看Unity圣典中的解釋。
?Awake()
當一個腳本實例被載入時Awake被調用。
Awake用于在游戲開始之前初始化變量或游戲狀態。在腳本整個生命周期內它僅被調用一次.Awake在所有對象被初始化之后調用,所以你可以安全的與其他對象對話或用諸如 GameObject.FindWithTag 這樣的函數搜索它們。每個游戲物體上的Awke以隨機的順序被調用。因此,你應該用Awake來設置腳本間的引用,并用Start來傳遞信息。Awake總是在Start之前被調用。它不能用來執行協同程序。
Start()
Start僅在Update函數第一次被調用前調用。Start在behaviour的生命周期中只被調用一次。它和Awake的不同是Start只在腳本實例被啟用時調用。
你可以按需調整延遲初始化代碼。Awake總是在Start之前執行。這允許你協調初始化順序。
接下來我們就這樣弄,首先我們設置一個int saved,讓它等于零。(如果不賦值的話,它也會默認等于零)
//用于判斷是否是否保存int saved = 0;
接下來我們就寫下這么一個Awake函數:
void Awake(){saved = PlayerPrefs.GetInt ("Game Saved");if (saved == 1) {curEXP = PlayerPrefs.GetInt ("Player EXP");level = PlayerPrefs.GetInt ("Player Level");maxEXP = level * 50;maxHealth = level + 2;curHealth = maxHealth;} } 在運行這個Awake函數的時候,就會讓saved先獲取我們保存的值。(等一下在下面存檔的那部分腳本里面,我們會保存先把saved賦值為1,再進行保存),因為PlayerPrefs不能保存布爾值,所以我們用一個int的0和1來代替就行了,一樣的。如果我們的游戲沒有存檔的話,saved讀取不到任何數據,就會默認為零,那么就相當于不會接下去讀取我們保存的數據了,反之,如果讀取到了1,就相當于讀取到了“已經有保存的數據”的情況,就需要繼續執行。
在保存數據部分,我是這樣弄的:
if (pauseMenu) {//“保存游戲”按鈕if(GUI.Button(new Rect(Screen.width*.25f,Screen.height*.4f,Screen.width*.5f,Screen.height*.1f),"保存游戲")){print ("已保存");saved = 1;PlayerPrefs.SetInt("Player Level",level);PlayerPrefs.SetInt("Player EXP",curEXP);PlayerPrefs.SetInt("Game Saved",saved);}//“顯示保存的數據”按鈕if(GUI.Button(new Rect(Screen.width*.25f,Screen.height*.6f,Screen.width*.5f,Screen.height*.1f),"顯示保存的數據")){print ("顯示保存的數據");print("當前等級:"+ PlayerPrefs.GetInt("Player Level"));print("當前經驗:"+ PlayerPrefs.GetInt("Player EXP"));print("是否保存:"+ PlayerPrefs.GetInt("Game Saved"));}} 我們可以看到,在保存之前,我們先將saved賦值為1,然后再保存,這樣的話,當我們重新載入游戲時,就會進行一個是否保存了游戲的判斷。
至于下面那個“顯示保存的數據”的按鈕功能,只是我用來debug.log的,沒什么用,純屬調試,可以無視。
可能有人會說,保存游戲就直接保存,然后直接讀取數據不就行了,為什么還要弄一個saved來判斷呢?我一開始也是沒有弄這個玩意的,后來發現了一個問題,在這里解釋一下:假設我們現在刪除了游戲存檔,而沒有這個用來判斷是否有存檔的saved值的話,那么我們的腳本自然就不管三七二十一,你沒有存檔它也會當成你是有存檔的,這樣會出現什么問題呢?這樣的話,我們的curEXP = PlayerPrefs.GetInt ("Player EXP");和level = PlayerPrefs.GetInt ("Player Level");這兩句話就會得不到任何數據,那么就會默認為零,那么,下面的maxEXP = level * 50;還有maxHealth = level + 2;這兩句的計算就肯定會出問題了。maxEXP會變成0,而maxHealth會變成2,最大經驗值變成零也就算了,我們的血值還從3變成了2,這不是坑爹么……哈哈,所以,這下子明白為什么要做一個是否saved的判斷了吧。對于我這樣的小游戲來說,就已經需要制作一個是否saved的判斷了,對于需要保存大量數據的大游戲來說那就更是如此。希望大家如果是制作RPG類型的游戲的話,也可以養成類似的習慣。
接著要解決刪檔的問題。這個也是RPG游戲的一個重點內容。我們打開MainMenu腳本,然后在if (showGUIOutline)里面加入以下內容:
//“刪除存檔”按鈕 if (GUI.Button (new Rect (Screen.width * guiPlacementX3, Screen.height * guiPlacementY3, Screen.width * .5f, Screen.height * .1f), "刪除存檔")) { PlayerPrefs.DeleteAll(); print ("已刪除存檔"); } 其實主要就是添加一個PlayerPrefs.DeleteAll();而已,沒什么復雜的。
當然,為了方便我們在窗口中調整GUI的位置,我們也增加了guiPlacementX3還有guiPlacementY3這兩個public的float值。這里就沒必要貼出來了。
接著在Mainmenu場景里面的Maincamera上面調整好三個GUI按鈕的位置:
接著我們來測試運行一下,我們進入Mainmenu場景,然后運行,點擊“刪除存檔”按鈕:
我們會看到print出了一句已刪除存檔的提示。
接著我們載入游戲,來到我們那個丑丑的游戲場景,然后按C,查看一下我們當前的各項狀態。
現在我們還是初始狀態,我們去隨便刷掉兩只怪,然后順便去作死一下,扣掉一滴血。然后按P鍵調出保存菜單。
現在我已經按了保存,然后我按了“顯示保存的數據”按鈕,現在我們可以從右邊的Console列表里面看到我們保存的數據。
好了,我們退出,重新進來~,再查看一下:
細心一點的朋友應該注意到,這次稍微有了點不同。那就是我又恢復成3滴血了。這是因為我們重新加載的時候,curHealth會變成當前等級的maxHealth,所以我們重新載入之后不是2滴血,而是3滴血。
現在我們重新進入Mainmenu場景,然后刪除存檔,再重新進來一遍:
可以看到,我們的角色的全部資料都清零了。(右手邊的Console顯示了我剛剛有進行“刪除存檔”按鈕的點擊操作,沒有造假,哈哈)
好了,現在我們已經解決了角色存檔的問題。這部分可能自己實際操作的時候會碰到一些問題,需要大家多做幾次,特別是不同的游戲,情況肯定不一樣,這個沒辦法有統一的標準,這里只是提供一個思路。
十六、自動存檔
前面我們提到了存檔的方法了。有些游戲是即時存檔的,就是在Update每一幀都進行一次存檔的操作,對于小游戲來說,這種做法無可厚非,但是對于類似寵物小精靈這種有龐大數據的游戲來講,即時存檔是不太可行的。也許有其他優化的方法,比如在獨立游戲Terraria中,它就是支持大數據即時存檔的,目前我尚不知道這種方法要如何實現。
下面,我們可以利用類似技能冷卻的原理,設置一個定時自動存檔器,比如說每隔五秒鐘或者十秒鐘自行存檔一次,為了讓玩家知道有自動存檔的情況,我們可以調用一些GUI來顯示(我這里就直接print了)。同理,我們也可以在場景中布置一些特殊物體,進行自動的存檔,比如說某一個關卡末尾的大門,我們的Player碰到大門之后就會跳到下一個關卡,同時自動保存我們Player身上的全部數據。(至于這個場景跳轉門或者說是位置跳轉門能不能雙向跳動,這個就要看你的游戲是怎么布置的了。)
廢話不多說,馬上開始:
首先我們需要在GameManager腳本里面創建一個叫做SaveGame()的函數,然后改為public類型(為什么要這樣做后面會解釋),然后把我們前面
在if(pauseMenu)里面的部分內容移到這個函數里面去:
public void SaveGame(){saved = 1;PlayerPrefs.SetInt("Player Level",level);PlayerPrefs.SetInt("Player EXP",curEXP);PlayerPrefs.SetInt("Game Saved",saved);print ("已保存");}
只有加上public,才能在外部進行調用嘛。好了,現在我們增加下面的內容:
if (other.tag == "Door") {string thisLevel = Application.loadedLevelName;int intThisLevel = int.Parse(thisLevel);int intNextLevel = intThisLevel+1;string nextLevel = intNextLevel.ToString();Application.LoadLevel(nextLevel);} 想要跳到下一個關卡有個很簡單的函數可以使用,就是Application.LoadLevel(),可是有個問題,就是如果我們直接選擇加載某一個關卡的話,我們這個腳本就不能重復利用了。也就是說,我們想要做成一個只要一碰到就會自動跳轉到下一關的功能的腳本,這樣就不必在每一個關卡里面都來寫一個特定的腳本。首先,我們用string thisLevel = Application.loadedLevelName;這句話獲取當前關卡的名字,我們之前將關卡命名為Scene1,現在直接改成1就行了。這樣的話,我們就相當于得到了關卡的序列號。理論上來講,我們只需要Application.LoadLevel(thisLevel+1);就應該是可以跳轉到下一個關卡的了。如果這樣的話那就很方便了。可是實際上有個很大的問題,那就是關卡的名字是字符串,字符串可不能直接做加減法的,所以我們需要將字符串強制轉換為int類型。C#里面有Convert.ToInt32的轉換方法,但是我在unity里面沒辦法使用(難道是我的打開方式不對?)所以我采用了另一種方法,也就是上面腳本的int intThisLevel = int.Parse(thisLevel);這樣,我們獲取當前的字符串名稱1就會變成整數1(為了實現這個功能,我們必須將除了Mainmenu關卡之外的其他關卡全部命名為阿拉伯數字名稱,即1、2、3……)然后我們對這個整數1進行加1的處理,即:int intNextLevel = intThisLevel+1;接著,我們將得到的這個新的整數重新轉換為字符串string nextLevel = intNextLevel.ToString();到這里,加載下一個關卡的任務就完成了,最后我們加上這么一句:Application.LoadLevel(nextLevel);即可跳轉到下一關卡。
關于強制類型轉換的,可以參考這個:http://www.360doc.com/content/10/0907/16/2660674_51891839.shtml
C#,int轉成string,string轉成int
1,int轉成string
用toString?
或者Convert.toString()如下?
例如:
int varInt = 1;?
string varString = Convert.ToString(varInt);?
string varString2 = varInt.ToString();
2,string轉成int
如果確定字符串中是可以轉成數字的字符,可以用int.Parse(string s),該語句返回的是轉換得到的int值;
如果不能確定字符串是否可以轉成數字,可以用int.TryParse(string s, out int result),該語句返回的是bool值,指示轉換操作是否成功,參數result是存放轉換結果的變量。
例如:
string str = string.Empty;
str = "123";
int result=int.Parse(str);
string str = string.Empty;
str = "xyz";
int result;
int.TryParse(str, out result);
接下來處理下一個問題,我們在剛剛的if (other.tag == "Door") {}里面插入一句gameManager.SaveGame();當然,在此之前,我們需要在這個腳本里面加上這么一行:
//引用GameManagerpublic GameManager gameManager;
這個應該很好理解,就不再贅述了。使用public的原因是為了在外部進行拖拽操作,如果不將其設置為public的話,就會出現NullReferenceException: Object reference not set to an instance of an object字樣的報錯,這個我們在上一篇學習筆記里面已經有提到過了。出現這個的原因是因為我們此處引用了GameManager里面的一個SaveGame()函數,但是unity并不知道你到底在用哪個GameManager,所以會報Null,最簡單的解決方法就是拖拽大法,也可以用GetComponent<>的方法。
看下面,這個是我的Player身上的Controller2D腳本的設置,我已經把GameManager物體拖放到相應位置了。
剛開始的時候,原作者并沒有使用這個方法,而是直接復制了Bullet腳本里面的other.gameObject.SendMessage("TakenDamage",SendMessageOptions.DontRequireReceiver);這句進行修改。現在我們順便來想想為什么不可以采用這種方法。因為這種方法是對著other.gameObject發送信號,而我們是對著自己發送信號啊。
可能有人會說,那還不簡單,我們直接把other.gameObject.刪掉不就可以了嗎?原視頻的作者也試了一次,不行。但是他沒解釋。我想了一段時間,終于明白了。因為SendMessage不可以把內容發送給自己這個腳本。發送給同個物體身上的不同腳本還是行得通的,但是自己發送給自己就不行了。所以,這種方法是行不通的,就只能用上面提到的那種方法了。
現在我們已經實現了場景跳轉和按鍵存檔功能,接下來就是自動存檔功能了。我們在GameManager腳本的Update函數里面進行如下增添:
//自動存檔功能autoSaveTimer += Time.deltaTime;if (autoSaveTimer >= 1000f) {SaveGame(); print ("保存啦~~");autoSaveTimer =0;} 上面的autoSaveTimer是自定義的一個int變量,我順便設置成public,方便在外面修改。我將它改為10,也就是每隔10秒就會自動保存一次,前面我們將保存的功能整合成一個SaveGame()函數就是為了這個目的。那句print(“保存啦~~”)只是用來賣萌和提示自己的,嘿嘿~
每次保存之后autoSaveTimer這個自動保存的計時器就會清零,然后重新進入下一輪的計時,這樣就可以實現循環保存的功效了。因為我們的游戲數據比較小,所以可以每隔一段時間就自動保存一次,大數據的游戲就不建議這樣弄了。
這一講很長,也涉及到了很多問題。所以我花了很長時間整理。如果大家有什么不太清楚的話,最好多看幾遍,然后自己再試著操作一下試試。當然,這個是針對新手說的,各路大神可以無視哈。
十七、關卡選擇
在這一講里面,我們首先創建一個新的場景(為了方便測試,我把上一次弄的場景2直接重命名為場景LevelSelect,你要新建一個當然也沒問題,這個不是重點,哈哈),這個場景的作用就是在進入時可以用來選擇進入各個關卡的。很多橫版的闖關游戲由于關卡比較多,所以在游戲開始的時候都會有這樣的選擇畫面,讓玩家自行選擇這次要玩哪一關,就不必要每次都從第一關開始玩起了。而且由于Unity在游戲保存這一塊非常給力,所以我們完全不需要擔心數據丟失方面的問題。
接著我們先點擊進入我們的Mainmenu場景,在攝像機的MainMenu腳本里面,把Level里面原本的Scene1改成現在的LevelSelect,這樣我們就可以通過點擊按鈕直接跳轉到我們現在要用到的這個LevelSelect場景了。
接著進入這個場景,然后創建一個同名腳本扔到攝像機上面,然后我們開始編輯:
public class LevelSelect : MonoBehaviour {int sw = Screen.width;int sh = Screen.height;public string Level;void OnGUI(){//進入第一關的按鈕if (GUI.Button (new Rect (0, 0, sw * .5f, sh), "Level: 1")) {Application.LoadLevel(Level);}} } 內容很簡單,我就不做解釋了。前面兩個int是因為覺得打screen.width和screen.height很麻煩,所以才那樣弄的,至于那個public string Level,之前介紹過了就不再解釋為什么要這樣做了。我這里只是隨便弄了一個出來,大家可以在這個LevelSelect的腳本里面設置一堆不同的按鈕,每個按鈕連接到你的各個不同關卡里面去,這樣就達到了關卡選擇的效果啦。
接下來看看我們之前的Controller2D腳本,我們在這個腳本里面有這么一段:
void OnTriggerEnter(Collider other){if (other.tag == "HealthPotion") {GameManager.curHealth++;Destroy(other.gameObject);}if (other.tag == "Door") {gameManager.SaveGame();string thisLevel = Application.loadedLevelName;int intThisLevel = int.Parse(thisLevel);int intNextLevel = intThisLevel+1;string nextLevel = intNextLevel.ToString();Application.LoadLevel(nextLevel);}} 看了原作者的視頻,他是打算做一個叫做Door的腳本,把上面的那部分功能轉移到這個腳本里面去,然后再去把這個東西拉到我們的Door物體上,其實這一步可做可不做。我想來想去,這么做的好處大概就是可以為每一個Door弄一個public string Level,然后設置那些穿越什么的方便一些吧。
視頻里面的Door腳本里面的GameManager腳本還是采用將它改為public,然后再在外面進行拖拽的方法。那么有沒有什么方法可以不進行這些拖拽呢?當然可以的啊。只要利用GetComponent就可以了。下面貼出我寫的腳本:
public class Door : MonoBehaviour {public string Level;GameManager gameManager;GameObject gameObject;void Start() {gameObject = GameObject.FindGameObjectWithTag("GameManager");gameManager = gameObject.GetComponent<GameManager>();}void OnTriggerEnter(Collider other){if (other.tag == "Player"){gameManager.SaveGame();Application.LoadLevel(Level);}} } 我們首先要定義一個GameObject來利用FindGameObjectWithTag得到它(使用這種方法需要增加tag,其實和拖拽方法大同小異,這個就看個人喜好了。兩種方法大家都可以使用的)當然,我們要給GameManager加上一個同名的tag。
接著我們就利用這個:gameManager = gameObject.GetComponent<GameManager>();很簡單,我就不解釋了,不過需要注意一下格式。
如果在函數上有什么不太明白的,建議大家自己去查查手冊。鏈接:http://game.ceeger.com/search/
另外呢,在實際操作過程中,如果大家按照我上面的腳本照抄一遍,就會出現這樣的黃字警告:Assets/Scripts/Door.cs(8,16): warning CS0108: `Door.gameObject' hides inherited member `UnityEngine.Component.gameObject'. Use the new keyword if hiding was intended 這到底是怎么回事呢?一開始我真的想不明白,用百度和Google也找不到解答。后來自己想了想,然后又查了一下詞典,總算明白到底是怎么一回事了。我們都知道,黃字警告一般是不會影響游戲的運行的,但是肯定是有某些不太合理的地方,所以我們的Unity會非常人性化地給我們提個醒。我經常碰到的黃字提醒一般都是定義了某個變量,但是從未使用過。而這次碰到的這個情況,是腳本里面的gameObject變量和系統里面本身自帶的gameObject實例名稱重復了。因為我們知道,gameObject是個關鍵詞,所以如果我起了個這樣的名字的話,雖然是可以運行的,但是Unity也許容易混淆,所以它就會給我們一個這樣的提醒。這里也順便提醒一下大家,在定義變量的時候盡量不要使用和系統自定義的關鍵詞重復的東西。否則可能會出現比較嚴重的問題。
上面的這個問題,我們只需要把gameObject改成gameObjectGM,就不會報錯了。
好了,那么這一講想要實現的功能就都實現了。我們可以接著下一講的內容了:
(順便說一下,我不是很喜歡unity4.3版本里面自帶的monodevelop腳本編輯器,所以我改成了VS2013,這篇寫完之后我會寫一篇替換編輯器的內容。)
這是我處理過的VS2013的界面,感覺還是挺和諧的嘻嘻。下一篇我們再聊這個。
由于排版方面出了很奇怪的問題,所以第二篇就只能先到這里結束了。大概是因為我弄了太多天的同一篇,又有些東西是復制進來的,導致排版的時候總是出錯吧。先這樣吧,下一篇我會將沒有說完的補完。
總結
以上是生活随笔為你收集整理的Unity学习笔记2 简易2D横版RPG游戏制作(二)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ACL实现单向访问
- 下一篇: Unity学习笔记3 简易2D横版RPG