我正在制作游戏,我希望有一个带有背景图像的主菜单。
但是,我发现方法Graphics.DrawImage()
确实很慢。我做了一些测量。我们假设MenuBackground是我的资源图像,分辨率为800 x 1200像素。我将它绘制到另一个800 x 1200位图上(我首先将所有内容渲染到缓冲区位图,然后我将其缩放并最终将其绘制到屏幕上 - 这就是我如何处理多个玩家分辨率的可能性。但它不应该影响它以任何方式,见下一段)。
所以我测量了以下代码:
Stopwatch SW = new Stopwatch();
SW.Start();
// First let's render background image into original-sized bitmap:
OriginalRenderGraphics.DrawImage(Properties.Resources.MenuBackground,
new Rectangle(0, 0, Globals.OriginalScreenWidth, Globals.OriginalScreenHeight));
SW.Stop();
System.Windows.Forms.MessageBox.Show(SW.ElapsedMilliseconds + " milliseconds");
结果让我感到惊讶 - Stopwatch
衡量40 - 50 milliseconds
之间的某些内容。并且因为背景图像不是唯一要绘制的东西,所以整个菜单需要大约100毫秒来显示,这意味着可观察到的延迟。
我试图将它绘制到Paint事件给出的Graphics对象,但结果是30 - 40 milliseconds
- 没有太大变化。
那么,是否意味着Graphics.DrawImage()
无法用于绘制更大的图像?如果是这样,我该怎么做才能提高游戏性能?
答案 0 :(得分:97)
是的,它太慢了。
几年前,我在开发Paint.NET时遇到了这个问题(从一开始,实际上,它非常令人沮丧!)。渲染性能非常糟糕,因为它总是与位图的大小成比例,而不是它被重新绘制的区域的大小。也就是说,当位图的大小上升时,帧率下降,并且当实现OnPaint()并调用Graphics.DrawImage()时,帧率从未上升,因为无效/重绘区域的大小下降。一个小位图,比如800x600,总是工作得很好,但是较大的图像(例如2400x1800)非常慢。 (无论如何,对于前面的段落,您可以假设没有任何额外的事情发生,例如使用一些昂贵的双立方滤波器进行缩放,这会对性能产生负面影响。)
可以强制WinForms使用GDI而不是GDI +,甚至可以避免在幕后创建Graphics
对象,此时你可以在其上层叠另一个渲染工具包(例如Direct2D)。但是,这并不简单。我在Paint.NET中执行此操作,您可以在SystemLayer DLL中的类GdiPaintControl
上使用类似Reflector的内容查看所需的内容,但是对于您正在执行的操作,我认为这是最后的手段。 / p>
但是,你正在使用的位图大小(800x1200)在GDI +中仍然可以正常工作而不必采用高级互操作,除非你的目标是低至300MHz的Pentium II。以下提示可能有所帮助:
Graphics.DrawImage()
时使用不透明位图(无alpha /透明度),特别是如果它是带有Alpha通道的32位位图(但您知道它是不透明的,或者你不关心),然后在调用Graphics.CompositingMode
之前将CompositingMode.SourceCopy
设置为DrawImage()
(确保将其设置回原始值之后,否则常规绘图基元将会显示十分难看)。这会为每个像素跳过很多额外的混合数学。Graphics.InterpolationMode
未设置为InterpolationMode.HighQualityBicubic
。使用NearestNeighbor
将是最快的,但如果有任何拉伸可能看起来不太好(除非它正好拉伸2x,3x,4x等)Bilinear
通常是一个很好的折衷方案。如果位图大小与您要绘制的区域匹配,则不应使用NearestNeighbor
以外的任何内容,以像素为单位。Graphics
对象
OnPaint()
进行绘图。如果您需要重绘某个区域,请致电OnPaint
。如果您需要立即进行绘图,请在Invalidate()
之后致电Update()
。这是一种合理的方法,因为WM_PAINT消息(导致调用Invalidate()
)是“低优先级”消息。窗口管理器的任何其他处理都将首先完成,因此最终可能会导致大量的跳帧和挂钩。OnPaint()
作为帧率/滴答计时器将无法正常工作。这些是使用Win32的System.Windows.Forms.Timer
实现的,并导致WM_TIMER消息,然后导致SetTimer
事件被引发,WM_TIMER是另一个低优先级消息,仅在消息队列为空时发送。您最好使用Timer.Tick
,然后使用System.Threading.Timer
(以确保您使用正确的主题!)并致电Control.Invoke()
。Control.Update()
。 (“总是吸引Control.CreateGraphics()
”和“总是使用OnPaint()
给你的Graphics
”的必然结果OnPaint()
,该类应该来自OnPaint()
。从另一个类派生,例如Control
或PictureBox
,不会为您添加任何值,也不会增加额外费用。 (BTW UserControl
经常被误解。你可能几乎不会想要使用它。)希望有所帮助。
答案 1 :(得分:3)
GDI +可能不是游戏的最佳选择。 DirectX / XNA或OpenGL应该是首选,因为它们可以利用任何可能的图形加速并且非常快。
答案 2 :(得分:3)
GDI +无论如何都不是速度恶魔。任何严肃的图像处理通常都必须进入本机方面(pInvoke调用和/或通过调用LockBits
获得的指针进行操作。)
你看过XNA / DirectX / OpenGL吗?这些是为游戏开发而设计的框架,与使用WinForms或WPF等UI框架相比,它们的效率和灵活性要高出几个数量级。这些库都提供C#绑定。
您可以使用BitBlt之类的函数pInvoke到本机代码,但是也存在与跨越托管代码边界相关的开销。
答案 3 :(得分:2)
尽管这是一个古老的问题,而WinForms是一个古老的Framework,但我想分享一下我刚刚偶然发现的内容:将Bitmap绘制到BufferedGraphics中,然后将其渲染到OnPaint提供的图形上下文是比将位图直接绘制到OnPaint的图形上下文更快-至少在Windows 10计算机上。
这是令人惊讶的,因为直觉上我已经假设两次复制数据会稍微慢一些(所以我认为通常只有在要手动执行双缓冲时才有理由这样做)。但是显然,BufferedGraphics对象正在发生一些更复杂的事情。
因此,在将承载位图的控件的构造函数中创建一个BufferedGraphics(在我的情况下,我想绘制全屏位图1920x1080):
using (Graphics graphics = CreateGraphics())
{
graphicsBuffer = BufferedGraphicsManager.Current.Allocate(graphics, new Rectangle(0,0,Screen.PrimaryScreen.Bounds.Width,Screen.PrimaryScreen.Bounds.Height));
}
并在OnPaint中使用它(同时使OnPaintBackground无效)
protected override void OnPaintBackground(PaintEventArgs e) {/* just rely on the bitmap to fill the screen */}
protected override void OnPaint(PaintEventArgs e)
{
Graphics g = graphicsBuffer.Graphics;
g.DrawImage(someBitmap,0,0,bitmap.Width, bitmap.Height);
graphicsBuffer.Render(e.Graphics);
}
不是天真地定义
protected override void OnPaintBackground(PaintEventArgs e) {/* just rely on the bitmap to fill the screen */}
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.DrawImage(someBitmap,0,0,bitmap.Width, bitmap.Height);
}
请参见以下屏幕截图,以比较所产生的MouseMove事件频率(我正在实现一个非常简单的位图草绘控件)。顶部是直接绘制位图的版本,底部是BufferedGraphics。在两种情况下,我都以相同的速度移动了鼠标。