[Unity3D]Unity3D遊戲開發之Xml解析實現NPC對話系統,unity3dnpc
各位朋友,大家好,我是秦元培,歡迎大家關注我的部落格,我的部落格地址是blog.csdn.net/qinyuanpei。今天我們來說說Unity3D中Xml的解析,為什麼要說Xml的解析呢?因為在項目中我們常常需要從外部讀取內容或者將內容以一定地形式儲存起來,而Xml就是我們最為常用的一種檔案形式。是博主目前正在做的一款仙劍同人遊戲《仙古之境》(姑且先叫做這個名字吧)。
在這個遊戲中,博主精心為玩家設計了大量有趣的台詞,內容涉及了《仙劍奇俠傳》曆代故事情節及《古劍奇譚》的相關內容。如果我們直接將這些台詞寫進代碼的話,雖然遊戲同樣可以運行,可是一旦遊戲策劃修改了劇情或者某些內容的話,我們就不得不重新編寫代碼、重新編譯、重新測試。所以,像《仙劍奇俠傳》和《古劍奇譚》這種RPG類遊戲,一般都不會講劇本直接寫進代碼,而是使用類似Lua這種指令碼語言來實現的,因為指令碼語言相對簡單,適合策劃人員使用。那麼,好了,現在我們回到Unity3D,我們今天將使用Xml檔案來儲存我們的對話內容,然後通過指令碼來讀取,實現與NPC的對話。這樣做的好處就是,我們可以隨時隨地地修改Xml檔案的內容,而無需改動整個項目的代碼。其實博主在以前的一篇文章中曾經提到過NPC對話系統的實現,不過當時博主對Unity3D處於一知半解的狀態(其實現在依然是一知半解,哈哈),所以當時的那種設計是一種很稚嫩的想法,那麼其實今天的這篇文章就是在其基礎上做了大量改進才做出來的(不過博主依然不滿意啊,博主喜歡追求極致的完美)。好了,首先講述一下原理,我們從攝像機的位置發射一條經過滑鼠位置的射線,然後檢測這條射線是否擊中了NPC,如果擊中了NPC,就會將滑鼠指標修改為對話的樣式,此時玩家按下滑鼠鍵,NPC和玩家將面對面(這塊的代碼目前有點問題,稍後會提到),並且顯示對話方塊,玩家繼續按下空格鍵,可以與NPC將整個對話進行下去,直到全部的對話顯示完畢。這裡的對話方塊是用Unity3D的GUI系統完成的,預設情況下,對話方塊是隱藏的,只有玩家觸發對話後,才會顯示出來,當整個對話結束後,對話方塊對自己消失。是博主設計的對話方塊,仿照《仙劍奇俠傳四》的樣式做的(不過沒有做頭像啊,哈哈),這裡博主不想說得太多,因為Unity3D目前的GUI系統沒有完全統一。
首先呢,我們來講Unity3D中Xml的解析,博主在Unity3D中使用的指令碼語言是C#,所以博主很果斷地使用了.NET下解析Xml的API,即System.Xml命名空間,相信學習過.NET的人一定不會不知道吧。我們來看今天要解析的Xml檔案,一個十分簡單的Xml檔案(當初設計的是一行就是一句對話,可是後來發現對話太長的話不行,所以就分成多行,可是Unity3D的GUI系統不能自動換行,所以這裡實際的效果並不是太好,我真後悔沒有直接用NGUI,官方的GUI系統可不可以給力點啊):
<?xml version="1.0" encoding="utf-8"?><Dialogs> <Dialog>青陽長老:如果我們知道玄霄從禁地破冰而出是這種結果,我們一定不會告訴他尋找三寒器的方</Dialog> <Dialog>法。他要殺我和重光,我無話可說,可是他練功走火入魔,禍及蒼生卻是極大的不對。</Dialog> <Dialog>瑕:我不認識你說的那個人,可是我知道人一旦被慾望迷失心智,就會做出錯誤的事情。薑承</Dialog> <Dialog>如果不是被枯木利用,他根本不會走到那一步。瑾軒一直想幫他洗脫冤情,可是當他走到覆天</Dialog> <Dialog>頂的時候,他才發現無論他怎麼努力,薑承已經沒有辦法回頭了。</Dialog> <Dialog>青陽長老:將玄霄冰封的事情我二人亦有參與,我二人此生終究愧對一人啊</Dialog> <Dialog>瑕:或許你們都有自己的苦衷,可是這世上的善惡是非又怎麼能理得清楚啊</Dialog> <Dialog>青陽長老:此地沒有爭執、沒有喧囂,我二人正好在此了卻殘生。</Dialog></Dialogs>
在C#裡面解析Xml是比較簡單的,所以這裡直接給出代碼吧,博主在看金曾璽的《Unity3D遊戲開發》一書時發現,作者在書中推薦使用的來自開源社區的Xml解析指令碼並不是很完美,因為在C#中無法正確讀取js的指令碼類,後來博主嘗試添加了許多引用,最後依然無法解決這個問題,所以到最後只好用了.NET解析XmlDe類空間。
//Xml數組解析private NPC[] ReadXmls(){//初始化NPC數組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);//擷取根節點XmlElement mElement=mDocuemnt.DocumentElement;//讀取節點值XmlNodeList mNodeList=mElement.SelectNodes("/Dialogs/Dialog");//建立數組string[] mArray=new string[mNodeList.Count];for(int i=0;i<mNodeList.Count;i++){mArray[i]=mNodeList[i].InnerText;}//返回數組return mArray;}//通過ID返回一個NPCprivate NPC getNPCByID(int ID){NPC mResult=null;foreach(NPC mNPC in mNPCs){ if(mNPC.ID==ID.ToString()) { mResult=mNPC; break; }}return mResult;}}
那麼,在解析了Xml後,我們就可以將內容和NPC聯絡起來了,我們下面來看NPC的指令碼NPCScript.cs
在這個指令碼中,遊戲管理器負責的是全域控制,比如控制滑鼠的樣式、控制對話方塊的顯示、控制攝像機等等,該指令碼定義如下:
using UnityEngine;using System.Collections;using System.Xml;public class NPCScript : MonoBehaviour {//遊戲管理器private GameManager mManager;//Xml數組public TextAsset[] XmlDatas;//對話數組private string[] mDialogs;//對話索引private int index=0;//NPC數組private NPC[] mNPCs;public int ID;void Start () {//擷取遊戲管理器mManager=GameObject.Find("GameManager").GetComponent<GameManager>();//讀取NPCmNPCs=ReadXmls();}void Update () {//對話觸發RaycastHit mHit;Ray mRay=mManager.Manager_Camera.ScreenPointToRay(Input.mousePosition);bool isHit=Physics.Raycast(mRay,out mHit);if(isHit && mHit.collider.gameObject.tag=="NPC"){//根據ID擷取對應的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);//計算玩家和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);//計算v1,v2連線的向量Vector3 mDir=(v1-v2).normalized;//計算NPC的旋轉角度float NpcAngle=getAngle(new Vector3(0,0,1),mDir);float PlayerAngle=getAngle(new Vector3(0,0,1),mDir);//將NPC旋轉到面向主角NPC.forward=mDir;//對話控制mManager.SetDialogBox(mDialogs[0].ToString());mManager.SetDialogBoxActive(true);//設定遊戲狀態mManager.SetGameState(GameState.InEvent);}}else{mManager.Mangager_Cursor.SetCursor(Cursor.CursorType.Default);}//按空格鍵進行對話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));//將數組和索引重設index=0;mDialogs=null;}else{mManager.SetDialogBox(mDialogs[index].ToString());mManager.SetDialogBoxActive(true);mManager.SetGameState(GameState.InEvent);}}}//Xml數組解析private NPC[] ReadXmls(){//初始化NPC數組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);//擷取根節點XmlElement mElement=mDocuemnt.DocumentElement;//讀取節點值XmlNodeList mNodeList=mElement.SelectNodes("/Dialogs/Dialog");//建立數組string[] mArray=new string[mNodeList.Count];for(int i=0;i<mNodeList.Count;i++){mArray[i]=mNodeList[i].InnerText;}//返回數組return mArray;}//通過ID返回一個NPCprivate NPC getNPCByID(int ID){NPC mResult=null;foreach(NPC mNPC in mNPCs){ if(mNPC.ID==ID.ToString()) { mResult=mNPC; break; }}return mResult;}}
在這裡我們先使用SetDialogBox()方法來設定對話方塊要顯示的對話內容,然後使用SetDialogBoxActive()方法啟用對話方塊,這樣我們就可以看到博主精心設計出來的劇情對話了。
最後說一下博主對這個方案不滿意的一個地方,就是在當前遊戲中任意一個時刻只能有一個NPC,因為博主是將所有的NPC都綁定了同一個指令碼,在這個指令碼中,首先會讀取全部NPC的對話資料,然後在射線檢測這雷根據ID擷取指定的NPC對話資料。理論上這樣應該是沒有問題的,可是在實際測試的時候,發現如果情境中有多個NPC,就會出現對話沒有說完就隱藏對話方塊或者NPC與對話內容不匹配的Bug。起初博主認為是多個NPC共用同一個遊戲指令碼導致內部變數發生了衝突,可是博主覺得一個私人的變數怎麼會受到外部的影響呢?博主曾經嘗試為每一個NPC寫一個指令碼,即每個NPC只負責自己的那一部分,可是這樣依然出現前面提到的Bug,這個Bug幾乎讓博主喪失做完這個小遊戲的信心,目前博主的一種思路就是通過人為地改變每個指令碼的Enable來保證任意一個時刻情境中只有一個NPC,正在痛苦地修改著Bug。後來博主想出的一種比較有效解決方案是增加下面的指令碼:
using UnityEngine;using System.Collections;public class NPCManager : MonoBehaviour {//NPCpublic Transform[] NPCs;//玩家public Transform Player;//初始化NPCvoid Awake(){foreach(Transform mTrans in NPCs){mTrans.GetComponent<NPCScript>().enabled=false;}}//啟用NPCvoid Update(){ //只有玩家進入對話範圍時才會觸發對話foreach(Transform mTrans in NPCs){//計算玩家與NPC之間的距離float mDistance=Vector3.Distance(mTrans.position,Player.position);//當距離小於4.0時觸發對話指令碼,大於4.0時將隱藏對話指令碼if(mDistance<=4.0F){mTrans.GetComponent<NPCScript>().enabled=true;}else{mTrans.GetComponent<NPCScript>().enabled=false;}}}}
這段指令碼其實有點猥瑣,就是判斷玩家和NPC之間的距離,當這個距離小於對話觸發的距離時,綁定在NPC上的指令碼便被啟用了,這樣情境中任意時刻只有一個NPC被啟用。
面對自己產生的Bug,如果知道是怎麼回事,最好在第一時間解決;如果不知道是怎麼回事,那就只有用非正常手段來解決了。博主做這款小遊戲,主要是因為博主喜歡《仙劍奇俠傳》和《古劍奇譚》這兩個系列的遊戲,很多時候,我們都只是平凡世界中平凡的一員,可正是因為平凡,我們才想要去改變,遊戲總有打到通關的那一刻,可是我們的人生才剛剛開始。《仙古之境》這個小遊戲講述的是虛擬世界中串連仙劍世界與古劍世界的一個過渡世界,類似於《仙劍奇俠傳》中神魔之井的設定。或許是因為太喜歡那個藍衣白衫的少年劍客,或許是因為太喜歡那個白髮飄飄的孤獨背影,總之,當即墨那晚的燈火散盡之時,當瓊華派轉眼滄海桑田,那個少年依然做著他少年時的夢。
好了,最後一起來看看遊戲情境展示吧:
好了,今天的內容就是這樣啦,希望大家喜歡啊。
每日箴言:我情願化成一片落葉,讓風吹雨打到處飄零;或流雲一朵,在澄藍天,和大地再沒有些牽連。——林徽因
喜歡我的部落格請記住我的名字:秦元培,我部落格地址是blog.csdn.net/qinyuanpei。
轉載請註明出處,本文作者:秦元培,本文出處:http://blog.csdn.net/qinyuanpei/article/details/38982335
Unity3D遊戲開發的執行個體,或原始碼 推薦書籍或則下載原始碼的地址
unity 3D遊戲開發這本書
[Unity3D]手機3D遊戲開發:怎使用Unity3D中內建的重力感應
汪 汪海的實驗室 海的實驗室- - 各種圖形學實驗和資料結構實驗以及其他一切瑣碎雜亂的小筆記們都相遇在此齊聚一堂共同守候 0 - - 各種圖形學實驗和資料結構實驗以及其他一切瑣碎雜亂的小筆記們都相遇在此齊聚一堂共同守候 0error(s), 0 warning(s) 這神奇時刻的到來 error(s), 0 warning(s) 這神奇時刻的到來學習Unity指令碼推薦:Unity3D官網索引重力感應在手機遊戲的開發中非常常見。Unity3D本身集合了重力感應的相關內容。一個簡單的JS指令碼示範一下重力感應的使用。CSDNGravity.js://物體的貼圖var round : Texture2D;//物體在螢幕中顯示的X Y座標var x = 0;var y = 0;//物體螢幕顯示的最大 X Y 範圍var cross_x = 0;var cross_y = 0;function Start(){//初始化賦值cross_x = Screen.width - round.width;cross_y = Screen.height - round.height;}function OnGUI () {//整體顯示 x y z 重力感應的重力分量GUI.Label(Rect(0,0,480,100),"position is " + Input.acceleration);//繪製物體GUI.DrawTexture(Rect(x,y,256,256),round);}function Update(){//根據重力分量修改物體的位置這裡乘以30的意思是讓物體移動的快一些x += Input.acceleration.x * 30;y += -Input.acceleration.y * 30;//避免物體超出螢幕if(x < 0){x = 0;}else if(x > cross_x){x = cross_x;}if(y < 0){y = 0;}else if(y > cross_y){y = cross_y;}}這裡的Input是指Unity中的輸入,acceleration便是其重力了,x和y分別代表其重力分量。建立完畢之後只需要添加紋理圖片即可:12