« LoadLibrary 的搜索次序 | 返回首页 | VC6 warning level 4 的问题 »

在 Windows 下使用 Timer 驱动游戏

在 Windows 平台下写游戏,相比 console 等其它平台,最麻烦之事莫过于让游戏窗口于其它窗口良好的相处。

即使是全屏模式,其实也还是一个窗口。如果你不去跑窗口的消息循环,一个劲的刷新屏幕,我估计要被所有 Windows 用户骂死。

那么怎样让你的游戏程序做一个 Windows 下的良好公民呢?

最简单的方法是用循环用 PeekMessage 来处理 Windows 消息,一旦消息队列为空,就转去跑一帧游戏逻辑,这帧逻辑完成后游戏屏幕也被刷新了一帧。主循环代码大概看起来是这样的:

for(;;) {
    MSG msg;
    while(PeekMessage(&msg,NULL,0,0,PM_REMOVE)) {
        if (msg.message==WM_QUIT) {
            goto _quit;
        }
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    render_frame();
}

_quit:

现在大部分游戏,尤其是许多 3d 游戏,大概就是这样的框架吧。如果你用 MFC 的话,消息循环的代码不由自己写,那么 render_frame() 一般就 OnOdle 里面。OnIdle 的位置大置和这段代码中 render_frame() 的位置相当。

这么干其实并不是一个 Windows 下的好公民。因为程序主循环一跑起来,虽然处理了窗口系统上的各种消息,但是几乎占据了所有的 CPU 资源。

为了不让游戏显得那么霸道,最简单的方法是在 render_frame() 调用一下 Sleep ,甚至只是 Sleep(0) 也可以暂时让出 CPU 。可惜这并不解决根本问题。

对以上方式的改进是对游戏限帧,然后在 render_frame() 时检查是否过了当前帧的时间段,只要没有超时,就一直等待下去。其实,就是加一个一个不断查询时刻并让出 CPU 的循环。这样看起来会好一些,只要你的 render_frame() 负担不是很重且 CPU 足够快的话,运行起来 CPU 占有率很相对低很多。

这种方式在现代的许多 2d 游戏中用的很多。但是 3d 游戏大多不这样用,由于各种原因,几乎所有的 PC 上的 3d engine/游戏 都追求所谓的高帧率。三位数的 fps 数不光是 engine 制作者的追求,也是众多玩家的梦想。实际上,太低的帧率也的确让 3d 游戏的操作感下降,通过 Sleep 让出 CPU 的方式,也就是通知 os 暂时放弃控制权,难以将游戏维持在一个恒定的高 fps 数上。

退一步说,即使我们容忍游戏固定在一个较低帧率上,以上改进方案依然不够优雅。Windows 整个是以消息驱动方式工作着,非消息驱动的模式的桌面程序在 Windows 平台下都仿佛是个异种。

那么,可以让游戏用 Windows 的 Timer 来驱动吗?利用 Windows 的 timer 消息,似乎我们也可以完成游戏中的动感画面,而不需要脱离消息循环。那么主循环只需要写成:

while(GetMessage(&msg,NULL,0,0)) {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

这样就够了,设置 Timer 在消息循环中相应 WM_TIMER ,解决方案似乎很完美。GetMessage 在没有消息时完全阻塞等待,os 会帮我们处理好 CPU 的利用问题。

实际上,大部分游戏程序员都不会选择这个方案。主要原因是,WM_TIMER 这个东西太不可靠了。

而不能信任 WM_TIMER 的主要原因则是精度不够。在 Windows 98 下,只有可怜的不到 20Hz 的频率。这显然不能满足大多数交互性强的游戏的需要。Windows NT 下要好一点,可以达到 100Hz ,这才差不多够用。

即使有足够精度的 timer ,我们依然不能直接使用 WM_TIMER 这个消息。因为它并不能完全准确的表示时刻。它和 WM_PAINT 一样,属于比较特别的消息:优先级低,在消息队列中同时只存在一份(即多个同样的消息可能被合并)。

不过这个思路走下去却行的通,云风这个给出自己的一个解决方案:

首先,我们需要自己实现一个定时器,并可以调节精度。也就是说,定时器的时间单位可以是 ms 甚至ns 都没关系,我们设定一个精度后,可以把精度范围内的事件都当成同时发生的。

这样一个定时器很容易实现,因为一般游戏动画的控制需要的定时器间隔都很短,假如我们把精度设为 1/100 秒的话。开一个 100 的队列数组,把需要设的定时器按精度量化后,只需要放在数组指定位置的队列中去就可以了。这个数组也可以随着时间流逝循环使用。

Windows 的 Timer 消息只用来触发我们自己的定时器系统,把最近的一个事件的时刻告诉 os 。那么到那个时间来临时,消息循环就被唤醒。然后我们可以不段的查询当前时刻,并依次处理自己的定时器系统中应该被执行的那些事件就可以了。

我们的定时器不光为了控制游戏的帧率,可以让游戏对象中的每一个对象都受单独的定时器事件控制。那么何时刷新游戏画面呢?只需要设置一个需要被刷新的事件,类似 WM_PAINT 消息就可以了。(但不建议直接发 WM_PAINT 消息,以便于跟系统产生的 WM_PAINT 相区分)

为了解决 Windows 的定时器精度问题,我们可以使用 WaitableTimer 这个内核对象。具体可以查询 msdn 上关于 CreateWaitableTimer 和 SetWaitableTimer 的说明。

另外我们在创建一个自己的 Event 用于刷新画面的事件通知。

这样,主循环就变成了这个样子:

#define EVENT_TIMER 0
#define EVENT_RENDER 1
#define EVENT_TOTAL 2

static HANDLE g_event[EVENT_TOTAL];

/* 初始化 */
g_event[EVENT_TIMER]=CreateWaitableTimer(NULL,FALSE,NULL);
g_event[EVENT_RENDER]=CreateEvent(NULL,FALSE,FALSE,NULL);

/* 主循环 */

for (;;) {
    DWORD result=MsgWaitForMultipleObjects(
        EVENT_TOTAL,g_event,FALSE,INFINITE,QS_ALLEVENTS);
    switch (result-WAIT_OBJECT_0) {
        case EVENT_TIMER:
            /* 处理 timer */
            process_timer();
            break;
        case EVENT_RENDER:
            render_frame();
            break;
        case EVENT_TOTAL: {
            MSG msg;
            while (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) {
                if (msg.message==WM_QUIT) {
                    goto _quit;
                }
                else {
                    TranslateMessage(&msg);
                    DispatchMessage(&msg);
                }
            }
            break;
        }
    }
}

_quit:

具体的代码就不多贴了,提供一个思路而已。用 Windows 的 WaitableTimer 来触发自己实现的 timer 设施,并用一个额外的 event 来通知画面重绘。

当我们调整自己的 timer 设施的精度时,同时也调整了程序的 cpu 占用率。

Comments

精确延时函数http://blog.sina.com.cn/s/blog_5fb3f1250101dpu8.html

我明白前辈的想法和意思,这确实是个好办法,可是我目前接触的2D游戏貌似不是这么做的,而且我做的里面仅仅用的是单纯的windows处理机制,貌似也不是很占cpu资源。可能这也是3D和2D的一个重要区别吧。
不过在服务器为了和客户端的同步,发送数据包我基本是这样的思路做的。
PS:我是今年应届的毕业生,目前在一家2D公司实习,半年来进步斐然,非常失败的是今年没能进入网易公司。

自己写的消息循环,可以在idle里处理一些事情,CPU负担也很轻,贴出来请大家批评指正:
void MessageLoop(){
MSG msg;

T* pT=static_cast<T*>(this);
for(;;){
while(!::PeekMessage(&msg,NULL,0,0,PM_NOREMOVE))
if(!pT->OnIdle())
::WaitMessage();

do{
if(!::GetMessage(&msg,NULL,0,0)){
m_nRetCode=(int)msg.wParam;
return;
}
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}while(::PeekMessage(&msg,NULL,0,0,PM_NOREMOVE));
}
}

wtl里对消息循环处理的很好,没有多少资源占用。从理论上讲,对于循环线程来说,要想降低cpu占用,必需在循环中等待事件。具体怎么搞,则各有千秋

为什么不用多线程来解决这个问题呢?

把渲染部分专门放到一个线程里,如果不需要渲染直接返回不行吗?

既然frame和render也都作为事件来处理了,那么它们的地位和输入事件的检测应该是平等的了.也就不存在"太低的帧率也的确让 3d 游戏的操作感下降"的问题了吧?

这是个很棒的模式,我对如何降低cpu占用率很感兴趣!但看了很久都没有懂.到底什么时候发送EVENT_RENDER消息呢?

在追求高质量画面效果的前提下个人认为3D游戏100%的CPU占用应该不会造成恶劣的影响……对人眼来说,100帧以上的刷新率是没有意义的,但现在的3D游戏很多都限制到30、40帧,一是技术水平问题,二是国内的硬件水平和国外的也不能比……如果玩一个3D游戏…还干其它的事…现在的动辄数G的游戏资源和内存占用也不容忽视吧

水平没到这么高的地步 ,吹毛求疵下,只为文章更严谨些:
/* 处始化 */
错别字

我个人感觉,如果单单是考虑流畅性,这个方案并不比全占 cpu 尽量多刷新的方法差。

如果 3d 游戏只是担心中间的 cpu 控制权让出会导致操作手感下降,而采用不让出 cpu 的方法,完全没有必要。这里给出的方案可以兼顾。

其中的要点的确是定时器的实现,我的实现方案在测试时,曾经把精度调整到 0.001 秒,工作是很正常的。也就是说这个情况下,只有刷新率达到 1000fps 时,才会有差别(换句话说,是绝对的高精度)。大多数场景中的动画,我们只要估算出动画频率,(比如大多数不要紧的东西可以设在 30Hz 以下),只要场景中没有什么运动特别快的物体,游戏跑起来是非常流畅的。至于输入设备引起的画面变化,取决于输入设备的输入频率。它也是有限的。

ps. 我的定时器写的比较脏,所以不想贴出来了 ;)

如果是游戏逻辑不适合用时钟驱动的话,要不要采用如此方法倒是可以商榷的。

这个的确因不同的类型的游戏尔异。

可否把你的定时器设计共享下,如果方便的话~

用这个方法还有一个比较好的扩展思路:通过当前窗口的判断来控制画面重绘。如果游戏窗口不是当前窗口,则可以减少cpu占用。

这个很难说,因为事实上在玩3d game的同时也有很多人在做其它事情的。特别对于那种包月制的游戏来说(外国很多)。

我提这个方法就是想兼顾。如果真的不用了,既真的显示画面两帧之间毫无差别,就释放掉资源。

如果有一点改动,那么一定会触发渲染。

这里面定时器是最难设计的,我昨天做了一通宵。好的定时器可以做到最少的调用 os 的 api ,包括查询时刻。在任务紧张的时候,这样就没有额外的运行开销了,也不会故意把 cpu 让出。

对于《梦幻》《大话》来说,处理CPU利用是有意义的。因为大多数人在玩这两个游戏的时候,都是基于窗口模式,并且还并行的做其他许多事情。
不过对于全屏的3D-game意义不大,因为玩这种游戏的时候很少会做别的不相关事情。流畅而高质量的体验是一个重要的内容,玩家的意思就是,在不crash掉的前提下,把能用上的都用上吧~

Post a comment

非这个主题相关的留言请到:留言本