<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>猴子靈藥 [Monkey Potion] &#187; 進階技術</title>
	<atom:link href="http://blog.monkeypotion.net/category/gameprog/advanced/feed" rel="self" type="application/rss+xml" />
	<link>http://blog.monkeypotion.net</link>
	<description>遊戲開發‧遊戲程式‧遊戲設計</description>
	<lastBuildDate>Mon, 06 Sep 2010 01:52:21 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.0.1</generator>
		<item>
		<title>記憶體配置：Pooled Allocation技術評比與效能測試</title>
		<link>http://blog.monkeypotion.net/gameprog/advanced/profiling-in-pooled-allocation-techniques</link>
		<comments>http://blog.monkeypotion.net/gameprog/advanced/profiling-in-pooled-allocation-techniques#comments</comments>
		<pubDate>Thu, 04 Dec 2008 16:13:56 +0000</pubDate>
		<dc:creator>半路</dc:creator>
				<category><![CDATA[進階技術]]></category>

		<guid isPermaLink="false">http://blog.monkeypotion.net/?p=842</guid>
		<description><![CDATA[從剛進入遊戲業界開始，我就一直對於「記憶體配置」(Memory Allocation) 的系統架構與相關議題很感興趣。後來隨著閱讀書籍量的增加與工作經驗上的累積，逐漸接觸到各種不同設計架構與實作方法的記憶體管理機制，現在終於能夠將一些初步的心得整理出來了。 在遊戲程式設計的領域中，程式設計者經常需要即時且動態地產生出大量的小型物件，例如怪物、特效、場景物乃至於低階的節點物件等等。如果遊戲程式設計者只是天真爛漫地使用著單純無害的 operator new 以及 operator delete 程序，在經過遊戲執行中不斷反覆地配置與歸還記憶體的行為之後，很快就會使得完整的記憶體區段面臨嚴重的「記憶體破碎」(Memory Fragmentation) 問題。更糟的是，記憶體破碎的問題往往很難被偵測出來，而容易被開發者所忽略。 為了盡可能降低記憶體破碎的狀況，並且得到高效的物件配置程序，除了從高階層面利用資源管理 (Resource Management) 機制減少遊戲物件的生成與毀滅行為之外，同時也需要從比較低階的層面，也就是屬於記憶體配置的功能面向著手改善。在使用者每次進行 operator new 操作向作業系統索取記憶體空間時，C 語言的函式庫除了配置所要求的區塊大小以外，還會另外生成一小塊額外的區塊以簿記相關的資訊。對於經常進行生成與毀滅程序的小型物件來說，這樣的行為模式就顯得十分浪費而不具效率。如果能夠一次性的配置出一大塊記憶體，然後再依使用者的需求傳回部分區塊，可望就能夠改善 operator new 程序所產生的額外負擔。 為了能夠妥善管理一大塊的記憶體空間，程式設計者發展出了「Free List」這個用來處理動態配置記憶體的特殊資料結構。Free List 通常是以鏈結串列 (Linked List) 做為基底結構，將目前可使用以及使用中的記憶體空間紀錄並且連結起來。一般常聽到的「記憶體池」(Memory Pool) 或者「池式配置」(Pooled Allocation)，就是利用 Free List 資料結構實作出來的記憶體管理機制，也是遊戲程式設計者不可不知的記憶體系統管理技巧。而應該如何實作出高效能的 Free List 結構，使創建物件與刪除物件時的負擔減至最小，更是池式配置記憶體機制中最關鍵的要點。 前陣子除了在《Modern C++ Design》與《Game Programming Gems 7》兩本書裡，學習了兩種池式配置的設計實作方法之外，最近正好也剛踏入 Boost C++ Libraries 的國界中，發現其中有個專門做為池式配置功能用途的記憶體管理函式庫 Boost.Pool，不禁使我非常好奇 Boost.Pool 函式庫與其他池式配置技術的效能相比如何，於是決定以下列四個項目做為這次效能試驗的目標： operator new：原始的 C++ [...]]]></description>
			<content:encoded><![CDATA[<p>從剛進入遊戲業界開始，我就一直對於<strong>「記憶體配置」(Memory Allocation)</strong> 的系統架構與相關議題很感興趣。後來隨著閱讀書籍量的增加與工作經驗上的累積，逐漸接觸到各種不同設計架構與實作方法的記憶體管理機制，現在終於能夠將一些初步的心得整理出來了。</p>
<p><strong>在遊戲程式設計的領域中，程式設計者經常需要即時且動態地產生出大量的小型物件</strong>，例如怪物、特效、場景物乃至於低階的節點物件等等。如果遊戲程式設計者只是天真爛漫地使用著單純無害的 operator new 以及 operator delete 程序，在經過遊戲執行中不斷反覆地配置與歸還記憶體的行為之後，很快就會使得完整的記憶體區段面臨嚴重的<a href="http://en.wikipedia.org/wiki/Fragmentation_(computer)"><strong>「記憶體破碎」</strong></a><strong>(Memory Fragmentation)</strong> 問題。更糟的是，記憶體破碎的問題往往很難被偵測出來，而容易被開發者所忽略。</p>
<p>為了盡可能降低記憶體破碎的狀況，並且得到高效的物件配置程序，除了從高階層面利用資源管理 (Resource Management) 機制減少遊戲物件的生成與毀滅行為之外，同時也需要從比較低階的層面，也就是屬於記憶體配置的功能面向著手改善。在使用者每次進行 operator new 操作向作業系統索取記憶體空間時，C 語言的函式庫除了配置所要求的區塊大小以外，還會另外生成一小塊額外的區塊以簿記相關的資訊。對於經常進行生成與毀滅程序的小型物件來說，這樣的行為模式就顯得十分浪費而不具效率。如果能夠一次性的配置出一大塊記憶體，然後再依使用者的需求傳回部分區塊，可望就能夠改善 operator new 程序所產生的額外負擔。</p>
<p>為了能夠妥善管理一大塊的記憶體空間，程式設計者發展出了<a href="http://en.wikipedia.org/wiki/Free_list">「Free List」</a>這個用來處理動態配置記憶體的特殊資料結構。Free List 通常是以鏈結串列 (Linked List) 做為基底結構，將目前可使用以及使用中的記憶體空間紀錄並且連結起來。<strong>一般常聽到的「記憶體池」(Memory Pool) 或者「池式配置」(Pooled Allocation)，就是利用 Free List 資料結構實作出來的記憶體管理機制，也是遊戲程式設計者不可不知的記憶體系統管理技巧。</strong>而應該如何實作出高效能的 Free List 結構，使創建物件與刪除物件時的負擔減至最小，更是池式配置記憶體機制中最關鍵的要點。</p>
<p><span id="more-842"></span></p>
<p>前陣子除了在《Modern C++ Design》與《Game Programming Gems 7》兩本書裡，學習了兩種池式配置的設計實作方法之外，最近正好也剛踏入 Boost C++ Libraries 的國界中，發現其中有個專門做為池式配置功能用途的記憶體管理函式庫 <strong>Boost.Pool</strong>，不禁使我非常好奇 Boost.Pool 函式庫與其他池式配置技術的效能相比如何，於是決定以下列四個項目做為這次效能試驗的目標：</p>
<ul>
<li><strong>operator new</strong>：原始的 C++ 記憶體配置操作。</li>
<li><a href="http://loki-lib.sourceforge.net/"><strong>Loki Library SmallObj</strong></a>：在《Modern C++ Design》書中，作者以 Policy-Based Design 設計思維做為起始點，實作了一個專為小型物件設計的池式配置機制，並且包裝在開源專案 Loki 函式庫中。而在本文的測試專案中，將只使用其中的 SmallObj 類別做為測試用途；在 SmallObj 的類別架構中，完整實作了書中所涵蓋的「小型物件配置器」。另外需注意的是，SmallObj 的預設行為只會處理大小在 256 bytes 以下的物件，若使用者要求創建更大的物件，系統便會直接交由 operator new 配置記憶體。</li>
<li><strong>High Performance Heap Allocator</strong>：在《Game Programming Gems 7》裡，「High Performance Heap Allocator」（簡稱為 HPHA）一文的作者，對 Loki 的池式配置機制提出改善方法，將使用者對於記憶體的要求，分成了兩種不同的運作策略：在 256 bytes 以下的小型物件，使用 Win32 平台的 VirtualAlloc() 函式，配置出自然對齊的記憶體區塊，使物件的歸還程序能夠達到很好的執行效能；而對於超過 256 bytes 的大型物件，則使用「紅黑樹」(Red-Black Tree) 結構實作出特製化的管理機制。</li>
<li><a href="http://www.boost.org/doc/libs/1_37_0/libs/pool/doc/index.html"><strong>Boost.Pool Library</strong></a>：從屬於 Boost C++ Libraries 內的函式庫之一，專門做為記憶體管理與池式配置的功能用途。</li>
</ul>
<p>在使用方法上，Loki、HPHA 與 Boost.Pool 三者各不相同。若使用 Loki SmallObj 的技術，必須先讓使用者的類別繼承自 Loki::SmallObject 類別，而在 Loki::SmallObject 中已經覆寫了類別的 operator new，所以接下來就能夠直接使用 new 與 delete 進行配置與刪除動作；HPHA 則定義了二個全域函式 custom_new< T >() 與 custom_delete() 供使用者配置及刪除物件；最後，在 Boost.Pool 裡需要先建立出管理記憶體用的「物件池」boost::object_pool< T >，然後再以物件池的成員函式 construct() 與 destroy() 創建並且毀滅物件。程式碼如下所示：</p>
<pre name="code" class="cpp">
class TestObject
{
    char data[8];
};

class TestLokiObject : public Loki::SmallObject<>
{
    char data[8];
};

// Loki SmallObj
TestLokiObject* obj = new TestLokiObject;
delete obj;

// HPHA
TestObject* obj = custom_new< TestObject >();
custom_delete(obj);

// Boost Pool
boost::object_pool< TestObject > objPool;
TestObject* obj = objPool.construct();
objPool.destroy(obj);
</pre>
<p>首先，在第一個測試套件中，採用的是「交錯配置」的方式，也就是在創建物件之後，立即將物件刪除：</p>
<pre name="code" class="cpp">
Timer timer;

timer.Start();
for (unsigned int i = 0; i < testNum; i ++)
{
    // 創建物件
	TestObject* obj1 = new TestObject(i);
	// 立即刪除物件
	delete obj1;
	TestObject* obj2 = new TestObject(i);
	delete obj2;
	TestObject* obj3 = new TestObject(i);
	delete obj3;
	TestObject* obj4 = new TestObject(i);
	delete obj4;
	TestObject* obj5 = new TestObject(i);
	delete obj5;
}
timer.Stop();
</pre>
<p>其中的 testNum 變數，為使用者可指定的總測試次數，在此我輸入的測試次數為 500000 次。另外，為了量測物件的大小是否會對各項試驗目標產生影響，在測試專案中會對三個不同大小的物件類別，分別為 8 bytes、64 bytes 以及 280 bytes，進行相同的測試程序。結果如下圖所示：</p>
<div id="attachment_948" class="wp-caption aligncenter" style="width: 650px"><img src="http://blog.monkeypotion.net/wp-content/uploads/2008/12/test-suite-1.jpg" alt="Test Suite 1" title="test-suite-1" class="size-full wp-image-948" /><p class="wp-caption-text">Test Suite 1</p></div>
<p>由測試結果可以清楚地瞭解，<strong>無論物件大小為何，Boost Pool 的表現都獲得了壓倒性的勝利</strong>，特別是當物件大小為 8 bytes 與 64 bytes 時，更是呈現出超過其他試驗目標 10 倍以上的效能！此外，在 HPHA 與 Loki SmallObj 的比較上，在小型的 8 bytes 與 64 bytes 物件上，Loki 的表現仍然勝過 HPHA；而在 280 bytes 的物件上，HPHA 則毫無意外地超越了 Loki 的效能。</p>
<p>接著在第二項測試套件中，則採用「整體配置」的方式，先一次創建出所有的物件後，然後再將這些物件一一刪除：</p>
<pre name="code" class="cpp">
Timer timerNew;
Timer timerDelete;

std::vector< TestObject* > pool;
pool.reserve(testNum);

// 創建所有的物件
timerNew.Start();
for (unsigned int i = 0; i < testNum; i ++)
{
	TestObject* obj = new TestObject(i);
	pool.push_back(obj);
}
timerNew.Stop();

// 一一刪除所有的物件
timerDelete.Start();
for (unsigned int i = 0; i < testNum; i ++)
{
	delete (pool[i]);
}
timerDelete.Stop();
</pre>
<p>測試次數為 50000 次，量測的時間數據則分成「Allocation」與「Deallocation」兩個部分，結果如下圖所示：</p>
<div id="attachment_949" class="wp-caption aligncenter" style="width: 650px"><img src="http://blog.monkeypotion.net/wp-content/uploads/2008/12/test-suite-2.jpg" alt="Test Suite 2" title="test-suite-2" class="size-full wp-image-949" /><p class="wp-caption-text">Test Suite 2</p></div>
<p>喔買尬，<strong>Boost Pool 的表現真是出人意表的慘烈！</strong>對各個物件一一進行刪除程序時，Boost Pool 竟然得出如此令人難以接受的結果，難道之前的「整體配置」測試成果不過是假象而已？為了找出挽救 Boost Pool 的可能性，繼續修改第二項測試，增加一項「Boost Pool Ptr」測試程序，在刪除程序時，不對物件一一進行刪除動作，而是直接刪除掉整個 Boost Pool 的物件池：</p>
<pre name="code" class="cpp">
boost::object_pool< TestObject >* objPoolPtr = new boost::object_pool< TestObject >();

// 創建物件
timerNew.Start();
for (unsigned int i = 0; i < testNum; i ++)
{
	TestObject* obj = objPoolPtr->construct(i);
	pool.push_back(obj);
}
timerNew.Stop();

// 刪除 Boost Pool 物件池
timerDelete.Start();
delete objPoolPtr;
timerDelete.Stop();
</pre>
<p>測試結果如下圖所示：</p>
<div id="attachment_950" class="wp-caption aligncenter" style="width: 650px"><img src="http://blog.monkeypotion.net/wp-content/uploads/2008/12/test-suite-2-mod.jpg" alt="Test Suite 2 Mod" title="test-suite-2-mod" class="size-full wp-image-950" /><p class="wp-caption-text">Test Suite 2 Mod</p></div>
<p>由新增的 Boost Pool Ptr 試驗結果可知，如果直接對 Boost Pool 物件池進行刪除（池中所有物件都會被刪除），將能夠獲得非常出色的效能表現；但是如果對單一物件進行物件毀滅與記憶體歸還動作時，就只能夠得出非常緩慢而糟糕的效能表現。</p>
<p>量測出三者的執行時期效能之後，在其他面向的考量上，Loki Library 與 Boost Pool Library 都擁有能夠跨平台的優勢，而 HPHA 由於使用了 Win32 平台的特殊記憶體配置函式，所以沒辦法直接移植到其他平台上使用。評估跨平台、多執行緒支援、易用性與平均效能表現後，總的來說，我對於這三項池式配置技術的評比如下：</p>
<blockquote>
<ul>
<li>HPHA：★★★</li>
<li>Loki SmallObj：★★★</li>
<li>Boost Pool：★★★★</li>
</ul>
</blockquote>
<p>最後我所得出的結論是，<strong>只要程式設計者能夠確保在遊戲系統中，不會連續性地刪除單一的遊戲物件，就可以放心地使用 Boost Pool 進行記憶體池式配置與管理</strong>，保證能夠得到非常令人滿意的效能表現，遠遠勝過於使用傳統的 operator new 操作，以及 Loki Library 與 HPHA 的池式配置技術。</p>
<p>如果讀者有興趣深入瞭解 Boost.Pool 的設計架構與實作細節，請參考這篇由侯捷先生所寫的<a href="http://www.runpc.com.tw/pdf/160/R160E12.pdf"><strong>「Boost技術與應用系列文章（2）Boost.Pool」</strong></a>（PDF 檔）。我在網路上搜尋 Boost 函式庫的相關資訊時，恰巧挖到了這篇絕佳的文章，才知道侯捷在《Run! PC》雜誌上刊載過許多與 Boost C++ Libraries 主題相關的深入技術文章；可惜的是，目前我只有搜尋到唯一這篇文章可供下載。</p>
<p>對於任何程式系統來說，不論是再精巧神妙的設計或實作方式，都會具有獨特的優勢以及劣勢。有的解決方案執行時期效能很高，代價卻是多出一倍的記憶體使用量；有的解決方案記憶體用量極少，卻會佔去大量的 CPU 資源；有的解決方案兼具速度與空間的優勢，卻無法滿足跨平台的需求。<strong>這是程式系統的設計者與使用者都必然會遭遇到的「取捨」(Tradeoff) 難題，也正是程式設計領域裡最令人目眩神迷的樂趣之一。</strong></p>
<p>執行檔及原始碼下載：<a href="http://blog.monkeypotion.net/download-manager.php?id=19"><strong>PooledAllocation_TestSuite.7z</strong></a> <small>(下載次數： 376 )</small><br />
（附註說明：如果要自行建置測試專案，專案裡已經包含了 HPHA 的原始碼以及編譯完成的 Loki Library 函式庫，另外還需要自行下載 Boost C++ Libraries，並且設定正確的表頭檔路徑才能夠順利建置專案。）</p>
<img src="http://blog.monkeypotion.net/?ak_action=api_record_view&id=842&type=feed" alt="" />]]></content:encoded>
			<wfw:commentRss>http://blog.monkeypotion.net/gameprog/advanced/profiling-in-pooled-allocation-techniques/feed</wfw:commentRss>
		<slash:comments>22</slash:comments>
		</item>
		<item>
		<title>Database Hot Loader 最終曲：Tuning and Measuring Performance</title>
		<link>http://blog.monkeypotion.net/gameprog/advanced/database-hot-loader-tuning-and-measuring-performance</link>
		<comments>http://blog.monkeypotion.net/gameprog/advanced/database-hot-loader-tuning-and-measuring-performance#comments</comments>
		<pubDate>Thu, 06 Nov 2008 16:12:38 +0000</pubDate>
		<dc:creator>半路</dc:creator>
				<category><![CDATA[進階技術]]></category>

		<guid isPermaLink="false">http://blog.monkeypotion.net/?p=582</guid>
		<description><![CDATA[歷經了漫長的 DHL 系統一二三部曲後，終於來到了這一系列文章的最終曲。 本篇的重點在於對 DHL 系統進行效能的優化以及量測。首先，對於 LuaDatabaseManager 類別提出幾項效能優化的可能性。以資料庫系統的作用來說，一般使用者會期望在索取資料的程序中，得到最佳化的執行效能，才不會因為對於資料庫的頻繁讀取而拖累了遊戲系統的整體效能表現。在二部曲中曾經提過，為了取得資料庫中的數值，使用者需要利用 LuaDatabaseManager 類別中的 GetData() 函式： void LuaDatabaseManager::GetData(std::string&#038; sheetName, std::string&#038; fieldName, unsigned int id, std::string&#038; value) { int oldTop = lua_gettop(m_DBState); lua_getglobal(m_DBState, sheetName.c_str()); lua_pushstring(m_DBState, fieldName.c_str()); lua_gettable(m_DBState, LUA_GETTABLE_INDEX); lua_pushnumber(m_DBState, id); lua_gettable(m_DBState, LUA_GETTABLE_INDEX); value = luaL_checkstring(m_DBState, lua_gettop(m_DBState)); lua_settop(m_DBState, oldTop); } 在 GetData() 函式中，必須進行三次取出 table 結構的步驟；首先以 lua_getglobal() 函式取得最上層的 table，然後傳入欄位名稱以 lua_gettable() 函式取得第二層 table，最後再傳入資料 [...]]]></description>
			<content:encoded><![CDATA[<p>歷經了漫長的 DHL 系統一二三部曲後，終於來到了這一系列文章的最終曲。</p>
<p>本篇的重點在於對 DHL 系統進行效能的優化以及量測。首先，對於 LuaDatabaseManager 類別提出幾項效能優化的可能性。以資料庫系統的作用來說，一般使用者會期望<strong>在索取資料的程序中，得到最佳化的執行效能</strong>，才不會因為對於資料庫的頻繁讀取而拖累了遊戲系統的整體效能表現。在二部曲中曾經提過，為了取得資料庫中的數值，使用者需要利用 LuaDatabaseManager 類別中的 GetData() 函式：</p>
<pre name="code" class="cpp">
void LuaDatabaseManager::GetData(std::string&#038; sheetName, std::string&#038; fieldName, unsigned int id, std::string&#038; value)
{
	int oldTop = lua_gettop(m_DBState);

	lua_getglobal(m_DBState, sheetName.c_str());
	lua_pushstring(m_DBState, fieldName.c_str());
	lua_gettable(m_DBState, LUA_GETTABLE_INDEX);
	lua_pushnumber(m_DBState, id);
	lua_gettable(m_DBState, LUA_GETTABLE_INDEX);
	value = luaL_checkstring(m_DBState, lua_gettop(m_DBState));

	lua_settop(m_DBState, oldTop);
}
</pre>
<p>在 GetData() 函式中，必須進行三次取出 table 結構的步驟；首先以 lua_getglobal() 函式取得最上層的 table，然後傳入欄位名稱以 lua_gettable() 函式取得第二層 table，最後再傳入資料 ID 以 lua_gettable() 函式以取得使用者要求的資料值。</p>
<p>熟悉 Lua 語言的讀者應該很清楚，除了使用 lua_gettable() 函式取得 table 結構中的數值以外，還有另外一項替代方案：<strong>使用 lua_rawget() 函式。</strong>預設情形下，在 lua_gettable() 函式中，會喚起 table 的 metamethod，而對於將 table 單純當成陣列結構處理的資料表格來說，其實並不需要這層額外的彈性，所以如果使用 lua_rawget() 函式取代原來的 lua_gettable() 函式，理論上應該能夠獲得某種程度的效能提升。</p>
<p><span id="more-582"></span></p>
<p>所以在 LuaDatabaseManager 類別中，以此實作出一個使用 lua_rawget() 版本的 GetDataRawed() 函式：</p>
<pre name="code" class="cpp">
void LuaDatabaseManager::GetDataRawed(std::string&#038; sheetName, std::string&#038; fieldName, unsigned int id, std::string&#038; value)
{
	int oldTop = lua_gettop(m_DBState);

	lua_getglobal(m_DBState, sheetName.c_str());
	lua_pushstring(m_DBState, fieldName.c_str());
	lua_rawget(m_DBState, LUA_GETTABLE_INDEX);
	lua_pushnumber(m_DBState, id);
	lua_rawget(m_DBState, LUA_GETTABLE_INDEX);
	value = luaL_checkstring(m_DBState, lua_gettop(m_DBState));

	lua_settop(m_DBState, oldTop);
}
</pre>
<p>第二種優化的可能性，考量點在於每次向 Lua 端索取資料值時，都必須將表格名稱與欄位名稱傳遞過去，如果能夠省略部分傳入字串的動作，或許有機會增進索取資料的效能。為了達到這個目標，LuaDatabaseManager 類別裡需要新增兩個成員變數，用來記錄前一次使用的表格名稱與欄位名稱；在向 Lua 端取得資料前，先以字串比較的方法檢查名稱是否相同，只要表格名稱或欄位名稱與之前相同，就能夠省略掉傳遞字串值與取得 table 結構的步驟：</p>
<pre name="code" class="cpp">
void LuaDatabaseManager::GetDataCached(std::string&#038; sheetName, std::string&#038; fieldName, unsigned int id, std::string&#038; value)
{
	if (sheetName != m_CurrentSheetName)
	{
		m_CurrentSheetName = sheetName;
		m_CurrentFieldName = fieldName;

		if (lua_gettop(m_DBState) >= 2)
		{
			lua_pop(m_DBState, 2);
		}

		lua_getglobal(m_DBState, sheetName.c_str());
		lua_pushstring(m_DBState, fieldName.c_str());
		lua_gettable(m_DBState, LUA_GETTABLE_INDEX);
	}
	else if (fieldName != m_CurrentFieldName)
	{
		m_CurrentFieldName = fieldName;

		if (lua_gettop(m_DBState) >= 1)
		{
			lua_pop(m_DBState, 1);
		}

		lua_pushstring(m_DBState, fieldName.c_str());
		lua_gettable(m_DBState, LUA_GETTABLE_INDEX);
	}

	lua_pushnumber(m_DBState, id);
	lua_gettable(m_DBState, LUA_GETTABLE_INDEX);
	value = luaL_checkstring(m_DBState, lua_gettop(m_DBState));

	lua_pop(m_DBState, 1);
}
</pre>
<p>如上述程式碼所示，在 GetDataCached() 函式中，<strong>以 std::string 字串比較的代價，換取使用 lua_pushstring() 與 lua_gettable() 函式的成本</strong>。接著再使用前述的第一項優化技巧，以 lua_rawget() 取代 lua_gettable() 函式，製作出另外一個 GetDataRawedCached() 版本的函式，以進一步獲得效能增進的可能性。</p>
<p>以上是對 LuaDatabaseManager 類別取得資料程序進行效能改善的優化程序。而為了與一般傳統做法的資料庫系統進行效能測試評比，在此另外創建了一個 MapDatabaseManager 類別，用來<strong>模擬以 std::map 結構為基礎的 Database 實作方式</strong>。在這個類別中，主要是以資料表格、資料欄位以及資料值三個階層，建立出三層深度的 std::map 結構。其中實作的 GetData() 函式如下所示：</p>
<pre name="code" class="cpp">
void MapDatabaseManager::GetData(std::string&#038; sheetName, std::string&#038; fieldName, DataId id, DataValue&#038; value)
{
	DataSheetListItor dsItor = m_DatabaseList->find(sheetName);
	if (dsItor == m_DatabaseList->end())
	{
		return;
	}

	DataFieldList* dataSheet = dsItor->second;
	DataFieldListItor dfItor = dataSheet->find(fieldName);
	if (dfItor == dataSheet->end())
	{
		return;
	}

	DataIdList* dataId = dfItor->second;
	DataIdListItor idItor = dataId->find(id);
	if (idItor == dataId->end())
	{
		return;
	}

	value = idItor->second;
}
</pre>
<p>總結上述所有的效能優化方法與模擬方法，共計有以下五項測試程序：</p>
<ul>
<li>MapDatabaseManager::GetData()</li>
<li>LuaDatabaseManager::GetData()</li>
<li>LuaDatabaseManager::GetDataRawed()</li>
<li>LuaDatabaseManager::GetDataCached()</li>
<li>LuaDatabaseManager::GetDataRawedCached()</li>
</ul>
<p>在測試程序所使用的資料中，共有 5 張資料表格，每張表格裡有 10 項資料欄位，每項欄位中有 30 筆資料。進行測試時，每一次都會以亂數值決定所取的資料表、資料欄位以及資料 ID，接著再以 Performance Counter 實作的 Timer 類別，計算出每一項測試程序所花費的時間。以 MapDatabaseManager::GetData() 測試程序為例：</p>
<pre name="code" class="cpp">
Timer timer;
timer.Start();

for (unsigned int i = 0; i < testNum; i ++)
{
	GetRandomQuery(sheetName, fieldName, id);
	mapDBMgr.GetData(sheetName, fieldName, id, value);
}

timer.Stop();
</pre>
<p>其中，testNum 變數為使用者可自行輸入的測試次數。以此程序執行後的測試結果如下所示：</p>
<blockquote>
<pre>
<u><strong>測試次數：100</strong></u>

[MapDatabaseManager]
   GetData(): 0.120127 ms

[LuaDatabaseManager]
   GetData(): 0.102527 ms
   GetDataRawed(): 0.0896762 ms
   GetDataCached(): 0.261765 ms
   GetDataRawedCached(): 0.105879 ms
</pre>
</blockquote>
<blockquote>
<pre>
<u><strong>測試次數：5000</strong></u>

[MapDatabaseManager]
   GetData(): 5.40963 ms

[LuaDatabaseManager]
   GetData(): 5.00818 ms
   GetDataRawed(): 4.44218 ms
   GetDataCached(): 5.72419 ms
   GetDataRawedCached(): 5.23363 ms
</pre>
</blockquote>
<blockquote>
<pre>
<u><strong>測試次數：250000</strong></u>

[MapDatabaseManager]
   GetData(): 286 ms

[LuaDatabaseManager]
   GetData(): 251 ms
   GetDataRawed(): 219 ms
   GetDataCached(): 287 ms
   GetDataRawedCached(): 277 ms
</pre>
</blockquote>
<p>由測試的結果可知，LuaDatabaseManager::GetDataRawed() 是無庸置疑的第一名！而使用字串比較的方法，效能並沒有如預期般的效果。更重要的是，不論是 LuaDatabaseManager 的哪一個實作版本，幾乎都比使用三層 std::map 結構的 MapDatabaseManager::GetData() 效能更好更快！</p>
<p>最後，這五項效能測試程序的排名為：</p>
<blockquote><p>
<strong>第一名：LuaDatabaseManager::GetDataRawed()</strong><br />
第二名：LuaDatabaseManager::GetData()<br />
第三名：LuaDatabaseManager::GetDataRawedCached()<br />
第四名：LuaDatabaseManager::GetDataCached()<br />
備取：MapDatabaseManager::GetData()
</p></blockquote>
<p>在我的工作經驗裡，常遇到許多程式設計者習慣以「直覺」、「感覺」，或是個人的喜好來決定效能優化所使用的方法。然而，通常以「我覺得」或者「我不這麼想」這種方式進行優化程序的取捨，往往容易導致偏離甚至是完全相反的優化成果。在程式系統的優化層面上，除了應該奉行<strong>「不要過早進行優化」</strong>這條金科玉律以外，正確的優化技術，<strong>更應當立基於實際的量測程序與實驗數據上，才能夠以客觀的事實選擇出最合適的解決方案。</strong></p>
<p>從 DHL 系統的需求緣由、設計實作、應用實例到效能量測一路走來，可見得 <strong>Database Hot Loader</strong> 系統的確有搞頭，<strong>不僅能夠減輕修改遊戲數據的繁瑣步驟，加速遊戲企畫者的測試流程，更能夠為資料庫系統提供效能提升的附加效益！</strong>在讀完這一系列文章後，你是不是想大聲喊出<a href="http://www.sonance.com.tw/lulala/Obj_Nar.swf?inputStr=Database+Hot+Loader+%8Cn%93%9D%97L%96%E2%91%E8%21%21"><strong>「異議あり！」</strong></a>？歡迎提出你的看法、意見，或是更好的測試程序與效能改善方法吧！</p>
<p>執行檔下載：<a href="http://blog.monkeypotion.net/download-manager.php?id=17"><strong>DatabaseHotLoader_Ep4_Release.zip</strong></a>（需要先行安裝 <a href="http://www.microsoft.com/downloads/details.aspx?displaylang=zh-tw&#038;FamilyID=32bc1bee-a3f9-4c13-9c99-220b62a191ee">vcredist_x86.exe</a>） <small>(下載次數： 230 )</small><br />
原始碼下載：<a href="http://blog.monkeypotion.net/download-manager.php?id=18"><strong>DatabaseHotLoader_Ep4_Source.zip</strong></a> <small>(下載次數： 228 )</small></p>
<img src="http://blog.monkeypotion.net/?ak_action=api_record_view&id=582&type=feed" alt="" />]]></content:encoded>
			<wfw:commentRss>http://blog.monkeypotion.net/gameprog/advanced/database-hot-loader-tuning-and-measuring-performance/feed</wfw:commentRss>
		<slash:comments>8</slash:comments>
		</item>
		<item>
		<title>Database Hot Loader 三部曲：Implementing Observer Entity</title>
		<link>http://blog.monkeypotion.net/gameprog/advanced/database-hot-loader-implementing-observer-entity</link>
		<comments>http://blog.monkeypotion.net/gameprog/advanced/database-hot-loader-implementing-observer-entity#comments</comments>
		<pubDate>Sun, 26 Oct 2008 16:13:19 +0000</pubDate>
		<dc:creator>半路</dc:creator>
				<category><![CDATA[進階技術]]></category>

		<guid isPermaLink="false">http://blog.monkeypotion.net/?p=580</guid>
		<description><![CDATA[在前篇文章裡，以 VSTO 與 Lua 實作了 DHL 系統中資料匯出、讀取以及管理的核心功能後，本文將以一個簡單的 3D 程式範例，介紹如何將 DHL 系統與遊戲物件相互結合，以實現真正能夠使用於遊戲專案中的預定目標。 藉由之前建立完成的 LuaDatabaseManager 類別，已經能夠幫助我們動態修改並且載入 Lua 格式的資料庫表格，但是該如何使遊戲中的物件對資料庫的變化產生自動化反應呢？只要利用設計模式中的 Observer 模式，就能夠使我們順利達成目標。 如圖所示，在範例程式中，以 OpenGL 繪製出一個基本的三角形物件以及一個矩形物件，同時在視窗裡不停地旋轉；而視窗程式的建立，則使用 GLUT 函式庫以簡化相關的程序。 首先，在程式碼中創建出 Triangle 與 Quad 類別，分別用來代表畫面中的三角形與矩形物件。而這兩個物件的頂點、顏色、位移、旋轉軸與旋轉速度數值，則由 Excel 工作表中的資料數據進行設定。在範例程式的 Excel 檔案中，共有以下三張工作表： triangle quad speed 在 triangle 表格裡的 vertex1、vertex2、vertex3、color1、color2 與 color3 欄位，分別定義了三角形物件的三個頂點及顏色的資料，而最末的 translate 與 rotate 欄位則用來定義三角形物件的位移量與旋轉軸；quad 表格以此類推。在 speed 表格中，只有 triangle 與 quad 兩個欄位，分別定義了這兩個物件的旋轉速度。 建立了上述遊戲資料與遊戲物件之間的關連性之後，可以很清楚地瞭解，對於 Triangle [...]]]></description>
			<content:encoded><![CDATA[<div id="attachment_812" class="wp-caption alignleft" style="width: 366px"><img src="http://blog.monkeypotion.net/wp-content/uploads/2008/10/observer-entity.jpg" alt="Observer Entity" title="observer-entity" width="356" height="374" class="size-full wp-image-812" /><p class="wp-caption-text">Observer Entity</p></div>
<p>在前篇文章裡，以 VSTO 與 Lua 實作了 DHL 系統中資料匯出、讀取以及管理的核心功能後，本文將以一個簡單的 3D 程式範例，介紹如何將 DHL 系統與遊戲物件相互結合，以實現真正能夠使用於遊戲專案中的預定目標。</p>
<p>藉由之前建立完成的 LuaDatabaseManager 類別，已經能夠幫助我們動態修改並且載入 Lua 格式的資料庫表格，但是<strong>該如何使遊戲中的物件對資料庫的變化產生自動化反應呢？</strong>只要利用設計模式中的 Observer 模式，就能夠使我們順利達成目標。</p>
<p>如圖所示，在範例程式中，以 OpenGL 繪製出一個基本的三角形物件以及一個矩形物件，同時在視窗裡不停地旋轉；而視窗程式的建立，則使用 GLUT 函式庫以簡化相關的程序。</p>
<p>首先，在程式碼中創建出 Triangle 與 Quad 類別，分別用來代表畫面中的三角形與矩形物件。而這兩個物件的頂點、顏色、位移、旋轉軸與旋轉速度數值，則由 Excel 工作表中的資料數據進行設定。在範例程式的 Excel 檔案中，共有以下三張工作表：</p>
<ul>
<li>triangle</li>
<li>quad</li>
<li>speed</li>
</ul>
<p>在 triangle 表格裡的 vertex1、vertex2、vertex3、color1、color2 與 color3 欄位，分別定義了三角形物件的三個頂點及顏色的資料，而最末的 translate 與 rotate 欄位則用來定義三角形物件的位移量與旋轉軸；quad 表格以此類推。在 speed 表格中，只有 triangle 與 quad 兩個欄位，分別定義了這兩個物件的旋轉速度。</p>
<p><span id="more-580"></span></p>
<p>建立了上述遊戲資料與遊戲物件之間的關連性之後，可以很清楚地瞭解，對於 Triangle 類別來說，會和 triangle 與 speed 這兩張資料表具有依存關係；而對於 Quad 類別來說，則是和 quad 與 speed 這兩張資料表具有依存關係。</p>
<p><strong>在 Observer 設計模式中，將會以 Triangle 類別與 Quad 類別負責扮演 Observer 的角色，而每張資料表則扮演 Subject 的角色。</strong>為了達成 Observer 設計模式的架構，首先宣告一個 ObserverEntity 純虛擬介面類別：</p>
<pre name="code" class="cpp">
// @file ObserverEntity.h

class ObserverEntity
{
public:
    virtual void Notify(std::string&#038; aspect) = 0;
};
</pre>
<p>接著處理 Subject 所需的訂閱功能，在原先用來處理資料的 LuadDatabaseManager 類別中，加入幾項新的成員函式與成員變數：</p>
<pre name="code" class="cpp">
// @file LuaDatabaseManager.h

class LuaDatabaseManager
{
public:
	void Subscribe(ObserverEntity* observer, std::string&#038; sheetName);

private:
	void NotifySubscribers(std::string&#038; sheetName);

private:
	typedef std::vector< ObserverEntity* > ObserverEntityList;
	typedef std::map< std::string, ObserverEntityList > SubscribersList;

	SubscribersList m_SubscribersList;
};
</pre>
<p>其中，由 Subscribe() 函式提供公用方法給外部的 Observer，使它們能夠自由訂閱自己感興趣的主題，也就是個別的資料表格。而 NotifySubscribers() 函式則是在 Subject 產生變化時，負責通知相對應的 Observer 們：</p>
<pre name="code" class="cpp">
// @file LuaDatabaseManager.cpp

void LuaDatabaseManager::Subscribe(ObserverEntity* observer, std::string&#038; sheetName)
{
	SubscribersListItor itor = m_SubscribersList.find(sheetName);
	if (itor != m_SubscribersList.end())
	{
		itor->second.push_back(observer);
	}
}

void LuaDatabaseManager::NotifySubscribers(std::string&#038; sheetName)
{
	SubscribersListItor itor = m_SubscribersList.find(sheetName);
	if (itor == m_SubscribersList.end())
	{
		return;
	}

	ObserverEntityList list = itor->second;
	for (unsigned int i = 0; i < list.size(); i ++)
	{
		list[i]->Notify(sheetName);
	}
}
</pre>
<p>回過頭來，在 Observer 這一方，需要使 Triangle 與 Quad 類別繼承自 ObserverEntity 介面類別以符合 Observer 的身份，然後再接著覆寫 Notify() 純虛擬函式：</p>
<pre name="code" class="cpp">
// @file Triangle.h

class Triangle : public ObserverEntity
{
public:
	virtual void Notify(std::string&#038; subject);
};
</pre>
<p>為了建立起 Observer 與 Subject 之間的聯繫關係，Triangle 與 Quad 類別需要先在建構式裡，以資料表的名稱向 LuaDatabaseManager 類別訂閱 Subject。當全部或者部分資料表格發生變動時，例如在重新載入 Lua 檔案之後，就能夠藉由呼叫 Notify() 函式的方式通知所有訂閱者，然後<strong>交由各個 Observer 自行操作相關的資料處理程序</strong>。以 Triangle 類別為例：</p>
<pre name="code" class="cpp">
// @file Triangle.cpp

Triangle::Triangle()
{
	g_LuaDataBaseMgr->Subscribe(this, std::string("triangle"));
	g_LuaDataBaseMgr->Subscribe(this, std::string("speed"));
}

void Triangle::Notify(std::string&#038; subject)
{
	if (subject == "triangle")
	{
		g_LuaDataBaseMgr->GetData(TRIANGLE, VERTEX_1, 1, m_Vertex1X);
		g_LuaDataBaseMgr->GetData(TRIANGLE, VERTEX_1, 2, m_Vertex1Y);
		g_LuaDataBaseMgr->GetData(TRIANGLE, VERTEX_1, 3, m_Vertex1Z);

		g_LuaDataBaseMgr->GetData(TRIANGLE, VERTEX_2, 1, m_Vertex2X);
		g_LuaDataBaseMgr->GetData(TRIANGLE, VERTEX_2, 2, m_Vertex2Y);
		g_LuaDataBaseMgr->GetData(TRIANGLE, VERTEX_2, 3, m_Vertex2Z);

		g_LuaDataBaseMgr->GetData(TRIANGLE, VERTEX_3, 1, m_Vertex3X);
		g_LuaDataBaseMgr->GetData(TRIANGLE, VERTEX_3, 2, m_Vertex3Y);
		g_LuaDataBaseMgr->GetData(TRIANGLE, VERTEX_3, 3, m_Vertex3Z);

		g_LuaDataBaseMgr->GetData(TRIANGLE, COLOR_1, 1, m_Color1R);
		g_LuaDataBaseMgr->GetData(TRIANGLE, COLOR_1, 2, m_Color1G);
		g_LuaDataBaseMgr->GetData(TRIANGLE, COLOR_1, 3, m_Color1B);

		g_LuaDataBaseMgr->GetData(TRIANGLE, COLOR_2, 1, m_Color2R);
		g_LuaDataBaseMgr->GetData(TRIANGLE, COLOR_2, 2, m_Color2G);
		g_LuaDataBaseMgr->GetData(TRIANGLE, COLOR_2, 3, m_Color2B);

		g_LuaDataBaseMgr->GetData(TRIANGLE, COLOR_3, 1, m_Color3R);
		g_LuaDataBaseMgr->GetData(TRIANGLE, COLOR_3, 2, m_Color3G);
		g_LuaDataBaseMgr->GetData(TRIANGLE, COLOR_3, 3, m_Color3B);

		g_LuaDataBaseMgr->GetData(TRIANGLE, TRANSLATE, 1, m_TranslateX);
		g_LuaDataBaseMgr->GetData(TRIANGLE, TRANSLATE, 2, m_TranslateY);
		g_LuaDataBaseMgr->GetData(TRIANGLE, TRANSLATE, 3, m_TranslateZ);

		g_LuaDataBaseMgr->GetData(TRIANGLE, ROTATE, 1, m_RotateX);
		g_LuaDataBaseMgr->GetData(TRIANGLE, ROTATE, 2, m_RotateY);
		g_LuaDataBaseMgr->GetData(TRIANGLE, ROTATE, 3, m_RotateZ);
	}
	else if (subject == "speed")
	{
		g_LuaDataBaseMgr->GetData(SPEED, TRIANGLE, 1, m_RotateSpeed);
	}
}
</pre>
<p>最後，只需要在 LuaDatabaseManager 類別載入資料庫檔案後，使用 NotifySubscribers() 通知訂閱者即可：</p>
<pre name="code" class="cpp">
// @file LuaDatabaseManager.cpp

bool LuaDatabaseManager::RunScript(std::string&#038; scriptName)
{
	std::string fileName = scriptName + LUA_EXT;
	int result = luaL_dofile(m_DBState, fileName.c_str());

	if (result != LUA_CALL_SUCCESS)
	{
		return false;
	}

	NotifySubscribers(scriptName);

	return true;
}
</pre>
<p>要做到動態熱載入資料的功能，其中很關鍵的一點，就是<strong>遊戲物件的「初始化程序」與「重新載入程序」最好能夠達到完全相同的程度</strong>，否則會使得遊戲程式裡充滿許多重複的程式碼，因而造成不必要的系統複雜度。在 DHL 系統裡，借助了 Observer 設計模式的威力，如前述程式碼所示，不論是物件初始化或者是重新載入遊戲資料，都是使用相同的程序；這樣一來，即使在遊戲資料編輯完成後，也不需要大費周章地將程式碼替換成另外一套資料載入程序。</p>
<p>只要利用以上 Observer 設計模式的概念與技巧，套用到遊戲系統的玩家角色、裝備道具、魔法技能或者場景物件之中，就能夠順利地將遊戲資料與遊戲內容天衣無縫地接合起來，達成原先設計以及實作 DHL 系統的預設目標：<strong>在不需要重新啟動遊戲程式的情況下，讓企畫設計者能夠即時調整遊戲的資料庫數據！</strong></p>
<p>在次篇最終回的 Database Hot Loader 系列裡，將演出感人肺腑的精彩團圓大結局，探索幾項效能優化的可能性，並且實際進行效能的量測，與一般常見的資料庫系統實作方式進行評比。</p>
<p>執行檔下載：<a href="http://blog.monkeypotion.net/download-manager.php?id=15"><strong>DatabaseHotLoader_Ep3_Release.zip</strong></a>（需要先行安裝 <a href="http://www.microsoft.com/downloads/details.aspx?displaylang=zh-tw&#038;FamilyID=32bc1bee-a3f9-4c13-9c99-220b62a191ee">vcredist_x86.exe</a>） <small>(下載次數： 645 )</small><br />
原始碼下載：<a href="http://blog.monkeypotion.net/download-manager.php?id=16"><strong>DatabaseHotLoader_Ep3_Source.zip</strong></a> <small>(下載次數： 661 )</small></p>
<img src="http://blog.monkeypotion.net/?ak_action=api_record_view&id=580&type=feed" alt="" />]]></content:encoded>
			<wfw:commentRss>http://blog.monkeypotion.net/gameprog/advanced/database-hot-loader-implementing-observer-entity/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Database Hot Loader 二部曲：Using VSTO and Lua</title>
		<link>http://blog.monkeypotion.net/gameprog/advanced/database-hot-loader-using-vsto-and-lua</link>
		<comments>http://blog.monkeypotion.net/gameprog/advanced/database-hot-loader-using-vsto-and-lua#comments</comments>
		<pubDate>Tue, 14 Oct 2008 16:22:11 +0000</pubDate>
		<dc:creator>半路</dc:creator>
				<category><![CDATA[進階技術]]></category>

		<guid isPermaLink="false">http://blog.monkeypotion.net/?p=577</guid>
		<description><![CDATA[接續前篇文章的簡短介紹，本文將開始使用 VSTO 與 Lua 語言實作 DHL 系統。首先，定義 DHL 系統的開發環境以及執行環境： Database Hot Loader 系統 程式開發環境：Visual Studio 2005 Team Suite 使用者執行環境：Office 2003 為了使 Office 2003 達成轉換資料格式的任務，必須仰賴 VSTO 所提供的擴充功能。Visual Studio Tools for Office （簡稱為 VSTO）的作用，簡單來說就是讓程式設計者能夠在 Office 應用程式中，使用 .NET 撰寫附加的擴充元件，使一般的文件檔案，也能夠具備客製化的特殊功能。另外，微軟 .NET 平台的優勢，就是能夠使用 .NET 家族中的任何一種語言來撰寫程式，在此我選擇 C# 做為開發 DHL 系統的語言。 只要使用 VSTO，幾乎就能夠達成一切 Winform 可以做到的事情。在活頁簿上放個按鈕元件？沒問題；另外產生一個全新的 Winform？小意思；在 Excel 的主選單上增加自訂項目？一片蛋糕。總是覺得 Office 應用程式某些地方用得不夠順手嗎？有了 VSTO 以後，就可以幫助我們達成自己動手撰寫擴充功能的願望。 [...]]]></description>
			<content:encoded><![CDATA[<p>接續前篇文章的簡短介紹，本文將開始使用 VSTO 與 Lua 語言實作 DHL 系統。首先，定義 DHL 系統的開發環境以及執行環境：</p>
<blockquote><p>
<strong>Database Hot Loader 系統</strong></p>
<ul>
<li>程式開發環境：Visual Studio 2005 Team Suite</li>
<li>使用者執行環境：Office 2003</li>
</ul>
</blockquote>
<p>為了使 Office 2003 達成轉換資料格式的任務，必須仰賴 VSTO 所提供的擴充功能。<a href="http://en.wikipedia.org/wiki/Visual_Studio_Tools_for_Office">Visual Studio Tools for Office</a> （簡稱為 VSTO）的作用，簡單來說就是讓程式設計者能夠在 Office 應用程式中，使用 .NET 撰寫附加的擴充元件，使一般的文件檔案，也能夠具備客製化的特殊功能。另外，微軟 .NET 平台的優勢，就是能夠使用 .NET 家族中的任何一種語言來撰寫程式，在此我選擇 C# 做為開發 DHL 系統的語言。</p>
<p>只要使用 VSTO，幾乎就能夠達成一切 Winform 可以做到的事情。在活頁簿上放個按鈕元件？沒問題；另外產生一個全新的 Winform？小意思；在 Excel 的主選單上增加自訂項目？一片蛋糕。總是覺得 Office 應用程式某些地方用得不夠順手嗎？有了 VSTO 以後，就可以幫助我們達成自己動手撰寫擴充功能的願望。</p>
<p>在前篇文章中，曾特別提到需要使用 Visual Studio 2005 的 <a href="http://www.microsoft.com/taiwan/msdn/vs2005/howtobuy/editions/team/default.mspx">Team Suite 版本</a>，原因在於 DHL 系統必須使用 <strong>VSTO 的「文件層級」(document-level) 擴充程式</strong>，而在 Visual Studio 的眾多版本中，只有 Team Suite 版本能夠開發文件層級的擴充程式，一般程式設計者常用的 Professional 版本則只能夠開發 VSTO 的「應用程式層級」(application-level) 擴充程式。關於文件層級與應用程式層級的介紹，請參考<a href="http://msdn.microsoft.com/zh-tw/library/d2tx7z6d(VS.80).aspx">「Visual Studio Tools for Office 」</a>中文線上文件裡的詳細說明。</p>
<p><span id="more-577"></span></p>
<p>只要安裝了 Team Suite 版本之後，就可以在 Visual Studio 2005 的「新增專案」選項中，「Visual C#」的「Office」頁面裡，看到以下幾種專案範本：</p>
<ul>
<li>Excel 活頁簿</li>
<li>Word 文件</li>
<li>Excel 範本</li>
<li>Word 範本</li>
<li>Outlook 增益集</li>
</ul>
<p>首先，選擇建立<strong>「Excel 活頁簿」</strong>專案類型，Visual Studio 會自動產生出 ThisWorkbook.cs、Sheet1.cs、Sheet2.cs、Sheet3.cs 四個原始碼檔案。然後就可以開始在 ThisWorkbook.cs 裡，撰寫資料格式的轉換程序：</p>
<pre name="code" class="csharp">
// 匯出指定的工作表
public void ExportSheet(Excel.Worksheet ws)
{
    // 以工作表名稱為檔名，創建一個文字格式的檔案
    StreamWriter sw = File.CreateText(String.Format("{0}/{1}{2}", GetCurrentPath(), ws.Name, EXPORT_FILE_EXT));

    sw.WriteLine("{0}{1}", ws.Name, TOKEN_EQUAL);
    sw.WriteLine(TOKEN_LEFT_BRACKET);

    int currentRow = 1;
    int currentColumn = 1;
    string fieldName = GetRangeString(ws, currentRow, currentColumn);
    while (!String.IsNullOrEmpty(fieldName)) {
        // 寫入欄位名稱
        sw.WriteLine("{0}{1}{2}", TOKEN_INDENT, fieldName, TOKEN_EQUAL);
        sw.WriteLine("{0}{1}", TOKEN_INDENT, TOKEN_LEFT_BRACKET);

        ++currentRow;
        string id = GetRangeString(ws, currentRow, 1);
        while (!String.IsNullOrEmpty(id)) {
            string value = GetRangeString(ws, currentRow, currentColumn);

            // 寫入每筆資料
            sw.WriteLine("{0}{1}{2}{3}{4}{5}{6}{7}{8}",
                TOKEN_DOUBLE_INDENT, TOKEN_LEFT_SQ_BRACKET, id, TOKEN_RIGHT_SQ_BRACKET, TOKEN_EQUAL,
                TOKEN_DOUBLE_QUOTE, value, TOKEN_DOUBLE_QUOTE, TOKEN_DOT);

            ++currentRow;

            id = GetRangeString(ws, currentRow, 1);
        }

        // 寫入右括弧與逗號
        sw.WriteLine("{0}{1}{2}", TOKEN_INDENT, TOKEN_RIGHT_BRACKET, TOKEN_DOT);
        sw.WriteLine("");

        currentRow = 1;
        ++currentColumn;
        fieldName = GetRangeString(ws, currentRow, currentColumn);
    }

    // 寫入最終的右括弧
    sw.WriteLine(TOKEN_RIGHT_BRACKET);

    // 關閉檔案
    sw.Flush();
    sw.Close();
}
</pre>
<p>ExportSheet() 函式的作用，就是<strong>抓取 Excel 儲存格中的資料，並且格式化為合法的 Lua 原始碼語法寫入檔案</strong>。利用 .NET 所提供的基底函式庫功能，僅需不到 100 行的程式碼，就能夠相當輕易地寫出轉換程序。接著開啟 Visual Studio 工具箱介面，在專案的 Excel 頁面裡放上一個 Button 元件，然後在 Button 的 Click 事件函式中，把活頁簿裡所有的工作表 (worksheet) 傳入 ExportSheet() 函式中處理。完成之後，Excel 文件的使用者，只需按下文件上的按鈕，就能夠立即將活頁簿中所有的資料輸出為合法的 Lua 檔案。</p>
<p>在轉換資料格式時，會以活頁簿裡各張工作表的名稱，做為檔案名稱個別輸出成 .lua 檔案，同時也做為 Lua 程式碼中最上層的 table 結構。接著把每張工作表中的每個欄位名稱，格式化為第二層的 table 結構，最後再將該欄位每一筆資料輸出成 table 的內容物。輸出完成的 Lua 資料格式非常單純易懂，<strong>第一層是資料表的名稱，第二層是欄位的名稱，最後則將每一筆資料以 id 對應數值的方式，嵌在 table 最內層</strong>。以 triangle 工作表為例：</p>
<pre name="code" class="lua">
triangle =
{
    id =
    {
        [1] = "1",
        [2] = "2",
        [3] = "3",
    },

    vertex1 =
    {
        [1] = "0",
        [2] = "1",
        [3] = "0",
    },

    vertex2 =
    {
        [1] = "-1",
        [2] = "-1",
        [3] = "0",
    },

    vertex3 =
    {
        [1] = "1",
        [2] = "-1",
        [3] = "0",
    },

    color1 =
    {
        [1] = "1",
        [2] = "0",
        [3] = "0",
    },

    color2 =
    {

        [1] = "0",
        [2] = "1",
        [3] = "0",
    },

    color3 =
    {
        [1] = "0",
        [2] = "0",
        [3] = "1",
    },

    translate =
    {
        [1] = "-1.5",
        [2] = "0",
        [3] = "-6",
    },

    rotate =
    {
        [1] = "0",
        [2] = "1",
        [3] = "0",
    },

}
</pre>
<p>專案建置完成後，會產生出一個 Excel 文件檔案以及一個 DLL 檔案。但是如果你馬上把這兩個檔案複製到電腦中的其他目錄下，或者傳給其他人使用，在開啟 Excel 檔案後應該會看到這樣的錯誤訊息：「找不到或無法載入自訂組件」。這是因為 <strong>Office 對於 .NET 擴充元件，有比較嚴格的安全性規範，只要沒有經過安全性設定允許的檔案目錄，就無法順利載入擴充元件的 .dll 檔案</strong>，所以必須再經過一道安全性變更的手續。</p>
<p>舉例來說，如果把專案建置出來的 ExcelToLuaExporter.xls 與 ExcelToLuaExporter.dll 放在 D:\test 資料夾中，就要在程式集選單中，開啟 Microsoft .NET Framework SDK v2.0 的「SDK 命令提示字元」，然後輸入 DLL 檔案所在的完整路徑：</p>
<pre name="code" class="cpp">
caspol.exe -u -ag All_Code -url "D:\test\ExcelToLuaExporter.dll" FullTrust
</pre>
<p>出現確認提示訊息後，按下 y 鍵確認即可：</p>
<blockquote><p>
執行的作業將會變更安全性原則。<br />
您確定要執行這項作業嗎? (yes/no)
</p></blockquote>
<p>為了簡化這道安全性變更的步驟，在文章最後的<strong>執行檔下載</strong>檔案中，我提供了一個 <strong>PermissionConsole.exe</strong> 程式，只要直接點擊執行，然後按下 y 鍵確認，就會執行以上程序而無須手動輸入。</p>
<p>除此之外，要在沒有安裝 Visual Studio Team Suite 的一般使用者電腦上執行 VSTO 程式，必須要先行安裝 VSTO Runtime 與其他相關的項目。VSTO 有許多不同的版本存在，而我使用的是 Visual Studio 2005 能夠運作的 <strong>VSTO 2005 Second Edition</strong> 版本：</p>
<ul>
<li><a href="http://www.microsoft.com/downloads/details.aspx?familyid=0856EACB-4362-4B0D-8EDD-AAB15C5E04F5&#038;displaylang=zh-tw">Microsoft .NET Framework 2.0</a>: dotnetfx.exe (22.4 MB)</li>
<li><a href="http://www.microsoft.com/downloads/details.aspx?familyid=3c9a983a-ac14-4125-8ba0-d36d67e0f4ad&#038;displaylang=en">Office 2003 Update: Redistributable Primary Interop Assemblies</a>: O2003PIA.EXE (4.1 MB)</li>
<li><a href="http://www.microsoft.com/downloads/details.aspx?FamilyID=8315654b-a5ae-4108-b7fc-186402563f2b&#038;DisplayLang=en">Microsoft Visual Studio 2005 Tools for Office Second Edition Runtime</a>: vstor.exe (1.3 MB)</li>
</ul>
<p>只要安裝完上列三個項目後，關於 VSTO 的部分，就大功告成了！</p>
<p>比較熟悉 Excel 操作的讀者可能會想問：「為什麼不直接用 VBA 撰寫資料轉換的程式，而選擇使用比較複雜的 VSTO？」答案很簡單，因為我不會，所以我選擇自己最熟悉的 C# 語言做為開發工具。理論上使用 VBScript 也能撰寫出相同的功能並且得到同樣的結果，只要能夠將 Excel 文件的資料轉換為合法的 Lua 檔案格式即可。</p>
<p>處理完 VSTO 之後，相對來說 Lua 的部分則單純許多。在範例程式中，我創建了一個 <strong>LuaDatabaseManager 類別，用來處理所有相關於 Lua 資料庫的功能</strong>。首先，使用 luaL_dofile() 函式將由 Excel 匯出的檔案載入執行，接著使用 GetData() 函式，傳入資料表名稱、欄位名稱、資料 ID 以及回傳值的參考之後，就可以使用 Lua 的 C API 取得指定 table 中的資料：</p>
<pre name="code" class="cpp">
void LuaDatabaseManager::GetData(std::string&#038; sheetName, std::string&#038; fieldName, unsigned int id, std::string&#038; value)
{
	int oldTop = lua_gettop(m_DBState);

	lua_getglobal(m_DBState, sheetName.c_str());
	lua_pushstring(m_DBState, fieldName.c_str());
	lua_gettable(m_DBState, LUA_GETTABLE_INDEX);
	lua_pushnumber(m_DBState, id);
	lua_gettable(m_DBState, LUA_GETTABLE_INDEX);
	value = luaL_checkstring(m_DBState, lua_gettop(m_DBState));

	lua_settop(m_DBState, oldTop);
}
</pre>
<p>在範例程式中，將 LuaDatabaseManager.exe、ExcelToLuaExporter.xls 與 ExcelToLuaExporter.dll 以及 triangle.lua 這四個檔案放在同目錄下，開啟 ExcelToLuaExporter.xls 並且執行 LuaDatabaseManager.exe。接著任意改變 triangle 工作表中的資料後，按下 Excel 上的「匯出全部資料表」按鈕，再於 LuaDatabaseManager.exe 裡按下 x 鍵即可重新載入資料！</p>
<p>搞定了繁瑣的 VSTO 轉換程序與簡單的 Lua 檔案讀取機制後，下一篇文章將利用 Observer 設計模式，實作 DHL 系統在遊戲程式中的應用範例。</p>
<p>執行檔下載：<a href="http://blog.monkeypotion.net/download-manager.php?id=13"><strong>DatabaseHotLoader_Ep2_Release.zip</strong></a>（需要先行安裝 <a href="http://www.microsoft.com/downloads/details.aspx?displaylang=zh-tw&#038;FamilyID=32bc1bee-a3f9-4c13-9c99-220b62a191ee">vcredist_x86.exe</a>） <small>(下載次數： 663 )</small><br />
原始碼下載：<a href="http://blog.monkeypotion.net/download-manager.php?id=14"><strong>DatabaseHotLoader_Ep2_Source.zip</strong></a> <small>(下載次數： 648 )</small></p>
<img src="http://blog.monkeypotion.net/?ak_action=api_record_view&id=577&type=feed" alt="" />]]></content:encoded>
			<wfw:commentRss>http://blog.monkeypotion.net/gameprog/advanced/database-hot-loader-using-vsto-and-lua/feed</wfw:commentRss>
		<slash:comments>8</slash:comments>
		</item>
		<item>
		<title>Database Hot Loader 首部曲：Introduction</title>
		<link>http://blog.monkeypotion.net/gameprog/advanced/database-hot-loader-introduction</link>
		<comments>http://blog.monkeypotion.net/gameprog/advanced/database-hot-loader-introduction#comments</comments>
		<pubDate>Sun, 28 Sep 2008 16:25:24 +0000</pubDate>
		<dc:creator>半路</dc:creator>
				<category><![CDATA[進階技術]]></category>

		<guid isPermaLink="false">http://blog.monkeypotion.net/?p=575</guid>
		<description><![CDATA[「我有一個夢想，期望有天我們能夠免於調校遊戲資料數據的苦痛。」馬丁路德博士的夢想，是人人生而平等並且免於種族歧視的迫害。而我的夢想，沒有這麼崇高偉大，只是希望能夠幫助團隊成員改善遊戲的開發流程。 每次當我看到遊戲企畫者，為了調整遊戲中的各種數據，而必須不斷重複一連串繁瑣而且漫長的操作程序，就替他們感到很辛苦。為了測試資料數據的正確性，首先要在 Microsoft Office Excel 上輸入資料內容，存檔後匯出，接著以外部工具將文字檔轉換為遊戲自訂的檔案格式，把檔案置入遊戲目錄中，最後再開啟遊戲，等待遊戲程式以及遊戲資料載入完成，然後檢視執行的結果。 「咦，食人怪的偵察範圍太遠，而火球魔法的距離太近了些。」於是按下離開遊戲的快速鍵，然後在 Excel 表格中修改相對應的數據後，再次存檔匯出，再次以外部工具轉換為遊戲自訂的檔案格式，再次把檔案置入遊戲目錄中，然後再次開啟遊戲，再次等待遊戲程式與遊戲資料載入完成。 （圖片來源：www.play-gadgets.com） 除非企畫設計者擁有強大的靈動感知能力，能夠精準地預測出合適的遊戲數值，否則總是需要不斷地嘗試錯誤並且經歷許多修改程序，才能夠將遊戲數據調校到完美平衡的狀態。不停地、不停地、不停地重開遊戲，幾乎是每位遊戲企畫設計者都曾經歷過的處境。在遊戲專案開發初期時，可能還感受不到特別的困擾之處；然而當專案到了後期階段，遊戲中的美術素材動輒佔有成千上百 MB 的份量，所以每次開啟遊戲都必須要等待漫長的遊戲初始化載入時間，甚至只是修改單一一個欄位的數據，同樣難以免於遊戲重開的等待過程。 有沒有可能改善這種狀況，減少重新開啟遊戲程式的頻率？有沒有可能在不需要重新啟動遊戲程式的情況下，讓企畫設計者能夠即時調整遊戲的資料庫數據？ 為了達成這個目標，我將以大約四篇文章左右的篇幅，從無到有完整實做出 Database Hot Loader 系統（簡稱為 DHL），在文章裡討論其中各種設計決定、實作細節以及系統效能的測試成果，並且附上完整程式碼與應用範例。這個系統是在我思考另一個設計問題時，意外衍生出來的想法；最近進行了初步的實作測試後，發現系統的可行性很高，值得將想法與實作的過程整理出來給各位參考。目前 DHL 系統仍有許多未盡完善之處，隨著後續文章的介紹，如果有其他的功能需求、設計想法或者實作技巧的改進意見，還望各位讀者能不吝給予評論指教。 遊戲界裡，幾乎所有的商業遊戲，都需要某種形式的資料庫，不論遊戲中所使用的是純文字、二進位制、XML 格式或是其他自訂的格式，「資料庫」可以說是最適合企畫設計者用於建立資料數據的方法。而為了能夠系統化且大量化執行資料庫的相關工作，業界中大部分的企畫設計者，都相當倚重 Microsoft 的 Office Excel 試算軟體做為建立與編輯資料表格的得力工具。 我曾經以程式設計者的角度，詢問企畫設計者以自製的資料編輯器取代 Excel 使用的可能性，但是如果未能提供大量作業、搜尋字串、取代內容以及公式運算等等功能，對企畫設計者來說，自製的編輯器並沒有足夠的吸引力。另外，在使用者的熟悉度與介面操作的習慣性上，Excel 同樣具有極大的優勢，所以除了極少部分比較特殊的資料，使用其他編輯器能夠得到良好的成效以外，Excel 仍然是無可取代的資料編輯器霸主。 在遊戲程式資料庫的傳統作法裡，我們會先將 Excel 檔案另存為以逗號或者 Tab 字元分隔的 .csv 純文字格式，接著由於程式系統讀取效能上的考量以及防止遊戲玩家修改資料的目的，通常會再將 .csv 格式的檔案經過自製程式的轉換程序，進一步轉換為二進位制格式的資料。所以對於企畫設計者來說，如果要修改遊戲所使用的測試資料，大致上可以分為以下三個程序： 於 Excel 中修改資料數據。 轉換成遊戲程式可使用的資料格式。 重新開啟遊戲以載入新的資料檔案。 由上述步驟可知，程式設計者需要撰寫一個資料格式的轉換程式，能夠讀取 .csv 檔案並且產生出二進位制的檔案以供企畫設計者使用；接著再於遊戲主程式系統中，撰寫出能夠讀取這個二進位制檔案的類別與函式；最後，達成在遊戲中可動態重新讀取資料的功能。所以，如果再進一步將以上的程序以操作步驟詳細說明，設計 DHL 系統的目標步驟為： 匯出資料：在 Excel [...]]]></description>
			<content:encoded><![CDATA[<p><img src="http://blog.monkeypotion.net/wp-content/uploads/2008/09/mana-energy-potion.jpg" alt="mana-energy-potion" title="mana-energy-potion" width="340" height="332" class="alignleft size-full wp-image-598" />「我有一個夢想，期望有天我們能夠免於調校遊戲資料數據的苦痛。」<a href="http://zh.wikipedia.org/w/index.php?title=%E9%A9%AC%E4%B8%81%C2%B7%E8%B7%AF%E5%BE%B7%C2%B7%E9%87%91&#038;variant=zh-hant">馬丁路德博士</a>的<a href="http://zh.wikipedia.org/w/index.php?title=%E6%88%91%E6%9C%89%E4%B8%80%E4%B8%AA%E6%A2%A6%E6%83%B3&#038;variant=zh-hant">夢想</a>，是人人生而平等並且免於種族歧視的迫害。而我的夢想，沒有這麼崇高偉大，只是希望能夠幫助團隊成員改善遊戲的開發流程。</p>
<p>每次當我看到遊戲企畫者，<strong>為了調整遊戲中的各種數據，而必須不斷重複一連串繁瑣而且漫長的操作程序</strong>，就替他們感到很辛苦。為了測試資料數據的正確性，首先要在 Microsoft Office Excel 上輸入資料內容，存檔後匯出，接著以外部工具將文字檔轉換為遊戲自訂的檔案格式，把檔案置入遊戲目錄中，最後再開啟遊戲，等待遊戲程式以及遊戲資料載入完成，然後檢視執行的結果。</p>
<p>「咦，食人怪的偵察範圍太遠，而火球魔法的距離太近了些。」於是按下離開遊戲的快速鍵，然後在 Excel 表格中修改相對應的數據後，再次存檔匯出，再次以外部工具轉換為遊戲自訂的檔案格式，再次把檔案置入遊戲目錄中，然後再次開啟遊戲，再次等待遊戲程式與遊戲資料載入完成。</p>
<p><small>（圖片來源：www.play-gadgets.com）</small></p>
<p>除非企畫設計者擁有強大的靈動感知能力，能夠精準地預測出合適的遊戲數值，否則總是需要不斷地嘗試錯誤並且經歷許多修改程序，才能夠將遊戲數據調校到完美平衡的狀態。不停地、不停地、不停地重開遊戲，幾乎是每位遊戲企畫設計者都曾經歷過的處境。在遊戲專案開發初期時，可能還感受不到特別的困擾之處；然而當專案到了後期階段，遊戲中的美術素材動輒佔有成千上百 MB 的份量，所以每次開啟遊戲都必須要等待漫長的遊戲初始化載入時間，甚至只是修改單一一個欄位的數據，同樣難以免於遊戲重開的等待過程。</p>
<blockquote><p>
有沒有可能改善這種狀況，減少重新開啟遊戲程式的頻率？<strong>有沒有可能在不需要重新啟動遊戲程式的情況下，讓企畫設計者能夠即時調整遊戲的資料庫數據？</strong>
</p></blockquote>
<p><span id="more-575"></span></p>
<p>為了達成這個目標，我將以大約四篇文章左右的篇幅，從無到有完整實做出 <strong>Database Hot Loader</strong> 系統（簡稱為 <strong>DHL</strong>），在文章裡討論其中各種設計決定、實作細節以及系統效能的測試成果，並且附上完整程式碼與應用範例。這個系統是在我思考另一個設計問題時，意外衍生出來的想法；最近進行了初步的實作測試後，發現系統的可行性很高，值得將想法與實作的過程整理出來給各位參考。目前 DHL 系統仍有許多未盡完善之處，隨著後續文章的介紹，如果有其他的功能需求、設計想法或者實作技巧的改進意見，還望各位讀者能不吝給予評論指教。</p>
<p>遊戲界裡，幾乎所有的商業遊戲，都需要某種形式的資料庫，不論遊戲中所使用的是純文字、二進位制、XML 格式或是其他自訂的格式，<strong>「資料庫」可以說是最適合企畫設計者用於建立資料數據的方法</strong>。而為了能夠系統化且大量化執行資料庫的相關工作，業界中大部分的企畫設計者，都相當倚重 Microsoft 的 Office Excel 試算軟體做為建立與編輯資料表格的得力工具。</p>
<p>我曾經以程式設計者的角度，詢問企畫設計者以自製的資料編輯器取代 Excel 使用的可能性，但是如果未能提供大量作業、搜尋字串、取代內容以及公式運算等等功能，對企畫設計者來說，自製的編輯器並沒有足夠的吸引力。另外，在使用者的熟悉度與介面操作的習慣性上，Excel 同樣具有極大的優勢，所以除了極少部分比較特殊的資料，使用其他編輯器能夠得到良好的成效以外，<strong>Excel 仍然是無可取代的資料編輯器霸主</strong>。</p>
<p>在遊戲程式資料庫的傳統作法裡，我們會先將 Excel 檔案另存為以逗號或者 Tab 字元分隔的 .csv 純文字格式，接著由於程式系統讀取效能上的考量以及防止遊戲玩家修改資料的目的，通常會再將 .csv 格式的檔案經過自製程式的轉換程序，進一步轉換為二進位制格式的資料。所以對於企畫設計者來說，如果要修改遊戲所使用的測試資料，大致上可以分為以下三個程序：</p>
<ol>
<li>於 Excel 中修改資料數據。</li>
<li>轉換成遊戲程式可使用的資料格式。</li>
<li>重新開啟遊戲以載入新的資料檔案。</li>
</ol>
<p>由上述步驟可知，程式設計者需要撰寫一個資料格式的轉換程式，能夠讀取 .csv 檔案並且產生出二進位制的檔案以供企畫設計者使用；接著再於遊戲主程式系統中，撰寫出能夠讀取這個二進位制檔案的類別與函式；最後，達成在遊戲中可動態重新讀取資料的功能。所以，如果再進一步將以上的程序以操作步驟詳細說明，設計 DHL 系統的目標步驟為：</p>
<ol>
<li><strong>匯出資料</strong>：在 Excel 中修改資料後，另存為 .csv 格式的檔案。</li>
<li><strong>轉換格式</strong>：使用轉換程式將 .csv 檔案轉換為遊戲的資料庫格式。</li>
<li><strong>載入資料</strong>：於遊戲中按下快速鍵，重新載入資料庫檔案。</li>
</ol>
<p>如果可以達成「載入資料」這個步驟，就能夠免除離開遊戲後再重新開啟遊戲的繁瑣程序了！以前在<a href="http://blog.monkeypotion.net/gameprog/advanced/lua-based-gui-system-from-commercial-game"><strong>「使用Lua實做GUI系統的遊戲實例」</strong></a>文中，曾提到能夠<strong>將 Lua 語言當作一種資料描述語言</strong>。在 DHL 系統中，就是要利用 Lua 語言的強大威力，幫助我們實現「資料熱抽換」的功能。所以只要能夠將 .csv 格式的檔案，轉換成為 .lua 格式的檔案，就可以輕易地達到即時載入資料庫的目標！</p>
<p>但光是這樣仍然有所不足。雖然匯出資料、轉換格式，最後載入資料的三拍子節奏，確實能夠達到即時修改數據且載入資料的目標，但對於使用者來說，這幾個連續動作仍然不是非常直覺易用的操作程序。在電腦作業系統中，企畫設計者必須同時開啟 Excel 軟體、轉換程式以及遊戲程式三個視窗，然後在這三個視窗之間依序切換，才能夠達到熱抽換遊戲資料的目標。</p>
<p>「如果能夠<strong>省略轉換格式的步驟</strong>就好了！」</p>
<p>你的心聲我聽到了！想要省略轉換格式的步驟其實並不困難，只要能夠把原來的 .csv 格式檔案，<strong>直接</strong>轉換成合法的 .lua 格式檔案即可達成目標。但是要怎麼在 Excel 中直接將 .csv 格式的檔案轉換成 .lua 格式的檔案呢？關於這個難題，<strong>Visual Studio Tools for Office</strong>（簡稱為 <strong>VSTO</strong>）的現身，將為我們帶來答案的曙光。因此，在只需要同時開啟 Excel 資料表以及遊戲程式的情況下，DHL 系統的理想操作步驟就可以修改為：</p>
<ol>
<li><strong>匯出資料</strong>：在 Excel 中修改資料後，匯出為 .lua 格式的檔案。</li>
<li><strong>載入資料</strong>：於遊戲中按下快速鍵，重新載入資料庫檔案。</li>
</ol>
<p>只要在 Excel 視窗以及遊戲程式視窗裡按下兩個 Click，就能夠輕鬆地重新載入遊戲資料，兩個步驟就搞定！如前文所述，DHL 系統的實作目標，就是在無須重新啟動遊戲的情況下，可以在 Excel 表格中修改資料數據，然後立即動態載入遊戲程式之中。在後續的 <strong>Database Hot Loader</strong> 系列文章裡，將大力倚賴 Lua 語言與 VSTO 幫助我們實現夢想中的目標。</p>
<p>當然，DHL 系統的設計與實作方法並不是無懈可擊般的完美。首先，列出使用 DHL 系統的缺點：</p>
<ul>
<li>企畫設計者使用 Microsoft Office Excel 軟體，需要錢。</li>
<li>程式設計者開發 DHL 系統必須使用 Microsoft Visual Studio 軟體的 Team Suite 版本，同樣需要錢。</li>
<li>DHL 系統的使用者必須額外安裝 .NET Framework Runtime 以及 VSTO Runtime。</li>
<li>與同事的聊天時間以及瀏覽網頁時間減少。</li>
</ul>
<p>以及使用 DHL 系統的優點：</p>
<ul>
<li>不需要撰寫額外的檔案格式轉換程式。</li>
<li>不需要在遊戲程式系統中撰寫資料庫的讀取類別與函式。</li>
<li>減少修改測試資料的時間，使企畫設計者能夠進行更多次測試程序，以獲得更佳的遊戲數據平衡性。</li>
<li>輕輕兩下，快又有效！</li>
</ul>
<p>經過本篇序曲的介紹之後，你是否有點心動了？次篇文章，將帶領各位進入美妙的 VSTO &#038; Lua 雙重奏鳴曲之中。</p>
<img src="http://blog.monkeypotion.net/?ak_action=api_record_view&id=575&type=feed" alt="" />]]></content:encoded>
			<wfw:commentRss>http://blog.monkeypotion.net/gameprog/advanced/database-hot-loader-introduction/feed</wfw:commentRss>
		<slash:comments>9</slash:comments>
		</item>
		<item>
		<title>物件導向設計新思維：深入Policy-Based Class Design新大陸</title>
		<link>http://blog.monkeypotion.net/gameprog/advanced/diving-into-policy-based-class-design</link>
		<comments>http://blog.monkeypotion.net/gameprog/advanced/diving-into-policy-based-class-design#comments</comments>
		<pubDate>Sun, 03 Aug 2008 16:04:10 +0000</pubDate>
		<dc:creator>半路</dc:creator>
				<category><![CDATA[進階技術]]></category>

		<guid isPermaLink="false">http://blog.monkeypotion.net/?p=117</guid>
		<description><![CDATA[自前一篇「物件導向設計新思維：探索Policy-Based Class Design新視界」發佈以來，已經過了很長的一段時間。在這篇千呼萬喚始出來的續集裡，將會進一步深入探索 Policy-Based Class Design 背後的設計概念與理論，以及在實作層面上經常會遭遇到的問題；最後，以一個遊戲系統中常見的音源引擎 (Audio Engine) 類別設計為範例，做為本文的片尾曲目。 記得曾經有人問過比爾蓋茲，如果有天他被迫一人獨自在荒島上生活，身旁只有一樣物品陪伴的話，最想要什麼東西？他的回答是：「一部電腦與編譯器。」許多程式設計者心裡所嚮往的美好世界，就是那種無拘無束、無邊無際的自由；好像只要手指還能夠活動、頭腦還能夠寫程式，天底下就沒有辦不到事情！只是，有時候這種無限度的自由，反而會對程式系統造成負面的影響。 在《C++ 設計新思維》(Modern C++ Design) 書中的第一章，就對於程式系統的設計就開宗明義地闡述： 理想上，一個良好設計應該在編譯期強制表現出大部分 constraint（約束條件、規範）。 身為一位程式設計者，或多或少都有把自己的程式碼交給其他程式設計者使用的經驗，相對於「程式撰寫者」來說，所謂的「程式使用者」，就是那些使用你所撰寫的程式碼的人。就像是遊戲程式中經常使用的 DirectX、OpenGL 或者 .NET Framework 以及遊戲函式庫與引擎等等，我們都是身為「程式使用者」的身份。 在一個龐大的程式系統框架中，動輒擁有成千上百的原始碼檔案與物件類別，在交錯複雜的物件體系與階層架構中，如何讓使用者不會迷失方向而誤入歧途？即使想要依賴文件資料、UML 圖示以及程式碼中的註解，也只能夠提供相當有限的指引。有經驗的程式設計者應該都知道，很多函式庫與引擎系統都有各自的「眉角」，如果不能真正瞭解這些系統的使用邏輯，經常會造成事半功倍的反效果。因此，換個方向以「程式撰寫者」的角度來看，當我們在撰寫程式時，最重要的關鍵就是如何正確地傳達設計者的意圖 (intention)。 舉例來說，C++ 語言中的在物件封裝設計，缺少了 package 的概念，只要是宣告為 public 的成員函式，就無法阻止其他的使用者進行操作，即使這些類別與函式的原意並非如此。由 C++ 語言的所提供的能力，從程式撰寫者的角度來看，是一種很大的自由度；然而如果從程式使用者的角度來看，卻很可能是一種設計上的缺陷。而 C# 的 internal 關鍵字則正好補足了這一點缺陷，讓 public 的屬性只存在於同一個專案的 package 中，對於其他的專案來說，則變成不能擅自使用的私有類別或者私有成員函式。 做為一個有經驗的程式設計者，我們經常會半強迫性或者半自願性地寫出「語法有效」但「語意無效」的程式碼。什麼叫做「語法有效，語意無效」的程式碼？舉個遊戲程式架構中常見的實例，例如 LifeEntity 是一個生物體的基礎類別，而遊戲中的怪物 Monster 類別與玩家 Player 類別，同樣繼承自 LifeEntity 類別。遊戲中的怪物需要 AI 功能的設計，而為了要使 Monster [...]]]></description>
			<content:encoded><![CDATA[<p>自前一篇<a href="http://blog.monkeypotion.net/gameprog/beginner/exploring-the-field-of-policy-based-class-design"><strong>「物件導向設計新思維：探索Policy-Based Class Design新視界」</strong></a>發佈以來，已經過了很長的一段時間。在這篇千呼萬喚始出來的續集裡，將會進一步深入探索 Policy-Based Class Design 背後的設計概念與理論，以及在實作層面上經常會遭遇到的問題；最後，以一個遊戲系統中常見的音源引擎 (Audio Engine) 類別設計為範例，做為本文的片尾曲目。</p>
<p>記得曾經有人問過<strong>比爾蓋茲</strong>，如果有天他被迫一人獨自在荒島上生活，身旁只有一樣物品陪伴的話，最想要什麼東西？他的回答是：<strong>「一部電腦與編譯器。」</strong>許多程式設計者心裡所嚮往的美好世界，就是那種無拘無束、無邊無際的自由；好像只要手指還能夠活動、頭腦還能夠寫程式，天底下就沒有辦不到事情！只是，有時候這種無限度的自由，反而會對程式系統造成負面的影響。</p>
<p>在<a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9864212672&#038;sid=15596"><strong>《C++ 設計新思維》</strong></a>(Modern C++ Design) 書中的第一章，就對於程式系統的設計就開宗明義地闡述：</p>
<blockquote><p>
理想上，一個良好設計應該在編譯期強制表現出大部分 constraint（約束條件、規範）。
</p></blockquote>
<p>身為一位程式設計者，或多或少都有把自己的程式碼交給其他程式設計者使用的經驗，相對於「程式撰寫者」來說，所謂的<strong>「程式使用者」，就是那些使用你所撰寫的程式碼的人</strong>。就像是遊戲程式中經常使用的 DirectX、OpenGL 或者 .NET Framework 以及遊戲函式庫與引擎等等，我們都是身為「程式使用者」的身份。</p>
<p><span id="more-117"></span></p>
<p>在一個龐大的程式系統框架中，動輒擁有成千上百的原始碼檔案與物件類別，在交錯複雜的物件體系與階層架構中，如何讓使用者不會迷失方向而誤入歧途？即使想要依賴文件資料、UML 圖示以及程式碼中的註解，也只能夠提供相當有限的指引。有經驗的程式設計者應該都知道，很多函式庫與引擎系統都有各自的「眉角」，如果不能真正瞭解這些系統的使用邏輯，經常會造成事半功倍的反效果。因此，換個方向以「程式撰寫者」的角度來看，當我們在撰寫程式時，最重要的關鍵就是<strong>如何正確地傳達設計者的意圖 (intention)</strong>。</p>
<p>舉例來說，C++ 語言中的在物件封裝設計，缺少了 package 的概念，只要是宣告為 public 的成員函式，就無法阻止其他的使用者進行操作，即使這些類別與函式的原意並非如此。由 C++ 語言的所提供的能力，從程式撰寫者的角度來看，是一種很大的自由度；然而如果從程式使用者的角度來看，卻很可能是一種設計上的缺陷。而 C# 的 internal 關鍵字則正好補足了這一點缺陷，讓 public 的屬性只存在於同一個專案的 package 中，對於其他的專案來說，則變成不能擅自使用的私有類別或者私有成員函式。</p>
<p>做為一個有經驗的程式設計者，我們經常會半強迫性或者半自願性地寫出<strong>「語法有效」但「語意無效」的程式碼</strong>。什麼叫做「語法有效，語意無效」的程式碼？舉個遊戲程式架構中常見的實例，例如 LifeEntity 是一個生物體的基礎類別，而遊戲中的怪物 Monster 類別與玩家 Player 類別，同樣繼承自 LifeEntity 類別。遊戲中的怪物需要 AI 功能的設計，而為了要使 Monster 類別能夠設定 AI 相關的程序，並且不至於影響操作介面，因此必須要在 LifeEntity 類別中，加入虛擬函式 SetAI() 以讓子類別們自行覆寫定義行為：</p>
<pre name="code" class="cpp">
class LifeEntity
{
public:
    virtual bool SetAI(AI* _pkAI) = 0;
}

class Monster
{
public:
    virtual bool SetAI(AI* _pkAI)
    {
        m_pkAI = _pkAI;

        // Do AI initialize stuff
        // ...

        return true;
    }
}

class Player
{
public:
    virtual bool SetAI(AI* _pkAI)
    {
        // 玩家不需要AI，忽略不處理
        return false;
    }
}
</pre>
<p>由於處理玩家的 Player 類別，不需要處理 AI 的相關程序，所以對於 Player 類別來說，SetAI() 函式就形成了「語法有效，語意無作用」的函式。像這樣的程式碼，需要程式設計者花費額外的心力辨識出使用的「眉角」。在物件導向設計的領域中，程式設計者經常難以避免地撰寫出如此語法有效但語意無效的程式碼，而當我們把這樣的程式碼交給其他的程式設計者使用時，如果缺少了清楚易懂的文件說明，就很容易會造成各種誤用的問題。Policy-Based Design，將能夠幫助程式設計者盡可能免除這些問題，達到良好的設計約束條件。</p>
<p><img src="http://blog.monkeypotion.net/wp-content/uploads/2008/08/inheritance-vs-policy-based.jpg" alt="inheritance-vs-policy-based" title="inheritance-vs-policy-based" width="415" height="198" class="alignright size-full wp-image-279" />在物件導向程式設計中，我們所熟悉的繼承體系通常是先定義出基底類別 (Base Class)，然後再視實際需求繼承出各個衍生類別 (Derived Class)，在盡量不更動類別介面的前提下，由子類別們擴充父類別的行為。有趣的是，<strong>以 Policy-Based Design 實做出來的系統架構恰好與傳統的繼承體系相反</strong>，反而是以「子類別」Host Class 定義出這組系統的操作介面，然後藉由「父類別」Policy Class 定義出實作的細節。如圖所示，Host Class 會繼承它所需的各個 Policy Class，並且在 Host Class 中定義出操作行為的骨架流程，至於真正的實作細節，則全權委派 (delegate) 給各個 Policy Class 進行處理，可以說是反轉了物件導向程式設計中的繼承關係。另外需要特別注意的是，與傳統的繼承概念不同，Policy-Based Class Design 中的繼承體系，並沒有模擬出 <strong>is-a</strong> 的階層關係。</p>
<blockquote><p>
決定「哪個 policy 被使用」的是使用者而非程式庫自身。和一般多重介面不同的是，policies 給予使用者一種能力，在型別安全 (typesafe) 的前提下擴增 host class 的功能。
</p></blockquote>
<p>表面上看起來，Policy-Based Design 和以虛擬函式實做的 State Pattern 或 Strategy Pattern 作用相同，都可以達到易於更換不同實做程式碼的設計目標。然而 Policy-Based Design 不同於虛擬函式於執行時期所提供的動態繫結 (Dynamic Binding) 能力，而是利用 template 的特性於<strong>編譯時期</strong>產生出靜態的類別結構以及型別資訊，<strong>由編譯器而非程式使用者把關</strong>，在編譯時期就可以阻擋誤用程式碼的可能性，提供給程式使用者更加安全的程式架構。因此，Policy-Based Design 並不能夠取代 State Pattern 或 Strategy Pattern 的動態繫結設計方法，但是也存在著虛擬函式所無法提供的優點。</p>
<p>再者，與傳統虛擬函式介面不同的是，Policy-Based Design 僅對於 Policy 類別的語法構造做出規範，<strong>屬於「語法導向」(Syntax Oriented) 而非「標記導向」(Signature Oriented) 的設計</strong>，所以 Policy 類別的成員函式可以是一般函式、虛擬函式甚至靜態函式。以之前使用的 ReadingPolicy< Subject >::Read() 為例，以下三種形式都是合法的形式：</p>
<pre name="code" class="cpp">
template< class T >
class BinaryReader
{
public:
    void Read();
}

template< class T >
class XmlReader
{
public:
    static void Read();
}

template< class T >

class TextReader
{
public:
    virtual void Read();
}
</pre>
<p>在前篇文章的迴響中，有人提到了一個疑問：「如果 XML 和 Binary 的 Read()、Write() 有參數，但參數不一樣的話，Manager 該怎麼辦才能達成需求？」關於這個實做上經常會遇到的問題，我們可以利用 template 的<strong>不完整具現化 (Incomplement Instantiation)</strong> 特徵，藉以實現各個參數不同的成員函式。</p>
<blockquote><p>
如果 class template 有一個成員函式未曾被用到，它不會被編譯器具體實現出來。編譯器不理會它，甚至也許不會為它進行語法檢驗。
</p></blockquote>
<p>有撰寫樣版類別經驗的程式設計者應該都知道，當我們在表頭檔中寫入樣版類別的成員函式宣告與定義，接著按下專案建置的按鈕後，編譯器並不會立即對這些程式碼進行語法驗證與編譯程序，而是需要等到其中的成員函式真正被使用之後，編譯器才會真正開始動手工作！例如，在 Visual Studio 2005 中，即使寫出這樣無意義而錯誤的程式碼，編譯器也不會發出抗議：</p>
<pre name="code" class="cpp">
template< class T >
class SuperReader
{
public:
    bool Read()
    {
        // 無意義的程式碼
        T asdasopqwe;
        return kerokero
    }
};
</pre>
<p>回到前篇文章迴響的 ReadingPolicy 類別問題中，假設 BinaryReader 與 XmlReader 需要使用不同的參數，以利於進行讀取程序：</p>
<pre name="code" class="cpp">
template< class T >
class BinaryReader
{
public:
    bool Read(DataStream&#038; rkStream);
}

template< class T >
class XmlReader
{
public:
    bool Read(TiXmlElement* pkRoot);
}
</pre>
<p>BinaryReader 需要接受 DataStream 類別以進行二進位制的檔案讀取，而 XmlReader 則是接受 TinyXML 中的 TiXmlElement 類別，對檔案進行讀取動作。接著，在原有的 ResourceIOManager 類別中，可以撰寫兩個接受不同參數版本的 Read() 函式：</p>
<pre name="code" class="cpp">
template
<
	class Subject,
	template< class > class ReadingPolicy,
	template< class > class WritingPolicy,
	template< class > class ErrorHandlingPolicy
>
class ResourceIOManager
	:
	public ReadingPolicy< Subject >,
	public WritingPolicy< Subject >,
	public ErrorHandlingPolicy< Subject >
{

public:
	void Read(DataStream&#038; rkStream)
	{
		ReadingPolicy< Subject >::Read(rkStream);
	}

	void Read(TiXmlElement* pkRoot)
	{
		ReadingPolicy< Subject >::Read(pkRoot);
	}
};
</pre>
<p>如果具現化的 ResourceIOManager 物件繼承自 BinaryReader，程式使用者只能夠呼叫 Read(DataStream&#038; rkStream) 函式；如果具現化的 ResourceIOManager 物件繼承自 XmlReader，程式使用者就只能夠呼叫 Read(TiXmlElement* pkRoot) 函式。萬一程式使用者不小心使用了錯誤的版本，編譯器馬上就會察覺使用者的意圖而立即阻攔下來，免於在程式系統中鑄成大錯。</p>
<p>瞭解了 Policy-Based Design 的概念之後，如果要把原來既存的類別架構設計，更改成為符合 Policy-Based Design 的設計，其中最重要也最困難的部分，就是如何將原有類別正確分解為 Policy Class。在將原有類別拆解成各個 Policy Class 時，必須要盡可能達成<strong>正交分解 (Orthogonal Decomposition)</strong> 的目標；也就是在拆解出來的 Policy Class 中，不能夠彼此相依或相互影響，才不會使類別的架構設計過於複雜而難以使用。越是達到相互獨立的 Policy Class，就越能夠發揮 Policy-Based Design 的功能性。</p>
<p>最後，來看一個應用於遊戲系統中的<strong>音源引擎設計實例</strong>。顧名思義，音源引擎是用來處理包含音效與音樂在內的相關程序；在 AudioEngine 類別中，需要接受程式使用者的需求來創建音源檔案，並且於類別內部對這些音源物件進行管理。對於程式使用者來說，只需認識 AudioEngine 類別以及由 AudioEngine 創建的 AudioObject 物件即可，即使在 AudioEngine 內部抽換了不同的實做版本，也不應該影響到使用者的程式碼。</p>
<pre name="code" class="cpp">
class AudioEngine
{
public:
	AudioObject* CreateAudio(std::string&#038; _kFileName)
	{
		AudioObject* pkAudio = NULL;
		// Create the audio!
		// ...
		return pkAudio;
	}
};
</pre>
<p>以 Policy-Based Design 的方法思考，在音源系統中，AudioEngine 類別就是最適合做為 Host Class 的類別。接下來，要考慮的設計概念就是如何拆解出各個正交的 Policy Class。以 Audio 系統的功能來說，Renderer 能夠有各種不同的版本，例如使用 OpenAL 或者 DirectSound 等等，因此合適於抽取出來做為 Policy Class。再者，在系統內部的管理層面，究竟應該使用以檔案名稱為鍵值的 std::map 結構，或者是使用單純的 std::vector 儲存每一個創建出來的音源檔？所以音源物件的儲存管理功能，也很適合抽取出來成為 Policy Class。</p>
<ul>
<li><strong>RendererPolicy</strong>：由 Renderer 創建、操作音源檔案。</li>
<li><strong>StoragePolicy</strong>：儲存並管理由 Renderer 創建出來的音源檔案。</li>
</ul>
<p>在此的 RendererPolicy 與 StoragePolicy 符合前述的正交分解觀念，功能彼此獨立而不會相互影響。接著，就可以開始動工打造 AudioEngine 類別：</p>
<pre name="code" class="cpp">
template
<
	class Subject,
	template< class > class RendererPolicy,
	template< class > class StoragePolicy
>
class AudioEngine
	:
	public RendererPolicy< Subject >,
	public StoragePolicy< Subject >
{
public:
	Subject* CreateAudio(std::string&#038; _kFileName)
	{
		Subject* pkAudio = NULL;
		if (!StoragePolicy< Subject >::GetAt(_kFileName, pkAudio))
		{
			pkAudio = RendererPolicy< Subject >::CreateAudio(_kFileName);
			StoragePolicy< Subject >::SetAt(_kFileName, pkAudio);
		}
		return pkAudio;
	}

	void PlayAudio(Subject* _pkAudio)
	{
		RendererPolicy< Subject >::PlayAudio(_pkAudio);
	}

	void PositionAudio(Subject* _pkAudio, float x, float y, float z)
	{
		RendererPolicy< Subject >::PositionAudio(_pkAudio, x, y, z);
	}
};
</pre>
<p>在 CreateAudio() 函式中，首先把檔案名稱丟進 StoragePolicy 的 GetAt() 函式，如果無法取得適當的物件，就需要交給 RendererPolicy 進行創建音源物件的動作；在創建完畢後，再將物件丟回 StoragePolicy 的 SetAt() 函式進行資源的管理。而播放音源檔案與調整音源位置的功能，則全部交由 RendererPolicy 處理，與 StoragePolicy 無關。</p>
<p>將音源引擎的功能分解為 RendererPolicy 與 StoragePolicy 之後，就可以衍生出 Policy Class 的各種不同實做版本：</p>
<ul>
<li><strong>RendererPolicy</strong>
<ul>
<li>DXSoundRenderer：以 DirectX Sound/Audio 建立的音源 Renderer。</li>
<li>OpenALSoundRenderer：以 OpenAL 建立的音源 Renderer。</li>
<li>WMMRenderer：以 Windows Muiltmedia 函式庫建立的音源 Renderer。</li>
</ul>
</li>
<li><strong>StoragePolicy</strong>
<ul>
<li>VectorStorage：使用 std::vector 做為音源物件的儲存容器。</li>
<li>StringMapStorage：使用 std::map 做為音源物件的儲存容器。</li>
</ul>
</li>
</ul>
<p>有了 Policy Class 之後，就可以依照程式使用者的需求，使用不同的 Policy 零件，自行組裝合適的 AudioEngine 類別：</p>
<pre name="code" class="cpp">
// 使用 DirectX Sound，以及 std::vector 容器
AudioEngine< DXSoundObject, DXSoundRenderer, VectorStorage > kEngine1;
// 使用 OpenAL ，以及 std::map 容器
AudioEngine< OpenALObject, OpenALSoundRenderer, StringMapStorage > kEngine2;
// 使用 Windows Muiltmedia，以及 std::map 容器
AudioEngine< WMMObject, WMMRenderer, StringMapStorage > kEngine3;
</pre>
<p>回過頭來重新檢視，Policy-Based Design 可以說是利用 Host Class 定義出程序的執行流程，而由各 Policy Class 定義執行細節的一種設計方法；<strong>Host Class 是骨架，而 Policy Class 們則是其中的血肉。</strong>如上述音源引擎的設計原理，同樣也能夠應用在繪圖系統、人物系統或者其他的系統中。</p>
<p>在撰寫一個程式系統時，經常會遭遇到效能或者其他的因素而必須變更程式碼的設計；例如，將資源物件的儲存容器由 std::vector 更改為 std::map，或是將原來的 Xml 讀檔格式轉換為 Binary 格式。在缺乏良好架構設計的情況下，我們很容易就會在程式碼中散佈大量的 #ifdef、#else 以及 #endif 敘述句，用來分隔出「目前需要」與「暫時不使用」的程式碼。然而這些散佈在程式碼中的的 #ifdef 區塊，經常會使得程式系統的維護更為困難，也更加難以理解程式碼的來龍去脈。如果使用了 Policy-Based Design，就能夠幫助我們優雅而輕巧地擴充新的實作版本，同時也不會影響到舊有的實作版本。</p>
<p>以目前的現況來說，使用 Policy-Based Design 最大的缺點在於多數<strong>編譯器對於 template 語法的友善度不足</strong>；以 Visual Studio 2005 為例，只要不小心多使用了一個逗號，可能就會產生十數條莫名其妙的錯誤訊息，令人摸不著頭緒而難以排除問題。另外，為了要使團隊中的其他同儕徹底瞭解 Policy-Based Design 的設計概念與實作方法，也需要額外花費一番功夫與時間。</p>
<p>以前剛看到 Policy-Based Design 設計方法時，心裡只有滿滿的讚嘆之情，而目前我已經實際將 Policy-Based Design 應用在工作的專案中，執行效果相當不錯，所以我也希望能夠讓更多的程式設計者認識這個有趣的設計方法，期待未來能夠繼續延伸更多的應用與發展。Policy-Based Design 巧妙地結合了多重繼承與樣版類別的語言特徵，<strong>藉由「限制」的方式使程式設計者獲得更多的「自由」</strong>，帶領程式設計者航向一片充滿全新視野的新大陸。如果只是犧牲一點點個人的自由，而能夠換來更美好的世界，又何樂而不為之呢？</p>
<p>程式碼範例下載：<a href="http://blog.monkeypotion.net/download-manager.php?id=12"><strong>PolicyBasedAudioEngine_SampleCode.zip</strong></a> <small>(下載次數： 277 )</small></p>
<img src="http://blog.monkeypotion.net/?ak_action=api_record_view&id=117&type=feed" alt="" />]]></content:encoded>
			<wfw:commentRss>http://blog.monkeypotion.net/gameprog/advanced/diving-into-policy-based-class-design/feed</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
		<item>
		<title>容器們，奮起吧！—以常整數映射型別改進 STL Containers 的介面類別</title>
		<link>http://blog.monkeypotion.net/gameprog/advanced/container-int2type-improvement</link>
		<comments>http://blog.monkeypotion.net/gameprog/advanced/container-int2type-improvement#comments</comments>
		<pubDate>Sun, 02 Mar 2008 07:24:43 +0000</pubDate>
		<dc:creator>半路</dc:creator>
				<category><![CDATA[進階技術]]></category>

		<guid isPermaLink="false">http://blog.monkeypotion.net/gameprog/advanced/container-int2type-improvement</guid>
		<description><![CDATA[承續前篇「容器們，奮起吧！—實做 STL Containers 的包裝介面」的內容，本文將進一步改善容器 Wrapper Class 的實做設計。文中將利用《C++ 設計新思維》(Modern C++ Design) 書中，第二章第四節的常整數映射型別 (Mapping Integral Constants to Types) 技術，將原來程式碼重複性極高的 ValueDictionary 與 AutoPtrDictionary 類別合而為一。 所謂的常整數，就是一個整數型別 (Integer) 的編譯期常數值 (Constant)。而利用以下這個簡單的 template 結構就能夠將常整數映射成不同的型別 (Type)： template< int V > struct Int2Type { enum { value = V }; }; 這個長相奇怪的 Int2Type template 結構，是如何將常整數映射成為不同的型別呢？根據書中的論述： Int2Type 會根據引數所得的不同數值來產生不同型別。這是因為「不同的 template 具現體」本身便是「不同的型別」。 請仔細地思考這句話：「不同的 template 具現體」本身就是代表「不同的型別」。瞭解這句話的意涵，也是欲將 template 程式設計技術融會貫通的必經途徑！因此，由 [...]]]></description>
			<content:encoded><![CDATA[<p>承續前篇<a href="http://blog.monkeypotion.net/gameprog/beginner/containers-wrapper-class"><strong>「容器們，奮起吧！—實做 STL Containers 的包裝介面」</strong></a>的內容，本文將進一步改善容器 Wrapper Class 的實做設計。文中將利用<a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9864212672&#038;sid=15596"><strong>《C++ 設計新思維》(Modern C++ Design)</strong></a> 書中，第二章第四節的<strong>常整數映射型別 (Mapping Integral Constants to Types)</strong> 技術，將原來程式碼重複性極高的 ValueDictionary 與 AutoPtrDictionary 類別合而為一。</p>
<p>所謂的<strong>常整數</strong>，就是一個<strong>整數型別 (Integer) 的編譯期常數值 (Constant)</strong>。而利用以下這個簡單的 template 結構就能夠將常整數映射成不同的型別 (Type)：</p>
<pre name="code" class="cpp">
template< int V >
struct Int2Type
{
    enum { value = V };
};
</pre>
<p>這個長相奇怪的 <strong>Int2Type</strong> template 結構，是如何將常整數映射成為不同的型別呢？根據書中的論述：</p>
<blockquote><p>
<strong>Int2Type 會根據引數所得的不同數值來產生不同型別。這是因為「不同的 template 具現體」本身便是「不同的型別」。</strong>
</p></blockquote>
<p><span id="more-58"></span></p>
<p>請仔細地思考這句話：<strong>「不同的 template 具現體」本身就是代表「不同的型別」。</strong>瞭解這句話的意涵，也是欲將 template 程式設計技術融會貫通的必經途徑！因此，由 Int2Type 這個 template 結構所具現化出來的 Int2Type< 0 >、Int2Type< 1 >、Int2Type< 2 > 等等實體物件，全部都能夠視為不同的資料型別來進行處理。那麼在什麼樣的情況下，會需要使用到這個奇怪的 Int2Type 具現體型別？</p>
<blockquote><p>
一般而言，符合下列兩個條件便可使用 Int2Type：</p>
<ul>
<li>有必要根據某個編譯期常數呼叫一個或數個不同的函式。</li>
<li>有必要在編譯期實施「分派」(dispatch)。</li>
</ul>
</blockquote>
<p><strong>「根據某個編譯期常數呼叫一個或數個不同的函式」</strong>，似乎和前篇文章中的所遇到的實做問題有點關連性？回顧先前 RemoveAll() 函式中的問題點：</p>
<pre name="code" class="cpp">
template < class TKey, class TValue >
inline void Dictionary< TKey, TValue >::RemoveAll()
{
    // 如果是內存 Heap 物件
    if (m_bUseHeapObject) {
        // 巡訪整個map
        for (m_kIter = m_kMap.begin(); m_kIter != m_kMap.end(); m_kIter ++) {
            // 使用 C++ 的 operator delete
            delete m_kIter->second;
        }
    }

    // 不是 Heap 物件，只需要直接清除map
    m_kMap.clear();
}

// 無法通過編譯！
Dictionary< int, float > kItWillNotCompileDict;
</pre>
<p>如果試著將這段程式碼敲入你最愛的程式編輯器中，然後輕鬆愉快地按下「建置」，期待著美好圓滿的成果，編譯器會立刻冷冷地打回這天真爛漫的作法。上述的程式碼，在傳入非 Heap 型別物件時<strong>無法成功編譯</strong>。</p>
<p>即使做為上述程式碼使用者的我們，知道當使用的物件型別不是 Heap 物件時，<strong>在 if 敘述句中的程式碼不應該也不會被執行</strong>。但是對於編譯器來說，並無法評估出條件式的哪一條分支不會被執行。所以，編譯器依舊會勤勞地編譯程式碼中的每個分支，因而造成編譯錯誤的產生。如果，編譯器能夠判斷哪個分支需要執行才進行編譯，而不去編譯不需要的部分，就能夠順利達到我們所需要的目標了。而這樣的情況有可能成功實現嗎？</p>
<p>請在心無旁騖、真心誠意的狀態下，默唸以下咒語七遍：</p>
<blockquote><p>
<strong>如果在 class template 中有一個成員函式未曾被使用到，它不會被編譯器具體實現出來。編譯器不理會它，甚至也許不會為它進行語法檢驗。</strong>
</p></blockquote>
<p>腦袋裡有沒有突然被閃電擊中的快感？如果沒有，請先去除心中雜念，再回頭念咒語三遍。</p>
<p>於是在有了 Int2Type 這項能夠用來「將數值轉換為型別」的武器之後，就得以應付這個本來非常棘手的介面設計問題了。我們能夠以 Int2Type 具現化出來的型別，使編譯器能夠判斷哪個函式會被編譯生成程式碼，然後再依據不同的型別，呼叫不同的實做函式。為了能夠在同一個類別內，做到「自動毀滅 Heap 物件」與「一般非 Heap 型別」兩者的處理，在建構 Templated 化的 Dictionary 物件時，需要多傳入一個 true 或 false 的 Boolean 數值用來做為 Wrapper Class 內部的判斷依據：</p>
<pre name="code" class="cpp">
// 建構一個處理「一般非 Heap 型別」的 Dictionary 物件
Dictionary< int, float, false >* pkDict = new Dictionary< int, float, false >();
// 建構一個處理「Heap 型別物件」的 Dictionary 物件
Dictionary< int, TestObjectType*, true >* pkAutoPtrDict = new Dictionary< int, TestObjectType*, true >();
</pre>
<p>同樣地，在 Dictionary 類別宣告中需要新增一個 Boolean 型別的 template parameter 成為：</p>
<pre name="code" class="cpp">
template < class TKey, class TValue, bool UseAutoPtr >
class Dictionary
{
    // ...
};
</pre>
<p>將介面更改完畢之後，就能夠開始處理原先無法實做的刪除程序細節了。以 RemoveAt() 函式為例：</p>
<pre name="code" class="cpp">
template < class TKey, class TValue, bool UseAutoPtr >
class Dictionary
{
public:
    inline void RemoveAt(TKey _kKey);

private:
    inline void RemoveAt(TKey _kKey, Int2Type< true >);
    inline void RemoveAt(TKey _kKey, Int2Type< false >);
};
</pre>
<p>除了原有的 RemoveAt() 公開成員函式之外，在類別裡再新增了兩個同名稱的私有成員函式，其一處理自動毀滅 Heap 物件的刪除程序，其二則是處理一般非 Heap 型別的刪除：</p>
<pre name="code" class="cpp">
template < class TKey, class TValue, bool UseAutoPtr >
inline void Dictionary< TKey, TValue, UseAutoPtr >::RemoveAt(TKey _kKey, Int2Type< true >)
{
    m_kIter = m_kMap.find(_kKey);

    if (m_kIter != m_kMap.end()) {
        SAFE_DELETE(m_kIter->second);
        m_kMap.erase(_kKey);
    }
}

template < class TKey, class TValue, bool UseAutoPtr >
inline void Dictionary< TKey, TValue, UseAutoPtr >::RemoveAt(TKey _kKey, Int2Type< false >)
{
    m_kMap.erase(_kKey);
}
</pre>
<p>然後在原有公開介面的 RemoveAt() 函式中，依建構 Dictionary 物件時所傳入的 true 或 false 值，以及 Int2Type 結構的巧妙利用，以決定要呼叫哪個版本的私有成員函式：</p>
<pre name="code" class="cpp">
template< class TKey, class TValue, bool UseAutoPtr >
inline void Dictionary< TKey, TValue, UseAutoPtr >::RemoveAt(TKey _kKey)
{
    RemoveAt(_kKey, Int2Type< UseAutoPtr >());
}
</pre>
<p>還記得之前建構 Dictionary 物件時額外傳入的 Boolean 值嗎？就是利用這個傳入的 true 或 false 值做為 Int2Type 的建構參數型別，從而建構出 <strong>Int2Type< true ></strong> 與 <strong>Int2Type< false ></strong> 兩種不同的「型別」。也正是因為兩者代表著不同的型別，所以才能成為<strong>重載化 (Overloaded) 的成員函式</strong>。</p>
<ul>
<li>如果建構時傳入 <strong>true</strong>，編譯器會產生對應於 RemoveAt(TKey _kKey, <strong>Int2Type< true ></strong>) 的實做程式碼。</li>
<li>如果建構時傳入 <strong>false</strong>，編譯器會產生對應於 RemoveAt(TKey _kKey, <strong>Int2Type< false></strong>) 的實做程式碼。</li>
</ul>
<p>以上只會有其中一個狀況為真，而不論是哪個狀況成真，另一個不成立而<strong>沒有被使用到的 RemoveAt() 私有成員函式，就不會被編譯器所理會</strong>，也就因此得以逃過一劫！</p>
<p>依照上述的方式，在 RemoveAll() 函式的部分，也是以相同的方式處理：</p>
<pre name="code" class="cpp">
template < class TKey, class TValue, bool UseAutoPtr >
inline void Dictionary< TKey, TValue, UseAutoPtr >::RemoveAll()
{
    RemoveAll(Int2Type< UseAutoPtr >());
}

template < class TKey, class TValue, bool UseAutoPtr >
inline void Dictionary< TKey, TValue, UseAutoPtr >::RemoveAll(Int2Type< true >)
{
    for (m_kIter = m_kMap.begin(); m_kIter != m_kMap.end(); m_kIter ++) {
        SAFE_DELETE(m_kIter->second);
    }

    m_kMap.clear();
}

template < class TKey, class TValue, bool UseAutoPtr >
inline void Dictionary< TKey, TValue, UseAutoPtr >::RemoveAll(Int2Type< false >)
{
    m_kMap.clear();
}
</pre>
<p>按照上述的步驟進行，就能將原來重複性極高的 ValueDictionary 類別與 AutoPtrDictionary 類別，合併成為唯一一個的 <strong>Dictionary</strong> 類別了。</p>
<p>善加利用 <strong>Template</strong>，以及 <a href="http://en.wikipedia.org/wiki/Template_metaprogramming"><strong>Template Metaprogramming</strong></a> 的強大威力，將能夠使程式架構的設計與實做層面更加完善。本篇文章裡，只使用了 Template 程式設計其中非常微小的一部份技術而已，在往後的文章裡，將會陸續探討其他 Template 技術的實做與應用。</p>
<p>然而，使用上述這種 Template 實做方法並非全無缺點。要在專案中實際運用的困難點在於：<strong>業界中太少人會使用這種技術！</strong>所以在你迫不及待想要對主管與同事推銷宣揚這項技術之前，請先準備好你的工具書、參考書以及相關的應用實例。要能夠在多數團隊成員都能瞭解並熟習 Template Programming 的情況下，才能夠真正獲得使用這些 Template 技術的益處。目前來說，仍然有一段不算短的路需要努力。</p>
<p>以編譯器的能力來說，一開始對於 Template Metaprogramming 的各種設計實做程式碼，往往處於支援不完善而無法通過編譯的窘境；而如今，也已經逐漸改進並且更新至比較穩定的狀態了。雖然 IntelliSense 常常不夠聰明得以辨認出 Template 的相關語意，雖然編譯器與除錯器總會在出錯時吐出一大堆奇怪的訊息，但是做為一個程式設計者，我們絕對不能忽略這個新型（其實也不新了）且強大的技術利器。等到 <a href="http://en.wikipedia.org/wiki/C%2B%2B0x"><strong>C++0x</strong></a>（新一代的 C++ 語言標準，將加入更多新的語言特性與技術應用）將這些技術正式納入語言標準後，學習並且熟習 Template 相關技術，就不僅止是小孩子對於新奇玩具的把玩心態而已，而將會成為<strong>每個 C++ 程式設計者必備的基礎知識以及程式工具</strong>。</p>
<p>在這兩篇文章裡，提到了將 STL Container 包裝成 Wrapper Class 的一種實做方法，能夠為程式設計者免除些許未來可能遭遇的不幸厄運。而其實世界上的聰明腦袋們，還有更多更聰明的設計與實做方法。以 <a href="http://www.ogre3d.org/"><strong>OGRE</strong></a> 為例，在這個繪圖引擎的物件架構中，使用了 <a href="http://en.wikipedia.org/wiki/Iterator_pattern"><strong>Iterator Pattern</strong></a> 的方法，封裝整個引擎系統對於容器物件的巡訪行為，也是一個非常值得借鏡參考的包裝手法。詳細的作法，可以參照 <strong>Ogre::MapIterator< T ></strong> 與 <strong>Ogre::VectorIterator< T ></strong> 的文件說明。</p>
<p>完整程式碼下載：<a href='http://blog.monkeypotion.net/download-manager.php?id=2'><strong>ContainerInt2TypeImp_SampleCode.zip</strong></a> <small>(下載次數： 347 )</small></p>
<img src="http://blog.monkeypotion.net/?ak_action=api_record_view&id=58&type=feed" alt="" />]]></content:encoded>
			<wfw:commentRss>http://blog.monkeypotion.net/gameprog/advanced/container-int2type-improvement/feed</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>深入Lua-based GUI系統架構與實做細節</title>
		<link>http://blog.monkeypotion.net/gameprog/advanced/lua-based-gui-system</link>
		<comments>http://blog.monkeypotion.net/gameprog/advanced/lua-based-gui-system#comments</comments>
		<pubDate>Wed, 13 Feb 2008 16:17:31 +0000</pubDate>
		<dc:creator>半路</dc:creator>
				<category><![CDATA[進階技術]]></category>

		<guid isPermaLink="false">http://blog.monkeypotion.net/gameprog/advanced/lua-based-gui-system</guid>
		<description><![CDATA[在前篇「使用Lua實做GUI系統的遊戲實例」中介紹了 Lua 於 GUI 系統的基本用法後，本文開始進入 GUI 的核心功能層面。 一般來說，有數種不同的架構方式能夠結合 Lua 與 C++ 實做 GUI 系統。其一是將 Lua Script 當作純粹資料描述用的程式碼，僅儲存 UI Layout 相關的資料（如前篇文章所示），而由 C++ Code 掌控核心功能並且讀取 Lua Script 進行資料的處理。其二則是於 C++ 端實做出一組完整的 UI Widget 類別，然後再將這組 Widget 的所有函式、甚至所有類別，註冊給 Lua 端自行呼叫使用。 另一種方法則是在 Lua Script 中包含資料描述以及核心功能，將 GUI 系統的全部相關功能全權交由 Lua 端處理。而 C++ 端程式，則負責發送鍵盤與滑鼠的輸入訊息給 Lua 端程式，供 GUI 系統判斷各種 UI 事件。最後再將測試的結果，傳回給 C++ 端程式進行後續判斷與處理。本文將使用這一種架構來實做 Lua-based GUI [...]]]></description>
			<content:encoded><![CDATA[<p><img src='http://blog.monkeypotion.net/wp-content/uploads/2008/02/cpp-to-lua-architecture.jpg' alt='cpp-to-lua-architecture' class='alignleft'/>在前篇<a href="http://blog.monkeypotion.net/gameprog/advanced/lua-based-gui-system-from-commercial-game"><strong>「使用Lua實做GUI系統的遊戲實例」</strong></a>中介紹了 Lua 於 GUI 系統的基本用法後，本文開始進入 GUI 的核心功能層面。</p>
<p>一般來說，有數種不同的架構方式能夠結合 Lua 與 C++ 實做 GUI 系統。其一是將 Lua Script 當作純粹資料描述用的程式碼，僅儲存 UI Layout 相關的資料（如前篇文章所示），而由 C++ Code 掌控核心功能並且讀取 Lua Script 進行資料的處理。其二則是於 C++ 端實做出一組完整的 UI Widget 類別，然後再將這組 Widget 的所有函式、甚至所有類別，註冊給 Lua 端自行呼叫使用。</p>
<p>另一種方法則是在 Lua Script 中包含<strong>資料描述</strong>以及<strong>核心功能</strong>，將 GUI 系統的全部相關功能<strong>全權交由 Lua 端處理</strong>。而 C++ 端程式，則負責<strong>發送鍵盤與滑鼠的輸入訊息</strong>給 Lua 端程式，供 GUI 系統判斷各種 UI 事件。最後再將測試的結果，傳回給 C++ 端程式進行後續判斷與處理。本文將使用這一種架構來實做 <strong>Lua-based GUI 系統</strong>。</p>
<p><span id="more-43"></span></p>
<p>使用上述的架構，在 C++ 端只需要實現唯一一個類別：<strong>GuiManager</strong>，做為 <a href="http://en.wikipedia.org/wiki/Fa%C3%A7ade_pattern"><strong>Facade</strong></a> 介面與遊戲引擎的其他系統溝通。然後在遊戲主迴圈的更新程序中，呼叫 GuiManager::Update() 函式進行 GUI 系統相關的更新程序；而在遊戲主迴圈的繪圖程序中，呼叫 GuiManager::Render() 函式將控制權遞交給 Lua 端程式，以進行 GUI 系統的繪圖流程。其他與 GUI 系統相關的操作，例如鍵盤事件與滑鼠事件，同樣是在移動滑鼠或按下鍵盤按鍵時，呼叫相對應的函式，並傳入滑鼠的座標或是按下的按鍵，以供 Lua 端程式進行判斷處理。這裡以 GuiManager 類別的部分程式碼為例：</p>
<pre name="code" class="cpp">
// @file GuiManager.cpp

void GuiManager::Render() {
    g_ScriptManager->CallFunction("GuiRender", "Gui");
}

bool GuiManager::OnMouseDown(eMouseButton button) {
    bool bHandled = false;

    g_ScriptManager->CallFunction("OnMouseDown", "Gui", button);
    g_ScriptManager->GetReturnValue(bHandled);

    return bHandled;
}

bool GuiManager::OnMouseMove(int x, int y) {
    bool bHandled = false;

    g_ScriptManager->CallFunction("OnMouseMove", "Gui", x, y);
    g_ScriptManager->GetReturnValue(bHandled);

    return bHandled;
}
</pre>
<p>對整個 Lua-based GUI 系統的架構有了基礎的概念，並且瞭解 GUI 系統的 C++ 端如何運作之後，接著先看看在遊戲中使用 GUI Script 的實例：</p>
<pre name="code" class="lua">
Frame
{
    name = "ingame",
    x = 0, y = 0,
    width = 20, height = 20,
    backdrop = "Image/bk.png",

    Button
    {
        name = "ingame_main",
        x = 0, y = 0,
        width = 15, height = 20,
        graphics = StandardButtonGraphics,

        mouse_up =
            function()
                Gui.ShowFrame("main_menu");
                Core.SetGameState(GAME_PAUSE);
            end,
    };
}
</pre>
<p>上述這段程式碼，定義了一個名稱為 <em>in_game</em> 的 Frame 元件，位於螢幕座標 (0, 0) 的位置，長度與寬度的大小都是 20 個像素，背景圖片使用 <em>Image/bk.png</em>。然後在這個 UI Frame 中內含了一個 Button 元件，當「放開滑鼠按鍵」的事件產生時，會執行 <em>mouse_up</em> 函式內的程序，顯示出名稱為 <em>main_menu</em> 的 UI Frame，並同時將遊戲的 State 設定為暫停的狀態。</p>
<p>由於這裡是利用<strong>「將 Table 當作物件建構參數」</strong>的技巧，建立起 UI Frame 與 Widget 的階層架構，所以元件的建立順序為<strong>由內而外進行</strong>；也就是說，在上述 GUI Script 的實例中，會先呼叫並且執行 Button() 函式後，才會執行 Frame() 函式。</p>
<pre name="code" class="lua">
function Button(t)
    local widget = ButtonData:Instance(t);

    widget.displaylists["normal"] = CreateWidgetGraphic(widget, "normal");
    widget.displaylists["hover"] = CreateWidgetGraphic(widget, "hover");
    widget.displaylists["pushed"] = CreateWidgetGraphic(widget, "pushed");
    widget.displaylists["disabled"] = CreateWidgetGraphic(widget, "disabled");
    widget.displaylists["current"] = widget.displaylists["normal"];

    table.insert(g_TempWidgets, widget);
end
</pre>
<p>在 Button() 函式中，先利用 <a href="http://www.lua.org/pil/16.html"><strong>Lua 的物件導向設計能力</strong></a>，具現化出一個 <strong>ButtonData</strong> 物件。然後使用 CreateWidgetGraphic() 函式，創建 Button 元件在各種狀態中所應顯示的圖片，包括：一般狀態 (Normal)、滑鼠移過 Button 的狀態 (Hover)、滑鼠按下按鍵的狀態 (Pushed)，與禁止使用的狀態 (Disabled)。</p>
<p>在 CreateWidgetGraphic() 函式裡，會由 Lua 端程式呼叫 C++ 端程式以建立起 UI Frame 所需的繪圖資源。這裡所使用的是 OpenGL 的 <strong>Display List</strong> 資源；藉由傳入 Vertex Coordinates、Texture Coordinates與 Texture ID，呼叫 C++ 端的繪圖引擎程式碼，產生出相對應的 Display List，然後再將 ID 回傳給 Lua 以供後續的繪圖程序使用。</p>
<p>將 Button 元件建立完成後，當 C++ 端程式傳來滑鼠事件時，就能夠在 ButtonData:OnMouseMove() 函式中，處理 Button 元件對於滑鼠移動事件的程序：</p>
<pre name="code" class="lua">
function ButtonData:OnMouseMove()
    if (self.disabled) then
        return;
    end

    if (IsPicked(self)) then
        if (not self.is_mouse_down) then
            SetButtonState(self, "hover");
            if (not self.sound) then
                Audio.Play("../Data/Sound/menu_rollover.ogg");
                self.sound = true;
            end
        end
    else
        SetButtonState(self, "normal");
        self.is_mouse_down = false;
        self.sound = false;
    end
end
</pre>
<p>在 ButtonData 物件中，還可以定義如 OnMouseDown()、OnMouseUp() 與 OnDisabled() 等函式，以處理各種不同功能作用的事件。</p>
<pre name="code" class="lua">
function ButtonData:OnMouseDown()
    if (self.disabled) then
        return;
    end

    if (IsPicked(self)) then
        SetButtonState(self, "pushed");
        self.is_mouse_down = true;
        Audio.Play("../Data/Sound/menu_click.ogg");

        if (self.mouse_down ~= nil) then
            self:mouse_down();
        end
    end
end
</pre>
<p>在 Button() 函式的程序處理完成後，如果 Frame 中還有其他的 UI 元件如 Picture 或 Label 等等，也會一一建置處理並將這些 UI Widget 全部插入 <em>g_TempWidgets</em> 中，直到所有內含於 Frame 的 UI Widget 都處理完畢後，最終才會處理 Frame() 函式。</p>
<pre name="code" class="lua">
function Frame(t)
    g_GuiFrames[t.name] = {};

    local var = g_GuiFrames[t.name];
    var.x = t.x or 0;
    var.y = t.y or 0;
    var.width = t.width or 32;
    var.height = t.height or 32;

    if (t.backdrop ~= nil) then
        var.texture = Graphics.CreateTexture(t.backdrop);
    end

    -- Frame display list
    var.displaylist = CreateQuad(var.width, var.height, var.texture)

    -- Widgets in temp table
    var.widgets = {};
    for widget in IterateTable(g_TempWidgets) do
        -- Translate widget vertices
        widget.x = widget.x + var.x;
        widget.y = widget.y + var.y;
        table.insert(var.widgets, widget);
    end
end
</pre>
<p>在 Frame() 函式的處理程序中，首先以 Frame 的名稱做為索引鍵值，將 Frame 物件加入預先定義好的 <em>g_GuiFrames</em> 中後，再依需求創建 Frame 背景圖的 Display List。最後，對之前建立完成插入 <em>g_TempWidgets</em> 中的 UI Widget 一一進行必要的處理。</p>
<p>在遊戲主迴圈進行繪圖程序時，由 C++ 端的 GuiManager 物件呼叫 Lua 端的 GuiRender() 函式：</p>
<pre name="code" class="lua">
function GuiRender()
    for frame in IterateTable(g_ActiveFrames) do
        frame:OnRender();

        for key, widget in pairs(frame.widgets) do
          widget:OnRender();
        end
    end
end
</pre>
<p>在 GuiRender() 函式的程序中，對於目前所有的<strong>有效 UI Frame</strong> 進行處理：首先交由 <strong>Frame 物件</strong>本身進行背景繪製與其他程序的處理，然後再將控制權交給 Frame 底下的<strong>每個 UI Widget</strong> 進行繪圖處理。以 Frame 與 Button 元件的 OnRender() 函式為例：</p>
<pre name="code" class="lua">
function FrameData:OnRender()
    Graphics.ApplyTransform2D(self.x, self.y);
    Graphics.DrawDisplayList(self.displaylist);
    Graphics.RestoreTransform();
end

function ButtonData:OnRender()
    Graphics.DrawDisplayList(self.displaylists.current);
end
</pre>
<p>參考以上的方法與說明，就能夠一步步建立起一個極具彈性與威力的 <strong>Lua-based GUI 系統</strong>。</p>
<p>將整個 GUI 系統建立完成後，更進一步的功能加強與改進，可以考慮使用<strong>多執行緒</strong>模式，使 GUI 系統在遊戲主迴圈外獨自擁有一個執行緒的資源。這樣就能夠減少遊戲程式的反應時間，即使是在進行漫長的 I/O 程序或複雜的繪圖運算時，玩家也能夠繼續操作部分的 GUI 行為，而不會使遊戲程式顯得好像完全失去反應作用與回應能力一樣。</p>
<p>對以上 <strong>Lua-based GUI 系統</strong>的架構與實做有什麼看法？有想到能夠改善這個架構的作法或可能性？或者是有其他結合 Lua 與 C++ 的實做方法？不論是任何意見都歡迎提出討論喔～</p>
<img src="http://blog.monkeypotion.net/?ak_action=api_record_view&id=43&type=feed" alt="" />]]></content:encoded>
			<wfw:commentRss>http://blog.monkeypotion.net/gameprog/advanced/lua-based-gui-system/feed</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
		<item>
		<title>使用Lua實做GUI系統的遊戲實例</title>
		<link>http://blog.monkeypotion.net/gameprog/advanced/lua-based-gui-system-from-commercial-game</link>
		<comments>http://blog.monkeypotion.net/gameprog/advanced/lua-based-gui-system-from-commercial-game#comments</comments>
		<pubDate>Wed, 30 Jan 2008 16:12:44 +0000</pubDate>
		<dc:creator>半路</dc:creator>
				<category><![CDATA[進階技術]]></category>

		<guid isPermaLink="false">http://blog.monkeypotion.net/gameprog/advanced/lua-based-gui-system-from-commercial-game</guid>
		<description><![CDATA[在這篇文章裡，將以國外一個非常著名的休閒遊戲 Diner Dash 2 的實例，探討 Lua 的應用功能之一：GUI 系統。自前篇「Scripting系統概論與Lua簡介」對 Lua 的 What（是什麼）與 Why（為什麼使用）有了初步的認識之後，接下來的重點就是瞭解 Lua 的 How（如何使用）。 在進入 How 的主題之前，先補充一下 Where（在何處使用）的概念。Lua 的能力十分強大，似乎無所不能，可以完成任何的任務與功能。但是就實際應用層面的考量來說，哪些功能與系統才是真正適合使用 Lua 的部分呢？又要如何區分 C++ 語言與 Lua 語言所能達成的任務呢？ 以兩者擅長處理的事項來分類： C++：低階核心、記憶體管理、資源管理、繪圖底層、數學運算、訊息發送。 Lua：使用者介面、人工智慧、資料管理、遊戲邏輯、動態行為。 複雜的運算與低階的底層核心，適合使用 C++；而因應設計需求，經常會變動的各種功能系統，例如使用者介面、人工智慧、遊戲邏輯等等就適合使用 Lua 來完成。另外，有安全性考量的功能層面，也應該使用 C++ 撰寫比較合適。 在 Diner Dash 2 這款遊戲中的圖形化使用者介面，也就是常聽到的 GUI（或簡稱為 UI）系統，都是以 Lua 進行開發設計的。首先，需要瞭解一點基本的 GUI 開發概念。在一個完整的 GUI 系統中，需要許多的基本元件，例如常見的按鈕 (Button)、文字 (Text)、圖片 (Picture)、編輯輸入 (Edit)、進度條 (Progress Bar) 等等，這些物件稱為介面元件 [...]]]></description>
			<content:encoded><![CDATA[<p>在這篇文章裡，將以國外一個非常著名的休閒遊戲 <a href="http://www.playfirst.com/game/dinerdash2"><strong>Diner Dash 2</strong></a> 的實例，探討 Lua 的應用功能之一：<strong>GUI 系統</strong>。自前篇<a href="http://blog.monkeypotion.net/gameprog/beginner/introduction-of-scripting-system-and-lua"><strong>「Scripting系統概論與Lua簡介」</strong></a>對 Lua 的 <strong>What（是什麼）</strong>與 <strong>Why（為什麼使用）</strong>有了初步的認識之後，接下來的重點就是瞭解 Lua 的 <strong>How（如何使用）</strong>。</p>
<p>在進入 How 的主題之前，先補充一下 <strong>Where（在何處使用）</strong>的概念。Lua 的能力十分強大，似乎無所不能，可以完成任何的任務與功能。但是就實際應用層面的考量來說，哪些功能與系統才是真正適合使用 Lua 的部分呢？又要如何區分 C++ 語言與 Lua 語言所能達成的任務呢？</p>
<p>以兩者擅長處理的事項來分類：</p>
<blockquote>
<ul>
<li><strong>C++</strong>：低階核心、記憶體管理、資源管理、繪圖底層、數學運算、訊息發送。</li>
<li><strong>Lua</strong>：使用者介面、人工智慧、資料管理、遊戲邏輯、動態行為。</li>
</ul>
</blockquote>
<p>複雜的運算與低階的底層核心，適合使用 C++；而因應設計需求，經常會變動的各種功能系統，例如使用者介面、人工智慧、遊戲邏輯等等就適合使用 Lua 來完成。另外，有安全性考量的功能層面，也應該使用 C++ 撰寫比較合適。</p>
<p><span id="more-33"></span></p>
<p><img src='http://blog.monkeypotion.net/wp-content/uploads/2008/01/ui-widget-architecture.jpg' alt='UI Widget Architecture' class='alignleft'/>在 <strong>Diner Dash 2</strong> 這款遊戲中的<strong>圖形化使用者介面</strong>，也就是常聽到的 <strong>GUI</strong>（或簡稱為 <strong>UI</strong>）系統，都是以 Lua 進行開發設計的。首先，需要瞭解一點基本的 GUI 開發概念。在一個完整的 GUI 系統中，需要許多的<strong>基本元件</strong>，例如常見的按鈕 (Button)、文字 (Text)、圖片 (Picture)、編輯輸入 (Edit)、進度條 (Progress Bar) 等等，這些物件稱為<strong>介面元件 (UI Widget)</strong>；藉由這些基礎的元件，就能夠組合出遊戲中所使用的各種 UI。而將個別的元件集合起來形成一個群組的容器，常稱做框架 (Frame) 或圖層 (Layer)。最後，在架構頂層做為上述這些 UI Widget 的管理者，包含住一至多個 Frame，負責高階功能與介面的元件，則稱做視窗 (Window) 或是對話盒 (Dialog)。</p>
<p>一般常見的 GUI 系統實做方法，是在 C++ 中將各個元件封裝成 Class，例如 CText、CButton、CPicture 等等，加上 CFrame 與 CDialog 類別，分別負責各自的功能，然後再以這些類別創建出來的物件，組合成一個一個的完整介面。這樣的實做方法，在物件導向程式的領域中是很直覺的設計模式，但是也存在著不少缺點；像是每個元件都需要存在個別獨立的 Class，而若要修改或新增元件的功能，即使只是很微小的部分，也必須在 C++ 中撰寫完畢後，重新建置整個程式專案完成後才能夠使用。另外，在處理資料的讀取程序中，也會有相當瑣碎且重複性極高的修改步驟；只要更動了資料的讀取順序或資料型態，同樣非得經過重新建置的程序不可。</p>
<p><strong>Lua，可以讓一切變得不同。</strong>以下，開始進入遊戲的實例說明：（閱讀以下內容，需具備基本的 Lua 程式設計能力）</p>
<p>先來看一段使用 Lua 製作 Text 元件的簡短程式碼：</p>
<pre name="code" class="lua">
Text
{
    font = DialogTitleFont, -- 使用 DialogTitleFont 字型
    name = "oktitle", -- 元件名稱為 oktitle
    x = 12, y = 8, -- 元件的位置
    w = kMax,h = 40, -- 元件的長度與寬度
    flags = kVAlignCenter + kHAlignLeft, -- 對齊用的旗標
    label = gDialogTable.title, -- 顯示的文字
};
</pre>
<p>上述這段程式碼，即定義出一個 Text 文字項元件的種種規格；如註解所示，這個元件使用 DialogTitleFont 這個字型，在指定的位置處顯示出  gDialogTable.title 變數的內容。而 DialogTitleFont 這個字型，定義在其他的程式碼中：</p>
<pre name="code" class="lua">
DialogTitleFont = {
    standardFont, -- 又是另一個變數
    18, -- 字級大小
    BorderColor -- 字型顏色
};

standardFont = "fonts/mercurius.mvec"; -- 字型使用的實體檔案
BorderColor = Color(30, 42, 102, 255); -- 顏色
</pre>
<p>由以上的程式碼片段可以瞭解，UI 元件的建構，是利用 Lua Function 能夠<strong>接受 Table 結構做為函式參數</strong>的強大特徵，將所需的參數傳入 Function 中進行處理。如果是沒有特別指定的參數，就直接使用預設值去建立，能夠毫不費力地建構出 Default Parameters 的功能，使得 UI 元件的建構方式變得非常具有彈性與可擴充性。</p>
<p>再來看一個製作 Button 元件的例子：</p>
<pre name="code" class="lua">
Button
{
    x = kCenter,
    y = 500,
    font = StandardButtonFont,
    graphics = StandardButtonGraphics,
    name = "back",
    type = kPush,
    flags = kHAlignCenter + kVAlignCenter,
    label = "back",
    command =
        function()
            PopModal();
        end
};

StandardButtonGraphics = {
    "buttons/dialog_button_a1",
    "buttons/dialog_button_a2",
    "buttons/dialog_button_a3"
};
</pre>
<p>在 <em>StandardButtonGraphics</em> 這個 Table 裡，定義了 Button 在 Normal、Pushed、Disabled 三個狀態下顯示的圖片。接下來的 <em>type</em> 變數，指定這個 Button 的型態是一般常見的 Push Button 類型；其他還包括了 Radio Button 與 Check Button 類型。而 <em>command</em> 變數，是使用者按下 Button 時所需執行的程序，也就是去呼叫 PopModal() 這個函式。除了一般按下 Button 所觸發的執行程序之外，也能夠很輕易地實做出滑鼠進入 Button 範圍的 Function，或是 Button 切換至其他狀態下所應該觸發的 Function。其他的各種元件也以同樣的方法製作，就能夠迅速而便利地建立起一組 UI 元件與完整的使用者介面。</p>
<p>依照以上 <em>standardFont</em>、<em>StandardButtonGraphics</em> 與 <em>BorderColor</em> 變數的定義方法，可以將所有 UI 相關的<strong>格式設定</strong>，以及與<strong>實體檔案</strong>相關的變數，全部定義在另外的 Lua 檔案中，便於集中管理、修改與使用。其中的程式碼片段如下：</p>
<pre name="code" class="lua">
-- File: style.lua

-- 字型的實體檔案
standardFont = "fonts/mercurius.mvec";

-- 定義各種顏色
BlackColor = Color(0, 0, 0, 255);
BlueColor = Color(16, 225, 226, 255);
YellowColor = Color(255, 255, 0, 255);

-- 標準按鈕用的圖片
StandardButtonGraphics = {
    "buttons/dialog_button_a1",
    "buttons/dialog_button_a2",
    "buttons/dialog_button_a3"
};

-- 小型按鈕用的圖片
SmallButtonGraphics = {
    "buttons/dialog_button_a_small_1",
    "buttons/dialog_button_a_small_2",
    "buttons/dialog_button_a_small_3"
};

-- 大型按鈕用的圖片
LargeButtonGraphics = {
    "buttons/dialog_button_a_large_1",
    "buttons/dialog_button_a_large_2",
    "buttons/dialog_button_a_large_3"
};
</pre>
<p>最後，以遊戲中的一個 Lua 檔案為例，列出建構起一個 Dialog 所需的完整程式碼：</p>
<pre name="code" class="lua">
-- File: ok.lua
require( "scripts/style.lua" );

MakeDialog
{
    Bitmap
    {
        name = "yesnobackground",
        image = "backgrounds/popup",
        x = kCenter,
        y = kCenter,

        Button
        {
            font = StandardButtonFont,
            graphics = StandardButtonGraphics,
            close = true,
            flags = 5,
            label = "ok",
            name = "ok",
            default = true,
            x = kCenter,
            y = 250,
        },

        Text
        {
            font = DialogTitleFont,
            name = "oktitle",
            x = 12, y = 8,
            w = kMax, h = 40,
            flags = kVAlignCenter + kHAlignLeft,
            label = gDialogTable.title,
        };

        Text
        {
            font = DialogBodyFont,
            name = "okbody",
            x = 20, y = 46,
            w = kMax-20, h = kMax-70,
            flags = kVAlignCenter + kHAlignCenter,
            label = gDialogTable.body,
        };
    },
}
</pre>
<p>以上這段 Lua 程式碼，製作出由一個 Bitmap、一個 Button 以及兩個 Text 元件所組成的 Dialog 介面。撰寫完成後，在遊戲中看到這個 Dialog 所呈現出來的結構，如圖所示：</p>
<p><img src='http://blog.monkeypotion.net/wp-content/uploads/2008/01/dialog-structure.jpg' alt='Dialog Structure' class='alignright'/>Bitmap 元件負責顯示整個 Dialog 的背景圖片；第一個 Text 用來顯示 Dialog 的標題列文字，第二個 Text 則顯示 Dialog 中的主要說明文字；而最後的 Button，就只是用來執行簡單的確認行為。所有的元件建構完成後，就當作 Function 的 Table 參數傳入 MakeDialog() 中創建出 Dialog。</p>
<p>以上文章所提的內容，其實只佔了整個 GUI 系統其中一半的部分：<strong>資料描述</strong>。也就是將 Lua 當成一種 <strong>Data Description Language</strong>，用來儲存與讀取遊戲所需的資料。藉由 Lua 的強大威力，能夠讓開發者輕易地進行各種資料寫入與讀取的動作，完全不必費力撰寫繁複瑣碎的 Parse 或 Serialize 程序。</p>
<p>資料處理的部分是一半，而另一半是與功能相關的部分，也就是真正在內部進行 <strong>GUI 相關行為處理</strong>的程序。將資料傳入 Text()、Button()、Picture() 這些函式中之後，裡面到底做了些什麼？如何建構 GUI 系統的資料結構？如何和原來的 C++ 端主程式相結合？如何傳遞滑鼠與鍵盤的輸入訊息？如何真正畫出這些 UI？</p>
<p>下篇待續。</p>
<p>（聲明：本文中的程式碼範例，版權皆屬 Diner Dash 2 原開發者所有。）</p>
<img src="http://blog.monkeypotion.net/?ak_action=api_record_view&id=33&type=feed" alt="" />]]></content:encoded>
			<wfw:commentRss>http://blog.monkeypotion.net/gameprog/advanced/lua-based-gui-system-from-commercial-game/feed</wfw:commentRss>
		<slash:comments>8</slash:comments>
		</item>
	</channel>
</rss>
