各位朋友,大家好,我是秦元培,歡迎大家關(guān)注我的博客,我的博客地址是blog.csdn.net/qinyuanpei。今天我們來說說Unity3D中Xml的解析,為什么要說Xml的解析呢?因?yàn)樵陧?xiàng)目中我們常常需要從外部讀取內(nèi)容或者將內(nèi)容以一定地形式儲(chǔ)存起來,而Xml就是我們最為常用的一種文件形式。如圖所示是博主目前正在做的一款仙劍同人游戲《仙古之境》(姑且先叫做這個(gè)名字吧)。 在這個(gè)游戲中,博主精心為玩家設(shè)計(jì)了大量有趣的臺(tái)詞,內(nèi)容涉及了《仙劍奇?zhèn)b傳》歷代故事情節(jié)及《古劍奇譚》的相關(guān)內(nèi)容。如果我們直接將這些臺(tái)詞寫進(jìn)代碼的話,雖然游戲同樣可以運(yùn)行,可是一旦游戲策劃修改了劇情或者某些內(nèi)容的話,我們就不得不重新編寫代碼、重新編譯、重新測試。所以,像《仙劍奇?zhèn)b傳》和《古劍奇譚》這種RPG類游戲,一般都不會(huì)講劇本直接寫進(jìn)代碼,而是使用類似Lua這種腳本語言來實(shí)現(xiàn)的,因?yàn)槟_本語言相對簡單,適合策劃人員使用。那么,好了,現(xiàn)在我們回到Unity3D,我們今天將使用Xml文件來存儲(chǔ)我們的對話內(nèi)容,然后通過腳本來讀取,實(shí)現(xiàn)與NPC的對話。這樣做的好處就是,我們可以隨時(shí)隨地地修改Xml文件的內(nèi)容,而無需改動(dòng)整個(gè)項(xiàng)目的代碼。其實(shí)博主在以前的一篇文章中曾經(jīng)提到過NPC對話系統(tǒng)的實(shí)現(xiàn),不過當(dāng)時(shí)博主對Unity3D處于一知半解的狀態(tài)(其實(shí)現(xiàn)在依然是一知半解,哈哈),所以當(dāng)時(shí)的那種設(shè)計(jì)是一種很稚嫩的想法,那么其實(shí)今天的這篇文章就是在其基礎(chǔ)上做了大量改進(jìn)才做出來的(不過博主依然不滿意啊,博主喜歡追求極致的完美)。好了,首先講述一下原理,我們從攝像機(jī)的位置發(fā)射一條經(jīng)過鼠標(biāo)位置的射線,然后檢測這條射線是否擊中了NPC,如果擊中了NPC,就會(huì)將鼠標(biāo)指針修改為對話的樣式,此時(shí)玩家按下鼠標(biāo)鍵,NPC和玩家將面對面(這塊的代碼目前有點(diǎn)問題,稍后會(huì)提到),并且顯示對話框,玩家繼續(xù)按下空格鍵,可以與NPC將整個(gè)對話進(jìn)行下去,直到全部的對話顯示完畢。這里的對話框是用Unity3D的GUI系統(tǒng)完成的,默認(rèn)情況下,對話框是隱藏的,只有玩家觸發(fā)對話后,才會(huì)顯示出來,當(dāng)整個(gè)對話結(jié)束后,對話框?qū)ψ约合АH鐖D是博主設(shè)計(jì)的對話框,仿照《仙劍奇?zhèn)b傳四》的樣式做的(不過沒有做頭像啊,哈哈),這里博主不想說得太多,因?yàn)?/span>Unity3D目前的GUI系統(tǒng)沒有完全統(tǒng)一。 首先呢,我們來講Unity3D中Xml的解析,博主在Unity3D中使用的腳本語言是C#,所以博主很果斷地使用了.NET下解析Xml的API,即System.Xml命名空間,相信學(xué)習(xí)過.NET的人一定不會(huì)不知道吧。我們來看今天要解析的Xml文件,一個(gè)十分簡單的Xml文件(當(dāng)初設(shè)計(jì)的是一行就是一句對話,可是后來發(fā)現(xiàn)對話太長的話不行,所以就分成多行,可是Unity3D的GUI系統(tǒng)不能自動(dòng)換行,所以這里實(shí)際的效果并不是太好,我真后悔沒有直接用NGUI,官方的GUI系統(tǒng)可不可以給力點(diǎn)啊):
<?xml version="1.0" encoding="utf-8"?> <Dialogs> <Dialog>青陽長老:如果我們知道玄霄從禁地破冰而出是這種結(jié)果,我們一定不會(huì)告訴他尋找三寒器的方</Dialog> <Dialog>法。他要?dú)⑽液椭毓?我無話可說,可是他練功走火入魔,禍及蒼生卻是極大的不對。</Dialog> <Dialog>瑕:我不認(rèn)識你說的那個(gè)人,可是我知道人一旦被欲望迷失心智,就會(huì)做出錯(cuò)誤的事情。姜承</Dialog> <Dialog>如果不是被枯木利用,他根本不會(huì)走到那一步。瑾軒一直想幫他洗脫冤情,可是當(dāng)他走到覆天</Dialog> <Dialog>頂?shù)臅r(shí)候,他才發(fā)現(xiàn)無論他怎么努力,姜承已經(jīng)沒有辦法回頭了。</Dialog> <Dialog>青陽長老:將玄霄冰封的事情我二人亦有參與,我二人此生終究愧對一人啊</Dialog> <Dialog>瑕:或許你們都有自己的苦衷,可是這世上的善惡是非又怎么能理得清楚啊</Dialog> <Dialog>青陽長老:此地沒有爭執(zhí)、沒有喧囂,我二人正好在此了卻殘生。</Dialog> </Dialogs>
在C#里面解析Xml是比較簡單的,所以這里直接給出代碼吧,博主在看金曾璽的《Unity3D游戲開發(fā)》一書時(shí)發(fā)現(xiàn),作者在書中推薦使用的來自開源社區(qū)的Xml解析腳本并不是很完美,因?yàn)樵贑#中無法正確讀取js的腳本類,后來博主嘗試添加了許多引用,最后依然無法解決這個(gè)問題,所以到最后只好用了.NET解析XmlDe類空間。
//Xml數(shù)組解析 private NPC[] ReadXmls() { //初始化NPC數(shù)組 mNPCs=new NPC[XmlDatas.Length]; for(int i=0;i<XmlDatas.Length;i++) { NPC mNPC=new NPC(); mNPC.ID=i.ToString(); mNPC.Data=ReadSingleXml(XmlDatas[i]); mNPCs[i]=mNPC; } return mNPCs; } private string[] ReadSingleXml(TextAsset mText) { XmlDocument mDocuemnt=new XmlDocument(); //加載Xml文本 mDocuemnt.LoadXml(mText.text); //獲取根節(jié)點(diǎn) XmlElement mElement=mDocuemnt.DocumentElement; //讀取節(jié)點(diǎn)值 XmlNodeList mNodeList=mElement.SelectNodes("/Dialogs/Dialog"); //創(chuàng)建數(shù)組 string[] mArray=new string[mNodeList.Count]; for(int i=0;i<mNodeList.Count;i++) { mArray[i]=mNodeList[i].InnerText; } //返回?cái)?shù)組 return mArray; } //通過ID返回一個(gè)NPC private NPC getNPCByID(int ID) { NPC mResult=null; foreach(NPC mNPC in mNPCs) { if(mNPC.ID==ID.ToString()) { mResult=mNPC; break; } } return mResult; } }
那么,在解析了Xml后,我們就可以將內(nèi)容和NPC聯(lián)系起來了,我們下面來看NPC的腳本NPCScript.cs 在這個(gè)腳本中,游戲管理器負(fù)責(zé)的是全局控制,比如控制鼠標(biāo)的樣式、控制對話框的顯示、控制攝像機(jī)等等,該腳本定義如下:
using UnityEngine; using System.Collections; using System.Xml; public class NPCScript : MonoBehaviour { //游戲管理器 private GameManager mManager; //Xml數(shù)組 public TextAsset[] XmlDatas; //對話數(shù)組 private string[] mDialogs; //對話索引 private int index=0; //NPC數(shù)組 private NPC[] mNPCs; public int ID; void Start () { //獲取游戲管理器 mManager=GameObject.Find("GameManager").GetComponent<GameManager>(); //讀取NPC mNPCs=ReadXmls(); } void Update () { //對話觸發(fā) RaycastHit mHit; Ray mRay=mManager.Manager_Camera.ScreenPointToRay(Input.mousePosition); bool isHit=Physics.Raycast(mRay,out mHit); if(isHit && mHit.collider.gameObject.tag=="NPC") { //根據(jù)ID獲取對應(yīng)的NPC對話 NPC mNpc=getNPCByID(ID); if(mNpc!=null) { mDialogs=new string[mNpc.Data.Length]; for(int i=0;i<mDialogs.Length;i++) { mDialogs[i]=mNpc.Data[i]; } } mManager.Mangager_Cursor.SetCursor(Cursor.CursorType.Talk); //計(jì)算玩家和NPC之間的距離 Transform NPC=mHit.collider.gameObject.transform; Vector3 v1=NPC.position; Vector3 v2=mManager.Player.position; if(Vector3.Distance(v1,v2)<=2.0F && Input.GetMouseButtonDown(0)) { //使v1,v2共面 v1=new Vector3(v1.x,0,v1.z); v2=new Vector3(v2.x,0,v2.z); //計(jì)算v1,v2連線的向量 Vector3 mDir=(v1-v2).normalized; //計(jì)算NPC的旋轉(zhuǎn)角度 float NpcAngle=getAngle(new Vector3(0,0,1),mDir); float PlayerAngle=getAngle(new Vector3(0,0,1),mDir); //將NPC旋轉(zhuǎn)到面向主角 NPC.forward=mDir; //對話控制 mManager.SetDialogBox(mDialogs[0].ToString()); mManager.SetDialogBoxActive(true); //設(shè)置游戲狀態(tài) mManager.SetGameState(GameState.InEvent); } }else { mManager.Mangager_Cursor.SetCursor(Cursor.CursorType.Default); } //按空格鍵進(jìn)行對話 if( mManager.Manager_State==GameState.InEvent && Input.GetKeyDown(KeyCode.Space)) { index+=1; if(index>mDialogs.Length-1) { //隱藏對話框 mManager.SetDialogBoxActive(false); mManager.SetGameState(GameState.Normal); //將NPC角度重置 transform.Rotate(new Vector3(0,180,0)); //將數(shù)組和索引重置 index=0; mDialogs=null; }else { mManager.SetDialogBox(mDialogs[index].ToString()); mManager.SetDialogBoxActive(true); mManager.SetGameState(GameState.InEvent); } } } //Xml數(shù)組解析 private NPC[] ReadXmls() { //初始化NPC數(shù)組 mNPCs=new NPC[XmlDatas.Length]; for(int i=0;i<XmlDatas.Length;i++) { NPC mNPC=new NPC(); mNPC.ID=i.ToString(); mNPC.Data=ReadSingleXml(XmlDatas[i]); mNPCs[i]=mNPC; } return mNPCs; } private string[] ReadSingleXml(TextAsset mText) { XmlDocument mDocuemnt=new XmlDocument(); //加載Xml文本 mDocuemnt.LoadXml(mText.text); //獲取根節(jié)點(diǎn) XmlElement mElement=mDocuemnt.DocumentElement; //讀取節(jié)點(diǎn)值 XmlNodeList mNodeList=mElement.SelectNodes("/Dialogs/Dialog"); //創(chuàng)建數(shù)組 string[] mArray=new string[mNodeList.Count]; for(int i=0;i<mNodeList.Count;i++) { mArray[i]=mNodeList[i].InnerText; } //返回?cái)?shù)組 return mArray; } //通過ID返回一個(gè)NPC private NPC getNPCByID(int ID) { NPC mResult=null; foreach(NPC mNPC in mNPCs) { if(mNPC.ID==ID.ToString()) { mResult=mNPC; break; } } return mResult; } }
在這里我們先使用SetDialogBox()方法來設(shè)置對話框要顯示的對話內(nèi)容,然后使用SetDialogBoxActive()方法激活對話框,這樣我們就可以看到博主精心設(shè)計(jì)出來的劇情對話了。 最后說一下博主對這個(gè)方案不滿意的一個(gè)地方,就是在當(dāng)前游戲中任意一個(gè)時(shí)刻只能有一個(gè)NPC,因?yàn)椴┲魇菍⑺械?/span>NPC都綁定了同一個(gè)腳本,在這個(gè)腳本中,首先會(huì)讀取全部NPC的對話數(shù)據(jù),然后在射線檢測這里根據(jù)ID獲取指定的NPC對話數(shù)據(jù)。理論上這樣應(yīng)該是沒有問題的,可是在實(shí)際測試的時(shí)候,發(fā)現(xiàn)如果場景中有多個(gè)NPC,就會(huì)出現(xiàn)對話沒有說完就隱藏對話框或者NPC與對話內(nèi)容不匹配的Bug。起初博主認(rèn)為是多個(gè)NPC共用同一個(gè)游戲腳本導(dǎo)致內(nèi)部變量發(fā)生了沖突,可是博主覺得一個(gè)私有的變量怎么會(huì)受到外部的影響呢?博主曾經(jīng)嘗試為每一個(gè)NPC寫一個(gè)腳本,即每個(gè)NPC只負(fù)責(zé)自己的那一部分,可是這樣依然出現(xiàn)前面提到的Bug,這個(gè)Bug幾乎讓博主喪失做完這個(gè)小游戲的信心,目前博主的一種思路就是通過人為地改變每個(gè)腳本的Enable來保證任意一個(gè)時(shí)刻場景中只有一個(gè)NPC,正在痛苦地修改著Bug。后來博主想出的一種比較有效的解決方案是增加下面的腳本:
using UnityEngine; using System.Collections; public class NPCManager : MonoBehaviour { //NPC public Transform[] NPCs; //玩家 public Transform Player; //初始化NPC void Awake() { foreach(Transform mTrans in NPCs) { mTrans.GetComponent<NPCScript>().enabled=false; } } //激活NPC void Update() { //只有玩家進(jìn)入對話范圍時(shí)才會(huì)觸發(fā)對話 foreach(Transform mTrans in NPCs) { //計(jì)算玩家與NPC之間的距離 float mDistance=Vector3.Distance(mTrans.position,Player.position); //當(dāng)距離小于4.0時(shí)觸發(fā)對話腳本,大于4.0時(shí)將隱藏對話腳本 if(mDistance<=4.0F){ mTrans.GetComponent<NPCScript>().enabled=true; }else{ mTrans.GetComponent<NPCScript>().enabled=false; } } } } 這段腳本其實(shí)有點(diǎn)猥瑣,就是判斷玩家和NPC之間的距離,當(dāng)這個(gè)距離小于對話觸發(fā)的距離時(shí),綁定在NPC上的腳本便被激活了,這樣場景中任意時(shí)刻只有一個(gè)NPC被激活。
面對自己產(chǎn)生的Bug,如果知道是怎么回事,最好在第一時(shí)間解決;如果不知道是怎么回事,那就只有用非正常手段來解決了。博主做這款小游戲,主要是因?yàn)椴┲飨矚g《仙劍奇?zhèn)b傳》和《古劍奇譚》這兩個(gè)系列的游戲,很多時(shí)候,我們都只是平凡世界中平凡的一員,可正是因?yàn)槠椒?,我們才想要去改變,游戲總有打到通關(guān)的那一刻,可是我們的人生才剛剛開始?!断晒胖场愤@個(gè)小游戲講述的是虛擬世界中連接仙劍世界與古劍世界的一個(gè)過渡世界,類似于《仙劍奇?zhèn)b傳》中神魔之井的設(shè)定。或許是因?yàn)樘矚g那個(gè)藍(lán)衣白衫的少年劍客,或許是因?yàn)樘矚g那個(gè)白發(fā)飄飄的孤獨(dú)背影,總之,當(dāng)即墨那晚的燈火散盡之時(shí),當(dāng)瓊?cè)A派轉(zhuǎn)眼滄海桑田,那個(gè)少年依然做著他少年時(shí)的夢。 好了,最后一起來看看游戲場景展示吧: 好了,今天的內(nèi)容就是這樣啦,希望大家喜歡啊。
每日箴言:我情愿化成一片落葉,讓風(fēng)吹雨打到處飄零;或流云一朵,在澄藍(lán)天,和大地再?zèng)]有些牽連?!只找?/span> |
|