多线程下的游戏循环
假定对每个游戏来讲,渲染整个场景需要30ms,它还需要额外的20ms去更新游戏世界。如果这些都在同一个线程执行,每帧将耗时50ms,最终导致降低帧率–20FPS,这是不可接受的。但如果渲染和更新逻辑同步执行,每帧只要30ms,30FPS的目标就可以完成。
主线程必须区里所有输入、更新游戏世界、处理所有图形以外的输出。它必须提交相关数据给第二条线程,那么第二条线程就可以渲染所有图像。渲染线程绘制的时候,主线程该干什么?让渲染线程比主线程慢一帧。
增量时间的出现,避免了固定帧率导致固定循环次数运行在不同设备上的出现的Bug
输入延迟,第二帧按下。在多线程游戏循环下,输入直到第三帧才开始处理,图形要到第4帧结束之后才能看到,如图所示。
C++中的钻石问题与解决方案
菱形继承
虽然有很多解决方案,但是通常要尽量避免,除非有很好的理由这么做。
1 | class Tiger : virtual public Animal { /* ... */ }; |
这种写法在当有类C同时继承这两种之后就不会出现两个Animal类对象了
真实时间和游戏时间-子弹时间,一盘篮球游戏的时间
##游戏循环中的游戏对象
一个基础的游戏对象GameObject
任何游戏对象公有的功能,不管什么对象类型,都应该放在基类里。这样就可以声明两个接口,一个是Drawable对象,一个是Updateable对象。
1 | interface Drawable |
就可以这两个接口和一个基类来表示3种游戏对象
只更新的对象
1 | class UGameObject inherits GameObject,implements Updateable |
只渲染的对象
1 | class DGameObject inherits GameObject,implements Drawable |
更新且绘制的游戏对象
1 | class DUGameObject inherits UGameObject,implements Drawable |
这里没有直接继承DGameObject 和 UGameObject就是避免了钻石问题
实现这三种对象之后,把它们整合在游戏循环中是很简单的。GameWorld类拥有两个列表,分别在游戏世界中管理Updateable对象和Drawable对象
强制锁定30FPS的帧率 将targetFrameTime = 33.3f
1 | targetFrameTime = 33.3f |
2D渲染基础
对于屏幕撕裂
喷枪在屏幕上绘制到一半时,刚好游戏循环到了”generater outputs”阶段。它开始为新的一帧往像素缓冲区写像素时,CRT还在上一帧的绘制过程中。
一个解决方案就是同步游戏循环,等到场消隐期在开始渲染。这样会消除分裂图像的问题,但是它限制了游戏循环的间隔,只有场消隐期间才能进行渲染,对于现在的游戏来说是不行的。
另一个解决方案叫作双缓冲技术。双缓冲技术里,有两块像素缓冲区。游戏交替地绘制在这两块缓冲区里。在一帧内,游戏循环可能将颜色写入缓冲区A,而CRT正在显示缓冲区B。到了下一帧CRT显示缓冲区A,而游戏循环写入缓冲区B。由于CRT和游戏循环都在使用不同的缓冲区,所以没有CRT绘制不完整的风险。
缓冲区交换要放在场消隐期进行。
1 | function RenderWorld() |
绘制精灵的算法
绘制方式先画背景色后画角色。这就像画家在画布上画画一样,也因为这样,这个算法叫做画家算法。在画家算法中,所有精灵是从后往前排序的,如下图所示。当它绘制场景时预先排好序的场景可以直接遍历渲染,得到正确的结果。
画家算法也可以运用在3D环境下,但它有很多缺陷。而在2D场景中,画家算法工作得很好。
为了保证动画的连续性,帧率最少要达到24FPS
用一组图片去表示一个角色所有状态,一个有走动和跑步的角色,每个用10帧表示,总共用了20张图片。顺序存储也就是0-9帧表示走路,10-19帧表示跑步
AnimatedSprite 要能够跟踪当前的动画数量,知道当前帧属于哪一个动画及当前动画需要用到多长时间,FPS也作为成员变量被存储了。可以通过修改FPS来让动画动态加速或减速。角色获得加速效果,让角色跑动的快一点
1 | class AnimatedSprite inherits Sprite |