上述过程成功后,我接着使用 Blender 制作更加精致的人物。下图展示了我制作出的第一个可操控的 3D 人物。
后来我又做了一大堆的工作,不过这里我想强调的重点是,我没有在动手编程之前先规划好引擎架构。事实上,每当要添加一个新特性时,我只着眼于用最简单的代码将其实现,然后观察这些代码,看看它们自然而然呈现出的是一种什么架构。这里所讲的引擎架构,指的是组成游戏引擎的模块集、模块之间的依赖关系,以及模块之间交互所使用的 API 。
况且在我看来,先绞尽脑汁地想出一个你认为能满足未来所有需求的架构,然后再着手编程,会比迭代开发浪费更多的时间。这里推荐一下我最喜欢的关于介绍过度工程危害的两篇文章,一篇是 Tomasz Dbrowski 的 The Vicious Circle of Generalization ,另一篇是 Joel Spolsky 的 Don’t Let Architecture Astronauts Scare You 。
在建立这样一个管道时,其中每个阶段的文件格式都由你设定。你也许会自己定义一些文件格式,这些文件格式可能会随着引擎功能的不断添加演变。随着它们的演变,有一天你或许会发现必须使某些程序与以前保存的文件格式保持兼容。但是,无论何种格式,你最终都得用
C++ 进行序列化。
C++ 实现序列化的方法数不胜数,一个比较容易想到的方法是在你想要序列化的 C++ 类中添加 load 函数和 save 函数。在文件头部中存储版本号,然后将版本号传递到每个 load 函数中,你就可以实现向后兼容性。这种办法可行,不过可能导致代码非常冗杂而难以维护。
void load(InStream& in, u32 fileVersion) {
// Load expected member variables
in >> m_position;
in >> m_direction;
// Load a newer variable only if the file version being loaded is 2 or greater
if (fileVersion >= 2) {
in >> m_velocity;
}
}
不过我们可以写出更灵活、更不容易出错的序列化代码,这里用到了反射(reflection)),具体来讲是创建描述 C++ 类型布局的运行时数据。如果想要快速了解一下如何在序列化时使用反射,可以看看开源项目 Blender 。
当你从源代码构建 Blender 时,会发生许多事情。首先,一个名为 makesdna 的程序会被编译并运行。这个程序会解析 Blender 源树中的一组 C 头文件,然后输出一个包含了被称为 SDNA 的自定义格式的文件,该文件中存放了这些头文件内部定义的所有 C 类型的紧凑摘要,这些 SDNA 数据就是反射数据(reflection data)。