Awesome
SGMemory
这是一个用来实现内存快照同步的技术方案。
这个项目是一个验证项目,其中逻辑并不是完整可商用的状态(商用级逻辑也不大可能开源哈)。但是正因为如此,它们的逻辑非常精炼简洁,去除了非常多烦杂的细节,方便有兴趣的同学去学习和理解。
1. 基本目的
1.1 对进程A中的一块内存生成快照。
1.2 对该内存快照进行网络传输,或者本地存档。
1.3 在进程B中加载该内存快照,然后恢复内存快照里的所有游戏对象,包括游戏对象之间的依赖关系。
2. 应用场景
2.1 帧同步游戏的断线重连
当客户端断线或者重启时,服务器可以将某个时刻的内存快照下发给客户端。将客户端的逻辑层直接恢复至该时刻,然后再在此基础上追帧到当前帧。
2.2 小内存系统的全状态同步
我们知道帧同步在MOBA和FTG游戏中被证明非常有效,但是帧同步的限制也很明显:
- (1)客户端需要遵守确定性原则(比如用定点数替代浮点数,当然还有其它更复杂的要求)。
- (2)对传输层协议的要求很高(一般要求是RUDP)。
- (3)需要实现复杂的预表现逻辑。
- (4)还有其它细节要求。
在有些情况下,状态同步会比帧同步更友好。但是,状态同步有一个最大的问题就是,你要定义好需要同步的状态,在实现网络同步模块时需要关注业务细节。而全
状态同步而可以帮助你忽略具体的状态细节。在小规模游戏中,或者某些模块(比如物理模拟)中,其逻辑层对内存的占用比较小,就可以考虑对整块内存进行同步。
2.3 单机游戏本地存档
对于单机游戏而言,本地存档是一个非常重要的功能。很多本地存储方案都是需要收集所需存档的状态,非常繁琐,与具体业务强相关。如果通过对整块逻辑内存进行存档,无疑可以极大简化存档和恢复的逻辑。
3. 基本原理
其基本原理就是对逻辑内存和逻辑对象单独管理。这里需要小心划分逻辑层与表现层的界限,对逻辑层的所有对象构造和内存分配都应该单独管理。所有表现层对逻辑层的依赖,都应该是基于对象模型指定的依赖方式(比如句柄),而非直接指针依赖。而逻辑层对表现层,则不应该有任何依赖(这是最基本的软件工程原则)。
总的来说,整个系统分为2部分:
3.1 内存管理
所有逻辑内存都需要单独管理,与引擎通用内存管理区分开。
为了实现较高的内存管理效能(性能与损耗),可以参考成功且成熟的商业引擎的通用内存管理方案。当然,这些商业的内存管理方案,其基本原理也都可以在《游戏引擎架构》一书中找到。这里无须赘述。
重点是,这里的内存管理,需要考虑到内存恢复时,如何恢复其内存结构。现代内存管理器都会将内存结构信息与待分配的自由内存共用内存块。所以在快照恢复过程中,其原始的内存结构是基于原始进程的,需要恢复成在当前进程有效的内存结构。
3.2 对象管理
内存快照恢复的重点是如何恢复对象实例,使对象实例在新的进程中依然有效。
那么很显然,需要对这些对象中的指针属性进行恢复。并且,需要防止逻辑层之上的表现层通过指针直接依赖逻辑对象。于是,需要实现如下特性:
- (1)句柄系统。表现层对逻辑层的访问都是依赖句柄的。在内存恢复时,会将句柄结构也恢复。
- (2)指针收集。在对象内部不可避免会使用到指针。需要在生成快照时,收集这些指针,与内存分配基址相减得到相对偏移地址。
- (3)自制容器。容器里会有大量的内存分配,需要使用专用的内存分配器。并且,容器里的逻辑都是性能密集型的,所以里面会使用裸指针,而非句柄。那么需要对容器的快照生成和恢复进行处理。这些逻辑都是STL等通用容器无法做到的。所以实现自定义容器是必不可少的一项工作。
- (4)类型系统。因为对象中存在一个隐藏的指针:<b>虚表指针</b>。虚表是在编译时生成的,其地址是与类型相关的,也是与进程相关的。虚表指针的赋值是在对象的构造函数里完成的。在对象恢复过程中,不应该执行额外的对象逻辑,所以无法借助对象类的构造函数来恢复虚表指针,只能通过对象的类型信息来获取与之对应的虚表地址,然后将该地址恢复给对象。
4. 项目说明
这个项目是基于CMake组织的。建议先对CMake有所了解。
如果不想关注CMake的细节,也可以直接执行Build.bat。
欢迎 Fork & Star.