使用 Microsoft .NET框架精简版编写移动游戏
作者:Ravi Krishnaswamy
相关技术:Visual Studio 2003、.NET Compact Framework、 Windows Powered Pocket PC
难度等级:★★★☆☆
读者类型:Windows CE 开发人员、移动设备游戏开发人员
[导读]本文讲述了如何创建基于.NET框架精简版的游戏以及编写面向小型设备的游戏的主要要求。在提高游戏性能方面,本文介绍了一些高级性能优化技术帮助我们突破游戏的速度瓶颈。
Microsoft .NET框架精简版是完整Microsoft .NET框架的子集。它是对完整的.NET框架进行精简后得到的版本虽然其规模大大减小,但多数功能仍然保持完整。
使用.NET框架精简版:可以针对Pocket PC和其他Windows CE .NET设备进行单一的二进制部署,提高开发人员的工作效率,加快产品投放市场的速度。
本文将讨论编写面向小型设备的游戏的主要环节以及部分高级性能优化技术,我们可以使用这些技术来突破游戏的极限。总之,我们将了解到使用.NET框架精简版来开发和优化游戏是多么容易。这将是一次有趣的旅行,请系好安全带并充分享受其中的乐趣吧。
在游戏应用程序中,我们经常会使用设备的全屏显示功能。占据整个屏幕区域的窗体称为全屏窗体(也称为游戏窗体)。换句话说,全屏窗体占据桌面(即工作区)以及非工作区,如顶部的标题/导航栏、边框和底部的菜单栏。
应用程序通过将其WindowState设置为Maximized来创建全屏窗体,如下所示:
form.WindowState = FormWindowState.Maximized;
如果该窗体附加了菜单栏(和/或Pocket PC中的工具栏),则不能使其成为全屏窗体。
在Pocket PC的.NET框架精简版1.0中,要创建全屏应用程序,必须在窗体的OnLoad内部设置WindowState属性。
下面的图1和图2阐明了Pocket PC中的全屏窗体和非全屏窗体之间的区别。
图1 非全屏窗体
图2 全屏窗体
拥有全屏窗体的主要特征是没有标题/导航栏或菜单栏。应用程序必须考虑这些因素,并且在必要时提供尽量回避调用菜单功能的手段。
如果我们只是希望我们的窗体仅填充可用的桌面区域(而不是全屏),则无须做任何事情。默认情况下,.NET框架精简版会自动调整窗体的大小以填充Pocket PC的屏幕。
事实上,我们最好不要明确设置窗体的ClientSize以达到这种效果,因为这可能会妨碍应用程序在各种Windows CE.NET设备之间的互操作性。例如,如果我们明确调整应用程序的大小以匹配其中一个设备的窗体指数,则该大小在其他设备上可能并不理想。明智的做法是采用窗体的默认大小。
典型的游戏应用程序会对窗体内容进行自定义绘制。它采取的办法是重写控件的OnPaint()事件并对窗体的绘制进行自定义处理。
protected override void OnPaint(PaintEventArgs paintg)
{
Graphics gx = paintg.Graphics;
// Custom draw using the graphics object
}
每当控件开始绘制时,都会首先自动刷新其背景。例如,OnPaint事件将首先用this.Backcolor中指定的颜色来绘制背景。但如果应用程序前景绘制完成之前调用了自动背景绘制,则屏幕会出现瞬间的闪烁。
要避免这一情况,我们建议每当应用程序重写OnPaint方法时,都要重写OnPaintBackground方法并自己来绘制背景。应用程序可以选择在OnPaint内部处理所有绘制工作,并将OnPaintBackground保留为空,如下面的示例所示。
protected override void OnPaintBackground(PaintEventArgs paintg)
{
// Left empty, avoids undesirable flickering
}
通过控件的this.CreateGraphics方法来获取屏幕的图形对象并直接向其进行绘制,我们可以进行屏上绘制。必须记住在不再需要屏上图形对象时将其处理。如果不这样做,可能导致有限的显示设备资源出现不足。
如上一节所示,我们可以在OnPaint和OnPaintBackground方法内通过PaintEventArgs.Graphics来访问屏幕的图形对象。在执行绘图方法以后,这些图形对象将被自动处理。
对于直接在屏幕上绘制的游戏应用程序,,当我们在屏幕上绘制多个对象时,将会看到屏幕闪烁。为避免这种现象,游戏开发人员通常会采用屏外绘制技术。
这一技术的思想是创建屏外位图,为其获取图形对象,在内存中执行所有绘图操作,然后将得到的屏外位图复制到屏幕上。
// Create off-screen graphics
Bitmap bmpOff = new Bitmap(this.ClientRectangle.Width,
this.ClientRectangle.Height);
Graphics gxOff = Graphics.FromImage(bmpOff);
在该例中,我将创建一个大小恰好与游戏窗体的工作区相同的屏外位图。在离屏和屏上绘图界限之间保持1:1的大小关系将有很大好处,尤其是在屏内(On-Screen)和屏外之间变换子图形的坐标时。
// Draw to off-screen graphics, using Graphics.Draw APIs
// Create on-screen graphics
Graphics gxOn = this.CreateGraphics();
// Copy off-screen image onto on-screen
gxOn.DrawImage(bmpOff, 0, 0, this.ClientRectangle, GraphicsUnit.Pixel);
// Destroy on-screen graphics
gxOn.Dispose();
这一技术可以避免屏幕闪烁,并且速度更快,因为所有屏外绘图操作都发生在内存中。
像位图这样的栅格图形都以矩形表示,而大多数实际的子图形都具有不规则的形状(不是矩形)。因此,我们需要找到相应的方法,以便从矩形光栅图形表示中提取形状不规则的子图形。
游戏开发人员常用的一种技术是颜色键技术,即在渲染时忽略位图中的指定颜色键。这一技术也称为色度键屏蔽、颜色取消和透明混合。
图3 子图形
图4 带有颜色键透明
图5 不带颜色键透明
在子图形位图(图3)中,非对象区域被填充了品红色,该颜色被用作颜色键。使用以及不使用该技术时得到的混合效果分别在图4和图5中进行了说明。
透明混合的第一步是设置需要在渲染时屏蔽的颜色键(色度键)。我们需要指定精确的颜色键值。
ImageAttributes imgattr = new ImageAttributes();
imgattr.SetColorKey(Color.Magenta, Color.Magenta);
与使用Color类提供的标准颜色集不同,我们可以通过指定红色、绿色和蓝色(RGB)值来直接构建自己的颜色,如下所示:
imgattr.SetColorKey(Color.FromArgb(255, 0, 255),
Color.FromArgb(255, 0, 255));
我们经常用来指定颜色键的另外一种技术是直接使用像素值。这可以避免处理RGB值的需要。而且,颜色键不是硬编码的,可以在位图中独立进行更改。
imgattr.SetColorKey(bmpSprite.GetPixel(0,0), bmpSprite.GetPixel(0,0));
现在,让我们看一下如何使用已经设置的颜色键来透明地绘制子图形。
gxOff.DrawImage(bmpSprite, new Rectangle(x, y, bmpSprite.Width,
bmpSprite.Height), 0, 0, bmpSprite.Width, bmpSprite.Height,
GraphicsUnit.Pixel, imgattr);
在上述代码中,目标矩形被指定为new Rectangle(x, y, bmpSprite.Width, bmpSprite.Height),其中x和y是子图形的预期坐标。
通常,在绘制时可能需要放大或缩小子图形。我们可以通过调整目标矩形的宽度和高度来做到这一点。同样,还可以调整源矩形以便仅绘制子图形的一部分。
在设置子图形位图时,最好考虑一下目标设备的颜色分辨率。例如,24位颜色位图在12位颜色分辨率设备上可能不会按预期方式渲染;由于所用颜色的差异,这两者之间的颜色梯度差异可能非常明显。而且,在选择要屏蔽的ColorKey时,请确保颜色值在目标显示器所支持的范围之内。请记住,只有精确的ColorKey匹配才会受到支持。
颜色键透明是.NET框架精简版中唯一受支持的混合技术。
我们可以将图形资源嵌入到我们的程序集中,方法是将该图形添加到项目中,并将其Build Action属性设置为“Embedded Resource”。这样,我们就可以按如下方式使用嵌入的bmp资源:
Assembly asm = Assembly.GetExecutingAssembly();
Bitmap bmpSprite = new Bitmap(asm.GetManifestResourceStream("Sprite"));
BMP、JPG、GIF和PNG图形格式受到Bitmap类的支持。
游戏中的绘图例程需要进行优化以便得到最佳的性能。比较笨拙的屏幕绘图方法是以离屏方式擦除并重绘所有子图形,然后用以离屏方式得到的图形进行屏上刷新。这样做效率很低,因为我们每次都不必要地重绘整个屏幕。这时,我们的帧速率将取决于框架精简版刷新整个屏幕的速率。
一种更好的办法是计算游戏中子图形的脏区,并且只刷新屏幕变脏的部分。子图形可能因为多种原因而变脏,例如,移动、图形/颜色发生改变或者与其他子图形发生冲突,等等。在本章中,我们将讨论可用于有效计算脏区的各种技术。
让我们考虑一个移动的子图形,一种计算脏区的简单方法是获取旧界限和新界限的并集。
RefreshScreen(Rectangle.Union(sprite.PreviousBounds, sprite.Bounds));
其中,RefreshScreen()是一个以屏上方式刷新指定矩形区域的方法。
图6 脏区:旧界限和新界限的并集
图7 巨大的增量产生巨大的脏区
注: 如图7所示,如果旧坐标和新坐标之间的增量很高并且/或者子图形很大,则这种技术会产生过大的脏矩形(为便于说明,旧的界限用不同颜色显示)。
这种情况下,更有效的方法是将子图形的脏区计算为多个单元矩形,这些矩形放到一起时表示脏区。
首先,让我们弄清楚旧的界限和新的界限有没有相互重叠。如果没有,则可以简单地将脏区计算为两个单独的矩形,分别表示旧的界限和新的界限。因此,对于图7说明的情形,明智的做法是将旧的界限和新的界限视为两个单独的脏区。
if (Rectangle.Intersection(sprite.PreviousBounds,
sprite.Bounds).IsEmpty)
{
// Dirty rectangle representing old bounds
RefreshScreen(sprite.PreviousBounds);
// Dirty rectangle representing current bounds
RefreshScreen(sprite.Bounds);
}
当我们并不介意将重叠区域重绘两次时,上述技术也将有效,如下面的图8所示。
图8 将脏区拆分为旧的界限和新的界限
现在,让我们看一下如何将脏区计算为多个单元矩形(这些矩形共同表示部分重叠的旧界限和新界限),以便不会重绘任何脏区,也就是说所有单元矩形都是互斥的。
首先,包含新的界限作为一个完整单元。请注意,这包括旧界限和新界限之间的重叠区域。
单元脏区1:表示当前界限的脏矩形
RefreshScreen(sprite.Bounds);
图9 脏区拆分为多个单元
现在,将旧界限的非重叠部分拆分为两个独立的单元,如下面的代码所示:
Rectangle rcIx, rcNew;
// Calculate the overlapping intersection
rcIx = Rectangle.Intersection(sprite.PreviousBounds, sprite.Bounds);
单元脏区2:
rcNew = new Rectangle();
if (sprite.PreviousBounds.X < rcIx.X)
{
rcNew.X = sprite.PreviousBounds.X;
rcNew.Width = rcIx.X - sprite.PreviousBounds.X;
rcNew.Y = rcIx.Y;
rcNew.Height = rcIx.Height;
}
else
{
// Means sprite.PreviousBounds.X should equal to rcIx.X
rcNew.X = rcIx.X + rcIx.Width;
rcNew.Width = (sprite.PreviousBounds.X +
sprite.PreviousBounds.Width) - (rcIx.X + rcIx.Width);
rcNew.Y = rcIx.Y;
rcNew.Height = rcIx.Height;
}
RefreshScreen(rcNew);
单元脏区3:
rcNew = new Rectangle();
if (sprite.PreviousBounds.Y < rcIx.Y)
{
rcNew.Y = sprite.PreviousBounds.Y;
rcNew.Height = rcIx.Y - sprite.PreviousBounds.Y;
rcNew.X = sprite.PreviousBounds.X;
rcNew.Width = sprite.PreviousBounds.Width;
}
else
{
rcNew.Y = rcIx.Y + rcIx.Height;
rcNew.Height = (sprite.PreviousBounds.Y +
sprite.PreviousBounds.Height) - (rcIx.Y + rcIx.Height);
rcNew.X = sprite.PreviousBounds.X;
rcNew.Width = sprite.PreviousBounds.Width;
}
RefreshScreen(rcNew);
现在,让我们看一下一个子图形与另一个子图形冲突的情形。从绘图角度来看,可以简单地忽略两个子图形之间的冲突,而只是使用前面讨论的技术逐个更新这些子图形的脏区。
但是,我们经常需要检测子图形的冲突以便使游戏做出响应。例如,在射击游戏中,当子弹击中目标时,我们可能希望通过爆炸或类似形式直观地做出反应。
在本文中,我们将所有重点讨论冲突检测技术其中的常用的几种技术。
可以回忆一下,大多数子图形的形状都是不规则的,但用于表示它们的光栅图形是矩形。很难将子图形的界限(或包络线)表示为开放的区域,因此我们将通过封闭的矩形来表示它。
当玩家看到屏幕上的子图形时,他/她实际上看到的是子图形区域,而觉察不到非子图形区域。因此,子图形之间的任何冲突检测都必须仅发生在其各自的子图形区域之间,而不应包括非子图形区域。
对于较小的子图形,在计算冲突并直观地避免冲突时,通常可以使用整个子图形界限。因为对象较小并且移动迅速,肉眼将不会注意到错觉。简单地计算两个子图形界限的矩形交集就足够了。
Rectangle.Intersect(sprite1.Bounds, sprite2.Bounds);
如果子图形的形状是圆形,则可以简单地计算它们的圆心之间的距离并减去其半径;如果结果小于零,则表明存在冲突。
对于逐个像素的碰撞检测,可以使用Rectangle.Contains方法:
if (sprite.Bounds.Contains(x,y))
DoHit();
首先应该应用快速边界相交技术来检测子图形之间的边界冲突。如果发生了冲突,则我们可以使用一种更为精确的方法(如冲突位图屏蔽技术)来确定相互重叠的像素。
如果我们不关心像素粒度,则可以使用我们已经在脏区计算一节中看到的技术来计算冲突区域。我们将处理两个发生冲突的子图形的界限,而不是处理一个子图形的旧界限和新界限。
冲突检测技术是专用的,应该根据具体情况加以确定。应该根据多种因素来选择特定的技术,如子图形的大小和形状、游戏的特性等。在一个游戏中使用上述技术中的多个技术是很常见的。
帧速率经常被单纯理解为移动子图形的速度,我们还应该控制子图形在每帧中移动的距离,以获得期望的净速度。
让我们考虑下面的示例,在该示例中,我们希望子图形每秒钟纵向移动100个像素单位。现在,我们可以将帧速率固定为10fps,并且将子图形每帧纵向移动10个像素,以便达到上述净速度,或者我们还可以将帧速率增加到20fps,并且使子图形的每帧纵向移动距离下降至5个像素。
采用任一种方法都可以达到相同的净速度,区别在于:在前一种情形下,子图形的移动看起来可能有一点跳跃性,因为与后一种情形相比,它移动相同距离所用的刷新周期要短一些。但是,在后一种情形中,我们依赖于游戏以20fps的帧速率渲染。因此,在决定使用哪一种方法之前,我们需要绝对确定硬件、系统和.NET 框架精简版的功能。
当屏幕随着时间的推移而发生变化并且直观地响应用户交互时,就说游戏正在前进。
在游戏内部,我们开始、维护和破坏循环,这使我们在必要时有机会渲染屏幕。通常,当游戏启动时,游戏循环开始,然后根据需要休眠和循环返回来进行维护,直到游戏结束为止,此时游戏循环将被弹出。
这种技术可以提供动作游戏所需的最大的灵活性和快速的周转速度。现在,我们将为我们的足球游戏实现一个游戏循环。让我们假设该游戏具有多个级别。
private void DoGameLoop()
{
// Create and hold onto on-screen graphics object
// for the life time of the loop
m_gxOn = this.CreateGraphics();
do
{
// Init game parameters such as level
DoLevel();
// Update game parameters such as level
// Ready the game for the next level
}
while (alive);
// End game loop
// Dispose the on-screen graphics as we don't need it anymore
m_gxOn.Dispose();
// Ready the game for next time around
}
private void DoLevel()
{
int tickLast = Environment.TickCount;
int fps = 8;
while ((alive) && (levelNotCompleted))
{
// Flush out any unprocessed events from the queue
Application.DoEvents();
// Process game parameters and render game
// Regulate the rate of rendering (fps)
// by sleeping appropriately
Thread.Sleep(Math.Abs(tickLast + 1000/fps) - Environment.TickCount));
tickLast = Environment.TickCount;
}
}
注意,在循环返回之前我们每次都要在循环内部调用Application.DoEvents(),我们这样做的目的是保持与系统之间的活动通讯,以及便于对事件队列中挂起的消息进行处理。这是有必要的,因为当我们的应用程序处于循环中时,我们基本上失去了处理任何来自系统的传入消息的能力;除非我们明确调用Application.DoEvents(),否则我们的应用程序将不会响应系统事件,并因此具有不合需要的副作用。
在游戏循环中需要考虑的另一个重要因素是渲染速率。大多数游戏动画至少需要每秒钟8-10帧(fps)的速率。为了使我们有一个大致的概念,以典型的卡通影片为例,它的渲染速率是每秒钟14-30帧。
一种简单的帧速率控制技术是决定所需的fps以及循环内部的休眠(1000/fps)毫秒。但是,我们还需要将处理当前走时所需的时间考虑在内。否则,我们的渲染速率将比期望的速率慢。处理时间可能相当可观,对于速度较慢的硬件尤其如此,因为这涉及到开销较高的操作,如处理用户输入、渲染游戏等等。
因此,在继续循环之前,我们需要休眠1000/fps减去处理当前走时所需的时间(毫秒)。
Thread.Sleep(Math.Abs(tickLast + (1000 / fps) - Environment.TickCount));
tickLast = Environment.TickCount;
另一项技术是设置一个定期回调的系统计时器。对于游戏循环而言,通常情况下,在游戏启动时实例化计时器,在游戏结束(此时计时器被处理)之前对计时器计时事件(定期发生)进行处理。
这要比游戏循环简单,因为我们让系统为我们处理计时器循环。我们只需要处理计时器回调,并且通过一次绘制一个帧来使游戏前进。
但是,我们必须非常小心地选择计时器的走时间隔,因为它决定了游戏的帧速率。
在游戏循环技术中,两次走时之间的时间间隔完全在我们的控制之下,并且正如前面所看到的,可以轻松地对其进行控制以便将处理时间考虑在内。另一方面,计时器回调意味着该时间间隔不能变化。因此,我们需要选择足够大的走时间隔以便完成每个回调的处理,或者通过显式跟踪处理一次走时所需的时间来控制走时的处理,并且在必要时跳过走时以保持节奏。
计时器回调的一个主要缺陷是我们需要依赖于操作系统计时器的分辨率。可能存在的最短走时间隔由该计时器可能具有的最高分辨率决定。在计时器分辨率较低的Pocket PC上,这可能成为一个限制因素,从而意味着这种方法中可能具有的fps也会比较低。另外,操作系统计时器事件的优先级非常低,这意味着游戏的响应速度也比较低。
虽然有这些局限,但对于前进速度较低的游戏(此时帧速率不太重要)而言,这一技术比较适用。例如,屏幕保护程序
private void StartTimer ()
{
int fps = 8;
// Create and hold onto on-screen graphics object
// for the life of the game/timer
m_gxOn = this.CreateGraphics();
// Setup timer callback to happen every (1000/fps) milliseconds
m_tmr = new System.Windows.Forms.Timer();
m_tmr.Interval = 1000/fps;
// Specify the timer callback method
m_tmr.Tick += new EventHandler(this.OnTimerTick);
// Start the timer
m_tmr.Enabled = true;
// Init game params such as level
}
protected void OnTick(object sender, EventArgs e)
{
if (alive)
{
// Regulate tick to include processing time,
// skip tick(s) if necessary
if (processTick)
{
// Process game parameters and render game
if (levelCompleted)
{
// Update game params such as level
}
}
}
else
EndTimer ();
}
private void EndTimer ()
{
// End game
// Dispose timer
m_tmr.Dispose();
// Dispose the on-screen graphics as we don't need it anymore
m_gxOn.Dispose();
m_gxOn= null; // Make sure the garbage collector gets it
// Ready the game for next time around
}
注:游戏结束时必须处理计时器。
请注意,没有必要使用Application.DoEvents(),因为我们既未循环也未阻塞任何系统事件,而计时器回调实际上只是一个系统事件。同时,请注意大多数游戏逻辑序列被放入到OnTimerTick()事件处理程序中。
另一种使游戏前进的方式是adhoc(内置动态寻址) 的基础上渲染它。每当游戏逻辑检测到屏幕需要刷新时,我们可以请求系统使屏幕的相应部分无效并对其进行刷新。
这一技术是最简单的,并且最适合基于用户交互前进的游戏。这是指那些不是持续不断地走时和渲染(如游戏循环、计时器回调中那样)的游戏,而是仅当用户与其交互时才前进的游戏,例如智力测验游戏。
我们可以通过调用this.Invalidate()使游戏窗体的整个工作区无效,或者通过调用this.Invalidate(dirtyRect)仅使其一部分无效。
只调用this.Invalidate()不能保证绘图操作会及时发生。我们必须通过调用this.Update()来确保屏幕在继续前进之前被刷新。在某些情况下,异步调用Invalidate()和Update()可能有助于获得更高的性能。但是如果不使用适当的帧同步技术,可能导致屏幕上出现不自然的画面,或者帧被丢弃。
如果我们能够承担得起每次都刷新整个屏幕的开销,则可以简单地调用this.Refresh(),它可以确保Invalidate()和Update()依次发生。
当我们作为所有者绘制窗体时,我们可以在屏幕的某个部分需要刷新时调用this.Refresh(),并且在内部跟踪屏幕的脏区,以及在OnPaint()和OnPaintBackground()内部有选择地刷新屏幕。
在游戏中,开发人员通常会预先初始化所有游戏参数,从而在游戏进行过程中避免不必要的运行时延迟。这一方法的缺陷在于延迟被转移到游戏启动过程中,如果游戏花费太长的时间加载,然后用户才能与其交互,则尤其会令人感到不快。
这种情况下,可取的做法是尽可能快地显示一个含有与游戏相关信息的启动画面,以便吸引用户。然后,我们可以在后台执行启动活动,如加载资源、初始化游戏参数等等。
我们可以使用单独的全屏窗体作为启动画面,也可以使用仅含有基本游戏信息的主游戏窗体本身。
public Game()
{
// Set visibility first
this.Visible = true;
// Create on-screen graphics
Graphics gxOn = this.CreateGraphics();
// Display Splash screen
DoSplashScreen(gxOn);
// Destroy on-screen graphics
gxOn.Dispose();
// Proceed with your Game Screen
}
void DoSplashScreen(Graphics gxPhys)
{
// Load minimal resources such as title bitmap
Assembly asm = Assembly.GetExecutingAssembly();
Bitmap bmpTitle =
new Bitmap(asm.GetManifestResourceStream("title"));
// Draw the title screen a€_ this is your splash screen
gxPhys.DrawImage(bmpTitle, 0, 0);
// Now proceed with loading rest of the resources
// and initializing the game
// Regulate the splash time if necessary
}
重要的是不要在启动画面中提供任何功能,而只应该将其用作简介信息页。并不总是需要通过启动画面来启动。
在Pocket PC中,导航键(即向左键、向右键、向上键和向下键)在游戏中发挥着至关重要的作用。我们可以通过重写游戏窗体的各个事件方法,访问这些键的KeyDown、KeyPress和KeyUp事件。
通常,我们需要处理这些导航键的KeyDown事件,并提供游戏级功能。
protected override void OnKeyDown(KeyEventArgs keyg)
{
switch(keyg.KeyData)
{
case Keys.Left:
// Provide game functionality for Left key
break;
case Keys.Right:
// Provide game functionality for Right key
break;
case Keys.Up:
// Provide game functionality for Up key
break;
case Keys.Down:
// Provide game functionality for Down key
break;
default:
// We don't care
break;
}
// Always call the base implementation
// so that the registered delegates for this event are raised.
base.OnKeyDown(keyg);
}
Pocket PC的笔针类似于台式电脑的鼠标。我们可以通过重写游戏窗体的各个事件方法来访问MouseDown、MouseMove和MouseUp事件。
protected override void OnMouseDown(MouseEventArgs mouseg)
{
Point ptHit = new Point(mouseg.X, mouseg.Y));
}
在Pocket PC上,到1.0版为止,.NET框架精简版不支持鼠标右键和硬件按钮。
如有可能,应绘制图形,而不是使用位图。这将减小内存占用提高性能。例如,在太空射击游戏中,最好不要使用滚动的位图作为背景,可以通过用黑色的矩形填充背景然后绘制星星来获得相同的效果。
尝试将类似的位图逻辑地组合为一个大型位图,以后根据需要使用相关坐标提取适当的单元位图。拥有一个大位图而不是多个小位图可以降低资源大小。
尽可能尝试使用其他图形格式而不是BMP,以便利用更好的图形压缩技术(例如JPEG)。
避免在所有者绘制的游戏窗体上使用控件。我们应该作为所有者绘制所有内容。例如,如果我们需要一个Label,应该使用Graphics.DrawString()而不是创建自定义的子图形。
以静态方式尽可能多地初始化游戏逻辑,从而在运行时避免执行开销较大的计算。例如,在智力测验游戏中,应该尽可能地预先静态存储获胜组合,而不是在游戏运行过程中使用开销较大的动态算法以及类似的功能。
在为诸如Pocket PC这样的设备编写游戏时,需要记住显示屏幕尺寸要比桌面计算机小得多,并且硬件的功能也没有桌面计算机那样强大。
因此,对于这些小型设备,应该比桌面计算机更加严格地优化游戏。同时在设计游戏时,还应该认真考虑目标硬件、操作系统和.NET框架精简版的功能。
游戏的性能高低主要取决于它的绘图例程。高效的绘图技术决定了游戏的响应速度,尤其是在诸如Pocket PC这样的小型设备中。因此,应该使用前面讨论的所有绘图优化技术(如脏区计算),以便节省每帧的绘图时间。
帧速率是另一个需要记住的重要参数,请基于目标设备的功能明智地加以选择。