MonoGame笔记(五)轮询

笔记三和笔记四讲到MonoGame中的event driven input并不是真正意义的event driven, 只是在轮询的基础上采用了一种C# event的方式去封装. 然而Windows系统的确是事件驱动的. 深入浅出MFC第一章Win32基本程序概念提到

所有的GUI系统,包括UNIX的X Window 以及OS/2 的Presentation Manager,都像Windows那样,是以消息为基础的事件驱动系统。

以消息为基础, 以事件驱动之

以鼠标和键盘为例, 由Windows系统捕捉到之后, 将该输入封装成消息(message)放入系统队列(system queue)中, 之后在主事件循环(main event loop)中获取它(PeekMessage), 再派发(dispatch)给窗口处理函数(WndProc)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
WndProc(hwnd, msg, wParam, lParam)
{
switch (msg) {
case WM_CREATE: ...
case WM_COMMAND: ...
case WM_LBUTTONDOWN: ...
case WM_KEYDOWN: ...
case WM_PAINT: ...
case WM_CLOSE: ...
case WM_DESTROY: ...
default: return DefWindowProc(...);
}
return(0);
}

WM_LBUTTONDOWN和WM_KEYDOWN分支就是对鼠标和键盘的输入处理. 而XNA是在每一帧Update时去检测鼠标和键盘的输入状态, 即每一帧轮询输入设备的状态. 为何XNA是这样处理的, 二者之间有什么差异, XNA和上面提到GUI有什么区别?

这些问题的答案存在于MonoGame的主事件循环中.笔记一中提到WinFormsGameWindow的RunLoop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
internal void RunLoop()
{
// https://bugzilla.novell.com/show_bug.cgi?id=487896
// Since there's existing bug from implementation with mono WinForms since 09'
// Application.Idle is not working as intended
// So we're just going to emulate Application.Run just like Microsoft implementation
_form.Show();

var nativeMsg = new NativeMessage();
while (_form != null && _form.IsDisposed == false)
{
if (PeekMessage(out nativeMsg, IntPtr.Zero, 0, 0, 0))
{
Application.DoEvents();

if (nativeMsg.msg == WM_QUIT)
break;

continue;
}

UpdateWindows();
Game.Tick();
}

// We need to remove the WM_QUIT message in the message
// pump as it will keep us from restarting on this
// same thread.
//
// This is critical for some NUnit runners which
// typically will run all the tests on the same
// process/thread.

var msg = new NativeMessage();
do
{
if (msg.msg == WM_QUIT)
break;

Thread.Sleep(100);
}
while (PeekMessage(out msg, IntPtr.Zero, 0, 0, 1));
}

从上面的代码中可以看到, 当PeekMessage为false, 表示没有消息, 系统处于空闲状态(Idle)时, 才会执行Game.Tick方法. 即Game的逻辑和渲染是在Idle时执行的.

这是MonoGame的处理方式, 可能其他的framework不是这样处理的.那可以看看它的原生版XNA的处理方式

1
2
3
4
5
6
7
8
9
//Game.cs 
//host为GameHost

this.host.Idle += new EventHandler<EventArgs>(this.HostIdle);

private void HostIdle(object sender, EventArgs e)
{
this.Tick();//重头戏
}

Game调用host的Run方法, host的具体实现为WindowsGameHost, 它的Run方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//WindowsGameHost.cs
internal override void Run()
{
if (this.doneRun)
{
throw new InvalidOperationException(Resources.NoMultipleRuns);
}
try
{
Application.Idle += new EventHandler(this.ApplicationIdle);
Application.Run(this.gameWindow.Form);
}
finally
{
Application.Idle -= new EventHandler(this.ApplicationIdle);
this.doneRun = true;
base.OnExiting();
}
}

注册了Winform的Application的Idle事件, 当系统空闲时, 调用ApplicationIdle方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void ApplicationIdle(object sender, EventArgs e)
{
NativeMethods.Message message;
while (!NativeMethods.PeekMessage(out message, IntPtr.Zero, 0u, 0u, 0u))
{
if (this.exitRequested)
{
this.gameWindow.Close();
}
else
{
this.RunOneFrame();
}
}
}

再看RunOneFrame方法

1
2
3
4
5
6
7
8
9
internal override void RunOneFrame()
{
this.gameWindow.Tick();
base.OnIdle();
if (GamerServicesDispatcher.IsInitialized)
{
this.gameWindow.IsGuideVisible = Guide.IsVisible;
}
}

内部调用base.OnIdle. 而GameHost的Idle事件注册了Game的HostIdle, 而后者又调用了Tick方法.

其实从HostIdle顾名思义可以得出结论

1
2
3
4
private void HostIdle(object sender, EventArgs e)
{
this.Tick();//重头戏
}

Game是在Host空闲的时候才调用Tick方法. Host顾名思义就是Game寄宿的载体.可以是Windows的Winform, 也可以Android的View, 等等等等.

或许还会有疑问, 那只是MonoGame(XNA)的做法, 其他game framework并不一定如此吧.

于是查了一本经典老书: Windows游戏编程大师技巧
第二章产生一个实时事件循环

代码基于Win32平台, 没有framework封装, 已经接近底层了.

或许还有一丝存疑, 那是该书对Idle的运用, 将Game logic放在那里, 不一定都是这样吧. 那看一下非游戏编程的书是怎么描述Idle的运用的.

深入浅出MFC第一章Win32基本程序概念专门有一节是空闲时间的处理: OnIdle

所谓空闲时间(idle time),是指「系统中没有任何消息等待处理」的时间。举个例子,没有任何程序使用定时器(timer,它会定时送来WM_TIMER),使用者也没有碰触键盘和鼠标或任何外围,那么,系统就处于所谓的空闲时间。空闲时间常常发生。不要认为你移动鼠标时产生一大堆的WM_MOUSEMOVE,事实上夹杂在每一个WM_MOUSEMOVE 之间就可能存在许多空闲时间。毕竟,计算机速度超乎想像。背景工作最适宜在空闲时间完成。传统的SDK 程序如果要处理空闲时间,可以以下列循环取代WinMain 中传统的消息循环:

1
2
3
4
5
6
7
8
9
10
while (TRUE) {
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) {
if (msg.message == WM_QUIT)
break;
TranslateMessage(&msg);
DispatchMessage(&msg);
} else {
OnIdle();
}
}

第6章的HelloMFC 将示范如何在MFC 程序中处理所谓的idle time

那有一个疑问: 游戏是背景工作吗? 要解答这样疑问看一篇笔记.