.NET每秒重绘60次?

6
我有一些在OpenGL上下文窗口中运行的动画,所以我需要不断地重绘它。因此,我想出了以下代码:
private void InitializeRedrawTimer()
{
    var timer = new Timer();
    timer.Interval = 1000 / 60;
    timer.Tick += new EventHandler(timer_Tick);
    timer.Start();
}

private void timer_Tick(object sender, EventArgs e)
{
   glWin.Draw();
}

然而,这只能让我获得40 FPS。如果我将间隔设置为1毫秒,我可以达到60 FPS。那么其他的20毫秒去了哪里?这只是计时器精度不足吗?如果我想让我的程序尽可能快地运行,有没有一种方法可以持续调用绘制函数?

4个回答

8
你可以尝试实现一个游戏循环。
参考链接:https://learn.microsoft.com/archive/blogs/tmiller/my-last-post-on-render-loops-hopefully 以下是基本循环(稍作修改以便更易读取):
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);

简单、优雅、有效。没有额外的分配,没有额外的集合,它只是起作用。当队列中没有消息时,Idle事件触发,然后处理程序持续循环直到出现消息,在这种情况下停止。一旦处理所有消息,再次触发空闲事件,过程重新开始。 此链接描述了使用应用程序的Idle事件的方法。它可能有用。你可以简单地执行时间测试或睡眠来减慢到所需的帧速率。尝试使用System.Diagnostics.StopWatch类获得最准确的计时器。 希望这能有所帮助。

不错!使用Application.Idle似乎比1毫秒计时器更优雅。我明天也要试试这个StopWatch。谢谢! 编辑:实际上,我撒了谎。当你调整大小或移动窗口时,它似乎停止了空闲状态,因此停止了重绘。这基本上降低了绘图的优先级。也许我会坚持使用计时器来强制执行最小重绘率...没有理由不能结合两种方法。 - mpen
3
停表只适用于计时,不能用于回调事件。 - mpen
3
当空闲事件结束时,您可以尝试开始计时器,然后在空闲事件触发时停止/再次检查它。通过这种方法,您可以接近所需的效果。 - Nanook
1
给定的URL似乎已经过时了。 博客文章已经移动到这里:https://learn.microsoft.com/en-us/archive/blogs/tmiller/my-last-post-on-render-loops-hopefully - Couitchy
我更新了链接。旧的链接是http://blogs.msdn.com/tmiller/archive/2005/05/05/415008.aspx,如果有人想查看/验证它。 - Xonatron

3
根据您所做的事情,可以查看WPF及其动画支持,这可能是比在WinForms中编写自己的动画更好的解决方案。此外,请考虑使用DirectX,因为它现在有一个.NET包装器并且非常快,但是它比WPF或WinForms更难使用。
要连续调用绘图函数: - 在repaint方法中进行绘制 - 在repaint方法的末尾添加BeginInvoke - 在传递给BeginInvoke的委托中,使正在绘制的控件无效
但是,您的用户可能不喜欢您杀死他们的计算机!Application.Idle也是另一个很好的选择,但我喜欢Windows在需要时减慢发送WmPaint消息的速率的事实。

我还发现,在绘制后立即使其失效比定时器方法产生更高的帧速率(65 fps是定时器的最高值,出于某种原因)。 - Markus Meyer

2
Windows Forms计时器的分辨率最终受限于计算机标准系统时钟的分辨率。这个分辨率因计算机而异,但通常介于1毫秒和20毫秒之间。
在WinForms中,无论何时您在代码中请求计时器间隔,Windows只保证您的计时器被调用的频率不会超过指定的毫秒数。它不能保证您的计时器将在您指定的确切毫秒数调用。
这是因为Windows是一种时间共享操作系统,而不是实时操作系统。其他进程和线程将被安排同时运行,因此您的线程将经常被迫等待处理器的使用。因此,您不应编写依赖于毫秒级别的精确间隔或延迟的WinForms计时器代码。
在您的情况下,将间隔设置为1毫秒实际上只是告诉Windows尽可能频繁地触发此计时器。正如您所见,这显然比1毫秒(60fps约为17毫秒)要少得多。
如果您需要更准确和更高分辨率的计时器,Windows API提供了高分辨率/性能计时器(如果硬件支持),请参见如何使用高分辨率计时器。不足之处是您的应用程序CPU使用率将增加以支持更高的性能。
总的来说,我认为最好实现一个独立于硬件/系统的游戏循环。这样,您场景的视觉动画看起来是恒定的,而不管实际帧速率达到多少。有关更多信息,请参见这些文章

-1

我注意到在给定的示例代码中,有一行是:

timer.Interval = 1000 / 60;

它使用整数除法操作,因此结果将被舍入为整数,而不是精确的16.6666毫秒,而是17毫秒。因此计时器会产生比屏幕刷新更大的滴答声。


1
整数除法截断而不是四舍五入。因此,1000/60为16(证明)。如果计时器准确,我应该得到约62.5 FPS。 - mpen
哦,我忘记了。 但是无论如何,如果是16毫秒,时钟与显示器不同步。 - Dennis
1
那取决于显示器。我很确定这不是gsync/freesync的问题,但无论如何,我相信timer.Interval在这里不是答案。 - mpen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接