淺談MMORPG任務編輯器的設計與實現
淺談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是標識當前任務被更改的部分。這裡的關鍵點是我們先前提到的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的內部資料結構是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
}
}
相關文章
- 淺談分散式任務排程系統Celery的設計與實現分散式
- 淺談VueUse設計與實現Vue
- 淺談許可權管理的設計與實現
- 編輯計劃任務並執行
- 淺析遊戲敘事結構和任務編排設計遊戲
- 淺談微服務基建的邏輯微服務
- 淺談任務分發中的機制與併發
- 淺談JS阻塞方式怎麼實現非同步任務佇列?JS非同步佇列
- 淺談uCOS-II的任務(上)
- 淺談設計模式及python實現設計模式Python
- Part 7: 編輯任務列表
- 淺析pplx庫的設計與實現。
- 淺談Hybrid技術的設計與實現第三彈——落地篇
- 百億流量微服務閘道器的設計與實現微服務
- lua定時器與定時任務的介面設計定時器
- 淺談分散式計算的開發與實現(一)分散式
- 淺談分散式計算的開發與實現(1)分散式
- 設計漫談:傳說任務的曲折道路
- java實現編輯器(一)Java
- 程式設計師常用的編輯器程式設計師
- 視覺化編輯器的設計視覺化
- 聊聊支付流程的設計與實現邏輯
- 淺談Hybrid技術的設計與實現第三彈——落地篇前言
- (Ajax) 淺談 JSONP 的原理與實現JSON
- Quill編輯器實現原理初探UI
- CompleteFuture實現簡單的任務編排實踐
- 聊聊訊息中心的設計與實現邏輯
- 淺談服務介面的高可用設計
- 淺談編譯器優化編譯優化
- 淺談分散式定時任務之quartz分散式quartz
- 淺談彙編器、編譯器和直譯器編譯
- 經典是否需守舊? MMORPG中師門任務的去與留
- 《無盡的任務》設計師:《無盡的任務》如何實現20年的使用者留存?
- 利用 javascript 實現富文字編輯器JavaScript
- xheditor編輯器上傳功能實現
- monaco-editor 實現SQL編輯器SQL
- 視覺化介面編輯器設計視覺化
- iOS使用UITableView實現的富文字編輯器iOSUIView