最近在读《游戏引擎架构》这本书,虽然感觉理论颇多,而且很多很多东西都看不懂,不过还是简单的把一些感觉很好的思想和方法或者概念记录下来。
1. 关于各个部件初始化,一个最简单粗暴的办法就是将各个部件按顺序排列起来,不但可以方便的看到初始化顺序,而且修改也十分容易。
2. 关于内存分配:
a) 游戏中最常见的法则是-维持最低限度的堆分配,并且永远不在紧凑的循环中使用堆分配。
b) 定制的内存分配器,先预先申请一大块内存,然后构建自己的内存分配器(stack allocator-堆栈分配器或者 池分配器, 单帧分配器,双缓冲分配器)
c) 高效能的代码体积越小越好,关键代码中避免函数调用,如果要调用该函数,最好将该函数放在调用函数附近,尤其不要放到其他编译单元中!谨慎使用内联函数,过多的内联会使代码体积过大,关键代码不能完全装入内存,导致命中失败。
3. 关于迭代器,最好使用前置递增,即++p,后置递增要备份其值,会导致性能损失。
4. 运用stl,但是要慎用。
5. 字符串相关:外部字符串涉及到本地化问题,需要考虑翻译的情况。内部字符串需要考虑性能问题,关于一个对象命名为一个字符串,但是比较或者查找这个字符串是一个很大的性能开销。
6. 唯一标识符:GUID <globally unique identifier>游戏中的对象都有一个全局标识符,一般为对象资源的路径名,然而这个对象名很长,每次查找时会耗费很大性能,所以考虑解决方案:把字符串散列hash:把字符串映射到唯一的一个数(string id)。这个id对应唯一的string,可以考虑将这个string id和string包装在一个类中(虚幻的做法)
7. 配置文件:引擎一定会有许多配置信息,一些是要给玩家使用的,一些是开发过程中使用的,而给玩家使用的还包括同一个游戏中不同玩家的信息,这些都要存储起来。一个好的方法是使用XML文件。读取后使用全局或者单例模式保存唯一的配置信息。
8. 资源管理器<resource manager>(资产管理器<assetmanager>,媒体管理器<media manager>)。管理各种游戏中的资源,保证资源不重复载入。关于资源的读入,可以使用原生的api但是如果涉及到平台移植的问题,最好将文件的api包装起来,然后根据平台调用不同的api。C/C++自带的文件api都是同步的,可以使用库来实现异步加载。
9. 异步读取:主线程将一个请求放入一个队列中,I/O线程从队列中读取请求,进行同步读取(I/O线程由于fread等函数阻塞),完成后,调用主线程提供的回调函数,告之操作已完成。
10. 资源管理器包括两个方面,一方面是建立资源的离线工具,负责创建资源,并把其转化为引擎可用的工具。另一方面是在执行期载入的资源管理,负责在使用前载入资源,不用时卸下资源。
11. 关于资源,一般都是会有多重依赖的,比如一个三维模型,对应一个或者多个三角形网格,可选的骨骼,可选的动画集合,每个网格还有一个材质,一个材质对应一个或者多个纹理。使用一个对象时,要注意将其依赖资源全部导入。
12. 游戏循环,引擎子系统需要周期性提供服务,而子系统的频率可能不同。
a) 最简单的就是单一循环更新所有子系统。
b) windows的消息泵,即Windows消息循环,在其中增加一项,作为引擎的循环,不过缺点是windows会优先处理系统消息,最为明显的表现是移动窗口时,游戏会卡住。
c) 回调驱动框架,有的引擎中已经写好框架了,只需要我们像其中填写内容即可。
d) 基于事件的更新机制。
13. 关于游戏时间的处理:
a) 最早的处理方式并不会在游戏循环中准确度量真实的时间,游戏中的速度完全依赖CPU的速度。
b) 基于经过时间的更新:一帧开始时和结束时取时间差,即可精确的获得两帧要隔的时间,根据上一帧的时间来估计下一帧的时间,但是这样可能会有恶性循环,所以平均一下。
c) 如果知道了帧率最好的时间差,直接用这个时间差来控制帧率不就好了嘛。如果到时间还没完成,那么白等下一个目标时间,如果提前完成了,那么就sleep一会儿。
总之最重要的就是要稳定帧率,可以保证画面流畅,或者为录播功能等提供保障。
14. 画面撕裂:这个异常发生在显示器电子枪在扫描过程中交换前景缓冲区和背景缓冲区时。
解决这个问题的方法是垂直同步,打开后能防止游戏画面高速移动时画面撕裂现象,当然打开后如果你的游戏画面FPS数能达到或超过你显示器的刷新率,这时你的游戏画面FPS数被限制为你显示器的刷新率。你会觉得原来移动时的游戏画面是如此舒服,如果达不到会出现不同程度的跳帧现象,FPS与刷新率差距越大跳帧越严重。关闭后除高速运动的游戏外其他游戏基本看不出画面撕裂现象。
15. 游戏循环受时间控制,那么通过时间我们就可以控制游戏的快慢,甚至可以直接封装一个时间相关的类,这个类中可以记录绝对时间,游戏时间,并且可以根据参数来控制游戏时间的快慢,达到快进或者慢放的效果。
16. 随着计算机对并行处理功能的支持越来越好,将各种功能分离给各个线程做或者各个处理机来做也是一种好方法,但是有一个坏处就是如果将整个一个功能给一个线程,可能会导致粒度过大,如果一个功能耗时过长,其他功能都要等待这个线程。更好的解决方法是使用更小粒度的作业,最大化的利用处理机的利用率。
17. 人体学接口相关:
a) 死区:如果是模拟信号,可能会有轻微干扰,要排除这个干扰,需要设定一个死区,保证输入结果不被干扰。
b) 一种好的思想:判断按钮状态时,保存上一帧的按钮状态,和这一帧异或,有变化的就是这一帧操作的,然后再判断这些按钮这一帧的情况,如果是按下,说明按钮被按下了,如果抬起了,说明按钮被松开了。
c) 弦:指一组按钮被按下,会有额外功能,但是我们操作往往没有这么精确,所以这个功能检测的代码必须足够健壮,需要处理n帧和第n+m帧之间玩家的操作,而且如果这些按钮还有单独的作用时,还要检测其他的按钮的状态。
还有一种是序列检测,比如一串输入,可以设定一个Buffer,每次输入存入buffer,并检测时间是否超时,如果超时就清空,如果达到要求就执行功能。
d) 关于不同平台输入处理,可以根据不同平台if__endif 但是更好的方法是来一个硬件抽象层。
e) 输入的重新映射:玩家可能想自己更改操作方式,这样我们就不能讲按键本身与功能相对于,而是用一个中间层来解决,更确切的说是一个映射表,将功能和按键映射起来,玩家修改操作方式只需要修改这张标的映射即可。
f) 上下文相关控制:我们在游戏中,一个按钮可能分有多种功能,比如拾取和对话,简单的实现是使用一个状态机,另一个相关的概念是控制拥有权,输入按钮属于游戏中不同对象的控制操作,比如摄像机和人物。
g) 禁用控制:有些时候我们要求游戏是不可控制的,或者某些按键无效。比较拙劣的方式就是直接从键位处禁止,但是这样的后果可能很大,万一出问题,可能不能再改回来,只能重启。这种功能的禁止最好写在游戏的逻辑里面而不是放在键位处。
版权声明:本文为博主原创文章,未经博主允许不得转载。