作者:Simon Yeung
引言

这次我要谈论的是我的业余项目,即编写iPhone游戏引擎,这个项目从去年8月开始,由2位美工配合进行。虽然项目尚未完成,但这里我想要分享几点自己从中学到的东西。首先先来看看若干游戏截屏:

和船只战斗

探索游戏世界

探索游戏世界

和船只战斗


在这款游戏中,玩家通过控制船只探索游戏世界,发现新城市,同其他船只进行战斗。玩家还可以在游戏的进程中改变船只的模式。下文我将谈论我在此小型引擎中所运用的技巧,主要包括如下内容:
—内存管理
—工具(Maya插件和关卡编辑器)
—脚本处理(Lua)
—串流机制
— 声音(音效和背景音乐)
—运行调试
下面是几幅游戏截图及我目前所开发的工具:
游戏轮廓

游戏轮廓


编辑器

编辑器


关卡编辑器

关卡编辑器


Mac版本

Mac版本


内存管理
iPhone平台,内存是非常重要的资源。若处理不当,应用就会收到1-2个内存警告,然后你的应用就会被系统终止。所以我决定编写自己的内存配置器,预先分配大部分内存,这样我的应用就不会在运行时被操作系统终止,它可以选择运行或不运行。这是我首次编写内存配置器,它没像“准备、设定、分配”过程那么精细,但足以应对我的项目。
在我的小型引擎中,pool配置器主要用于分配内存,其存预先设定容量大小,从8、16、32及64字节到1048546字节不等。由于我的目标平台是iPhone,而极限容量104854字节通常只运用至若干高分辨率的纹理中,因此我的游戏的多数内存都集中在较小的容量规格。在程序运行期间,很多内存都已形成,它们被分成如下更小的模块以适应各pool规格:
memory Layout

memory Layout


注意,大型字节模块通常位于较小内存空间,旨在实现字节对齐。而各模块则被进一步划分成同等大小的区块,以配合特定的容量配置:
memory 8 byte

memory 8 byte


各模块中的内存区块保持呈现链接表模式,这样当pool配置器需要分配/归还内存时,它只需返回列表中的自由内存区块/将其添加回链接表中。各内存区块中的内存主要用于储存指示链接表下个自由内存区块的“下个指示器”,这样我们就不需要通过分配额外内存追踪链接表:
memory Linked List

memory Linked List


在进行分配时,配置器需要根据配置容量决定所要运用的pool模块。然后在此模块中,自由内存区块就会被送回,这只是模块链接表的顶部。在解除配置过程中,将内存分解成不同模块后,我们就能够把握各pool模块的边界地址,所以通过查看解除配置指示器的地址,我们就能够决定其属于哪个pool模块,然后我们就能够将内存送回模块的自由内存模块链接表。此方式的一个缺点是它无法验证解除配置指示器是否真正由用户配置,以及其是否双倍释放。想要“部分”克服此问题,我需要添加极限检查,以检验解除配置过程的输入内容。首先,我会检验输入内容的字节是否对齐,例如若解除配置指示器处于1048546字节模块中,那么指示器地址就必须和1048546字节对齐。其次,在分割内存模块的过程中,我们将获悉各内存模块存在多少内存区块,我们可以保留一个当前自由区块的数量(dApps注:这会随着分配和解除分配而或增或减)。若程序结束后,免费区块数量不符区块总数量,那么内存也许就被遗漏或被双倍释放。但这只能够解决部分问题。
为真正解决此问题及查看内存遗漏情况,我需要记录各个分配和解除分配操作。原本,在分配过程中,我只需要通过宏指令__FILE__,__LINE__存储返回指示器的地址及各分配操作的位置(源文件和行编号)。但这无法追踪所有内存遗漏情况,因为有些文件已被模版化,例如Bullet Physics存储库中的btAlignedAllocator.h。运用宏指令__FILE__,__LINE__只能够记录这些头文件的分配情况,这无法协助我们进行内存漏洞追踪。因此,我还通过system call backtrace()和backtrace_symbols()记录各分配过程的调用栈。然后我就能够轻松追逐所有内存遗漏情况。但记录各分配过程是个缓慢的过程,这只能够在调试版本中实现。
总之,我的内存配置器还有很多需要改善的地方,例如检验用户解除分配过程;在内存区块中添加元数据(dApps注:例如配置规模)。出于线程安全性考虑,我目前通过互斥量保护内存,未来我也许会转向无锁版本。尽管存在这些缺点,但这个配置器的运作情况还是颇令人满意,它让我们不会再收到iOS的内存警告,让项目不再出现内存碎片问题,同时还协助我们追踪内存漏洞情况。
Maya工具
工具在游戏制作中非常重要,特别是在合作伙伴不懂如何编写代码的情况下。在我的项目中,我的合作伙伴是两位美工,所以我得编写某些工具,将他们的模型输出到我的引擎中。输出模型有不同的方式,你可以选择解析.obj文件的格式,通过FBX SDK阅读.fbx文件,或是阅读COLLADA文件,但我选择直接从美工采用的模型包中提取内容(dApps注:编写Maya插件提取模型数据)。
要给输出模型编写Maya插件,我们首先得弄清数据在Maya中的存储方式。通常Maya会将多数数据存储在Directed Acyclic Graphic(DAG)。在我的项目中,我需要找出这些存储网络数据(mesh data)的DAG节点位置。我们可以按照如下方式通过迭代器MItDag访问DAG:
MStatus status;
MItDag dagIter( MItDag::kDepthFirst, MFn::kInvalid, &status );
MDagPathArray meshPath; // store the DAG nodes that contains mesh
for ( ; !dagIter.isDone(); dagIter.next())
{
MDagPath dagPath;
status = dagIter.getPath( dagPath );
if ( status )
{
MFnDagNode dagNode( dagPath, &status );
// Filter out the DAG nodes that do not contain mesh
if ( dagNode.isIntermediateObject()) continue;
if ( !dagPath.hasFn( MFn::kMesh )) continue;
if ( dagPath.hasFn( MFn::kTransform )) continue;
meshPath.append(dagPath);
}
}

然后我们可以按照如下方式通过MFnMesh获得DAG中的网络数据:
for(int i=0; i< meshPath.length(); ++i) { MDagPath dagPath= meshPath[i]; MFnMesh fnMesh( dagPath ); MPointArray meshPoints;// store the position of vertices fnMesh.getPoints( meshPoints, MSpace::kWorld ); // get more mesh data such as normals, UV… }
想要获得更多细节内容,不妨参考《MAYA API How-To》和《Maya Exporter Factfile》。得到网络数据后,你就可以通过创建MPxFileTranslator的子类输出这些内容,取代函数writer()。
我选择编写插件而不是解析.fbx/COLLADA的另一原因和提取动画数据有关。在我的项目中,我只需要输出若干嵌入关键图框之间的简单动画数据,我想要获得美工基于Maya定义的关键图画。我尝试运用FBX SDK,但在输出动画数据时,它会将所有动画图像都聚集成关键帧。通过COLLADA所获得的结果则更加糟糕,我无法在Mac平台找到适合Maya的输出装置。而编写Maya插件能够消除所有这些问题,获得我想要的数据。我要编写一个脚本文件,帮助美工设定动画剪辑数据:
maya Anim Clip

maya Anim Clip


输出网络数据后,我觉得在Maya中编辑触碰几何图形是个很不错的主意,所以我编写另一插件定义模型的触碰形状:
maya Physics Exporter

maya Physics Exporter


插件的运作方式和Dynamica Plugin非常相似(dApps注:但作者的插件只是基于球体、盒子和胶囊图形定义出简单形状)。我的插件无法在Maya中进行物理仿真,它只能够定义触碰形状。这些触碰形状只是MPxLocatorNode的子类,它推翻draw()方式,通过调用openGL渲染出内容的对应形状。
总之,直接从Maya中提取网络数据并不困难。我们可以获得所有数据,例如顶点法线、UV集合和关键帧数据,无需担心基于其他格式输出内容会导致的数据丢失情况。Maya还提供获得这些数据的便捷API,这非常容易掌握。熟悉Maya API后,我还可以编写另一插件定义触碰形状。下次当你需要输出网络数据时,你也许会考虑直接从模型包中提取相关内容,而不是解析一个文件格式。