什么是标准C#/ Windows Forms游戏循环?


32

当用C#编写使用原始 Windows窗体和某些图形API包装器(如SlimDXOpenTK)的游戏时,应如何构建主游戏循环?

规范的Windows窗体应用程序的入口点看起来像

public static void Main () {
  Application.Run(new MainForm());
}

同时可以做到一些什么是挂钩neccessary 的各个事件Form,这些事件没有提供明显的地方放的码位进行不断定期更新游戏逻辑对象或开始和结束渲染帧。

这种游戏应该使用什么技术来实现类似于规范的游戏

while(!done) {
  update();
  render();
}

游戏循环以及最小的性能和GC影响?

Answers:


45

Application.Run调用将驱动Windows消息泵,最终将驱动您可以挂接到Form类(和其他事件)上的所有事件。要在此生态系统中创建游戏循环,您需要侦听应用程序的消息泵何时为空,而当消息泵为空时,执行原型游戏循环的典型“处理输入状态,更新游戏逻辑,渲染场景”步骤。 。

Application.Idle每次清空应用程序的消息队列并且应用程序转换为空闲状态时,都会触发该事件。您可以将事件挂接到主窗体的构造函数中:

class MainForm : Form {
  public MainForm () {
    Application.Idle += HandleApplicationIdle;
  }

  void HandleApplicationIdle (object sender, EventArgs e) {
    //TODO: Implement me.
  }
}

接下来,您需要确定应用程序是否处于空闲状态。Idle当应用程序变得空闲时,该事件仅触发一次。直到有消息进入队列然后再次清空该队列时,它才会再次被触发。Windows Forms没有公开查询消息队列状态的方法,但是您可以使用平台调用服务将查询委托给可以回答该问题的本机Win32函数。的导入声明PeekMessage及其支持类型如下:

[StructLayout(LayoutKind.Sequential)]
public struct NativeMessage
{
    public IntPtr Handle;
    public uint Message;
    public IntPtr WParameter;
    public IntPtr LParameter;
    public uint Time;
    public Point Location;
}

[DllImport("user32.dll")]
public static extern int PeekMessage(out NativeMessage message, IntPtr window, uint filterMin, uint filterMax, uint remove);

PeekMessage基本上可以让您查看队列中的下一条消息;如果存在则返回true,否则返回false。对于此问题,没有一个参数特别重要:只是返回值很重要。这使您可以编写一个函数来告诉您应用程序是否仍处于空闲状态(即,队列中仍然没有消息):

bool IsApplicationIdle () {
    NativeMessage result;
    return PeekMessage(out result, IntPtr.Zero, (uint)0, (uint)0, (uint)0) == 0;
}

现在,您拥有编写完整的游戏循环所需的一切:

class MainForm : Form {
  public MainForm () {
    Application.Idle += HandleApplicationIdle;
  }

  void HandleApplicationIdle (object sender, EventArgs e) {
    while(IsApplicationIdle()) {
      Update();
      Render();
    }
  }

  void Update () {
    // ...
  }

  void Render () {
    // ...
  }

  [StructLayout(LayoutKind.Sequential)]
  public struct NativeMessage
  {
      public IntPtr Handle;
      public uint Message;
      public IntPtr WParameter;
      public IntPtr LParameter;
      public uint Time;
      public Point Location;
  }

  [DllImport("user32.dll")]
  public static extern int PeekMessage(out NativeMessage message, IntPtr window, uint filterMin, uint filterMax, uint remove);
}

此外,此方法与规范的本机 Windows游戏循环尽可能匹配(尽可能减少对P / Invoke的依赖),如下所示:

while (!done) {
    if (PeekMessage(&message, window, 0, 0, PM_REMOVE)){
        TranslateMessage(&message);
        DispatchMessage(&message);
    }
    else {
        Update();
        Render();
    }
}

处理此类Windows API功能有什么需要?制作由精确秒表(用于fps控制)控制的一会儿时间,还不够吗?
埃米尔·利马

3
您需要在某个时候从Application.Idle处理程序返回,否则您的应用程序将冻结(因为它永远不允许发生进一步的Win32消息)。相反,您可以尝试基于WM_TIMER消息进行循环,但是WM_TIMER并没有达到您真正想要的精确度,即使这样做,也会将所有内容强制达到最低的公分母更新速率。许多游戏需要或希望拥有独立的渲染和逻辑更新速率,其中一些(例如物理)保持固定,而其他则不是。
乔什

本机Windows游戏循环使用相同的技术(我将答案修改为包括一个简单的比较用。强制固定更新速率的计时器不太灵活,您可以始终在PeekMessage的更广泛上下文中实现固定更新速率样式的循环(使用比WM_TIMER基于计时器的计时器具有更高的精度和对GC的影响)
Josh

@JoshPetrie需要明确的是,上面的空闲检查使用了SlimDX的功能。将其包含在答案中是否理想?还是只是偶然地您将代码编辑为读取SlimDX对应的“ IsApplicationIdle”?
沃恩·希茨

**请忽略我,我才意识到您在下面对其进行了定义... :)
沃恩·希茨(Faughan Hilts)2014年

2

同意约什的回答,只想加我的5美分。WinForms默认消息循环(Application.Run)可以替换为以下内容(无p / invoke):

[STAThread]
static void Main()
{
    using (Form1 f = new Form1())
    {
        f.Show();
        while (true) // here should be some nice exit condition
        {
            Application.DoEvents(); // default message pump
        }
    }
}

另外,如果要将一些代码注入到消息泵中,请使用以下命令:

public partial class Form1 : Form
{
    protected override void WndProc(ref Message m)
    {
        // this code is invoked inside default message pump
        base.WndProc(ref m);
    }
}

2
但是,如果选择此方法,则需要注意DoEvents()垃圾生成的 开销
乔什

0

我知道这是一个旧线程,但是我想提供上述建议技术的两种替代方法。在我介绍它们之前,这里是到目前为止提出的建议的一些陷阱:

  1. PeekMessage和调用它的库方法(SlimDX IsApplicationIdle)都有相当大的开销。

  2. 如果要使用缓冲的RawInput,则需要在UI线程以外的另一个线程上使用PeekMessage轮询消息泵,因此您不想被调用两次。

  3. 没有将Application.DoEvents设计为紧密循环调用,GC问题将很快出现。

  4. 使用Application.Idle或PeekMessage时,由于仅在空闲时才工作,因此在移动或调整窗口大小时,游戏或应用程序将不会运行,而不会产生代码异味。

要变通解决这些问题(如果您沿着RawInput前进,则除外2),您可以:

  1. 创建一个Threading.Thread并在其中运行游戏循环。

  2. 创建带有IsLongRunning标志的Threading.Tasks.Task,并在其中运行它。Microsoft建议这些天使用Tasks代替Threads,不难看出为什么。

这两种技术都建议将图形API与UI线程和消息泵隔离开来。在窗口调整大小期间,资源/状态破坏和重新创建的处理也得到了简化,并且从UI线程完成完成时(在进行适当的谨慎操作以避免消息泵时出现死锁)完成后,从美学上讲它更加专业。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.