跨平台游戏编程与 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
类还具有过滤仅要加载的场景部分的方法。