小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

Unity3D游戲開發(fā)之Xml解析實(shí)現(xiàn)NPC對話系統(tǒng)

 尹亮亮 2015-08-31

        各位朋友,大家好,我是秦元培,歡迎大家關(guān)注我的博客,我的博客地址是blog.csdn.net/qinyuanpei。今天我們來說說Unity3DXml的解析,為什么要說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)行下去,直到全部的對話顯示完畢。這里的對話框是用Unity3DGUI系統(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)一。


首先呢,我們來講Unity3DXml的解析,博主在Unity3D中使用的腳本語言是C#,所以博主很果斷地使用了.NET下解析XmlAPI,即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>




    本站是提供個(gè)人知識管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多