游戏主循环(Game Loop)
游戏主循环是游戏的心跳,一般使用while循环进行主动刷新。
一次循环由获取用户输入、更新游戏状态、处理AI、播放音乐和绘制画面组成。
这些行为可以分成两类:
update_game(); // 更新游戏状态(逻辑帧),一般不耗时
display_game(); // 更新显示(显示帧),耗时(场景越复杂越耗时)
几个概念
游戏速度:每秒调用update_game的次数。
FPS:即帧率;每秒调用display_game的次数。
可变显示FPS:即可变显示帧率,每秒调用display_game且显示画面有变化的次数。
最简单的游戏循环
bool game_is_running = true;
while( game_is_running )
{
update_game();
display_game();
}
该循环主要的问题是忽略了时间,游戏会尽情的飞奔,能有多快就运行多快
我们会看到在性能好的机器上,物体运动得更快一些
FPS依赖恒定的游戏速度
const int FRAMES_PER_SECOND = ;
const int SKIP_TICKS = / FRAMES_PER_SECOND;
DWORD next_game_tick = GetTickCount(); // 返回当前的系统已经运行的毫秒数
int sleep_time = ;
bool game_is_running = true;
while( game_is_running )
{
update_game();
display_game();
next_game_tick += SKIP_TICKS;
sleep_time = next_game_tick - GetTickCount();
if( sleep_time >= )
{
Sleep( sleep_time );
}
}
优点:重新播放游戏会显得简单(因为每帧时间间隔固定,只需要记录下每一帧游戏的状态,回放时按照25帧的速度播放即可)
配置差的机器的表现:到某些复杂的游戏场景时,display_game绘制会耗费大量时间,影响游戏输入和AI的响应,游戏会变得很慢(卡)
当场景变得简单时,游戏会加速运行,直到match到正常的步伐,然后稳定到25帧
牛逼的机器的表现:对于高速移动的物体,对视觉效果有一些影响(原来可以跑300帧,现在被强制只能运行25帧);另外,由于调用了Sleep,会比较省电一些
结论:FPS阈值定义得太高会使得配置差的机器机不堪重负,定义得太低则会使得高端硬件损失太多视觉效果
可变FPS决定游戏速度
DWORD prev_frame_tick;
DWORD curr_frame_tick = GetTickCount(); // 返回当前的系统已经运行的毫秒数
bool game_is_running = true;
while( game_is_running )
{
prev_frame_tick = curr_frame_tick;
curr_frame_tick = GetTickCount(); // 返回当前的系统已经运行的毫秒数
update_game( curr_frame_tick - prev_frame_tick );
display_game();
}
这种方案在update_game时需要考虑当前帧与上一帧的时间差。
配置差的机器的表现:到某些复杂的游戏场景时,display_game绘制会耗费大量时间,影响游戏输入和AI的响应,游戏会卡顿
然而在下一帧,就会强制match到正常的步伐,这样我们就会看到一些跳变(经常发生一些违反物理规律的怪事)
牛逼的机器的表现:也可能会出现问题,原因是update_game的调用次数存在差异;越牛逼的机器,update_game的调用次数越多。这种差异引起的浮点数误差,会导致致命的错误
结论:该游戏模型只能用于单机游戏和状态同步网游,不能用于帧同步网游
注:帧同步以帧为基本计时单位的一个同步方案,具体来说每台机器都必须运行一样的逻辑帧顺序(如同看视频,允许有缓冲,但是帧序列都是一样的);
每台机器只用发送其所有的输入事件给其他机器,就可以在其他机器上得到与本机相同的运行结果。
优点:简单、网络流量只与输入事件的多少有关;可以将多人单机游戏(黑盒)改造成网络游戏。缺点:不允许有随机逻辑,且反外挂困难
最大FPS和恒定速度游戏
const int TICKS_PER_SECOND = ;
const int SKIP_TICKS = / TICKS_PER_SECOND;
const int MAX_FRAMESKIP = ;
DWORD next_game_tick = GetTickCount();// 返回当前的系统已经运行的毫秒数
int loops;
bool game_is_running = true;
while( game_is_running )
{
loops = ;
while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP)
{
update_game();
next_game_tick += SKIP_TICKS;
loops++;
}
display_game();
}
配置差的机器的表现:当渲染帧率下降到5(TICKS_PER_SECOND/MAX_FRAMESKIP)即:loops>=MAX_FRAMESKIP,游戏才会变慢(卡)
当场景变得简单时,游戏会加速运行,直到match到正常的步伐,然后稳定到50帧
牛逼的机器的表现:游戏会以稳定的50帧速度更新,渲染速度也尽可能的快;但渲染速度超过了50帧时,有一些帧的画面将会完全相同,所以显示FPS实际上也等同于最快50帧
结论:如果定义过高的FPS阈值,会让配置差的机器吃不消,过低则会让牛逼的机器难以发挥性能
独立的可变显示FPS和恒定的游戏速度
const int TICKS_PER_SECOND = ;
const int SKIP_TICKS = / TICKS_PER_SECOND;
const int MAX_FRAMESKIP = ;
DWORD next_game_tick = GetTickCount();// 返回当前的系统已经运行的毫秒数
int loops;
float interpolation;
bool game_is_running = true;
while( game_is_running )
{
loops = ;
while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP )
{
update_game();
next_game_tick += SKIP_TICKS;
loops++;
}
interpolation = float( GetTickCount() + SKIP_TICKS - next_game_tick ) / float( SKIP_TICKS );
display_game( interpolation );
}
逻辑帧(玩家输入、AI)本身并不需要很高的速度,25帧就足够了
渲染则放任不管,任其飞奔;与前面的方案相比,display_game多了一个插值参数,我们需要在display_game里面实现一个接受插值参数的预言函数
逻辑帧为25帧,如果渲染时不时用插值计算,显示帧会被限定在25帧。25帧可以很好的展示游戏画面,不过对于高速的物体,更高的帧率会有更好的效果
所以,我们需要一个插值和预言函数让高速移动的物体在显示帧之间平滑的过度
插值和预言函数
游戏状态更新在一个恒定的帧率下运行着,当你渲染画面的时刻,很有可能就在两个逻辑帧之间
假设你已经第10次更新了你的游戏状态,现在你需要渲染你的场景,这次渲染就会出现在第10次和第11次逻辑帧之间
很有可能出现在第10.3帧的位置。那么插值的值就是0.3。举个例子说,一辆赛车以下面的方式计算位置
position = position + speed;
如果第10次逻辑帧后赛车的位置是500,速度是100,那么第11帧的位置就会是600. 那么在10.3帧的时候你会在什么位置渲染你的赛车呢?
显而易见,应该像下面这样:
view_position = position + (speed * interpolation)
注:position=500,speed=100,interpolation = 0.3
现在,赛车将会被正确地渲染在530这个位置。基本上,插值的值就是渲染发生在前一帧和后一帧中的位置。
你需要做的就是写出预言函数来预计你的赛车/摄像机或者其他物件在渲染时刻的正确位置。
你可以根据物件的速度来计算预计的位置。这些并不复杂。
对于某些预计后的帧中出现的错误现象,如某个物体被渲染到了某个物体之中的情况的确会出现。
由于游戏速度恒定在25帧,那么这种错误停留在画面上的时间极短,难以发现,并无大碍。
配置差的机器的表现:当渲染帧率下降到5(TICKS_PER_SECOND/MAX_FRAMESKIP),此时loops>=MAX_FRAMESKIP,游戏才会变慢
当场景变得简单时,游戏会加速运行,直到match到正常的步伐,然后逻辑帧稳定到25帧
牛逼的机器的表现:逻辑帧会保持25帧,插值的方案可以让游戏在高帧率中有更好的画面表现。
结论:最好的游戏主循环实现。不过,必须实现一个插值计算函数
整体总结
讨论了4个可能的实现方法,其中有一个方案是要坚决避免的,那就是可变FPS决定游戏速度的方案。
恒定的帧率对移动设备而言,可能是一个很好的实现;如果你想展示你的硬件全部的实力,那么最好使用独立的可变显示帧率和恒定的游戏速度的实现方案。
如果不想麻烦的实现一个预言函数,那么可以使用最大FPS和恒定的游戏速度的实现方案,唯一需要考虑的是找到一个合适的FPS阈值。
参考
http://www.koonsolo.com/news/dewitters-gameloop/
http://gameprogrammingpatterns.com/game-loop.html
游戏主循环(Game Loop)的更多相关文章
- Cocos2d-x 动手实现游戏主循环
因为Cocos2d-x封装的非常好,所以对于非常多新手,他们仅仅知道先new一个场景,在场景上加入布景或精灵,然后用Director的runWithScene便能够执行游戏了.假设给一个精灵加个动作, ...
- Cocos2d-x 3.2 学习笔记(十六)保卫萝卜 游戏主循环与定时器
保卫萝卜~想法一直存在于想法,实战才是硬道理!有想法就去实现,眼高手低都是空谈. 一.游戏主循环GameSchedule 主循环是游戏处理逻辑,控制游戏进度的地方,处理好主循环是很重要的 ...
- [UE4]游戏主循环
游戏的运行模型 理解游戏的运行模型,对处理很多游戏错误有非常大的帮助. 游戏是有一个主循环的.那么游戏主循环做了什么事情呢? 游戏主循环一次就表示一帧,游戏主循环包括:接受输入.处理游戏逻辑.渲染.S ...
- WordPress主循环(The Loop)函数have_posts(),the_post()详解
WordPress中调用文章标题是the_title();调用文章内容时用到the_content();调用文章的作者时用到the_author();等等这些函数,都需要在主循环中使用,下面就介绍一下 ...
- 【Unity3D与23种设计模式】游戏的主循环——Game Loop
游戏与其他软件最大的不同 就是游戏有Update逻辑 一般的软件是由"事件"驱动 因为它不会突然跑出来一只"兔子" 因此,只有游戏才有"帧" ...
- Vue+WebPack游戏设计:自动背景贴图和游戏主循环的实现
- cocos2d-x游戏引擎核心之三——主循环和定时器
一.游戏主循环 在介绍游戏基本概念的时候,我们曾介绍了场景.层.精灵等游戏元素,但我们却故意避开了另一个同样重要的概念,那就是游戏主循环,这是因为 Cocos2d 已经为我们隐藏了游戏主循环的实现.读 ...
- master线程的主循环,后台循环,刷新循环,暂停循环
InnoDB存储引擎的主要工作都是在一个单独的后台线程master thread中完成的.master thread的线程优先级别最高.其内部由几个循环(loop)组成:主循环(loop).后台循环( ...
- Cocos2dx源码赏析(1)之启动流程与主循环
Cocos2dx源码赏析(1)之启动流程与主循环 我们知道Cocos2dx是一款开源的跨平台游戏引擎,而学习开源项目一个较实用的办法就是读源码.所谓,"源码之前,了无秘密".而笔者 ...
随机推荐
- HDU 2861 四维dp打表
Patti and Terri run a bar in which there are 15 stools. One day, Darrell entered the bar and found t ...
- BZOJ 1044: [HAOI2008]木棍分割
Description 求 \(n\) 根木棍长度为 \(L\) ,分成 \(m\) 份,使最长长度最短,并求出方案数. Sol 二分+DP. 二分很简单啊,然后就是方案数的求法. 状态就是 \(f[ ...
- Python自动化之select、greenlet和gevent和事件驱动模型初探
进程.线程和协程的区别 进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度. 线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的). 协程和线程一样 ...
- 11.3---旋转有序数组之后查找元素(CC150)
思路,这道题用二分,唯一的不同就是,1,a[left]<a[mid].那么说明左右有序,如果key还在a[left],a[mid]之间,就在这里找,如果不在就在右边找.注意:这里<要改成& ...
- phpcms图片模型调用组图的问题
phpcms里面有个图片模型,之前一直没有用过,之前用的轮播图是用文章+缩略图+推荐位实现的 今天看了一下图片模型添加内容的地方,和平常的文章相比多了一个组图的地方:
- Docker内部存储结构(devicemapper)解析(续)
dm.fs 参数dm.fs可以指定容器的rootfs的文件系统,但只支持ext4/xfs: func NewDeviceSet(root string, doInit bool, options [] ...
- 【leetcode】Word Ladder
Word Ladder Total Accepted: 24823 Total Submissions: 135014My Submissions Given two words (start and ...
- Java for LeetCode 208 Implement Trie (Prefix Tree)
Implement a trie with insert, search, and startsWith methods. Note: You may assume that all inputs a ...
- 【python】dict的注意事项
1. key不能用list和set 由于列表是易变的,故不可做key.如果使用会报错 但是元组可以做key 2.遍历方法 for key in somedict: pass 速度快,但是如果要删除元素 ...
- 如何把一个excel工作薄中N个工作表复制到另一个工作薄中
一般遇到标题这样的情况,许多人可能会一个一个的复制粘贴,其实完全不必那么麻烦. 你可以按以下步骤来操作: 第一步:打开所有要操作的excel工作薄\n 第二步:按住Shift键,选择所有要复制的工作表 ...