Home > 圖像編程 | 腳本編程 | 遊戲編程 > 開始實作引擎的基本類別

開始實作引擎的基本類別

話說之前想做 Collada 滙入,便最基本要有一個內部存放 Collada 滙入後的資料。這個「雙休日」(內地常用語) 就留在家中乖乖寫程式……寫不出的時候也看了十幾集動畫。

架構上參考了之前看過的 Unity,採用 Component 型式。目前實現的幾個類別的關係如下圖:

採用 Component 架構的一個好處是,減少了傳統 Scene Graph 的類別繼承和節點 (node) 數目。一個實體 (entity, 有些引擎稱為 Game Object) 可以有 Mesh 元件,也可以同時加入腳本、碰撞物件等元件。

實作過程中出現許多問題,和大家 (及將來的自己) 分享一下。

物件擁有權 (Ownership)

之前做的 Lua 和 C# 接口並沒有理會物件擁有權的問題。但是如果 C++、C# 和 Lua 要同時取得一個物件的參考,問題就出現了,一方建立的物件可能被另一方刪去。其中一個解決方法是 Reference Counting,每方取得的物件的參考都計算 reference count。但實作起來比較複雜,也會有其他 Reference Counting 都會有的問題,例如 cyclic reference。

這兩天為這個問題想了很久,目前使用一個最簡單的方案: 這些新建的類別的擁有權皆由 C++ 方掌管,每個物件由一個物件全權擁有 (所以類別圖中用黑鑽代表 Composition),而 C# 或 Lua 只能拿到臨時的參考。

只要把 Swig 定義檔裡的 constructor 和 destructor 設定為 pravate,C# 和 Lua 便不能直接建立物件和取得物件的擁有權。如果設定為 public 的話 ,在 Lua 建立的物件歸 Lua 管理 (使用 GC),而 C++ 建立的歸 C++ 管理。

Lua 或 C# 要生成物件,就要呼叫 factory method,例如 Entity* World::CreateEntity()。這函數會建立物件並記錄在 World 裡,代表那個 World 物件擁有該 Entity,而 Lua 則取得一個 Entity 的臨時參考。

local game = Engine.Game_Instance()
local world = game:GetWorld()

self.sphere1 = world:CreateEntity()
self.sphere1:SetName("sphere1")

Swig 不直接支持 downcast

另一個遇到的問題是 downcast。

C++ 中可以利用 static_cast<> 或 dynamic_cast<> (如開啟了 RTTI) 去做 downcast:

Mesh *mesh = static_cast(entity->CreateComponent(MeshComponentType));
mesh->SetGeometry(geometry); // SetGeometry() 只定義在 Mesh 而不在 IComponent 

在 Swig 生成的 Lua API 便不可以

local mesh = entity:CreateComponent(Engine.MeshComponentType)
mesh:SetGeometry(geometry) -- Error: SetGeometry is nil
-- 因為 mesh 實際上只是 IComponent 的 wrapper,沒有 SetGeometry 函數

Swig 裡沒有簡單的方法做 downcast,只能靠編寫 C++ 函式去執行。在不污染 Entity 類別的前題下 (例如加入像 Mesh *CreateMesh() 等為每個 Component 的介面),只好在每個 Component 中加入 Cast() 函式:

Mesh* Mesh::Cast(IComponent *component) {
	if (component->GetComponentType() == MeshComponentType)
		return static_cast(component);
	else
		return 0;
}
local mesh = Engine.Mesh_Cast(self.sphere1:CreateComponent(Engine.MeshComponentType))
mesh:SetGeometry(geometry) -- OK

這個辦法是可行的,不過腳本比較難看。或許以下的辦法比較好好:

local mesh = Engine.Mesh_Create(self.sphere1)
mesh:SetGeometry(geometry) -- OK

不過 Entity::GetComponent() 也是傳回 IComponent*,Downcast 是比較通用的解決方案。

Entity 的層階 (Hierarchy) 關係

Entity 之間擁有層階關係,這闗係一般是用來作為層階式 Transform 所用的,例如人物的骨架 (skeleton) 由多個層階式的骨頭 (bone) 組成,上一層的 World Transform 會影響下一層的 World Transform。

這種層階關係可以用樹表達。這次我使用 intrusive 的方式記錄,每個節點可擁有 0 個或 1 個 parent、first child 和 next sibling。而這些關係並不直接影響 ownership,所有 Entity 皆為 World 擁有及管理。

層階的操作只需兩個: void Entity::Link(parent) 和 void Entity::Unlink()。前者把一個 Entity 認作父親;後者就是和父親斷絕關係。

目前為了簡單起見,World Transform 的計算是每次呼叫函式時重新計算。之後才找個辦法減少重複的運算吧 (但不希望要顯式地遍歷更新)。

const Math::Matrix44& Entity::GetWorldTransform() const {
	if (mParent) {
		mWorldTransform = mLocalTransform * mParent->GetWorldTransform();
		return mWorldTransform; // 因為要傳回 reference,所以加了一個 mutable
	}
	else
		return mLocalTransform;
}

結語

要盡量在編寫程式時先想好設計是很困難的,沒有一個完滿的設計可以解決將來會發生的所有問題。我在這兩天也曾經猶豫了很久,不敢敲鍵盤。我目前希望能以最短的時間寫出能滿足一些很簡單的目標,例如滙入及顯示一個 Collada Mesh、用鍵盤控制物件移動等。希望透過敏捷的方式,每次都能做出一點點看得到的成果出來,而不是 Bottom-up 去寫一堆預想會用到的代碼。方案有問題嗎?不要緊,簡單和有測試程式的代碼能給你力量去 Refactor!

接下來就應該可以開始做 Collada 滙入吧。

Comments:2

半路 08-09-11 (四) 16:09

很不賴的初始架構!
目前我也是使用 Component 的架構,以減少類別的階層關係並且達到元件復用性的目標。
同時我也讓 C++ 端管理物件的生成毀滅,Lua 端只能夠取得參考以操作物件行為。

另外在 Entity::GetWorldTransform() 裡,
或許可以用一個 dirtyFlag 來記錄是否需要重新計算 World Transform?
如此一來,就不需要每次進行 GetWorldTransform() 時都做矩陣相乘了。

UoU 08-10-07 (二) 11:03

Milo 加油~
^_^

Comment Form
Remember personal info

Trackbacks:0

Trackback URL for this entry
http://miloyip.seezone.net/wp-trackback.php?p=67
Listed below are links to weblogs that reference
開始實作引擎的基本類別 from Milo的遊戲開發

Home > 圖像編程 | 腳本編程 | 遊戲編程 > 開始實作引擎的基本類別

Search
Feeds
Meta

Return to page top