如何编写 C++ 游戏引擎

2023-06-03 0 611

(点选

校对:庞德公新浪网 – 李大萌  英语:Jeff Preshing

http://blog.jobbole.com/113960/

前段时间我在用 C++ 写格斗游戏发动机,皮德盖那个发动机做了一个终端端小格斗游戏跳一跳(Hop Out)。上面是截人格的 iPhone6 的两个小短片。

跳一跳是我想玩的格斗游戏类别:3D米老鼠外形的复古风红白机格斗游戏。最终目标是发生改变每一充填块的色调,就像Q * Bert那样。

Hop Out仍在合作开发中,但发动机的机能早已很健全了,因此我想在这儿撷取许多有关发动机合作开发的基本功。

你为何想写两个格斗游戏发动机?可能将有许多其原因:

你是个工头,讨厌Cubzac创建掌控系统,直至掌控系统顺利完成。

有关格斗游戏合作开发你想了解更多。我在格斗游戏金融行业组织机构工作了14年,那时我依然在时不时的思量。我即使不确认我与否能Cubzac撰写两个发动机,即使它与小型梦工厂的程式设计组织机构工作的日常生活职能大相径庭。我想晓得标准答案。

你讨厌掌控。对全然依照你想的形式组织标识符,晓得迪阿尔库这儿,深感令人满意。

你能从AGI(1984),id Tech 1(1993),Build(1995)等经典之作格斗游戏发动机和Unity和Unreal等金融行业巨擘那儿赢得意念。

你相信我们那个游戏产业应该试着去揭合作开发动机发展的序幕。我们并没有掌握制作格斗游戏的艺术。还离得很远!我们对那个过程的研究越多,改进的机会就越大。

2017年的格斗游戏平台 – 手机,格斗游戏机和电脑 – 非常强大,而且在许多方面都非常相似。格斗游戏发动机的合作开发并不是像过去那样,在脆弱和怪异的硬件上挣扎。在我看来,更多是有关自己制造出来的复杂性的斗争。创造两个怪物很容易!这就是为何本文建议围绕着保持事情可控的其原因。我把它分成三部分:

使用迭代方法

在统一事物前要三思

请注意,序列化是两个很大的课题

那个建议适用于任何类别的格斗游戏发动机。我不会告诉你怎样撰写着色器,八叉树是什么,或者怎样添加物体。这些事儿,都是我假设你早已晓得而且应该晓得 – 这很大程度上取决于你想制作的格斗游戏类别。相反,我故意选择了许多似乎没有被广泛承认或提及的观点 – 这些是我在试图揭开两个主题神秘面纱时最感兴趣的许多观点。

使用迭代方法

我的第一条建议是使许多东西(任何东西),快速运行起来,然后迭代。

如果可能将的话,从两个示例应用程序开始,初始化设备并在屏幕上绘制许多东西。就我而言,我下载了SDL,打开了Xcode-iOS / Test / TestiPhoneOS.xcodeproj,然后在我的iPhone上运行了testgles2示例。

如何编写 C++ 游戏引擎

瞧!我使用OpenGL ES 2.0,生成了两个可爱的旋转立方体。

下一步,是下载两个其他人制作的马里奥3D 模型。我写了两个快速和粗糙的OBJ文件加载器 – 文件格式并不太复杂 – 并且修改了例程,来呈现Mario,而不是两个立方体。我还集成了SDL_Image来帮助加载纹理。

如何编写 C++ 游戏引擎

然后我实现了两个双摇杆掌控器用来操控马里奥(我本来想创建的是两个双摇杆设计格斗游戏,并不是马里奥。)

如何编写 C++ 游戏引擎

接下来,我想探索骨骼动画,因此我打开了Blender,做了两个触手模型,并且用两个前后摆动的双骨架来操纵它。

如何编写 C++ 游戏引擎

此时,我放弃了OBJ文件格式,撰写了两个Python脚本来从Blender导出自定义的JSON文件。这些JSON文件描述了皮肤网格,骨架和动画数据。在C ++ JSON库的帮助下将这些文件加载到格斗游戏中。

如何编写 C++ 游戏引擎

一旦那个顺利完成,我回到了Blender,并做了更详细的角色设计。 (这是我创造的第两个被操纵的3D人,我为他深感骄傲。)

如何编写 C++ 游戏引擎

在接下来的几个月里,我采取了以下几个步骤:

开始将向量和矩阵函数分解成我自己的3D数学库。

用CMake项目替换.xcodeproj。

在Windows和iOS上运行发动机,即使我讨厌在Visual Studio下组织机构工作。

开始将标识符终端到单独的“发动机”和“格斗游戏”库中。随着时间的推移,我把它们分成更细粒度的库。

写了两个单独的应用程序将我的JSON文件转换为格斗游戏能直接加载的二进制数据。

最终从iOS版本中删除所有SDL库。 (Windows版本依然使用SDL。)

重点是:在开始程式设计之前,我没有对发动机架构进行设计。这是两个经过深思熟虑的选择。相反,我只是写了实现下两个特性的最简单的标识符,然后我会查看标识符,看看会出现什么自然生成的架构。我说的“发动机架构”是指组成格斗游戏发动机的模块集,这些模块之间的依赖关系,和用于与每一模块交互的API

如何编写 C++ 游戏引擎

这是两个迭代比较。显然,我假设你在使用某种源标识符管理工具

你可能将会认为这种方法浪费了许多时间,即使总是在撰写糟糕的标识符,之后需要清理。但是大部分的清理操作都是将标识符从两个.cpp文件终端到另两个,将函数声明提取到.h文件中,或者直接进行简单的修改。决定事情应该去哪是难点,但是这在早已有标识符的时候会更容易决定。

我认为用相反的方法:试图设计出两个能够提前顺利完成所有需求的架构,会浪费更多的时间。我最讨厌的两篇有关掌控系统过度设计风险的文章是Tomasz Dąbrowski 的《泛化的恶性循环》和 Joel Spolsky 的《不要让架构太空人吓到你》

我并不是说在用标识符处理问题之前,不应该在纸上进行设计。我也不是说你不应该事先决定你想的机能。比如,我从一开始就晓得我想让我的发动机在后台线程中加载所有资源。我只是没有尝试设计或实现该机能,直至我的发动机首先加载许多资源。

迭代的方法给了我两个比我以前盯着一张白纸冥思苦想更优雅的架构。我的发动机的iOS版本那时是 100% 原始标识符,包括自定义数学库,容器模板,反射/序列化掌控系统,渲染框架,物理模块和音频混合器。我能撰写每两个模块,但是你可能将没有必要自己写所有这些东西。你可能将会发现适合自己发动机的许多优秀的开源标识符库。GLMBullet Physics 和 STB 头文件只是许多有趣的例子。

在整合事物太多之前要三思

作为程序员,我们尽量避免标识符重复,讨厌标识符遵循统一的风格。不过,我认为不要让这些本能凌驾于每两个决定之上。

偶尔要抵制一下 DRY 原则

举个例子,我的发动机包含了几个“智能指针”模板类,与 std :: shared_ptr 类似。每两个指针作为两个原始指针的包装,有助于防止内存泄漏。

<> 是用于具有单个所有者的动态分配的对象。

Reference<> 使用引用计数来允许两个对象拥有多个所有者。

audio :: AppOwned <> 被音频混音器以外的标识符调用,允许格斗游戏掌控系统拥有音频混音器使用的对象,例如当前播放的语音。

audio :: AudioHandle <> 使用音频混音器内部的引用计数掌控系统。

这样可能将看起来像其中许多类复制了其它的机能,违反 DRY(不要重复自己)的原则。事实上,在合作开发早期,我尽可能将地重用现有的Reference <>类。但是,我发现音频对象的生命周期是由特殊规则来管理的:如果两个音频语音早已顺利完成了两个样本的播放,并且格斗游戏没有指向该语音的指针,那么该语音会被立即到删除排队等待。如果格斗游戏持有指针,则不应删除那个语音对象。如果格斗游戏持有两个指针,但指针的所有者在语音结束之前被销毁,这段语音应该被取消,而不是增加Reference <>的复杂性,我决定引入单独的模板类,这样更为实用。

95% 的时间都在重用现有的标识符。但是,如果你开始深感麻痹,或者发现自己增加了一件简单的事情的复杂性,那就问自己,标识符库中的东西与否应该是两件事。

能使用不同的调用规则

我不讨厌Java的一件事是,它强迫你在两个类中定义每一函数。在我看来,这是无稽之谈。这可能将会使你的标识符看起来更加一致,但是它也鼓励过度工程,并且不适合我前面描述的迭代方法。

在我的 C++ 发动机中,许多函数属于类,有些则不属于类。例如,格斗游戏中的每一敌人都是两个类,可能将就像你预料的那样,大部分敌人的行为都是在那个类内部实现的。另一方面,在我的发动机中投射的球体是通过调用sphereCast() 函数来执行的,这是物理命名空间中的两个函数。 sphereCast() 不属于任何类 – 它只是物理模块的一部分。我构建了两个掌控系统来管理模块之间的依赖关系,这使得我的标识符组织机构得很好。将那个函数包装在两个任意的类中不会以任何有意义的形式改善标识符的组织机构。

然后是动态调度,这是一种多态的形式。我们经常需要为两个对象调用两个函数,而不晓得该对象的确切类别。 C ++程序员的第一本能是用虚函数定义抽象基类,然后在派生类中重写这些函数。这是有效的,但这只是一种技术。还有其他动态调度技术,不会引入额外的标识符,或带来其他好处:

C ++ 11引入了std :: function,这是存储回调函数的两个简便方法。也能撰写自己的std :: function版本,这样在调试中不会那么痛苦。

许多回调函数能用一对指针来实现:两个函数指针和两个类别不确认的参数。它只需要在回调函数中进行明确的转换。你在纯C语言库中经常看到。

有时候,底层类别实际上是在校对时已知的,你能绑定那个函数调用而不用额外的运行开销。 Turf是我在格斗游戏发动机中使用的两个库,它非常依赖这种技术。例如看到turf:: Mutex,这只是针对特定平台类的定义。

有时,最直接的方法是自己构建和维护两个原始函数指针表。我在我的音频混音器和序列化掌控系统中使用了这种方法。Python解释器也大量使用这种技术,如下所述。

你即使能将函数指针存储在散列表中,使用函数名称作为关键字。我使用这种技术来调度输入事件,如多点触控事件。这是记录格斗游戏输入并用重放掌控系统回放的策略的一部分。

动态调度是两个很大的课题。我只是想表明,有许多方法来实现它。你撰写的可扩展底层标识符越多(这在格斗游戏发动机中很常见),越会发现替代方法越多。如果你不习惯这种程式设计,C语言撰写的Python解释器是两个很好的学习资源。它实现了两个强大的对象模型:每一PyObject都指向两个PyTypeObject,每一PyTypeObject都包含两个用于动态分配的函数指针表。如果你想直接跳转到其中的话,定义新类别的文档是两个很好的起点。

注意序列化是两个大问题

序列化是将运行时对象转换为字节序列的操作。换句话说,就是保存和加载数据。

对于许多格斗游戏发动机来说,格斗游戏内容以各种可编辑的格式创建,例如.png,.json,.blend或专有格式,然后最终转换为特定于平台的能快速加载到发动机的格斗游戏格式。流水线中的最后两个应用通常被称为“炊具”。炊具可能将被集成到另两个工具,即使分布在几台机器上。通常,炊具和许多工具是与格斗游戏发动机本身一起合作开发和维护的。

如何编写 C++ 游戏引擎

在创建这样的流水线时,每一阶段的文件格式的选择取决于你。你能定义自己的许多文件格式,这些格式可能将会随着添加发动机机能而变化。渐渐地可能将会发现有必要保持某些程序与以前保存的文件兼容。不管什么格式,你最终都需要用C++来序列化它。

用C ++实现序列化有无数种方法。两个相当明显的形式是将加载和保存函数添加到要序列化的C ++类。能通过在文件头中存储版本号来实现向后兼容,然后将那个数字传递给每一加载函数。这是可行的,尽管这样标识符可能将维护起来比较繁琐。

voidload(InStream& in,u32 fileVersion){

// 加载预期的成员变量

in >> m_position;

in >> m_direction;

// 仅当正在加载的文件版本是2或更大时才加载新的变量

if(fileVersion >= 2){

in >> m_velocity;

}

}

通过反射(特别是通过创建描述C ++类别布局的运行时数据),能撰写更灵活,不容易出错的序列化标识符。想快速介绍反射怎样进行序列化,请看一下开源项目Blender是怎样实现的。

如何编写 C++ 游戏引擎

从源标识符构建Blender时,有许多步骤。首先,校对并运行两个名为makesdna的自定义实用程序。该实用程序解析Blender源标识符树中的一组C语言头文件,然后以SDNA的自定义格式输出所有C定义类别的汇总。那个SDNA数据作为反射数据,链接到Blender本身,并保存在Blender写入的每一.blend文件中。从这一刻开始,每当Blender加载两个.blend文件,就会将.blend文件的SDNA与链接到当前版本的SDNA进行比较,并使用通用序列化标识符来处理差异。那个策略使Blender具有令人印象深刻的向前和向后兼容性。你依然能在最新版本的Blender中加载1.0版本的文件,也能在旧版本中加载新的.blend文件。

像Blender那样,许多格斗游戏发动机及其相关工具都会生成并使用自己的反射数据。有许多方法能做到这一点:能像Blender那样解析自己的C / C ++源标识符来提取类别信息。你能创建两个单独的数据描述语言,并撰写两个工具来从该语言生成C ++类别定义和反射数据。能使用预处理器宏和C ++模板在运行时生成反射数据。一旦你有反射数据可用,有无数的方法来撰写两个通用的序列化器。

显然,我省略了许多细节。在这篇文章中,我只想表明有许多不同的方法来序列化数据,其中许多非常复杂。程序员不会像其他发动机掌控系统那样讨论序列化,尽管大多数其他掌控系统依赖于它。例如,在GDC 2017给出的96个程序设计讲座中,我数了一下,共有31次有关图形,11次有关新浪网,10次有关工具,4次有关AI,3有关物理模块,2有关音频的 – 但只有两个直接涉及到序列化

至少,试着想一想你的需求会有多复杂。如果你正在制作两个像Flappy Bird这样的小格斗游戏,只有少数资源.,那么你可能将不需要想太多的序列化。你能直接从PNG加载纹理,这样很好处理。如果你需要两个向后兼容的紧凑的二进制格式,但不想自己合作开发,能看看第三方库,比如Cereal或者Boost.Serialization。我不认为Google协议缓冲区是序列化格斗游戏资产的理想选择,但是值得研究。

撰写两个格斗游戏发动机,即使是两个小格斗游戏发动机,也是两个很大的任务。有关那个我能说的还有许多,但是对于那个长度的帖子来说,这真的是我认为最有用的建议:迭代地组织机构工作,抵制统一标识符的冲动,并且晓得序列化是两个大问题,你需要选择两个合适的策略。根据我的经验,如果忽视这些事情,每一件事情都可能将成为两个绊脚石。

我讨厌比较这些东西,真的很想听到其他合作开发人员的意见。如果你早已写了两个发动机,你的经验与否让你有什么相同的结论吗?如果你没有写,或者只是在构思,我也对你的想法也很感兴趣。你认为何是好的学习资源?哪些部分对你来说看起来很神秘?你能在上面评论或在Twitter上给我留言!

看完本文有帮助?请分享给更多人

如何编写 C++ 游戏引擎

如何编写 C++ 游戏引擎

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务