跨平臺遊戲程式設計與 gameplay3d/gameplay3d 設計理念
本章概述了 gameplay3d 框架背後的部分基本設計原則。
與某些其他程式語言(例如 Java)不同,C++ 沒有自動垃圾回收機制。因此,需要跟蹤動態建立的物件並在需要時刪除它們。當代碼的多個部分引用同一個物件時,即存在“共享物件”時,可能會出現困難。
例如,考慮以下虛擬碼
// Create a boss enemy and aim the camera towards it
GameObject* pBossEnemy = new BossEnemy;
cutSceneCamera.setTarget(pBossEnemy);
//... some code ...
// Some time later, the boss is destroyed
delete pBossEnemy;
//... some more code ...
// Ooops, cutSceneCamera now references a dangling pointer!
cutSceneCamera.updateOrientation();
Gameplay3d 處理此問題的辦法是使用引用計數。基本目標是確保 (i) 在程式碼的任何部分仍在使用物件時,不銷燬該物件;以及 (ii) 一旦不再使用該物件,就將其刪除。透過在物件內部儲存引用計數來實現此目標。每次共享物件時,引用計數都會遞增,每次程式碼的一部分停止使用物件時,引用計數都會遞減。當計數達到 0 時,表示不再有任何人引用該物件,因此可以將其銷燬。
引用計數在 gameplay3d 中是透過使用從Ref 類繼承的類來實現的。原始碼的相關部分(可以在 Ref.h 和 Ref.cpp 中找到完整的程式碼和註釋)如下所示
class Ref
{
public:
void addRef(); // Increments the reference count
void release(); // Decrements the reference count
unsigned int getRefCount() const;
protected:
Ref();
Ref(const Ref& copy);
virtual ~Ref();
private:
unsigned int _refCount;
}
void Ref::addRef() {
++_refCount;
}
void Ref::release() {
if ((--_refCount) <= 0) {
delete this;
}
}
unsigned int Ref::getRefCount() const {
return _refCount;
}
在大多數情況下,您不需要直接呼叫 addRef()。相反,您將透過呼叫以 create* 開頭的靜態函式來建立物件(例如Mesh::createMesh()、Font::create()、Scene::create() 等)。這些函式返回從 Ref 繼承的類的例項,該例項的引用計數設定為 1。因此,需要在不再使用物件時呼叫 release()。通常,最好的做法是利用 gameplay3d 的SAFE_RELEASE() 宏,該宏既呼叫 release() 又將指標設定為 NULL。
您還應該注意,某些將物件作為引數的 gameplay3d 函式會增加該物件的引用計數。從直觀的角度來看,這是因為作為引數傳遞的物件現在與程式碼的另一部分共享。以下是一些示例
- Model::create() 方法,它以Mesh* 作為引數。(這將返回指向引用計數為 1 的 Model 的指標,並將 Mesh 的引用計數增加 1。)
- Texture::Sampler::create() 方法,它以Texture* 作為引數;以及
- Model 類中的setMaterial() 成員函式,它以Material* 作為引數。
一個非常簡單的示例(取自示例瀏覽器專案中的 CreateSceneSample.cpp),展示了 gameplay3d 的引用計數系統在實踐中如何工作,如下所示
void CreateSceneSample::initialize()
{
// Create the font for drawing the framerate.
_font = Font::create("res/ui/arial.gpb");
// Create a new empty scene.
_scene = Scene::create();
// ... omitted code setting up camera and lights ...
// Create the cube mesh and model.
Mesh* cubeMesh = createCubeMesh();
Model* cubeModel = Model::create(cubeMesh);
// Release the mesh because the model now holds a reference to it.
SAFE_RELEASE(cubeMesh);
// ... omitted code setting up the material for the cube model ...
// Add a node to the scene, then attach the cube model to it
_cubeNode = _scene->addNode("cube");
_cubeNode->setModel(cubeModel);
_cubeNode->rotateY(MATH_PIOVER4);
// Release the model because the node now holds a reference to it.
SAFE_RELEASE(cubeModel);
}
void CreateSceneSample::finalize()
{
SAFE_RELEASE(_font);
SAFE_RELEASE(_scene);
}
最後,值得注意的是,gameplay3d 的DebugMem 構建配置提供了一種有用的方法來檢查您是否已成功記住釋放所有 Ref 物件。在退出程式時,除錯輸出視窗將報告所有未能釋放 Ref 物件的錯誤,以及程式碼中出現的任何其他記憶體洩漏。
資料驅動設計的基本原則是在程式碼中放置需要頻繁更改的行為效果很差。解決此問題的一種方法是將遊戲行為從程式碼中移到資料檔案中。這應該會帶來更高效的調整內容和遊戲玩法的過程。
Gameplay3d 支援資料驅動設計,它允許從基於文字的資料檔案載入某些遊戲配置資料。這些檔案包括
- 用於高階設定的game.config 檔案(通常包含螢幕解析度、Lua 指令碼設定、預設 UI 主題等);
- 用於整體場景佈局的.scene 檔案(通常包含對其他資料檔案的交叉引用);
- 用於物理物件配置的.physics 檔案;
- 用於材質配置的.material 檔案;
- 用於 UI 主題的.theme 檔案;
- 用於 UI 表單的.form 檔案(使用 UI 主題);
- 用於動畫細節的.animation 檔案(例如,配置遊戲內可使用的命名動畫“片段”);以及
- 用於 Lua 指令碼的.lua 檔案。
有關這些資料檔案結構以及如何編寫您自己的檔案的更多詳細資訊將在與它們相關的主題章節中提供。
平臺抽象背後的基本理念是,儘可能地將平臺無關程式碼與平臺特定程式碼分離,以最大程度地提高程式碼重用率。
Gameplay3d 的設計理念是僅在必要時使用平臺特定程式碼。需要使用平臺特定程式碼的明顯領域包括
- 視窗建立;
- OpenGL 初始化;
- 訊息泵;以及
- 輸入處理。
但是,由於 gameplay3d 會自動處理這些任務,或允許使用者透過其平臺抽象 API 處理這些任務,因此通常可以在自己的專案中避免使用平臺特定程式碼。
如果您希望檢視(或修改)平臺特定程式碼,大多數程式碼都整齊地放在以下原始碼檔案中
- gameplay-main-android.cpp
- gameplay-main-blackberry.cpp
- gameplay-main-ios.mm
- gameplay-main-linux.cpp
- gameplay-main-macosx.mm
- gameplay-main-windows.cpp
- PlatformAndroid.cpp
- PlatformBlackberry.cpp
- PlatformiOS.mm
- PlatformLinux.cpp
- PlatformMacOSX.mm
- PlatformWindows.cpp
雖然 gameplay3d 目前不包含任何基於 GUI 的內容建立工具(除了“sample-particles” 專案中的粒子編輯器),但它支援各種現有的行業標準檔案格式,這意味著您可以在現有的內容建立包中建立資產,並將這些資產匯入您的 gameplay3d 專案。
某些資產可以直接匯入 gameplay3d,而其他格式則需要使用 gameplay-encoder 可執行檔案轉換為記錄的 gameplay 包格式 (.gpb)。需要進行額外的轉換步驟的原因是,儘管這些格式很流行,並且在工具選項中得到最廣泛的支援,但它們不被認為是高效的執行時格式。將它們轉換為二進位制格式可確保資產在平臺硬體限制內儘可能快地以最高質量載入。
以下列出了不需要轉換為.gbp格式的受支援外部檔案格式。
- .ogg 用於音訊;
- .wav 用於音訊(雖然建議在釋出遊戲時使用壓縮的 .ogg 格式);
- .png 用於影像檔案;
- .dds 和 .pvr 用於壓縮紋理;以及
- .lua 用於 Lua 原始碼。
以下列出了需要進行轉換的受支援外部檔案格式:
- .fbx (Autodesk) 用於建立 3D 場景和模型;以及
- .ttf (TrueType Font) 用於字型。
可以使用 gameplay-encoder 工具將資產轉換為 .gbp 格式。gameplay-encoder 可執行工具預先構建了 Windows 7、MacOS X 和 Linux 版本。它們位於 <gameplay-root>/bin 資料夾中。一般用法為
用法:gameplay-encoder [options] <file(s)>
您可以透過在不帶任何引數的情況下執行 gameplay-encoder 來顯示受支援選項的列表。
即使 gameplay-encoder 工具已經預先構建,您可能也希望對其進行自定義並自行重新構建它。要構建 gameplay-encoder 專案,請在 Visual Studio 或 XCode 中開啟 gameplay-encoder 專案,並構建可執行檔案。
字型和場景的內容管道如下所示
- 識別所有必需的 TrueType 字型和 FBX 場景檔案。
- 執行 gameplay-encoder 可執行檔案,傳入字型或場景檔案路徑和可選引數,以生成檔案的 gameplay 二進位制版本 (.gpb)。
- 打包您的遊戲,並將 gameplay 二進位制檔案作為二進位制遊戲資產包含在內。
- 使用
gameplay::Bundle類載入任何二進位制遊戲資產。
使用 C++ 遊戲原始碼中的 gameplay::Bundle 類將編碼的二進位制檔案載入為包。該類提供載入字型和場景的方法。場景被載入為節點的層次結構,各種實體附加到它們。這些實體包括諸如網格幾何體或網格組,以及相機和燈光。gameplay::Bundle 類還具有過濾僅要載入的場景部分的方法。