日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Unity学习笔记2 简易2D横版RPG游戏制作(二)

發(fā)布時(shí)間:2025/4/16 编程问答 42 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Unity学习笔记2 简易2D横版RPG游戏制作(二) 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

十二、敵人受攻擊時(shí)的閃爍和Player的生命值的修正

上一篇中,我們利用Controller2D中的IEnumerator TakenDamage接口,使得我們的Player受到攻擊時(shí)會(huì)進(jìn)行閃爍,我們同樣地也希望在我們的敵人身上可以實(shí)現(xiàn)相同的效果。所以我們現(xiàn)在需要復(fù)制Controller2D腳本里面的兩個(gè)內(nèi)容到我們的Enemy2D腳本里面去:

第一個(gè)內(nèi)容:

//顯示角色當(dāng)前正受到攻擊float takenDamage = 0.2f;

第二個(gè)內(nèi)容:

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);}

接下來我們需要對(duì)Bullet腳本進(jìn)行處理,使其在碰撞到我們的敵人時(shí),向敵人發(fā)送一個(gè)閃爍的信號(hào):(修改內(nèi)容如下)

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);}}
其實(shí)就是增加了一句:

other.gameObject.SendMessage("TakenDamage",SendMessageOptions.DontRequireReceiver);

解決了敵人的閃爍問題之后,我們接下來要處理另外一個(gè)問題了。這個(gè)問題就是,我們的角色每消滅一些敵人就會(huì)增加經(jīng)驗(yàn),而經(jīng)驗(yàn)的增加會(huì)自動(dòng)補(bǔ)充HP,這個(gè)有點(diǎn)不對(duì)頭和不合理,我們升級(jí)的時(shí)候增加的應(yīng)該是最大HP,在每個(gè)等級(jí)下,我們應(yīng)該設(shè)置一個(gè)當(dāng)前的最大HP,不能無限制地增加HP才對(duì),所以呢,我們需要將原來的GameManager腳本里面的playersHealth變量改名為curHealth,并且增加一個(gè)maxHealth變量,同時(shí)設(shè)置為3,將原本的playersHealth++修改為maxHealth++,這樣的話,消滅敵人只會(huì)增加你的最大HP,而不會(huì)增加你當(dāng)前的HP,至于升級(jí)之后是不是要讓HP全滿呢,這個(gè)功能很容易實(shí)現(xiàn),我暫時(shí)不想加進(jìn)去。
接下來我們就需要考慮下一個(gè)問題了,那就是角色的戰(zhàn)斗力必須隨著等級(jí)的提升而得到提升才比較合理。不能每次攻擊都只扣敵人一滴血啊。而且敵人只有3到6滴血的設(shè)定不太合理,其實(shí)反正敵人的血是不顯示出來的,完全可以設(shè)置一些大一點(diǎn)的數(shù)值,比如100,500之類的,這個(gè)就到后面有需要再改吧。現(xiàn)在暫時(shí)先不動(dòng)。

十三、補(bǔ)血藥的設(shè)置

在這一講里面,首先要處理一個(gè)問題:我們的Player應(yīng)該是有戰(zhàn)斗力的,而不是每次只向敵人發(fā)送1滴的傷害值。利用某某++的方法可以很容易實(shí)現(xiàn)升級(jí)時(shí)增加戰(zhàn)斗力,這個(gè)就不說了。關(guān)鍵是怎么弄成用我們的戰(zhàn)斗力去減敵人的血值。首先,我們?cè)贕ameManager腳本里面添加這么一行:

static public int bulletDamage = 1;

然后把我們的curHealth,也就是當(dāng)前HP的值也改成static public。這里順便補(bǔ)充一下,static在c#中的作用。

靜態(tài)分配的,有兩種情況:1. 用在類里的屬性、方法前面,這樣的靜態(tài)屬性與方法不需要?jiǎng)?chuàng)建實(shí)例就能訪問,通過類名或?qū)ο竺寄茉L問它,靜態(tài)屬性、方法只有“一份”:即如果一個(gè)類新建有N個(gè)對(duì)象,這N 個(gè)對(duì)象只有同一個(gè)靜態(tài)屬性與方法; 2. 方法內(nèi)部的靜態(tài)變量:方法內(nèi)部的靜態(tài)變量,執(zhí)行完靜態(tài)變量值不消失,再次執(zhí)行此對(duì)象的方法時(shí),值仍存在,它不是在棧中分配的,是在靜態(tài)區(qū)分析的, 這是與局部變量最大的區(qū)別; 如果這個(gè)說得不具體的話,那么可以看一下下面這個(gè),紅黑聯(lián)盟里面講的,非常形象,保證一看馬上就明白了:(我也是在看視頻教程的過程中碰到了static不懂,然后看下面這個(gè)理解透徹的)

提起static,一般理解為靜態(tài)、全局。
何為static?我理解的static屬于程序的直屬單位,而非static就是非直屬單位。
舉一個(gè)非常常見的例子,中國(guó)有4個(gè)直轄市,北京、上海、天津、重慶,這些相當(dāng)于static,而廣州、南京、杭州等就是非static,中央可以直接管理北京、上海、天津、重慶,而廣州、南京、杭州應(yīng)由各省政府管理,Main方法可以直接調(diào)用static,而調(diào)用非static需要實(shí)例化。

class City() { //4個(gè)直轄市static 靜態(tài)全局類型 public static void Beijing(){} public static void ShangHai(){} public static void Tianjin(){} public static void Chongqing(){} //其他城市 非靜態(tài) public void Guangzhou(){} public void Nanjing(){} } void Main() { //調(diào)用static類型的方法 City.Beijing();//調(diào)用北京 City.Shanghai();//調(diào)用上海 //調(diào)用非static類型的方法 //沒有直接調(diào)用權(quán)利,必須先實(shí)例化 City chengShi=new City(); chengShi.Guangzhou();//調(diào)用廣州 } 講的形象就達(dá)到目的了,為剛開始學(xué)習(xí) 編程 的同學(xué)加把勁兒。

原鏈接:http://www.2cto.com/kf/201209/152853.html

好啦,然后我們繼續(xù)。接著我們打開我們的Bullet腳本,把里面的內(nèi)容修改成這樣:

using UnityEngine; using System.Collections;public class Bullet : MonoBehaviour {//用于碰撞時(shí)摧毀兩個(gè)物體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);} } 其實(shí)上面的內(nèi)容只是將

other.gameObject.SendMessage("EnemyDamaged",GameManager.bulletDamage,SendMessageOptions.DontRequireReceiver);

這一行進(jìn)行了處理,我們?cè)镜哪莻€(gè)damageValue變量已經(jīng)被刪掉,然后替換成我們GameManager里面的bulletDamage,因?yàn)槲覀兊倪@個(gè)bulletDamage已經(jīng)修改為static,所以現(xiàn)在可以這樣調(diào)用了。以后如果想要升級(jí)增加角色的殺傷力的話,就容易多了。

接下來我們想要在運(yùn)行游戲中按C的時(shí)候順便顯示我們的戰(zhàn)斗力,這個(gè)太簡(jiǎn)單了,只需要對(duì)GameManager里面做一點(diǎn)點(diǎn)修改:

if (playerStats) {statsDisplay.text = "等級(jí):" + level + "\n經(jīng)驗(yàn):" + curEXP + "/" + maxEXP + "\n攻擊力:" + bulletDamage;}

這一部分大家可以隨便修改,你想顯示什么就修改什么。至于在畫面中的位置,可以調(diào)整GUIStats的transform,這個(gè)就不再贅述。

我稍微做了點(diǎn)修改。

接下來我們需要在場(chǎng)景中新增一個(gè)quad,去掉Mesh Collider,加上Box Collider,然后Box?Collider的size全部改成1,transform里面的position的z值別忘了設(shè)成0,名字隨便起,是用來做成補(bǔ)血藥的,我命名為HealthPotion,然后為它增加一個(gè)同名的tag。為了區(qū)分,我順便弄了一個(gè)同名的material扔上去,將顏色調(diào)成粉紅色。(呵呵……感覺是毒藥而不是血藥啊……)

接著我們打開上次弄的那個(gè)StickToPlatform腳本,把里面的東西復(fù)制到我們的Controller2D上面,其實(shí)因?yàn)閮蓚€(gè)腳本都是綁定在Player身上,就沒必要弄兩個(gè)腳本了,直接合并成一個(gè)就成了。

然后我們?cè)僭黾右欢涡〈a來使得我們的Player碰到帶有HealthPotion的物體時(shí),將這個(gè)物體摧毀并且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);}}

很簡(jiǎn)單吧,現(xiàn)在補(bǔ)血藥就已經(jīng)做好了。想要做出補(bǔ)多少血的補(bǔ)血藥都已經(jīng)不是什么問題了。想要做出補(bǔ)藍(lán)、增加戰(zhàn)斗力什么的補(bǔ)血藥,也不是什么問題了哈哈。

有個(gè)地方需要大家注意一下,那就是curHealth必須加上static,否則會(huì)出現(xiàn)An object reference is required to access non-static member這樣的報(bào)錯(cuò)顯示。在全局靜態(tài)函數(shù)里面是不可以使用非全局靜態(tài)變量的。

吃藥前(注意左上角兩個(gè)紅心)

吃藥后,哈哈,療程短見效快,一粒補(bǔ)一滴血~

關(guān)于Box Collider的范圍的問題,可以參考上一篇講到的Player的Character Controller組件的問題,所以我們可以順便把藥物的Box Collider的size的X值改為1.3。

十四、游戲暫停和游戲存檔

游戲存檔看似簡(jiǎn)單,不過也是個(gè)比較蛋疼的問題。我們打開GameManager腳本:
首先,我們?cè)黾右粋€(gè)布爾值:

//用于暫停的布爾值bool pauseMenu;

然后我們?cè)鎏硪韵麓a:

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),"顯示保存的數(shù)據(jù)")){print ("顯示保存的數(shù)據(jù)");print("當(dāng)前等級(jí):"+ PlayerPrefs.GetInt("Player Level"));print("當(dāng)前經(jīng)驗(yàn):"+ PlayerPrefs.GetInt("Player EXP"));}}

然后保存腳本。

現(xiàn)在我們?cè)谶\(yùn)行游戲的過程中按P鍵,就可以調(diào)出一個(gè)這樣的畫面:


當(dāng)我們點(diǎn)擊保存游戲時(shí),就會(huì)保存相應(yīng)的數(shù)據(jù)。我們可以先殺掉一只小怪,然后保存,然后重新運(yùn)行,然后點(diǎn)擊“顯示保存的數(shù)據(jù)”,這個(gè)時(shí)候就會(huì)看到:


但是這個(gè)時(shí)候有兩個(gè)問題,第一個(gè)問題是,雖然在這里我們可以看到保存的數(shù)據(jù),但是實(shí)際上在游戲里面,如果我們按C鍵查看角色的屬性時(shí),發(fā)現(xiàn)經(jīng)驗(yàn)值并沒有保存下來。第二個(gè)問題是,我們的存檔還不知道怎么刪除……

接下來馬上解決這個(gè)問題:

十五、載入游戲和刪除存檔

好了,我們繼續(xù)修改我們的GameManager腳本,首先我們要增加一個(gè)void Awake()函數(shù),這個(gè)東西和void Start()有什么不同呢?我順便摘錄了一段網(wǎng)上搬過來的筆記:

這是博客園的一篇介紹的原鏈接:http://www.cnblogs.com/xpvincent/p/3178042.html

Unity3D初學(xué)者經(jīng)常把Awake和Start混淆。

簡(jiǎn)單說明一下,Awake在MonoBehavior創(chuàng)建后就立刻調(diào)用,Start將在MonoBehavior創(chuàng)建后在該幀Update之前,在該Monobehavior.enabled == true的情況下執(zhí)行。

[javascript]?view plaincopy ?
  • void?Awake?(){??
  • }???????
  • //初始化函數(shù),在游戲開始時(shí)系統(tǒng)自動(dòng)調(diào)用。一般用來創(chuàng)建變量之類的東西。??
  • ??
  • void?Start(){??
  • }??
  • //初始化函數(shù),在所有Awake函數(shù)運(yùn)行完之后(一般是這樣,但不一定),在所有Update函數(shù)前系統(tǒng)自動(dòng)條用。一般用來給變量賦值。??
  • 我們通常書寫的腳本,并不會(huì)定義[ExecuteInEditMode]這個(gè)Attribute,所以Awake和Start都只有在Runtime中才會(huì)執(zhí)行。

    例1:

    [javascript]?view plaincopy ?
  • public?class?Test?:?MonoBehaviour?{??
  • ????void?Awake?()?{??
  • ????????Debug.Log("Awake");??
  • ????????enabled?=?false;??
  • ????}??
  • ???
  • ????void?Start?()?{??
  • ????????Debug.Log("Start");??
  • ????}??
  • } ?
  • 以上代碼,在Awake中我們調(diào)用了enabled = false; 禁止了這個(gè)MonoBehavior的update。由于Start, Update, PostUpdate等屬于runtime行為的一部分,這段代碼將使Start不會(huì)被調(diào)用到。

    在游戲過程中,若有另外一組代碼有如下調(diào)用:

    [javascript]?view plaincopy ?
  • Test?test?=?go.GetComponent<Test>();??
  • test.enabled?=?true; ?
  • 這個(gè)時(shí)候,若該MonoBehavior之前并沒有觸發(fā)過Start函數(shù),將會(huì)在這段代碼執(zhí)行后觸發(fā)。

    例2:

    player.cs

    [javascript]?view plaincopy ?
  • private?Transform?handAnchor?=?null;??
  • void?Awake?()?{?handAnchor?=?transform.Find("hand_anchor");?}??
  • //?void?Start?()?{?handAnchor?=?transform.Find("hand_anchor");?}??
  • void?GetWeapon?(?GameObject?go?)?{??
  • ????if?(?handAnchor?==?null?)?{??
  • ????????Debug.LogError("handAnchor?is?null");??
  • ????????return;??
  • ????}??
  • ????go.transform.parent?=?handAnchor;??
  • } ?
  • other.cs

    [javascript]?view plaincopy ?
  • ...??
  • GameObject?go?=?new?GameObject("player");??
  • player?pl?=?go.AddComponent<player>();?//?Awake?invoke?right?after?this!??
  • pl.GetWeapon(weaponGO);??
  • ... ?
  • 以上代碼中,我們?cè)趐layer Awake的時(shí)候去為handAnchor賦值。如果我們將這步操作放在Start里,那么在other.cs中,當(dāng)執(zhí)行GetWeapon的時(shí)候就會(huì)出現(xiàn)handAnchor是null reference.

    總結(jié):我們盡量將其他Object的reference設(shè)置等事情放在Awake處理。然后將這些reference的Object的賦值設(shè)置放在Start()中來完成。
    當(dāng)MonoBehavior有定義[ExecuteInEditMode]時(shí)

    當(dāng)我們?yōu)镸onoBehavior定義了[ExecuteInEditMode]后,我們還需要關(guān)心Awake和Start在編輯器中的執(zhí)行狀況。

    ??? 當(dāng)該MonoBehavior在編輯器中被賦于給GameObject的時(shí)候,Awake, Start 將被執(zhí)行。
    ??? 當(dāng)Play按鈕被按下游戲開始以后,Awake, Start 將被執(zhí)行。
    ??? 當(dāng)Play按鈕停止后,Awake, Start將再次被執(zhí)行。
    ??? 當(dāng)在編輯器中打開包含有該MonoBehavior的場(chǎng)景的時(shí)候,Awake, Start將被執(zhí)行。

    值得注意的是,不要用這種方式來設(shè)定一些臨時(shí)變量的存儲(chǔ)(private, protected)。因?yàn)橐坏┪覀冇|發(fā)Unity3D的代碼編譯,這些變量所存儲(chǔ)的內(nèi)容將被清為默認(rèn)值。

    下面再來看看Unity圣典中的解釋。

    ?Awake()

    當(dāng)一個(gè)腳本實(shí)例被載入時(shí)Awake被調(diào)用。

    Awake用于在游戲開始之前初始化變量或游戲狀態(tài)。在腳本整個(gè)生命周期內(nèi)它僅被調(diào)用一次.Awake在所有對(duì)象被初始化之后調(diào)用,所以你可以安全的與其他對(duì)象對(duì)話或用諸如 GameObject.FindWithTag 這樣的函數(shù)搜索它們。每個(gè)游戲物體上的Awke以隨機(jī)的順序被調(diào)用。因此,你應(yīng)該用Awake來設(shè)置腳本間的引用,并用Start來傳遞信息。Awake總是在Start之前被調(diào)用。它不能用來執(zhí)行協(xié)同程序。

    Start()

    Start僅在Update函數(shù)第一次被調(diào)用前調(diào)用。Start在behaviour的生命周期中只被調(diào)用一次。它和Awake的不同是Start只在腳本實(shí)例被啟用時(shí)調(diào)用。
    你可以按需調(diào)整延遲初始化代碼。Awake總是在Start之前執(zhí)行。這允許你協(xié)調(diào)初始化順序。

    好了,這樣就清楚為什么要用Awake()函數(shù)了吧。

    接下來我們就這樣弄,首先我們?cè)O(shè)置一個(gè)int saved,讓它等于零。(如果不賦值的話,它也會(huì)默認(rèn)等于零)

    //用于判斷是否是否保存int saved = 0;

    接下來我們就寫下這么一個(gè)Awake函數(shù):

    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;} }

    在運(yùn)行這個(gè)Awake函數(shù)的時(shí)候,就會(huì)讓saved先獲取我們保存的值。(等一下在下面存檔的那部分腳本里面,我們會(huì)保存先把saved賦值為1,再進(jìn)行保存),因?yàn)镻layerPrefs不能保存布爾值,所以我們用一個(gè)int的0和1來代替就行了,一樣的。如果我們的游戲沒有存檔的話,saved讀取不到任何數(shù)據(jù),就會(huì)默認(rèn)為零,那么就相當(dāng)于不會(huì)接下去讀取我們保存的數(shù)據(jù)了,反之,如果讀取到了1,就相當(dāng)于讀取到了“已經(jīng)有保存的數(shù)據(jù)”的情況,就需要繼續(xù)執(zhí)行。

    在保存數(shù)據(jù)部分,我是這樣弄的:

    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);}//“顯示保存的數(shù)據(jù)”按鈕if(GUI.Button(new Rect(Screen.width*.25f,Screen.height*.6f,Screen.width*.5f,Screen.height*.1f),"顯示保存的數(shù)據(jù)")){print ("顯示保存的數(shù)據(jù)");print("當(dāng)前等級(jí):"+ PlayerPrefs.GetInt("Player Level"));print("當(dāng)前經(jīng)驗(yàn):"+ PlayerPrefs.GetInt("Player EXP"));print("是否保存:"+ PlayerPrefs.GetInt("Game Saved"));}}

    我們可以看到,在保存之前,我們先將saved賦值為1,然后再保存,這樣的話,當(dāng)我們重新載入游戲時(shí),就會(huì)進(jìn)行一個(gè)是否保存了游戲的判斷。

    至于下面那個(gè)“顯示保存的數(shù)據(jù)”的按鈕功能,只是我用來debug.log的,沒什么用,純屬調(diào)試,可以無視。

    可能有人會(huì)說,保存游戲就直接保存,然后直接讀取數(shù)據(jù)不就行了,為什么還要弄一個(gè)saved來判斷呢?我一開始也是沒有弄這個(gè)玩意的,后來發(fā)現(xiàn)了一個(gè)問題,在這里解釋一下:假設(shè)我們現(xiàn)在刪除了游戲存檔,而沒有這個(gè)用來判斷是否有存檔的saved值的話,那么我們的腳本自然就不管三七二十一,你沒有存檔它也會(huì)當(dāng)成你是有存檔的,這樣會(huì)出現(xiàn)什么問題呢?這樣的話,我們的curEXP = PlayerPrefs.GetInt ("Player EXP");和level = PlayerPrefs.GetInt ("Player Level");這兩句話就會(huì)得不到任何數(shù)據(jù),那么就會(huì)默認(rèn)為零,那么,下面的maxEXP = level * 50;還有maxHealth = level + 2;這兩句的計(jì)算就肯定會(huì)出問題了。maxEXP會(huì)變成0,而maxHealth會(huì)變成2,最大經(jīng)驗(yàn)值變成零也就算了,我們的血值還從3變成了2,這不是坑爹么……哈哈,所以,這下子明白為什么要做一個(gè)是否saved的判斷了吧。對(duì)于我這樣的小游戲來說,就已經(jīng)需要制作一個(gè)是否saved的判斷了,對(duì)于需要保存大量數(shù)據(jù)的大游戲來說那就更是如此。希望大家如果是制作RPG類型的游戲的話,也可以養(yǎng)成類似的習(xí)慣。

    接著要解決刪檔的問題。這個(gè)也是RPG游戲的一個(gè)重點(diǎn)內(nèi)容。我們打開MainMenu腳本,然后在if (showGUIOutline)里面加入以下內(nèi)容:

    //“刪除存檔”按鈕 if (GUI.Button (new Rect (Screen.width * guiPlacementX3, Screen.height * guiPlacementY3, Screen.width * .5f, Screen.height * .1f), "刪除存檔")) { PlayerPrefs.DeleteAll(); print ("已刪除存檔"); }

    其實(shí)主要就是添加一個(gè)PlayerPrefs.DeleteAll();而已,沒什么復(fù)雜的。

    當(dāng)然,為了方便我們?cè)诖翱谥姓{(diào)整GUI的位置,我們也增加了guiPlacementX3還有guiPlacementY3這兩個(gè)public的float值。這里就沒必要貼出來了。
    接著在Mainmenu場(chǎng)景里面的Maincamera上面調(diào)整好三個(gè)GUI按鈕的位置:


    接著我們來測(cè)試運(yùn)行一下,我們進(jìn)入Mainmenu場(chǎng)景,然后運(yùn)行,點(diǎn)擊“刪除存檔”按鈕:


    我們會(huì)看到print出了一句已刪除存檔的提示。
    接著我們載入游戲,來到我們那個(gè)丑丑的游戲場(chǎng)景,然后按C,查看一下我們當(dāng)前的各項(xiàng)狀態(tài)。


    現(xiàn)在我們還是初始狀態(tài),我們?nèi)ルS便刷掉兩只怪,然后順便去作死一下,扣掉一滴血。然后按P鍵調(diào)出保存菜單。


    現(xiàn)在我已經(jīng)按了保存,然后我按了“顯示保存的數(shù)據(jù)”按鈕,現(xiàn)在我們可以從右邊的Console列表里面看到我們保存的數(shù)據(jù)。

    好了,我們退出,重新進(jìn)來~,再查看一下:


    細(xì)心一點(diǎn)的朋友應(yīng)該注意到,這次稍微有了點(diǎn)不同。那就是我又恢復(fù)成3滴血了。這是因?yàn)槲覀冎匦录虞d的時(shí)候,curHealth會(huì)變成當(dāng)前等級(jí)的maxHealth,所以我們重新載入之后不是2滴血,而是3滴血。

    現(xiàn)在我們重新進(jìn)入Mainmenu場(chǎng)景,然后刪除存檔,再重新進(jìn)來一遍:


    可以看到,我們的角色的全部資料都清零了。(右手邊的Console顯示了我剛剛有進(jìn)行“刪除存檔”按鈕的點(diǎn)擊操作,沒有造假,哈哈)

    好了,現(xiàn)在我們已經(jīng)解決了角色存檔的問題。這部分可能自己實(shí)際操作的時(shí)候會(huì)碰到一些問題,需要大家多做幾次,特別是不同的游戲,情況肯定不一樣,這個(gè)沒辦法有統(tǒng)一的標(biāo)準(zhǔn),這里只是提供一個(gè)思路。

    十六、自動(dòng)存檔

    前面我們提到了存檔的方法了。有些游戲是即時(shí)存檔的,就是在Update每一幀都進(jìn)行一次存檔的操作,對(duì)于小游戲來說,這種做法無可厚非,但是對(duì)于類似寵物小精靈這種有龐大數(shù)據(jù)的游戲來講,即時(shí)存檔是不太可行的。也許有其他優(yōu)化的方法,比如在獨(dú)立游戲Terraria中,它就是支持大數(shù)據(jù)即時(shí)存檔的,目前我尚不知道這種方法要如何實(shí)現(xiàn)。

    下面,我們可以利用類似技能冷卻的原理,設(shè)置一個(gè)定時(shí)自動(dòng)存檔器,比如說每隔五秒鐘或者十秒鐘自行存檔一次,為了讓玩家知道有自動(dòng)存檔的情況,我們可以調(diào)用一些GUI來顯示(我這里就直接print了)。同理,我們也可以在場(chǎng)景中布置一些特殊物體,進(jìn)行自動(dòng)的存檔,比如說某一個(gè)關(guān)卡末尾的大門,我們的Player碰到大門之后就會(huì)跳到下一個(gè)關(guān)卡,同時(shí)自動(dòng)保存我們Player身上的全部數(shù)據(jù)。(至于這個(gè)場(chǎng)景跳轉(zhuǎn)門或者說是位置跳轉(zhuǎn)門能不能雙向跳動(dòng),這個(gè)就要看你的游戲是怎么布置的了。)

    廢話不多說,馬上開始:

    首先我們需要在GameManager腳本里面創(chuàng)建一個(gè)叫做SaveGame()的函數(shù),然后改為public類型(為什么要這樣做后面會(huì)解釋),然后把我們前面

    在if(pauseMenu)里面的部分內(nèi)容移到這個(gè)函數(shù)里面去:

    public void SaveGame(){saved = 1;PlayerPrefs.SetInt("Player Level",level);PlayerPrefs.SetInt("Player EXP",curEXP);PlayerPrefs.SetInt("Game Saved",saved);print ("已保存");}

    只有加上public,才能在外部進(jìn)行調(diào)用嘛。好了,現(xiàn)在我們?cè)黾酉旅娴膬?nèi)容:

    if (other.tag == "Door") {string thisLevel = Application.loadedLevelName;int intThisLevel = int.Parse(thisLevel);int intNextLevel = intThisLevel+1;string nextLevel = intNextLevel.ToString();Application.LoadLevel(nextLevel);}

    想要跳到下一個(gè)關(guān)卡有個(gè)很簡(jiǎn)單的函數(shù)可以使用,就是Application.LoadLevel(),可是有個(gè)問題,就是如果我們直接選擇加載某一個(gè)關(guān)卡的話,我們這個(gè)腳本就不能重復(fù)利用了。也就是說,我們想要做成一個(gè)只要一碰到就會(huì)自動(dòng)跳轉(zhuǎn)到下一關(guān)的功能的腳本,這樣就不必在每一個(gè)關(guān)卡里面都來寫一個(gè)特定的腳本。首先,我們用string thisLevel = Application.loadedLevelName;這句話獲取當(dāng)前關(guān)卡的名字,我們之前將關(guān)卡命名為Scene1,現(xiàn)在直接改成1就行了。這樣的話,我們就相當(dāng)于得到了關(guān)卡的序列號(hào)。理論上來講,我們只需要Application.LoadLevel(thisLevel+1);就應(yīng)該是可以跳轉(zhuǎn)到下一個(gè)關(guān)卡的了。如果這樣的話那就很方便了。可是實(shí)際上有個(gè)很大的問題,那就是關(guān)卡的名字是字符串,字符串可不能直接做加減法的,所以我們需要將字符串強(qiáng)制轉(zhuǎn)換為int類型。C#里面有Convert.ToInt32的轉(zhuǎn)換方法,但是我在unity里面沒辦法使用(難道是我的打開方式不對(duì)?)所以我采用了另一種方法,也就是上面腳本的int intThisLevel = int.Parse(thisLevel);這樣,我們獲取當(dāng)前的字符串名稱1就會(huì)變成整數(shù)1(為了實(shí)現(xiàn)這個(gè)功能,我們必須將除了Mainmenu關(guān)卡之外的其他關(guān)卡全部命名為阿拉伯?dāng)?shù)字名稱,即1、2、3……)然后我們對(duì)這個(gè)整數(shù)1進(jìn)行加1的處理,即:int intNextLevel = intThisLevel+1;接著,我們將得到的這個(gè)新的整數(shù)重新轉(zhuǎn)換為字符串string nextLevel = intNextLevel.ToString();到這里,加載下一個(gè)關(guān)卡的任務(wù)就完成了,最后我們加上這么一句:Application.LoadLevel(nextLevel);即可跳轉(zhuǎn)到下一關(guān)卡。

    關(guān)于強(qiáng)制類型轉(zhuǎn)換的,可以參考這個(gè):http://www.360doc.com/content/10/0907/16/2660674_51891839.shtml

    C#,int轉(zhuǎn)成string,string轉(zhuǎn)成int

    1,int轉(zhuǎn)成string
    用toString?
    或者Convert.toString()如下?

    例如:
    int varInt = 1;?
    string varString = Convert.ToString(varInt);?
    string varString2 = varInt.ToString();

    2,string轉(zhuǎn)成int
    如果確定字符串中是可以轉(zhuǎn)成數(shù)字的字符,可以用int.Parse(string s),該語句返回的是轉(zhuǎn)換得到的int值;
    如果不能確定字符串是否可以轉(zhuǎn)成數(shù)字,可以用int.TryParse(string s, out int result),該語句返回的是bool值,指示轉(zhuǎn)換操作是否成功,參數(shù)result是存放轉(zhuǎn)換結(jié)果的變量。

    例如:
    string str = string.Empty;
    str = "123";
    int result=int.Parse(str);

    string str = string.Empty;
    str = "xyz";
    int result;
    int.TryParse(str, out result);


    接下來處理下一個(gè)問題,我們?cè)趧倓偟膇f (other.tag == "Door") {}里面插入一句gameManager.SaveGame();當(dāng)然,在此之前,我們需要在這個(gè)腳本里面加上這么一行:

    //引用GameManagerpublic GameManager gameManager;

    這個(gè)應(yīng)該很好理解,就不再贅述了。使用public的原因是為了在外部進(jìn)行拖拽操作,如果不將其設(shè)置為public的話,就會(huì)出現(xiàn)NullReferenceException: Object reference not set to an instance of an object字樣的報(bào)錯(cuò),這個(gè)我們?cè)谏弦黄獙W(xué)習(xí)筆記里面已經(jīng)有提到過了。出現(xiàn)這個(gè)的原因是因?yàn)槲覀兇颂幰昧薌ameManager里面的一個(gè)SaveGame()函數(shù),但是unity并不知道你到底在用哪個(gè)GameManager,所以會(huì)報(bào)Null,最簡(jiǎn)單的解決方法就是拖拽大法,也可以用GetComponent<>的方法。

    看下面,這個(gè)是我的Player身上的Controller2D腳本的設(shè)置,我已經(jīng)把GameManager物體拖放到相應(yīng)位置了。


    剛開始的時(shí)候,原作者并沒有使用這個(gè)方法,而是直接復(fù)制了Bullet腳本里面的other.gameObject.SendMessage("TakenDamage",SendMessageOptions.DontRequireReceiver);這句進(jìn)行修改。現(xiàn)在我們順便來想想為什么不可以采用這種方法。因?yàn)檫@種方法是對(duì)著other.gameObject發(fā)送信號(hào),而我們是對(duì)著自己發(fā)送信號(hào)啊。

    可能有人會(huì)說,那還不簡(jiǎn)單,我們直接把other.gameObject.刪掉不就可以了嗎?原視頻的作者也試了一次,不行。但是他沒解釋。我想了一段時(shí)間,終于明白了。因?yàn)?span style="font-family:Arial; line-height:26px">SendMessage不可以把內(nèi)容發(fā)送給自己這個(gè)腳本。發(fā)送給同個(gè)物體身上的不同腳本還是行得通的,但是自己發(fā)送給自己就不行了。所以,這種方法是行不通的,就只能用上面提到的那種方法了。

    現(xiàn)在我們已經(jīng)實(shí)現(xiàn)了場(chǎng)景跳轉(zhuǎn)和按鍵存檔功能,接下來就是自動(dòng)存檔功能了。我們?cè)贕ameManager腳本的Update函數(shù)里面進(jìn)行如下增添:

    //自動(dòng)存檔功能autoSaveTimer += Time.deltaTime;if (autoSaveTimer >= 1000f) {SaveGame(); print ("保存啦~~");autoSaveTimer =0;}

    上面的autoSaveTimer是自定義的一個(gè)int變量,我順便設(shè)置成public,方便在外面修改。我將它改為10,也就是每隔10秒就會(huì)自動(dòng)保存一次,前面我們將保存的功能整合成一個(gè)SaveGame()函數(shù)就是為了這個(gè)目的。那句print(“保存啦~~”)只是用來賣萌和提示自己的,嘿嘿~

    每次保存之后autoSaveTimer這個(gè)自動(dòng)保存的計(jì)時(shí)器就會(huì)清零,然后重新進(jìn)入下一輪的計(jì)時(shí),這樣就可以實(shí)現(xiàn)循環(huán)保存的功效了。因?yàn)槲覀兊挠螒驍?shù)據(jù)比較小,所以可以每隔一段時(shí)間就自動(dòng)保存一次,大數(shù)據(jù)的游戲就不建議這樣弄了。

    這一講很長(zhǎng),也涉及到了很多問題。所以我花了很長(zhǎng)時(shí)間整理。如果大家有什么不太清楚的話,最好多看幾遍,然后自己再試著操作一下試試。當(dāng)然,這個(gè)是針對(duì)新手說的,各路大神可以無視哈。

    十七、關(guān)卡選擇

    在這一講里面,我們首先創(chuàng)建一個(gè)新的場(chǎng)景(為了方便測(cè)試,我把上一次弄的場(chǎng)景2直接重命名為場(chǎng)景LevelSelect,你要新建一個(gè)當(dāng)然也沒問題,這個(gè)不是重點(diǎn),哈哈),這個(gè)場(chǎng)景的作用就是在進(jìn)入時(shí)可以用來選擇進(jìn)入各個(gè)關(guān)卡的。很多橫版的闖關(guān)游戲由于關(guān)卡比較多,所以在游戲開始的時(shí)候都會(huì)有這樣的選擇畫面,讓玩家自行選擇這次要玩哪一關(guān),就不必要每次都從第一關(guān)開始玩起了。而且由于Unity在游戲保存這一塊非常給力,所以我們完全不需要擔(dān)心數(shù)據(jù)丟失方面的問題。

    接著我們先點(diǎn)擊進(jìn)入我們的Mainmenu場(chǎng)景,在攝像機(jī)的MainMenu腳本里面,把Level里面原本的Scene1改成現(xiàn)在的LevelSelect,這樣我們就可以通過點(diǎn)擊按鈕直接跳轉(zhuǎn)到我們現(xiàn)在要用到的這個(gè)LevelSelect場(chǎng)景了。


    接著進(jìn)入這個(gè)場(chǎng)景,然后創(chuàng)建一個(gè)同名腳本扔到攝像機(jī)上面,然后我們開始編輯:

    public class LevelSelect : MonoBehaviour {int sw = Screen.width;int sh = Screen.height;public string Level;void OnGUI(){//進(jìn)入第一關(guān)的按鈕if (GUI.Button (new Rect (0, 0, sw * .5f, sh), "Level: 1")) {Application.LoadLevel(Level);}} }

    內(nèi)容很簡(jiǎn)單,我就不做解釋了。前面兩個(gè)int是因?yàn)橛X得打screen.width和screen.height很麻煩,所以才那樣弄的,至于那個(gè)public string Level,之前介紹過了就不再解釋為什么要這樣做了。我這里只是隨便弄了一個(gè)出來,大家可以在這個(gè)LevelSelect的腳本里面設(shè)置一堆不同的按鈕,每個(gè)按鈕連接到你的各個(gè)不同關(guān)卡里面去,這樣就達(dá)到了關(guān)卡選擇的效果啦。

    接下來看看我們之前的Controller2D腳本,我們?cè)谶@個(gè)腳本里面有這么一段:

    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);}}

    看了原作者的視頻,他是打算做一個(gè)叫做Door的腳本,把上面的那部分功能轉(zhuǎn)移到這個(gè)腳本里面去,然后再去把這個(gè)東西拉到我們的Door物體上,其實(shí)這一步可做可不做。我想來想去,這么做的好處大概就是可以為每一個(gè)Door弄一個(gè)public string Level,然后設(shè)置那些穿越什么的方便一些吧。

    視頻里面的Door腳本里面的GameManager腳本還是采用將它改為public,然后再在外面進(jìn)行拖拽的方法。那么有沒有什么方法可以不進(jìn)行這些拖拽呢?當(dāng)然可以的啊。只要利用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);}} }

    我們首先要定義一個(gè)GameObject來利用FindGameObjectWithTag得到它(使用這種方法需要增加tag,其實(shí)和拖拽方法大同小異,這個(gè)就看個(gè)人喜好了。兩種方法大家都可以使用的)當(dāng)然,我們要給GameManager加上一個(gè)同名的tag。

    接著我們就利用這個(gè):gameManager = gameObject.GetComponent<GameManager>();很簡(jiǎn)單,我就不解釋了,不過需要注意一下格式。

    如果在函數(shù)上有什么不太明白的,建議大家自己去查查手冊(cè)。鏈接:http://game.ceeger.com/search/

    另外呢,在實(shí)際操作過程中,如果大家按照我上面的腳本照抄一遍,就會(huì)出現(xiàn)這樣的黃字警告: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也找不到解答。后來自己想了想,然后又查了一下詞典,總算明白到底是怎么一回事了。我們都知道,黃字警告一般是不會(huì)影響游戲的運(yùn)行的,但是肯定是有某些不太合理的地方,所以我們的Unity會(huì)非常人性化地給我們提個(gè)醒。我經(jīng)常碰到的黃字提醒一般都是定義了某個(gè)變量,但是從未使用過。而這次碰到的這個(gè)情況,是腳本里面的gameObject變量和系統(tǒng)里面本身自帶的gameObject實(shí)例名稱重復(fù)了。因?yàn)槲覀冎?#xff0c;gameObject是個(gè)關(guān)鍵詞,所以如果我起了個(gè)這樣的名字的話,雖然是可以運(yùn)行的,但是Unity也許容易混淆,所以它就會(huì)給我們一個(gè)這樣的提醒。這里也順便提醒一下大家,在定義變量的時(shí)候盡量不要使用和系統(tǒng)自定義的關(guān)鍵詞重復(fù)的東西。否則可能會(huì)出現(xiàn)比較嚴(yán)重的問題。

    上面的這個(gè)問題,我們只需要把gameObject改成gameObjectGM,就不會(huì)報(bào)錯(cuò)了。

    好了,那么這一講想要實(shí)現(xiàn)的功能就都實(shí)現(xiàn)了。我們可以接著下一講的內(nèi)容了:

    (順便說一下,我不是很喜歡unity4.3版本里面自帶的monodevelop腳本編輯器,所以我改成了VS2013,這篇寫完之后我會(huì)寫一篇替換編輯器的內(nèi)容。)


    這是我處理過的VS2013的界面,感覺還是挺和諧的嘻嘻。下一篇我們?cè)倭倪@個(gè)。

    由于排版方面出了很奇怪的問題,所以第二篇就只能先到這里結(jié)束了。大概是因?yàn)槲遗颂嗵斓耐黄?#xff0c;又有些東西是復(fù)制進(jìn)來的,導(dǎo)致排版的時(shí)候總是出錯(cuò)吧。先這樣吧,下一篇我會(huì)將沒有說完的補(bǔ)完。


    總結(jié)

    以上是生活随笔為你收集整理的Unity学习笔记2 简易2D横版RPG游戏制作(二)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。