子系统的启动和终止程序员
C++的静态初始化次序不可用,由于C++在调用程序进入点(main())以前,全局及静态对象已经被构建,而咱们对这些构造函数的调用次序不可预知。算法
游戏引擎的子系统,常见的设计模式是为每一个子系统定义单例类。经常使用的单例模式的实现方法,难以控制它的析构次序,并且一个获取管理器的实例的方法,可能会有很高的开销。所以,游戏开发中,直接采用简单粗暴的方法。数据库
具体来讲,就是将构造和析构函数留空,内部不作任何事情,直接在main函数中按须要的次序调用自定义的启动和终止函数。编程
class RenderManager{ public: RenderManager(){} ~RenderManager(){} void startUp(){ //自定义的启动函数 } void shutDown(){ //自定义的终止函数 } }; class PhysicsManager{/*同上面相似*/ }; class AnimationManager{/*同上面相似*/ }; class MemoryManager{/*同上面相似*/ }; RenderManager gRenderManager; PhysicsManager gPhysicsManager; AnimationManager gAnimationManager; MemoryManager gMemoryManager; int _tmain(int argc, _TCHAR* argv[]) { //启动各个子系统 gMemoryManager.startUp(); ... gRenderManager.startUp(); gAnimationManager.startUp(); gPhysicsManager.startUp(); //运行游戏 //终止各个子系统 gPhysicsManager.shutDown(); gAnimationManager.shutDown(); gRenderManager.shutDown(); ... gMemoryManager.shutDown(); return 0; }
内存管理设计模式
动态内存分配效率较低:数组
所以,游戏开发中,维持最低限度的堆分配,而且永不在紧凑循环中使用堆分配。缓存
经常使用的定制分配器服务器
堆栈分配器数据结构
先预分配一块连续的内存,堆栈分配器管理这块内存;后面内存分配经过分配器的堆栈指针来实现,分配和释放内存便是指针的移动。ide
注意:
堆栈分配器释放内存时次序必须是分配时相反的顺序,而不是任意的。实现方法,能够不容许释放个别的内存块,而是由分配器提供一个函数,每次将堆栈指针回滚至标记的位置(它会释放这之间的多个内存块)。这个标记是位于分配的内存块之间的边界。
双端堆栈分配器
与堆栈分配器相似,可是,双端堆栈分配器在内存块的两端各有一个堆栈分配器,两个堆栈指针从两边向中间靠拢。
池分配器
游戏引擎可能会用到大量大小相同的小块内存,此时,能够用池分配器。
池分配器也会与分配一大块内存,大小时分配的元素的整数倍。池中的每一个元素存放在一个自由链表中,池分配器收到分配请求时,将链表中的一个元素取出,分配出去;释放元素时,把元素从新挂到链表中就能够了。
自由元素的链表可实现为单链表,单链表的next指针能够直接存在每一个元素内;若是元素的尺寸小于指针,能够用索引代替指针。
对齐功能的分配器
全部内存分配器都必须传回对齐的内存块,实现中,只需在分配内存时,分配多一点内存,而后将内存地址上调至对齐地址,最后传回调整的地址。多分配的内存字节等于对齐的字节。如何调整内存地址?即如何计算对其须要的最小字节数,这个很简单,这里省略。
这样释放时如何释放正确大小的内存呢?实现方式能够再调整好的内存地址的前一个字节处记录实际分配的内存大小,释放时按照实际的大小来释放。
单帧和双缓冲内存分配器
单帧分配器和堆栈分配器相似,只是单帧分配器在每帧开始时,会将堆栈指针重置为内存块的底端地址。这样就不须要手动释放内存,由于每帧开始就会自动释放前一帧的全部分配的内存。可是这也意味着,该分配器分配的内存只在当前帧上有效。
双缓冲分配器与单帧分配器的区别在于,它有两个大小相同的单帧分配器,它会交替使用两个单帧分配器,这样第i帧分配的内存在第i+1帧中仍然可用。
内存碎片
在支持虚拟内存的操做系统中,内存碎片不是大问题,可是不少游戏引擎不会使用虚拟内存,由于它会致使不少额外的开销。前面的介绍能够看出堆栈和池分配器不会产生内存碎片。
内存碎片的整理须要移动已分配的内存块,它会致使指针的失效;可使用重定位来解决这个问题。具体来讲有两种方法。
可是有些内存块不能被重定位,那么能够将这些内存块分配到不可重定位的内存区中,或者允许少许不可重定位的内存块的存在。
内存整理是很耗时的过程,可是不须要一次完成,因此能够把它分摊到多个帧中。
CPU会有多级高速缓存,缓存中存取数据的速度快与内存中的存取,为了提升效率就须要提升高速缓存的命中率。对于多级缓存CPU最外层的缓存命中失败的成本比内层的缓存命中失败的成本高。缓存分为指令缓存和数据缓存。
提升数据缓存命中率的方法
保证数据大小较小,且将他们尽量放到连续的内存块中,顺序访问这些数据。(和堆栈分配器很契合)
提升指令缓存命中率的方法
单个函数的机器码几乎老是置于连续的内存;编译器和连接器按函数在源代码中出现次序排列内存布局,所以一个源文件中的函数总在连续内存块中。
容器
经常使用容器:数组(array)、动态数组(dynamic array)、堆栈(stack)、队列(queue)、双端队列(deque)、优先队列(priority queue)、树(tree)、二叉查找树(binary search tree)、二叉堆(binary heap)、字典(dictionary)、集合(set)、图(graph)、有向非循环图(directed acyclic graph)
迭代器的优势
尽可能使用前置递增,由于后置递增有个拷贝的过程,若是是迭代器,这个拷贝可能很耗时。
如下的状况下可能创建自定义容器
比较自定义数据结构和第三方库,才能决定是否去自定义。为此要先了解第三方库。经常使用的包括:STL、STL的变种(STLport)、Boost。
STL
STL的优势:
STL的缺点
STL的应用时机
Boost
Boost的优势
Boost的缺点
模板元编程(template metaprogramming,TMP)是利用编译器作一些一般在运行期才会作的工做。Loki是一个强大的C++TMP库。(http://loki-lib.sourceforge.net)
缺点
游戏编程中常用固定大小的数组,由于它无需内存分配,且对缓存友好;可是编译期间难以决定数组的大小,因此倾向于使用链表和动态数组。可是最后当数组的大小可以肯定时,把它改成固定大小的数组。
链表的建议
字典和散列表:注意散列(把任意类型的键转换为整数)函数的选择是关键。若键为32位整数,把其位模式诠释为32位整数;若键为字符串,则把字符串中全部字符的ASCII或UTF码合并为单个32位整数,常见的字符串散列函数有LOOKUP三、CRC3二、MD5等。
字符串
字符串类虽然方便,但有隐性成本:传递字符串对象时,函数声明或使用不当引发多个拷贝构造函数的开销;复制字符串涉及动态内存分配。游戏编程中通常避免字符串类,若必定要使用字符串类,应该查明其运行性能特性在可接受的范围,并让全部使用它的程序员知悉其开销。在储存和管理文件系统路径时,使用特化的字符串类(如Path类)来处理多平台的字符串差别,在游戏引擎中是颇有价值的。
惟一标识符
惟一标识符(64位或128位的GUID字符串)用于识别游戏对象或资产,因为数量很是多,大量的比较在游戏中可能极有影响。最好找到一种方法,既保留字符串的表达能力和弹性,又要有整数操做的速度。方法是能够把字符串散列并存于表中(该过程称为字符串扣留),并经过散列码(也称为字符串标识符,string id或SID)取回原来的字符串,但要选取恰当的散列函数保证不碰撞。
由于字符串扣留(散列,分配字符串内存,复制至查找表)很是缓慢,因此一般在运行时就进行,并且仅进行一次,把结果储存备用。
#define U32 unsigned int #define StringId U32 static HashTable<StringId, const char *> gStringIdTable; StringId internString(const char *str){ StringId sid = hashCrc(str); if (gStringIdTable.find(sid) == gStringIdTable.end()){ //字符串未加入表中时,将其拷贝的副本加入表中 /* strdup函数原型: strdup()主要是拷贝字符串s的一个副本,由函数返回值返回,这个副本有本身的内存空间,和s不相干。 strdup函数复制一个字符串,使用完后要记得删除在函数中动态申请的内存,strdup函数的参数不能为NULL,一旦为NULL,就会报段错误. 由于该函数包括了strlen函数,而该函数参数不能是NULL. **/ gStringIdTable[sid] = strdup(str); } return sid; } static StringId sid_foo = internString("foo");//确保只调用一次,而不要放到判断条件时调用 static StringId sid_bar = internString("bar"); void fun(StringId id){ if (id == sid_foo){} else if (id == sid_bar){} }
本地化
对每一个向用户显示的字符串,都要事先翻译为须要支持的语言(程序内部使用的,永不显示于用户的字符串无须本地化)。除了经过使用合适的字体,为全部支持语言准备字符字形,游戏还须要处理不一样的文本方向(针对一些阅读顺序很特殊的语言)。
推荐先阅读这篇文章:《每一个软件开发者都绝对必知的Unicode及字元集必备知识(没有借口!)》。游戏引擎中最常采用的是UTF-8和UTF-16。
Windows下的Unicode
在Windows下,wchar_t用来表示单个“宽”UTF-16字符(WCS),char则用做ANSI字符及多字节UTF-16字符串(MBCS)。Windows允许程序员编写字符集无关的代码,即提供TCHAR数据类型,它会根据实际所用的字符集自动typedef为特定的类型。
注意Windows中各类API和标准函数库,无前缀表示普通ANSI字符,前缀为“w”“wcs”表示宽字符,缀为“mbs”表示多字节UTF-16,如strcmp()、wcscmp()和_mbscmp()。不一样的引擎采用哪一种编码并不重要,重要的是在项目中尽早决定,并始终贯彻使用。
其余本地化要考虑的事
引擎配置
读写选项
可配置选项可简单实现为全局变量或单例中的成员变量,这些选项必须可供用户配置,储存到硬盘、记忆卡或其余媒体,游戏能随时读取。下面是一些读写选项的方法:
个别用户选项
个别用户选项保留了每一个玩家本身配置其喜欢的选项,与全局选项区分开来。须要当心控制每一个玩家只能“看见”本身的选项,而不会碰见其余玩家在同一设备的选项。
在Windows上,应用程序一般在C:\Documents and Settings的隐藏文件夹Application Data文件夹中创建本身的文件夹,存放个别用户数据。或者经过读写注册表HKEY_CURRENT_USER下的注册表项,来存取管理当前用户的配置选项。
真实引擎中的配置管理
游戏内置菜单选项:每一个可配置选项都实现为全局变量,为选项建立菜单项目时,会提供全局变量的地址,以后菜单项目就能直接控制该全局变量的值
命令行参数:可指定要载入的关卡名称,以及其余经常使用参数
Scheme(一种Lisp方言)数据定义:经过脚本定义数据结构,并用自建的数据编译器转换为二进制文件,同时自动生成C/C++的头文件以解释二进制文件的数据。能够在运行期间重编译和重加载二进制文件,以便随时修改数据结构并当即看到效果。这种系统给予程序员巨大的弹性,能够定义复杂的数据结构,如细致的动画树、物理参数、游戏机制等。下面的代码示例,用于为动画定义属性,并导出2个动画
;; Scheme代码,定义一个新的数据类型,名为simple-animation (deftype simple-animation () ( (name string) (speed float: default 1.0) (fade-in-seconds float: default 0.25) (fade-out-seconds float: default 0.25) )) ;; 定义此数据结构2个实例 (define-export anim-walk (new simple-animation :name "walk" :speed 1.0 ) ) (define-export anim-jump (new simple-animation :name "jump" :fade-in-seconds 0.1 :fade-out-seconds 0.1 ) )
Scheme代码会产生如下C/C++头文件:
// simple-animation.h // 警告:本文件是Scheme自动生成的,不要手工修改 struct SimpleAnimation { const char* m_name; float m_speed; float m_fadeInSeconds; float m_fadeOutSeconds; };
在游戏编程中,可调用LookupSymbol()函数读取数据,该函数以返回类型为模板参数:
#include "simple-animation.h" void someFunction() { SimpleAnimation* pWalkAnim = LookupSymbol<SimpleAnimation*>("anim-walk"); SimpleAnimation* pJumpAnim = LookupSymbol<SimpleAnimation*>("anim-jump"); // 在此使用这些动画...... }