@Milo Yip 大大所翻译的游戏引擎架构中有一张图,解释了一个游戏引擎运行时架构是怎样的:
事实上,我在写GameMachine时,并没有太留意这种图,现在回过头来看,非常巧合地,确实是按照这样的架构来设计的。下面就来解释一下,在GameMachine中是怎么样的架构。
GameMachine从图中的第4、5层开始编写起,也就是第三方的库和软件包。目前实现了用DirectX11、OpenGL(3.3+)来进行绘制,使用Bullet3来实现物理,STL库作为容器。利用操作系统提供的各种API,实现了平台检测、原子数据(主要是std::atomic)、文件系统、线程库(Windows API和pthread)、时钟(QueryPerformanceFrequency系列函数)。在此基础之上,编写了很多实用类,如带SIMD的数学库、对齐内存分配器、字符串库、调试类、内存泄漏检测、智能指针、性能分析、随机数生成(C++11标准库)等。
再向上层走,GameMachine定义了一个资产管理器类GMAsset,用于管理资产生命周期。纹理、模型等等,在GameMachine中都称为资产。一个资产可能会被多个实例所引用,因此每个资产都有一个引用计数,如果一个资产的计数变为0,那么这个资产将会被释放。
同时,GameMachine定义了一个低阶的绘制接口IGraphicEngine,用于进行图元的绘制、混合、模板绘制、阴影绘制、字形管理等。当然,在DirectX和OpenGL下,它们会对应着不同的实例。任何上层的绘制都应该通过IGraphicEngine,而非直接调用OpenGL或者DirectX的函数。
所有GameMachine中的游戏对象都派生于GMGameObject,它包含一个对象的几何部分(mesh)和物理部分。几何部分将传递给GPU,物理部分将在运行时进行更新。
所有的游戏事件,都由窗口来传递。在不同操作系统下,窗口都不同,如Windows下使用HWND和消息循环,X11下使用XEvent等。因此,GameMachine抽象了IWindow,来对窗口进行操作。窗口的操作应该依赖IWindow,而不能依赖操作系统。IWindow将系统原生的消息,翻译成GameMachine自定义的事件,这样上层就不需要关心程序到底跑在哪个操作系统上了。
GameMachine自己写了一套UI,包括按钮、滚动条、单行/多行文本框等。毕竟,没有UI的引擎不能算是一个引擎。GameMachine UI可以让用户自定义控件,可以随意更改它的外观,并达到不错的效果。
同时,GameMachine内部也封装了一套Lua,通过封装的函数,可以非常方便将GameMachine的对象传入Lua。同时,也能很方便从Lua拿到一个GameMachine对象。
代码风格与命名规范:
代码采用tab缩进,除了非公开的类型(如cpp文件中定义的类型)、数学库和容器,其他类型都以GM开头,并且放在gm的命名空间中。
如果一个方法创建的对象是从参数返回,那么用户需要自己管理它的生命周期,例如:
如果某个类接受一个指针,且此指针参数带有AUTORELEASE,表明此类将管理此指针所指对象的生命周期,例如:
GameMachine中的基类:
就如同Qt的QObject一样,GameMachine大部分的类继承GMObject。如果一个类继承了GMObject,那么它有了以下超能力:
1. 默认屏蔽了它的拷贝构造、拷贝赋值、移动构造和移动赋值。
2. 可以为它连接一个信号,指向一个槽函数。在此信号被激发时,槽函数被调用。类似Qt。
3. 为自己的成员增加元信息
这点或许是最重要的。成员的元信息是指,某个类可以导出自己有哪些成员,名字叫什么,是什么样的类型。通过元信息,可以很方便来序列化/反序列化某个GMObject,可以很方便地和lua通信。
所有的GMObject由两部分组成,它的方法和它的成员。GameMachine有意将两者分开:
GMObject子类只包含一个指向数据成员的指针作为成员,其他都是函数。这类似于我们所说的pImpl手法。宏GM_PRIVATE_OBJECT(类型名)表示我们定义一个struct,用于作为某种类型的数据,宏GM_DECLARE_PRIVATE(类型名)表示使用我们之前定义的struct作为数据。在其成员函数中,通过宏D可以拿到指向数据对象的指针:
如果GMObject不是某子类的直接基类,那么应该使用宏GM_DECLARE_PRIVATE_AND_BASE(类型名, 基类)
它会多做一个事情,就是typedef GMGameObject Base。知道自己的直接基类有时候很重要。
可以通过GM_DECLARE_PROPERTY(属性名, 成员名, 类型)来为某个数据成员创建一个get和一个set函数。如上面的
它为GMGameObjectKeyframe创建了setTranslation和getTranslation两个方法。
由于类似pImpl手法,当GMObject发生拷贝的时候,默认情况下是指针的浅拷贝,因此我们要禁用它,或至少要重载它。事实上,可以通过GM_ALLOW_COPY_MOVE宏来表示,此GMObject类是可以被拷贝、移动的。它将会被重载拷贝构造、移动构造、赋值、移动4个方法,在被拷贝/移动时,其实是对它以及它的基类的数据成员进行拷贝/移动。但是,其最终的基类GMObject的数据(如信号和槽),不会被拷贝/移动。它只拷贝/移动GMObject的子类的数据。
如果一个GMObject子类需要声明自己的元数据,那么需要重载registerMeta并返回true,并且将需要暴露的数据成员用GM_META加上:
通过调用GMObject::meta(),可以获取一个Map,里面包含了暴露出来的数据的成员名、类型、大小以及指针等信息,这些信息在绝大多数情况下已经够用了。
以上便是基本的设计思路解释。如果你的机器里面装了doxygen,那么GameMachine会为你生成doxygen文档:
不过目前很多注释都没有加上,只有一些关键的点才有注释,之后会慢慢补充。