分类: C/C++
2008-02-20 13:05:22
事件处理机制
Framework类是sample framework中最重要的类,完成了创建窗体,初始化设备,创建命令行,事件处理(render loop)以及调节各种参数的任务。Framework类包含在dxmut.cs文件中。其中,比较特别的就是事件处理模型(或render loop)。
为了获得高性能的渲染以及事件处理机制,framework类在初始化的方法中使用Device.IsUsingEventHandlers = false;关闭了事件处理模型。我们先来看看为什么默认的事件处理机制会导致性能的损失。默认情况下,Managed DirectX中的类在每创建一个资源时,都会为device订阅一些必然的事件。在最简单的情况下,每个资源(比如纹理或顶点缓冲)将会订阅Disposing事件,以及其他一些诸如DeviceLost和DeviceReset的事件。这个步骤在整个对象的生存期都会发生。但是为什么我们在程序中不需要这种行为呢?
主要原因就是需要对这种行为付出一定代价,有些情况下,代价还会很大。我们举个例子来说明这一点。看看下面这段简单的伪代码:
SomeResource res = new SomeResource(device);
device.Render(res);
这段代码看起来几乎是“无害”的。只是创建了一个资源,并且渲染它。当这个对象不再使用时,垃圾回收器应该会智能的清除它。但是,这个想法是完全错误的。当创建新资源时,至少需要对device订阅一个事件,允许device正确的清除它。这种订阅实际上是一把“双刃剑”。
首先,订阅事件时需要分配(allocation)EventHander对象完成实际的订阅工作。虽然这种分配的代价很小,但是,我们稍候就会看到,就算是很小的分配也会迅速膨胀。第二,订阅事件之后,资源和设备之间就有了一个硬连接(hard link)。因此,在垃圾回收器的眼里,这个对象在device的生存期里仍然处于使用状态,并且没有取消事件的订约。设想一下,这代码如果在渲染每帧的时候都运行一次;再设想一下,你的程序每分钟需要进行上千次渲染,并且程序已经运行了两分钟。结果,你创建了120000个在device生存期间不会被回收的对象,以及120000个事件句柄。所创建的这些对象不但会迅速消耗内存,而且会导致额外的垃圾回收,严重影响程序性能。如果你的资源都位于显存中,那么很快就会耗尽显存。
这里,我们还没有考虑当最后释放设备时可能发生的情况。在前面的例子中,当释放device时,首先触发Disposing事件,此时,有120000个监听者订约了这个事件。你是否已经考虑到,调用这个巨大的事件句柄列表会将花费很多时间?实际上这将会花去几分钟时间,并且让用户认为程序已经处于死锁状态。
因此,最好只在最简单的Direct3D程序中使用Managed Direct3D内建的事件处理机制。在任何需要考虑内存容量和性能的应用中(比如游戏),都必须避免这些处理过程。
接下来,我们来看看framework中是如何实现事件处理模型的。实际上,SDK中的事件处理模型也是几经修改,现在使用的方法最早由Tom Mille 在他的bolg上贴出了具体的实现:
public void MainLoop()
{
// Hook the application's idle event
System.Windows.Forms.Application.Idle += new EventHandler(OnApplicationIdle);
System.Windows.Forms.Application.Run(myForm);
}
private void OnApplicationIdle(object sender, EventArgs e)
{
while (AppStillIdle)
{
// Render a frame during idle time (no messages are waiting)
UpdateEnvironment();
Render3DEnvironment();
}
}
private bool AppStillIdle
{
get
{
NativeMethods.Message msg;
return !NativeMethods.PeekMessage(out msg, IntPtr.Zero, 0, 0, 0);
}
}
And the declarations for those two native methods members:
[StructLayout(LayoutKind.Sequential)]
public struct Message
{
public IntPtr hWnd;
public WindowMessage msg;
public IntPtr wParam;
public IntPtr lParam;
public uint time;
public System.Drawing.Point p;
}
[System.Security.SuppressUnmanagedCodeSecurity] // We won't use this maliciously
[DllImport("User32.dll", CharSet=CharSet.Auto)]
public static extern bool PeekMessage(out Message msg, IntPtr hWnd, uint messageFilterMin, uint messageFilterMax, uint flags);
这里,通过平台调用,使用了一些Win32 API。首先,在main方法中订阅了Application.Idle事件。在程序处理完了所有消息(如果不熟悉消息,那么可以把消息理解为系统定义的一个32位的值,他唯一的定义了一个事件,向Windows发出一个通知,告诉应用程序某个事情发生了。例如,单击鼠标、改变窗口尺寸、按下键盘上的一个键都会使Windows发送一个消息给应用程序。)之后,将会触发Application.Idle事件。我们的目标是让程序尽可能快,尽可能多的处理消息,同时不打断wendows消息的输入。
在OnApplicationIdle事件处理程序中,使用的了简单的Win32 API PeekMessage来检查程序是否有任何未处理的消息。这里使用while循环的原因是保证在处理完所有消息,同时消息队列还为空时,只触发一次Application.Idle事件。所以,我没一直循环,直到有新的消息,然后,跳出循环。普通的.Net WinForm窗体消息句柄将会选择未处理的消息。
接下来,我们将使用框架,来显示一些物体了(源码请参考SDK中的empty project)。由于框架已经为我们完成了以上工作。我们只需要选择订阅那些事件就可以了。Sample framework通过这些事件通知应用程序关于改变设备、用户输入以及各种窗口消息。这些事件是可选的,但是,如果你没有设置,那么框架就不会为你处理相应的事件。在main方法中,创建了GameEngine对象之后,添加代码:
sampleFramework.Disposing += new EventHandler(blockerEngine.OnDestroyDevice);
sampleFramework.DeviceLost += new EventHandler(blockerEngine.OnLoseDevice);
sampleFramework.DeviceCreated += new DeviceEventHandler(blockerEngine.CreateDevice);
sampleFramework.DeviceReset += new DeviceEventHandler(blockerEngine.OnResetDevice);
sampleFramework.SetWndProcCallback(new WndProcCallback(blockerEngine.OnMsgProc));
sampleFramework.SetCallbackInterface(blockerEngine);
(注意,虽然在SDK October 2005的文档中还可以查到framework对象的SetKeyboardCallback方法,但实际上这个方法已经被删除了,老版本的SDK示例中使用了整个方法。)
这一段代码作了很多工作,首先,为四个事件订阅了事件处理程序,分别是创建设备,失去设备,重置设备,销毁设备。我们将在后面实现这些事件处理程序。SetWndProcCallback方法订阅了处理windows消息的方法。随后,使用当前game engine实例作为参数,调用SetCallbackInterface方法。之后,编写事件处理程序
private void OnCreateDevice(object sender, DeviceEventArgs e)
{
SurfaceDescription desc = e.BackBufferDescription;
}
private void OnResetDevice(object sender, DeviceEventArgs e)
{
SurfaceDescription desc = e.BackBufferDescription;
}
private void OnLostDevice(object sender, EventArgs e)
{
}
private void OnDestroyDevice(object sender, EventArgs e)
{
}
public IntPtr OnMsgProc(IntPtr hWnd, NativeMethods.WindowMessage msg, IntPtr wParam, IntPtr lParam, ref bool noFurtherProcessing)
{
}
由于之前的SetCallbackInterface需要接收一个IframeworkCallback的变量作为参数,但是我们的game engine类并没有实现这个类,所以添加以下代码:
public class EmptyProject : IFrameworkCallback, IdeviceCreation
实现这个接口所定义的方法
public void OnFrameMove(Device device, double appTime, float elapsedTime)
{
}
public void OnFrameRender(Device device, double appTime, float elapsedTime)
{
}
哦,框架性的东西总算是弄的差不多了。在SetCallbackInterface之后加上以下代码
try
{
sampleFramework.SetCursorSettings(true, true);
sampleFramework.Initialize( false, false, true );
sampleFramework.CreateWindow("haha");
sampleFramework.Window.KeyDown += new System.Windows.Forms.KeyEventHandler(blockerEngine.OnKeyEvent);
sampleFramework.CreateDevice( 0, true, Framework.DefaultSizeWidth, Framework.DefaultSizeHeight, blockerEngine);
sampleFramework.MainLoop();
}
#if(DEBUG)
catch (Exception e)
{
sampleFramework.DisplayErrorMessage(e);
#else
catch
{
// In release mode fail silently
#endif
// Ignore any exceptions here, they would have been handled by other areas
return (sampleFramework.ExitCode == 0) ? 1 : sampleFramework.ExitCode; // Return an error code here
}
return sampleFramework.ExitCode;
}
}
现在运行程序看看,虽然只是一个蓝色的窗口,但是我们背后所搭建的框架已经可以实际应用到一个游戏之中了。为了让程序开起来有一点点交互,我们还订阅了键盘事件,通过空格键可以改变程序的颜色。