您的位置:首页 > 其它

浅谈MMORPG任务编辑器的设计与实现

2012-09-12 18:04 513 查看
浅谈MMORPG任务编辑器的设计与实现

By 马冬亮(凝霜  Loki)

一个人的战争(http://blog.csdn.net/MDL13412)
定义
        MMORPG任务编辑器用于配置人物与地图NPC、怪物、玩家、场景等相关的任务交互操作,处理诸如与NPC对话、杀死BOSS、收集物品等事件,并设置这些事件的响应过程及触发/完成条件等等。例如:接受设置玩家可以通过与地图上某NPC对话接受一个任务,杀死10个指定的怪物,那么任务编辑器就需要对任务的接受条件、完成条件、任务相关变量、自动寻路、杀死怪物后的提示信息等功能进行配置。

设计理念
        首先,任务编辑器是提供给策划人员使用的,而策划人员一般都不懂得编程,所以其设计应遵循WYSIWYG(What You See Is What You Get ,所见即所得)的原则,做到傻瓜化;其次,编辑器应该具有按地图过滤任务、搜索指定任务、任务描述彩色显示、任务变量超链接样式显示(点击超链接时弹出编辑对话框)、插入自动寻路超链接(可以弹出的对话框中手动填写或按地图选择指定NPC)等功能;再次,任务编辑器的布局应该按照功能来分配,尽量做到符合用户的思维习惯,做到“最小惊奇”原则;最后,任务编辑器应该预留足够的扩展性,以满足游戏玩法的扩展。

带来的优势
        通过WYSIWYG的设计理念,可以最大程度的减少策划的心智负担,从而减少其出错的可能性,减少与程序员沟通、调试的成本,显著的提升了团队写作效率;通过提供任务搜索的功能,可以加速策划定位到相应的任务,完成修改、删除等操作,提升策划工作效率;编辑器可以实现源码级别上的复用,公司新开发一个MMORPG,那么只需要在原有的任务编辑器基础上增加、删除功能即可,降低总体拥有成本。

任务响应机制设计
        MMORPG具有强交互的特性,玩家的每一个操作都可以看作是一种特定类型的事件,因此基于消息的事件响应模型非常适合完成此任务。例如:玩家进入场景,可以抽象为on_enter事件,并且客户端向服务器发送on_enter事件,服务器端将其压入消息队列,并进行处理,而后返回处理结果。

任务脚本结构设计
        任务编辑器是对任务脚本的抽象,将其细节隐藏,而脚本最终还是需要由程序处理,因此必须仔细进行设计。这里给出的是国内某著名网络游戏的脚本设计,其任务脚本都包含在quest文件夹中,所有通过玩家任务列表查看的任务都保存在quests.xml中,而相应的事件,则保存在on_drop、on_enter、on_get这些事件文件夹中,事件文件夹中的文件是以NPC_ID进行命名,以此来配置不同NPC对不同事件的响应操作,如下图所示:



        首先,我们来看quests.xml文件中的数据结构:

<?xml version="1.0" encoding="gb2312"?>
<quests>
<quest id="1" name="编程语言选择" map="NsLib">
<description>
<body>
<p>
<n color="255,239,196,0">任务:编程语言选择</n>
</p>
<p>
<n> 如果想加入NsLib,可以找</n>
<a href="goto 134,12">凝霜(134,12)</a>
<n>咨询编程语言的相关信息。</n>
</p>
<p>
<n color="255,239,196,0">任务描述:</n>
</p>
<p>
<n> 听说凝霜成立了NsLib,还没有确定自己的第一门编程语言?快去去看看吧。</n>
</p>
<p>
<n color="255,0,196,0">任务奖励:</n>
</p>
<p>
<n color="255,239,196,0">经验奖励:</n>
<n> 5</n>
</p>
<diffcult level="1" />
</body>
</description>
<events>
<event type="on_visit" source="1.xml" />
<event type="on_visit" source="2.xml" />
<event type="on_enter" source="13412.xml" />
</events>
</quest>
<quest id="2" name="码农成长史">
<description>
<body>
<p>
<n color="255,239,196,0">任务:码农成长史</n>
</p>
<p>
<n> 去写10种语言的Hello World,然后向</n>
<a href="goto 134,12">凝霜(134,12)</a>
<n>汇报。</n>
</p>
<p>
<n color="255,239,196,0"> Hello World:</n>
<n var="kill_hello_world" task="2"/>
<n>/10</n>
</p>
<p>
<n color="255,239,196,0">任务描述:</n>
</p>
<p>
<n> 你的第一个程序是用10种语言写Hello World。</n>
</p>
<p>
<n color="255,0,0,240">任务奖励:</n>
</p>
<p>
<object id="4020"> Python电子书</object>
</p>
<p>
<object id="4050"> Lua电子书</object>
</p>
<diffcult level="2"/>
<message color="255,255,255,20">
<n>Hello World:</n>
<n var="kill_hello_world" task="2"/>
<n>/10</n>
</message>
</body>
</description>
<events>
<event type="on_visit" source="56001.xml"/>
<event type="on_kill" source="13412.xml"/>
</events>
</quest>
</quests>
        需要特别说明的是,<quest>标签中的id必须唯一,用于表示具体任务,name可以重复,对应任务名称,而map则描述在哪个地图接受此任务。

       <description>标签中的内容对应用户在任务列表中显示的描述,<a>是自动寻路的超链接,<n var=xxx>是任务变量,用于记录任务状态,<p><n>标签是文本描述,<p>在这里被转意为换行符。

        <difficult level=XXX>标签表示接受此任务的等级要求;<message>是完成指定事件时,通知用户的消息;<events>对应的是事件处理脚本。

        下面我们再来查看一下事件相应脚本:

<?xml version="1.0" encoding="GB2312"?>
<event id="100" name="任务10">
<quest id="100" new="1">
<embranchment id="1">
<conditions>
<var type="Equal" name="state" value="1"/>
<var type="Less" name="kill_XXXX" value="800"/>
</conditions>
<actions>
<var type="Add" name="kill_XXXX" value="1"/>
<refresh name="kill_XXXX"/>
</actions>
</embranchment>
<embranchment id="2">
<conditions>
<var type="Equal" name="state" value="1"/>
<var type="Great" name="kill_XXXX" value="799"/>
</conditions>
<actions>
<var type="Set" name="state" value="-3"/>
<refresh name="state"/>
<refresh name="kill_XXXX"/>
<notify content="恭喜你,你已经杀了800个40级的XXXX,去找NsLib的凝霜(134,12)领取奖赏吧!"/>
<notify1 content="去找NsLib的凝霜(134,12)吧!"/>
</actions>
</embranchment>
</quest>
</event>        此脚本描述任务的分支及触发/完成条件,供服务器端的事件处理器使用。

界面设计预览



关键技术点--RichTextBox字体着色
        字体着色非常简单,这里用到了一个RichTextBoxLinks的第三方控件,对于<description>标签中的XML文档,可以直接使用以下代码进行染色:

/// <summary>
/// 供MainForm中的任务描述RichTextBoxEx使用
/// 将任务描述字体染色并设置变量超链接
/// </summary>
/// <param name="missionId">要处理的任务Id</param>
/// <param name="editor">MainForm中的任务描述编辑框</param>
public static void ShowRtfDescription(String missionId, RichTextBoxEx editor)
{
QuestXmlHandler.QuestInfo qi = (QuestXmlHandler.QuestInfo)QuestXmlHandler.QuestInfos[missionId];
StringBuilder description = new StringBuilder();
// 以行为解析单元
String line = "";
editor.SelectAll();
editor.Text = "";

try
{
int curPos = qi.UserDescription.Description.IndexOf("\n");
int lastPos = 0;

while (-1 != curPos)
{
line = qi.UserDescription.Description.Substring(lastPos, curPos - lastPos);

ShowRtfDescriptionImpl(line, editor);

lastPos = curPos + 1;
curPos = qi.UserDescription.Description.IndexOf("\n", lastPos);
editor.SelectedText = "\n";
}
}
catch (System.Exception ex)
{
Logger.LogFunctionError("RichEditBoxExRtfBinder.ShowRtfDescription()", ex.Message);
}
}        其中,QuestXmlHandler.QuestInfo qi = (QuestXmlHandler.QuestInfo)QuestXmlHandler.QuestInfos[missionId];是对任务的抽象,其关键定义如下:
/// <summary>
/// 任务文件在程序中的抽象
/// </summary>
public sealed class QuestXmlHandler
{
#region // 内嵌类
public sealed class QuestInfo
{
/// <summary>
/// 新添加任务时自动生成的任务描述模版
/// </summary>
public static String QuestDescriptionTemplate =
@"<n color=""255,239,196,0"">任务:【TODO:任务名称】</n>
<n> 【TODO:简短描述】</n>
<n color=""255,239,196,0"">任务描述:</n>
<n> 【TODO:任务描述内容】</n>
<n color=""255,239,196,0"">经验奖励:</n><n>【TODO:经验】</n>
";
/// <summary>
/// 任务事件表,一种事件可能对应多个文件
/// </summary>
public sealed class EventList
{
public List<String> Events_ = new List<String>();
public List<String> Events
{
get { return Events_; }
}
}

public sealed class QuestDescriptionCollection
{
// 任务描述
private String Description_ = "";
public String Description
{
get { return Description_; }
set { Description_ = value; }
}

// 任务消息
private List<String> Messages_ = new List<String>();
public List<String> Messages
{
get { return Messages_; }
}
public Boolean HasMessages()
{
return 0 != Messages.Count;
}

// 附加,任务等级要求
private int DiffcultLevel_ = -1;
public int DiffcultLevel
{
get { return DiffcultLevel_; }
set { DiffcultLevel_ = value; }
}

}

// 任务Id,不可重复
int Id_ = -1;
public int Id
{
get { return Id_; }
set { Id_ = value; }
}

// 任务名称,可以重复
String Name_;
public String Name
{
get { return Name_; }
set { Name_ = value; }
}

// 任务所属地图
String Map_ = "";
public String Map
{
get { return Map_; }
set { Map_ = value; }
}

// 事件表,每个键对应的值是一个QuestInfo结构,对应此键的所有事件相应文件。
private Hashtable EventsTable_ = new Hashtable();
public Hashtable EventsTable
{
get { return EventsTable_; }
}

// 标记此任务是否具有某项事件
private Hashtable HasEvents_ = new Hashtable();
public Boolean HasEvent(String eventName)
{
try
{
return (Boolean)HasEvents_[eventName];
}
catch (System.Exception)
{
Logger.Error("QuestXmlReader.QuestInfo.HasEvent()键错误:key=" + eventName + " Id=" + Id.ToString() + " Name=" + Name);
return false;
}
}
public void SetEvent(String eventName)
{
try
{
HasEvents_[eventName] = true;
}
catch (System.Exception)
{
Logger.Error("QuestXmlReader.QuestInfo.SetEvent()键错误:key=" + eventName + " Id=" + Id.ToString() + " Name=" + Name);
}
}
public void ClearEvent(String eventName)
{
try
{
HasEvents_[eventName] = false;
}
catch (System.Exception)
{
Logger.Error("QuestXmlReader.QuestInfo.ClearEvent()键错误:key=" + eventName + " Id=" + Id.ToString() + " Name=" + Name);
}
}
public void UpdateEvents()
{
foreach (AppConfig.EventInfo eventInfo in AppConfig.EventInfos)
{
if (0 != ((EventList)EventsTable[eventInfo.EventType]).Events.Count)
HasEvent(eventInfo.EventType);
else
ClearEvent(eventInfo.EventType);
}
}

private QuestDescriptionCollection QuestDescription_ = new QuestDescriptionCollection();
public QuestDescriptionCollection UserDescription
{
get { return QuestDescription_; }
}

public QuestInfo()
{
foreach (AppConfig.EventInfo ei in AppConfig.EventInfos)
{
EventsTable.Add(ei.EventType, new EventList());
HasEvents_.Add(ei.EventType, false);
}
}
}
#endregion
/// ...
}
}
        对于选中的某个字符或者段落进行染色,只需使用下面代码:

private void BtnFontColor_Click(object sender, EventArgs e)
{
ColorDialog dlg = new ColorDialog();
dlg.AllowFullOpen = false; // 不允许使用自定义颜色
dlg.SolidColorOnly = true; // 只允许使用纯色
if (DialogResult.OK == dlg.ShowDialog())
UserMissionListDescription.SelectionColor = dlg.Color;
CurrentMissionEditState.UserMissionListDescriptionChanged = true;
}        其中,UserMissionListDescription为RichTextBoxLinks控件的标识符,CurrentMissionEditState是标识当前任务被更改的部分。
关键技术点--RichTextBox中带编辑功能的任务变量及自动寻路超链接

        这里的关键点是我们先前提到的RichTextBoxLinks第三方控件,我们用到了其中的InsertLink(前端显示的字符串,后台超链接);方法,下面是新插入自动寻路超链接的关键代码:

/// <summary>
/// 插入自动寻路超链接
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void BtnInsertGotoHref_Click(object sender, EventArgs e)
{
GotoHrefForm dlg = new GotoHrefForm(GotoHrefForm.DialogType.InsertNew);
dlg.ShowDialog();
if (GotoHrefForm.InsertSucceed)
{
CurrentMissionEditState.UserMissionListDescriptionChanged = true;

// 字符串格式 NPC名字(x,y)#<a href="goto x,y">NPC名字(x,y)</a>
StringBuilder href = new StringBuilder();
href.AppendFormat("<a href=\"goto {0},{1}\">{2}({3},{4})</a>",
GotoHrefForm.x, GotoHrefForm.y, GotoHrefForm.NpcName, GotoHrefForm.x, GotoHrefForm.y);

StringBuilder NpcNameHref = new StringBuilder();
NpcNameHref.AppendFormat("{0}({1},{2})", GotoHrefForm.NpcName, GotoHrefForm.x, GotoHrefForm.y);

UserMissionListDescription.InsertLink(NpcNameHref.ToString(), href.ToString());
}
}        这里用到了一个HACK,即使用格式  NPC名字(x,y)#<a href="goto x,y">NPC名字(x,y)</a>  
这个固定形式的字符串来标识文本框中显示的超链接内容,其符合RTF标准,此HACK为超链接编辑功能的前置条件。
        任务变量的插入和自动寻路超链接类似,不过不会显示任务变量的具体名称,而统一显示为“任务变量”超链接的形式,代码如下:

/// <summary>
/// 插入任务变量超链接
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void BtnInsertVar_Click(object sender, EventArgs e)
{
MissionVarForm dlg = new MissionVarForm(CurrentEditMission, MissionVarForm.DialogType.InsertNew);
dlg.ShowDialog();
if (MissionVarForm.InsertSucceed)
{
CurrentMissionEditState.UserMissionListDescriptionChanged = true;
// <n var="变量名" task="任务"></n>
StringBuilder href = new StringBuilder();
href.AppendFormat("<n var=\"{0}\" task=\"{1}\"></n>", MissionVarForm.MissionVar, MissionVarForm.Task);

UserMissionListDescription.InsertLink("任务变量", href.ToString());
}
}        为了支持点击超链接弹出修改对话框的功能,我们需要相应RichTextBox控件的LinkClicked事件,代码如下:
/// <summary>
/// 编辑自动寻路和任务变量
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void UserMissionListDescription_LinkClicked(object sender, LinkClickedEventArgs e)
{
try
{
CurrentMissionEditState.UserMissionListDescriptionChanged = true;

// text字符串的格式为 NPC名字(x,y)#<a href="goto x,y">NPC名字(x,y)</a>
// 或者 任务变量#<n var="变量名" task="任务"><n>
String text = e.LinkText;
// 通过查找"#<"来定位我们硬编码的超链接数据
int pos = text.LastIndexOf("#<");
if (-1 == pos)
return;

if ('a' == text[pos + "#<".Length]) // 编辑自动寻路功能
MainFormImpl.EditNpcAutomaticPathfinding(text, pos, UserMissionListDescription);
else if ('n' == text[pos + "#<".Length]) // 编辑任务变量
MainFormImpl.EditMissionVar(text, pos, CurrentEditMission, UserMissionListDescription);
}
catch (System.Exception ex)
{
Logger.LogFunctionError("MainForm.UserMissionListDescription_LinkClicked()", ex.Message);
}
}        在LinkClicked中判断所点击的超链接是自动寻路还是任务变量,并将其传递给相应的对话框,并完成相应的逻辑工作,前面的HACK是为了全局替换所有相同的超链接,做到修改一处,自动更新其他超链接的效果。
关键技术点--RichTextBox中数据的保存

        这个是本任务编辑器中最复杂的部分,由于RichTextBox的内部数据结构是RTF格式,因此我们需要自己手动解析RTF文档,并将其转换成XML格式。

        这里我采用的办法是以一行为一个解析单元,将行中的超链接先提取出来,并依次将字符串分割,分割效果如下图所示:



        对于上图的情况,使用贪心算法将“分组1”和“分组3”的字体颜色进行提取,并构造XML数据,这里“分组1”和“分组3”字体颜色一样,贪心算法计算的结果是将整个分组封装成一个标签。
        之所以说这个算法复杂是因为实际的情况远比上面的例子要复杂,需要处理各种边界问题,下面将关键代码贴出,其复杂性大家一看便知:
// Copyright © 2012 123u. All Rights Reserved
// 作者:凝霜 博客 http://blog.csdn.net/mdl13412
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using RichTextBoxLinks;
using System.Windows.Forms;
using System.Xml;
using System.Drawing;

namespace MissionEditor
{
class RichTextBoxExRtfBinder
{
#region // 通用
/// <summary>
/// 获取XML标签中的颜色
/// </summary>
/// <param name="color"></param>
/// <returns></returns>
public static Color GetRgb(String color)
{
// 解析失败则返回黑色
try
{
int pos = 0;
String r = "";
String g = "";
String b = "";
String a = "";

while ('0' <= color[pos] && color[pos] <= '9')
{
r += color[pos];
++pos;
}
++pos;
while ('0' <= color[pos] && color[pos] <= '9')
{
g += color[pos];
++pos;
}
++pos;
while ('0' <= color[pos] && color[pos] <= '9')
{
b += color[pos];
++pos;
}
++pos;
while (pos < color.Length && '0' <= color[pos] && color[pos] <= '9')
{
a += color[pos];
pos++;
}
Color c = Color.FromArgb(Convert.ToInt32(a), Convert.ToInt32(r), Convert.ToInt32(g), Convert.ToInt32(b));
return c;
}
catch (Exception)
{
return Color.Black;
}
}
/// <summary>
/// 获取RTF标签中的颜色
/// </summary>
/// <param name="rtf"></param>
/// <returns></returns>
private static Color GetRtfColor(String rtf)
{
// rtf字符串中的字体颜色相关字符串格式 {\colortbl ;\red255\green0\blue0;}

try
{
int colorPos = rtf.IndexOf("{\\colortbl");
if (-1 == colorPos)
return Color.Black;
colorPos += "{\\colortbl".Length;

int pos = 0;
String r = "";
String g = "";
String b = "";

while (!('0' <= rtf[colorPos + pos] && rtf[colorPos + pos] <= '9'))
++pos;
while ('0' <= rtf[colorPos + pos] && rtf[colorPos + pos] <= '9')
{
r += rtf[colorPos + pos];
++pos;
}
while (!('0' <= rtf[colorPos + pos] && rtf[colorPos + pos] <= '9'))
++pos;
while ('0' <= rtf[colorPos + pos] && rtf[colorPos + pos] <= '9')
{
g += rtf[colorPos + pos];
++pos;
}
while (!('0' <= rtf[colorPos + pos] && rtf[colorPos + pos] <= '9'))
++pos;
while ('0' <= rtf[colorPos + pos] && rtf[colorPos + pos] <= '9')
{
b += rtf[colorPos + pos];
++pos;
}
Color c = Color.FromArgb(Convert.ToInt32(r), Convert.ToInt32(g), Convert.ToInt32(b));
return c;
}
catch (Exception)
{
return Color.Black;
}
}
#endregion

#region // 供MainForm中的任务描述RichTextBoxEx使用
/// <summary>
/// 供MainForm中的任务描述RichTextBoxEx使用
/// 将任务描述字体染色并设置变量超链接
/// </summary>
/// <param name="missionId">要处理的任务Id</param>
/// <param name="editor">MainForm中的任务描述编辑框</param>
public static void ShowRtfDescription(String missionId, RichTextBoxEx editor)
{
QuestXmlHandler.QuestInfo qi = (QuestXmlHandler.QuestInfo)QuestXmlHandler.QuestInfos[missionId];
StringBuilder description = new StringBuilder();
// 以行为解析单元
String line = "";
editor.SelectAll();
editor.Text = "";

try
{
int curPos = qi.UserDescription.Description.IndexOf("\n");
int lastPos = 0;

while (-1 != curPos)
{
line = qi.UserDescription.Description.Substring(lastPos, curPos - lastPos);

ShowRtfDescriptionImpl(line, editor);

lastPos = curPos + 1;
curPos = qi.UserDescription.Description.IndexOf("\n", lastPos);
editor.SelectedText = "\n";
}
}
catch (System.Exception ex)
{
Logger.LogFunctionError("RichEditBoxExRtfBinder.ShowRtfDescription()", ex.Message);
}
}
private static void ShowRtfDescriptionImpl(String line, RichTextBoxEx editor)
{
// 用户描述部分只有两种标签<n>和<a>
int nTagPos = line.IndexOf("<n");
int aTagPos = line.IndexOf("<a");

while (-1 != nTagPos || -1 != aTagPos)
{
try
{
XmlDocument doc = new XmlDocument();

if (-1 == nTagPos) // 解析<a>
{
aTagPos = ParseRtfDescriptionATag(line, editor, aTagPos, doc);
}
else if (-1 == aTagPos) // 解析<n>
{
nTagPos = ParseRtfDescriptionNTag(line, editor, nTagPos, doc);
}
else
{
// 同时解析<a>和<n>
if (nTagPos < aTagPos)
{
nTagPos = ParseRtfDescriptionNTag(line, editor, nTagPos, doc);
}
else if (nTagPos > aTagPos)
{
aTagPos = ParseRtfDescriptionATag(line, editor, aTagPos, doc);
}
else
{
throw new Exception("节点解析错误<n>和<a>位移相同");
}
}
}
catch (Exception ex)
{
Logger.LogFunctionError("RichEditBoxExRtfBinder.ShowRtfDescriptionImpl()", ex.Message);
}
}
}
/// <summary>
/// 解析<a>节点
/// </summary>
/// <param name="line">行文本</param>
/// <param name="editor">绑定的编辑框</param>
/// <param name="aTagPos"><a>标签的位移</param>
/// <param name="doc">xml节点</param>
/// <returns>下一个<a>节点位移</returns>
private static int ParseRtfDescriptionATag(String line, RichTextBoxEx editor, int aTagPos, XmlDocument doc)
{
int endATagPos = line.IndexOf("</a>", aTagPos);

if (-1 == endATagPos)
throw new Exception("节点解析错误" + line);

// 使用XML来简化编程
String text = line.Substring(aTagPos, endATagPos - aTagPos + "</a>".Length);
XmlElement Root = doc.CreateElement("node");
Root.InnerXml = text;
doc.AppendChild(Root);

try
{
editor.InsertLink(doc.ChildNodes[0].ChildNodes[0].InnerText, doc.ChildNodes[0].InnerXml);
}
catch (System.Exception)
{
}

return line.IndexOf("<a", endATagPos + "</a>".Length);
}
/// <summary>
/// 解析<n>节点
/// </summary>
/// <param name="line">行文本</param>
/// <param name="editor">绑定的编辑框</param>
/// <param name="aTagPos"><n>标签的位移</param>
/// <param name="doc">xml节点</param>
/// <returns>下一个<n>节点位移</returns>
private static int ParseRtfDescriptionNTag(String line, RichTextBoxEx editor, int nTagPos, XmlDocument doc)
{
int endNTagPos = line.IndexOf("</n>", nTagPos);

if (-1 == endNTagPos)
throw new Exception("节点解析错误" + line);

String text = line.Substring(nTagPos, endNTagPos - nTagPos + "</n>".Length);
XmlElement Root = doc.CreateElement("node");
Root.InnerXml = text;
doc.AppendChild(Root);

try
{
// 如果获取颜色失败,则判断是否为任务变量
String color = doc.ChildNodes[0].ChildNodes[0].Attributes["color"].Value;
editor.SelectionColor = GetRgb(color);
editor.SelectedText = doc.ChildNodes[0].ChildNodes[0].InnerText;
}
catch (System.Exception)
{
try
{
// 获取任务变量信息
String var = doc.ChildNodes[0].ChildNodes[0].Attributes["var"].Value;
String task = doc.ChildNodes[0].ChildNodes[0].Attributes["task"].Value;

// <n var="变量名" task="任务">
editor.InsertLink("任务变量", doc.ChildNodes[0].InnerXml);
}
catch (System.Exception)
{
try
{
// 默认颜色的普通文本
editor.SelectionColor = Color.Black;
editor.SelectedText = doc.ChildNodes[0].ChildNodes[0].InnerText;
}
catch (System.Exception)
{
}
}
}

return line.IndexOf("<n", endNTagPos + "</n>".Length);
}
/// <summary>
/// 将编辑框中的RTF转换为XML文件
/// </summary>
/// <param name="editor">MainForm中的任务描述编辑框</param>
/// <returns>XML文本</returns>
public static String ConverterRtfDescriptionToXml(RichTextBoxEx editor)
{
StringBuilder description = new StringBuilder();

try
{
// 以行为解析单元
int lineStart = 0;
int lineEnd = editor.Text.IndexOf("\n");
// 只有一行数据且没有换行
if (-1 == lineEnd && 0 != editor.Text.Length)
lineEnd = editor.Text.Length;
int length = lineEnd - lineStart;
String line = "";

while (-1 != lineEnd)
{
length = lineEnd - lineStart;
line = editor.Text.Substring(lineStart, lineEnd - lineStart);

if ("" != line) // 跳过空白行
{
try
{
List<Tuple<int, int, String>> hrefs = ConverterRtfDescriptionHrefHelper(line);
if (null == hrefs)
throw new Exception("获取链接集合错误");

ConverterRtfDescriptionTags(description, line, hrefs, editor, lineStart);
}
catch (System.Exception ex)
{
Logger.LogFunctionError("RichTextBoxExRtfBinder.ConverterRtfDescriptionToInnerXml()[Tag_解析标签]", ex.Message);
return "";
}

description.Append("\n");
}

lineStart = lineEnd + 1;
if (lineStart >= editor.Text.Length)
{
lineEnd = -1;
}
else
{
lineEnd = editor.Text.IndexOf("\n", lineStart);

if (-1 == lineEnd)
lineEnd = editor.Text.Length;
}
}

return description.ToString();
}
catch (Exception ex)
{
Logger.LogFunctionError("RichTextBoxExRtfBinder.ConverterRtfDescriptionToInnerXml()[Tag_解析一行数据]", ex.Message);
return "";
}
}
/// <summary>
/// 解析出一行中的所有超链接<a>和任务变量<n var>
/// </summary>
/// <param name="line">行文本</param>
/// <returns>所有超链接的集合</returns>
private static List<Tuple<int, int, String>> ConverterRtfDescriptionHrefHelper(String line)
{
try
{
// 所有变量链接的起始点,终点,XML数据
List<Tuple<int, int, String>> hrefs = new List<Tuple<int, int, String>>();

// 首先找出所有的变量链接
int hrefStart = 0;
int hrefEnd = 0;
int hrefTag = line.IndexOf("#");

while (-1 != hrefTag)
{
int pos1 = 0;
int pos2 = 0;
String href = "";
String info = "";
String var = "";

switch (line[hrefTag + 2])
{
case 'n':
// <n var="get_物品2" task="2">
pos1 = line.IndexOf("</n>", hrefTag);
var = line.Substring(hrefTag + "#".Length, pos1 + "</n>".Length - hrefTag - "#".Length);
hrefStart = hrefTag - "任务变量".Length;
hrefEnd = pos1 + "</n>".Length;
hrefs.Add(new Tuple<int, int, String>(hrefStart, hrefEnd, var));
break;
case 'a':
// 格式为 NPC(x,y)#<a href="goto x,y">NPC(x,y)</a>
pos1 = line.IndexOf(">", hrefTag);
pos2 = line.IndexOf("</a>", hrefTag);
href = line.Substring(hrefTag + "#".Length, pos2 + "</a>".Length - hrefTag - "#".Length);
info = line.Substring(pos1 + 1, pos2 - pos1 - ">".Length);
hrefStart = hrefTag - info.Length;
hrefEnd = pos2 + "</a>".Length;
hrefs.Add(new Tuple<int, int, String>(hrefStart, hrefEnd, href));
break;
default:
throw new Exception("意外的标签,请仔细检查任务文件");
}

hrefTag = line.IndexOf("#", hrefTag + "#".Length);
}

return hrefs;
}
catch (Exception ex)
{
Logger.LogFunctionError("RichTextBoxExRtfBinder.ConverterRtfDescriptionHref()[Tag_解析超链接]", ex.Message);
return null;
}
}
/// <summary>
/// 构造普通的<n>节点
/// </summary>
/// <param name="description">XML构建器</param>
/// <param name="color"><n>节点的颜色</param>
/// <param name="text"><n>节点的内容</param>
private static void ConverterRtfDescriptionMakeNTag(StringBuilder description, Color color, String text)
{
if (Color.Black.R == color.R && Color.Black.G == color.G && Color.Black.B == color.B)
description.AppendFormat("<n>{0}</n>", text);
else
description.AppendFormat("<n color=\"{0},{1},{2},0\">{3}</n>", Convert.ToInt32(color.R), Convert.ToInt32(color.G), Convert.ToInt32(color.B), text);
}
/// <summary>
/// 贪心算法实现标签的解析
/// 将连续相同颜色的标签分为一组,并构建XML
/// </summary>
/// <param name="description"></param>
/// <param name="wordBlock"></param>
/// <param name="editor"></param>
/// <param name="blockStart"></param>
private static void ConverterRtfDescriptionProcessWordBlock(StringBuilder description, String wordBlock, RichTextBoxEx editor, int blockStart)
{
Color lastWordColor = Color.Black;
Color curWordColor = Color.Black;

editor.Select(blockStart, 1);
curWordColor = GetRtfColor(editor.SelectedRtf);
lastWordColor = curWordColor;

// 本行只有一个字符的情况
if (1 == wordBlock.Length)
{
ConverterRtfDescriptionMakeNTag(description, curWordColor, editor.SelectedText);
return;
}

int counter = 1;
for (int i = 1; i < wordBlock.Length; ++i)
{
editor.Select(blockStart + i, 1);
curWordColor = GetRtfColor(editor.SelectedRtf);
if (wordBlock.Length - 1 == i)
{
if (lastWordColor == curWordColor)
{
editor.Select(blockStart + i - counter, counter + 1);
ConverterRtfDescriptionMakeNTag(description, curWordColor, editor.SelectedText);
}
else
{
editor.Select(blockStart + i - counter, counter);
ConverterRtfDescriptionMakeNTag(description, lastWordColor, editor.SelectedText);

editor.Select(blockStart + i, 1);
ConverterRtfDescriptionMakeNTag(description, curWordColor, editor.SelectedText);
}
}
else
{
if (lastWordColor == curWordColor)
{
++counter;
}
else
{
editor.Select(blockStart + i - counter, counter);
ConverterRtfDescriptionMakeNTag(description, lastWordColor, editor.SelectedText);
counter = 1;
}
}

lastWordColor = curWordColor;
}
}
/// <summary>
/// 转换RTF到XML
/// </summary>
/// <param name="description"></param>
/// <param name="line"></param>
/// <param name="hrefs"></param>
/// <param name="editor"></param>
/// <param name="lineStart"></param>
private static void ConverterRtfDescriptionTags(StringBuilder description, String line, List<Tuple<int, int, String>> hrefs, RichTextBoxEx editor, int lineStart)
{
try
{
if (0 == hrefs.Count) // 处理没有超链接的行
ConverterRtfDescriptionProcessWordBlock(description, line, editor, lineStart);
else
{
#region // 处理有超链接的行
// 非链接字符串区块
int blockStart = 0;
int blockEnd = hrefs[0].Item1;
String wordBlock = line.Substring(blockStart, blockEnd - blockStart);
if (blockStart == blockEnd)
{
#region // 一行的起始是链接的情况
description.Append(hrefs[0].Item3);
for (int i = 1; i < hrefs.Count; ++i)
{
blockStart = hrefs[i - 1].Item2;
blockEnd = hrefs[i].Item1;
wordBlock = line.Substring(blockStart, blockEnd - blockStart);
ConverterRtfDescriptionProcessWordBlock(description, wordBlock, editor, lineStart + blockStart);
description.Append(hrefs[i].Item3);
if (0 == wordBlock.Length)
continue;
}

// 判断最后一个链接后是否还有普通文本
if (hrefs[hrefs.Count - 1].Item2 != line.Length)
{
wordBlock = line.Substring(hrefs[hrefs.Count - 1].Item2, line.Length - hrefs[hrefs.Count - 1].Item2);
ConverterRtfDescriptionProcessWordBlock(description, wordBlock, editor, lineStart + hrefs[hrefs.Count - 1].Item2);
}
#endregion
}
else
{
#region // 一行起始是普通文本的情况
ConverterRtfDescriptionProcessWordBlock(description, wordBlock, editor, lineStart + blockStart);

if (1 == hrefs.Count)
description.Append(hrefs[0].Item3);
else
{
for (int i = 1; i < hrefs.Count; ++i)
{
description.Append(hrefs[i].Item3);
blockStart = hrefs[i - 1].Item2;
blockEnd = hrefs[i].Item1;
wordBlock = line.Substring(blockStart, blockEnd - blockStart);
ConverterRtfDescriptionProcessWordBlock(description, wordBlock, editor, lineStart + blockStart);
if (0 == wordBlock.Length)
continue;
}

description.Append(hrefs[hrefs.Count - 1].Item3);
}

// 判断最后一个链接后是否还有普通文本
if (hrefs[hrefs.Count - 1].Item2 != line.Length)
{
wordBlock = line.Substring(hrefs[hrefs.Count - 1].Item2, line.Length - hrefs[hrefs.Count - 1].Item2);
ConverterRtfDescriptionProcessWordBlock(description, wordBlock, editor, lineStart + hrefs[hrefs.Count - 1].Item2);
}
#endregion
}
#endregion
}

}
catch (System.Exception ex)
{
Logger.LogFunctionError("RichTextBoxExRtfBinder.ConverterRtfDescriptionTags()", ex.Message);
}
}
#endregion

#region // 供MainForm中的消息RichTextBoxEx使用
public static void ShowRtfMessage(String missionId, RichTextBoxEx editor)
{
QuestXmlHandler.QuestInfo qi = (QuestXmlHandler.QuestInfo)QuestXmlHandler.QuestInfos[missionId];
StringBuilder messageBuilder = new StringBuilder();
String line = "";
editor.SelectAll();
editor.Text = "";

if (qi.UserDescription.HasMessages())
{
for (int i = 0; i < qi.UserDescription.Messages.Count; ++i)
messageBuilder.Append(qi.UserDescription.Messages[i] + "\n");
}
else
{
return;
}

String messages = messageBuilder.ToString();

try
{
int curPos = messages.IndexOf("\n");
int lastPos = 0;

while (-1 != curPos)
{
line = messages.Substring(lastPos, curPos - lastPos);

ShowRtfMessageImpl(line, editor);

lastPos = curPos + 1;
curPos = messages.IndexOf("\n", lastPos);
editor.SelectedText = "\n";
}
}
catch (System.Exception ex)
{
Logger.LogFunctionError("RichEditBoxExRtfBinder.ShowRtfMessage()", ex.Message);
}
}
private static void ShowRtfMessageImpl(String line, RichTextBoxEx editor)
{
// 用户消息部分只有一种标签<n>
int nTagPos = line.IndexOf("<n");

while (-1 != nTagPos)
{
try
{
XmlDocument doc = new XmlDocument();

nTagPos = ParseRtfMessageNTag(line, editor, nTagPos, doc);
}
catch (Exception ex)
{
Logger.LogFunctionError("RichEditBoxExRtfBinder.ShowRtfDescriptionImpl()", ex.Message);
}
}
}
private static int ParseRtfMessageNTag(String line, RichTextBoxEx editor, int nTagPos, XmlDocument doc)
{
int endNTagPos = line.IndexOf("</n>", nTagPos);

if (-1 == endNTagPos)
throw new Exception("节点解析错误" + line);

String text = line.Substring(nTagPos, endNTagPos - nTagPos + "</n>".Length);
XmlElement Root = doc.CreateElement("node");
Root.InnerXml = text;
doc.AppendChild(Root);

try
{
String color = doc.ChildNodes[0].ChildNodes[0].Attributes["color"].Value;
editor.SelectionColor = GetRgb(color);
editor.SelectedText = doc.ChildNodes[0].ChildNodes[0].InnerText;
}
catch (System.Exception)
{
try
{
String var = doc.ChildNodes[0].ChildNodes[0].Attributes["var"].Value;
String task = doc.ChildNodes[0].ChildNodes[0].Attributes["task"].Value;

// <n var="变量名" task="任务">
editor.InsertLink("任务变量", doc.ChildNodes[0].InnerXml);
}
catch (System.Exception)
{
try
{
editor.SelectionColor = Color.Black;
editor.SelectedText = doc.ChildNodes[0].ChildNodes[0].InnerText;
}
catch (System.Exception)
{
}
}
}

return line.IndexOf("<n", endNTagPos + "</n>".Length);
}
public static String ConverterRtfMessageToXml(RichTextBoxEx editor)
{
StringBuilder message = new StringBuilder();

try
{
// 以行为解析单元
int lineStart = 0;
int lineEnd = editor.Text.IndexOf("\n");
// 只有一行数据且没有换行
if (-1 == lineEnd && 0 != editor.Text.Length)
lineEnd = editor.Text.Length;
int length = lineEnd - lineStart;
String line = "";

while (-1 != lineEnd)
{
length = lineEnd - lineStart;
line = editor.Text.Substring(lineStart, lineEnd - lineStart);

if ("" != line) // 跳过空白行
{
try
{
List<Tuple<int, int, String>> hrefs = ConverterRtfMessageHrefHelper(line);
if (null == hrefs)
throw new Exception("获取链接集合错误");

ConverterRtfMessageTags(message, line, hrefs, editor, lineStart);
}
catch (System.Exception ex)
{
Logger.LogFunctionError("RichTextBoxExRtfBinder.ConverterRtfMessageToInnerXml()[Tag_解析标签]", ex.Message);
return "";
}

message.Append("\n");
}

lineStart = lineEnd + 1;
if (lineStart >= editor.Text.Length)
{
lineEnd = -1;
}
else
{
lineEnd = editor.Text.IndexOf("\n", lineStart);

if (-1 == lineEnd)
lineEnd = editor.Text.Length;
}
}

return message.ToString();
}
catch (Exception ex)
{
Logger.LogFunctionError("RichTextBoxExRtfBinder.ConverterRtfMessageToInnerXml()[Tag_解析一行数据]", ex.Message);
return "";
}
}
private static List<Tuple<int, int, String>> ConverterRtfMessageHrefHelper(String line)
{
try
{
// 所有变量链接的起始点,终点,XML数据
List<Tuple<int, int, String>> hrefs = new List<Tuple<int, int, String>>();

// 首先找出所有的变量链接
int hrefStart = 0;
int hrefEnd = 0;
int hrefTag = line.IndexOf("#");

while (-1 != hrefTag)
{
int pos = 0;
String var = "";

switch (line[hrefTag + 2])
{
case 'n':
// <n var="get_物品2" task="2">
pos = line.IndexOf("</n>", hrefTag);
var = line.Substring(hrefTag + "#".Length, pos + "</n>".Length - hrefTag - "#".Length);
hrefStart = hrefTag - "任务变量".Length;
hrefEnd = pos + "</n>".Length;
hrefs.Add(new Tuple<int, int, String>(hrefStart, hrefEnd, var));
break;
default:
throw new Exception("意外的标签,请仔细检查任务文件");
}

hrefTag = line.IndexOf("#", hrefTag + "#".Length);
}

return hrefs;
}
catch (Exception ex)
{
Logger.LogFunctionError("RichTextBoxExRtfBinder.ConverterRtfMessageHrefHelper()[Tag_解析超链接]", ex.Message);
return null;
}
}
private static void ConverterRtfMessageMakeNTag(StringBuilder message, Color color, String text)
{
if (Color.Black.R == color.R && Color.Black.G == color.G && Color.Black.B == color.B)
message.AppendFormat("<n>{0}</n>", text);
else
message.AppendFormat("<n color=\"{0},{1},{2},0\">{3}</n>", Convert.ToInt32(color.R), Convert.ToInt32(color.G), Convert.ToInt32(color.B), text);
}
private static void ConverterRtfMessageProcessWordBlock(StringBuilder message, String wordBlock, RichTextBoxEx editor, int blockStart)
{
Color lastWordColor = Color.Black;
Color curWordColor = Color.Black;

editor.Select(blockStart, 1);
curWordColor = GetRtfColor(editor.SelectedRtf);
lastWordColor = curWordColor;

// 本行只有一个字符的情况
if (1 == wordBlock.Length)
{
ConverterRtfMessageMakeNTag(message, curWordColor, editor.SelectedText);
return;
}

int counter = 1;
for (int i = 1; i < wordBlock.Length; ++i)
{
editor.Select(blockStart + i, 1);
curWordColor = GetRtfColor(editor.SelectedRtf);
if (wordBlock.Length - 1 == i)
{
if (lastWordColor == curWordColor)
{
editor.Select(blockStart + i - counter, counter + 1);
ConverterRtfMessageMakeNTag(message, curWordColor, editor.SelectedText);
}
else
{
editor.Select(blockStart + i - counter, counter);
ConverterRtfMessageMakeNTag(message, lastWordColor, editor.SelectedText);

editor.Select(blockStart + i, 1);
ConverterRtfMessageMakeNTag(message, curWordColor, editor.SelectedText);
}
}
else
{
if (lastWordColor == curWordColor)
{
++counter;
}
else
{
editor.Select(blockStart + i - counter, counter);
ConverterRtfMessageMakeNTag(message, lastWordColor, editor.SelectedText);
counter = 1;
}
}

lastWordColor = curWordColor;
}
}
private static void ConverterRtfMessageTags(StringBuilder message, String line, List<Tuple<int, int, String>> hrefs, RichTextBoxEx editor, int lineStart)
{
try
{
if (0 == hrefs.Count) // 处理没有超链接的行
ConverterRtfMessageProcessWordBlock(message, line, editor, lineStart);
else
{
#region // 处理有超链接的行
// 非链接字符串区块
int blockStart = 0;
int blockEnd = hrefs[0].Item1;
String wordBlock = line.Substring(blockStart, blockEnd - blockStart);

if (blockStart == blockEnd)
{
#region // 一行的起始是链接的情况
message.Append(hrefs[0].Item3);
for (int i = 1; i < hrefs.Count; ++i)
{
blockStart = hrefs[i - 1].Item2;
blockEnd = hrefs[i].Item1;
wordBlock = line.Substring(blockStart, blockEnd - blockStart);
ConverterRtfMessageProcessWordBlock(message, wordBlock, editor, lineStart + blockStart);
message.Append(hrefs[i].Item3);
if (0 == wordBlock.Length)
continue;
}

// 判断最后一个链接后是否还有普通文本
if (hrefs[hrefs.Count - 1].Item2 != line.Length)
{
wordBlock = line.Substring(hrefs[hrefs.Count - 1].Item2, line.Length - hrefs[hrefs.Count - 1].Item2);
ConverterRtfMessageProcessWordBlock(message, wordBlock, editor, lineStart + hrefs[hrefs.Count - 1].Item2);
}
#endregion
}
else
{
#region // 一行起始是普通文本的情况
ConverterRtfMessageProcessWordBlock(message, wordBlock, editor, lineStart + blockStart);

if (1 == hrefs.Count)
message.Append(hrefs[0].Item3);
else
{
for (int i = 1; i < hrefs.Count; ++i)
{
message.Append(hrefs[i].Item3);
blockStart = hrefs[i - 1].Item2;
blockEnd = hrefs[i].Item1;
wordBlock = line.Substring(blockStart, blockEnd - blockStart);
ConverterRtfMessageProcessWordBlock(message, wordBlock, editor, lineStart + blockStart);
if (0 == wordBlock.Length)
continue;
}
message.Append(hrefs[hrefs.Count - 1].Item3);
}

// 判断最后一个链接后是否还有普通文本
if (hrefs[hrefs.Count - 1].Item2 != line.Length)
{
wordBlock = line.Substring(hrefs[hrefs.Count - 1].Item2, line.Length - hrefs[hrefs.Count - 1].Item2);
ConverterRtfMessageProcessWordBlock(message, wordBlock, editor, lineStart + hrefs[hrefs.Count - 1].Item2);
}
#endregion
}
#endregion
}

}
catch (System.Exception ex)
{
Logger.LogFunctionError("RichTextBoxExRtfBinder.ConverterRtfMessageTags()", ex.Message);
}
}
#endregion

}
}

总结
        衡量任务编辑器的好坏没有一个固定的标准,只要符合项目的实际需要即可,不过可以肯定的是,“所见即所得”的功能越强大、操作越简单,那么这个编辑器的价值就越大。欢迎大家探讨游戏编辑器的相关话题,包括但不局限于任务编辑器:-)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: