Database Hot Loader 三部曲:Implementing Observer Entity

Observer Entity
Observer Entity

在前篇文章裡,以 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 類別來說,會和 triangle 與 speed 這兩張資料表具有依存關係;而對於 Quad 類別來說,則是和 quad 與 speed 這兩張資料表具有依存關係。

在 Observer 設計模式中,將會以 Triangle 類別與 Quad 類別負責扮演 Observer 的角色,而每張資料表則扮演 Subject 的角色。為了達成 Observer 設計模式的架構,首先宣告一個 ObserverEntity 純虛擬介面類別:

// @file ObserverEntity.h

class ObserverEntity
{
public:
    virtual void Notify(std::string& aspect) = 0;
};

接著處理 Subject 所需的訂閱功能,在原先用來處理資料的 LuadDatabaseManager 類別中,加入幾項新的成員函式與成員變數:

// @file LuaDatabaseManager.h

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

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

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

	SubscribersList m_SubscribersList;
};

其中,由 Subscribe() 函式提供公用方法給外部的 Observer,使它們能夠自由訂閱自己感興趣的主題,也就是個別的資料表格。而 NotifySubscribers() 函式則是在 Subject 產生變化時,負責通知相對應的 Observer 們:

// @file LuaDatabaseManager.cpp

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

void LuaDatabaseManager::NotifySubscribers(std::string& 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);
	}
}

回過頭來,在 Observer 這一方,需要使 Triangle 與 Quad 類別繼承自 ObserverEntity 介面類別以符合 Observer 的身份,然後再接著覆寫 Notify() 純虛擬函式:

// @file Triangle.h

class Triangle : public ObserverEntity
{
public:
	virtual void Notify(std::string& subject);
};

為了建立起 Observer 與 Subject 之間的聯繫關係,Triangle 與 Quad 類別需要先在建構式裡,以資料表的名稱向 LuaDatabaseManager 類別訂閱 Subject。當全部或者部分資料表格發生變動時,例如在重新載入 Lua 檔案之後,就能夠藉由呼叫 Notify() 函式的方式通知所有訂閱者,然後交由各個 Observer 自行操作相關的資料處理程序。以 Triangle 類別為例:

// @file Triangle.cpp

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

void Triangle::Notify(std::string& 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);
	}
}

最後,只需要在 LuaDatabaseManager 類別載入資料庫檔案後,使用 NotifySubscribers() 通知訂閱者即可:

// @file LuaDatabaseManager.cpp

bool LuaDatabaseManager::RunScript(std::string& 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;
}

要做到動態熱載入資料的功能,其中很關鍵的一點,就是遊戲物件的「初始化程序」與「重新載入程序」最好能夠達到完全相同的程度,否則會使得遊戲程式裡充滿許多重複的程式碼,因而造成不必要的系統複雜度。在 DHL 系統裡,借助了 Observer 設計模式的威力,如前述程式碼所示,不論是物件初始化或者是重新載入遊戲資料,都是使用相同的程序;這樣一來,即使在遊戲資料編輯完成後,也不需要大費周章地將程式碼替換成另外一套資料載入程序。

只要利用以上 Observer 設計模式的概念與技巧,套用到遊戲系統的玩家角色、裝備道具、魔法技能或者場景物件之中,就能夠順利地將遊戲資料與遊戲內容天衣無縫地接合起來,達成原先設計以及實作 DHL 系統的預設目標:在不需要重新啟動遊戲程式的情況下,讓企畫設計者能夠即時調整遊戲的資料庫數據!

在次篇最終回的 Database Hot Loader 系列裡,將演出感人肺腑的精彩團圓大結局,探索幾項效能優化的可能性,並且實際進行效能的量測,與一般常見的資料庫系統實作方式進行評比。

執行檔下載:DatabaseHotLoader_Ep3_Release.zip(需要先行安裝 vcredist_x86.exe(下載次數: 1603 )
原始碼下載:DatabaseHotLoader_Ep3_Source.zip (下載次數: 2200 )

Leave a Reply