65.9K
CodeProject 正在变化。 阅读更多。
Home

简单的 Managed DirectX 渲染循环

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (22投票s)

2005年7月15日

5分钟阅读

viewsIcon

101936

downloadIcon

549

一个简单的框架,用于为使用 Microsoft .NET 编写的游戏或模拟实现最佳的托管 DirectX 渲染循环。

引言

这是托管 DirectX 渲染循环的框架。这些循环用于使游戏或模拟以最高性能渲染图形和更新状态,同时仍然响应用户输入和系统事件,而不会拖慢整个系统。本文不解释如何使用 DirectX 或 Direct3D,只解释如何最好地安排游戏或模拟的主引擎循环以获得最佳性能和响应能力。

背景

此实现直接来自 Tom Miller 于 2005 年 5 月 5 日发布的 博客文章。Tom Miller 是一位知名的作家,也是 Microsoft 托管 DirectX 团队的首席开发人员。经过多次迭代,他发现这种模式可以在不依赖 WinAPI 技巧的情况下,为 .NET 托管应用程序提供最佳性能。DirectX SDK 的 2005 年 6 月版本包含一个更完整的框架,但我将向您展示最简单的实现,并解释它为什么/如何工作以及为什么它是最好的。使用 C++ 并避免 .NET WinForms 基类可以更快,但已经有许多公开的技术可用。这里的目的是创建一个 100% 托管的解决方案,仅使用 .NET 代码。

使用代码

我将通过一次查看代码片段来分析整个类。可下载的源代码包含一个完整的 MainForm 类。

当应用程序启动时,在 Main 方法中,我们会绑定到一个不常见的事件:Application.Idle

[STAThread]
public static void Main(){
    MainForm myForm = new MainForm();
    Application.Idle += new EventHandler(myForm.Application_Idle);
    Application.Run(myForm);
}

如果您不熟悉 Windows 消息,它们是按键和鼠标移动等事件的简单通知,但也可以涵盖系统关闭、最小化您的应用程序等。您计算机中发生的几乎所有事情最终都会变成 Windows 消息。

每当我们的应用程序完成处理所有传入的 Windows 生成的消息后,就会触发 Application.Idle 事件。我们的目标是让我们的应用程序尽快处理尽可能多的内容,但我们不希望阻止传入 Windows 消息的流动。

这是我们的 Application_Idle 事件处理程序

protected void Application_Idle(object sender, EventArgs e){
    while(AppStillIdle){
        // TODO: Main game loop goes here.
    }
}

public bool AppStillIdle{
   get{
      PeekMsg msg;
      return !PeekMessage(out msg, IntPtr.Zero, 0, 0, 0);
   }
}

AppStillIdle 属性利用了一个简单的 Win32 API 函数 (PeekMessage),该函数检查是否有我们应用程序需要处理的待处理 Windows 消息。我们进入 while 循环的原因是,Application.Idle 只会在应用程序完成处理所有可能的 Windows 消息并且待处理消息队列为空时触发一次。因此,我们希望继续循环,直到 Windows 碰巧给我们另一条消息来处理(当 PeekMessage 返回 true 时)。然后我们将退出循环并离开 Application_Idle。当 PeekMessage 返回 true 时,正常的 .NET WinForms Windows 消息处理程序将捕获我们看到的待处理消息。

根据 Win32 API 文档,PeekMessage 返回的结构与 System.Windows.Forms.Message 中 .NET 定义的结构不同。我们必须重新定义自己的 Message 类版本,称为 PeekMsg

[StructLayout(LayoutKind.Sequential)]
public struct PeekMsg {
   public IntPtr hWnd;
   public Message msg;
   public IntPtr wParam;
   public IntPtr lParam;
   public uint time;
   public System.Drawing.Point p;
}

使用 .NET 结构传递给 Win32 或 COM 时,需要 StructLayout 属性。

这是 PeekMessage 的原生方法定义

[DllImport("User32.dll", CharSet=CharSet.Auto)]
private static extern bool PeekMessage(out PeekMsg msg, IntPtr hWnd, 
        uint messageFilterMin, uint messageFilterMax, uint flags);

加快速度

我们可以对这个应用程序进行一些增强。其中一些并不难做,但原因并不十分清楚。第一个是设置我们窗体的 ControlStyle。这必须在窗体构造函数中完成

this.SetStyle(ControlStyles.UserPaint, true);
this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);

这些行会导致窗体使用标准的 .NET 和 Windows 窗体绘制代码,并绕过额外的绘制相关消息和事件。显然,如果我们打算使用 DirectX 来绘制我们的游戏或模拟,我们不需要 Windows 和 GDI+ 为我们做大量永远不可见的事情。对于全屏和窗口模式的游戏和模拟都是如此。

我们还可以做的另一件事是修改 PeekMessage 的原生方法定义

[System.Security.SuppressUnmanagedCodeSecurity]
[DllImport("User32.dll", CharSet=CharSet.Auto)]
private static extern bool PeekMessage(out PeekMsg msg, IntPtr hWnd, 
         uint messageFilterMin, uint messageFilterMax, uint flags);

通过将 SuppressUnmanagedCodeSecurity 属性添加到调用中,.NET 将跳过在允许您的应用程序调用原生 Windows 方法之前进行的与安全相关的检查。这极其危险,不应在仔细考虑调用所带来的安全影响之前进行。在这种情况下,将方法声明设为私有就足够了,特别是 since 我们进行的调用仅仅是告诉我们是否有消息在等待我们。不幸的是,所讨论的方法在有消息的情况下会返回下一条 Windows 消息。恶意代码可以利用这一点来拦截击键、监视网络流量,或解释和重定向鼠标点击。请务必通过将其设为私有,不要将此方法调用留给恶意代码可以访问的地方。

关注点

您是否注意到我们根本没有处理 Control.Paint 事件?原因是该事件不是必需的。由于设置了 UserPaint 样式,.NET 的控件绘制代码会被禁用。Windows 仍然会发送 WM_PAINT 消息,但一旦您的应用程序完成…什么都不做,Application.Idle 就会触发,您将刷新您的 DirectX 曲面。如果您确实想这样做,可以在 Control.Paint 事件处理程序中进行额外的调用来渲染您的场景,但我不知道这对性能或响应能力有什么影响。

历史

  • 发布于 2005 年 7 月 14 日。
© . All rights reserved.