Unity3D游戏开发之Xml解析实现NPC对话系统
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
Unity3D游戏开发之Xml解析实现NPC对话系统
各位朋友,大家好,我是秦元培,欢迎大家关注我的博客,我的博客地址是/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返回一个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").GetCompo nent<GameManager>();
//读取NPC
mNPCs=ReadXmls();
}
void Update ()
{
//对话触发
RaycastHit mHit;
Ray
mRay=mManager.Manager_Camera.ScreenPointT oRay(Input.mo usePosition);
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.T alk);
//计算玩家和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].T oString()); 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,如果知道是怎么回事,最好在第一时间解决;如果不知道是怎么回事,那就只有用非正常手段来解决了。
博主做这款小游戏,主要是因为博主喜欢《仙剑奇侠传》和《古剑奇谭》这两个系列的游戏,很多时候,我们都只是平凡世界中平凡的一员,可正是因为平凡,我们才想要去改变,游戏总有打到通关的那一刻,可是我们的人生才刚刚开始。
《仙古之境》这个小游戏讲述的是虚拟世界中连接仙剑世界与古剑世界的一个过渡世界,类似于《仙剑奇侠传》中神魔之井的设定。
或许是因为太喜欢那个蓝衣白衫的少年剑客,或许是因为太喜欢那个白发飘飘的孤独背影,总之,当即墨那晚的灯火散尽之时,当琼华派转眼沧海桑田,那个少年依然做着他少年时的梦。
好了,最后一起来看看游戏场景展示吧:
好了,今天的内容就是这样啦,希望大家喜欢啊。
每日箴言:我情愿化成一片落叶,让风吹雨打到处飘零;或流云一朵,在澄蓝天,和大地再没有些牵连。
——林徽因。