[Unity3D]Unity3D遊戲開發之Xml解析實現NPC對話系統
各位朋友,大家好,我是秦元培,歡迎大家關注我的部落格,我的部落格地址是。今天我們來說說Unity3D中Xml的解析,為什麼要說Xml的解析呢?因為在專案中我們常常需要從外部讀取內容或者將內容以一定地形式儲存起來,而Xml就是我們最為常用的一種檔案形式。如圖所示是博主目前正在做的一款仙劍同人遊戲《仙古之境》(姑且先叫做這個名字吧)。
在這個遊戲中,博主精心為玩家設計了大量有趣的臺詞,內容涉及了《仙劍奇俠傳》歷代故事情節及《古劍奇譚》的相關內容。如果我們直接將這些臺詞寫進程式碼的話,雖然遊戲同樣可以執行,可是一旦遊戲策劃修改了劇情或者某些內容的話,我們就不得不重新編寫程式碼、重新編譯、重新測試。所以,像《仙劍奇俠傳》和《古劍奇譚》這種RPG
首先呢,我們來講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返回一個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後,我們就可以將內容和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>();
//讀取NPC
mNPCs=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返回一個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()方法來設定對話方塊要顯示的對話內容,然後使用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 {
//NPC
public Transform[] NPCs;
//玩家
public Transform Player;
//初始化NPC
void Awake()
{
foreach(Transform mTrans in NPCs)
{
mTrans.GetComponent<NPCScript>().enabled=false;
}
}
//啟用NPC
void 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