UML软件工程组织

使用 Microsoft .NET 框架压缩版编写移动游戏
Ravi Krishnaswamy Microsoft Corporation

摘要:了解如何创建基于 .NET 框架压缩版的游戏。了解编写面向小型设备的游戏的主要要求,并且理解 .NET 框架压缩版能够轻松地满足这些要求。本文包括一些高级性能优化技术,您可以用来突破游戏的极限。

简介

Microsoft?.NET 框架压缩版是完整 Microsoft .NET 框架的子集。它是对完整的 .NET 框架进行压缩后得到的版本,目的是在不影响用户正常使用的前提下,更好地适应资源受到限制的设备。虽然其规模大大减小,但多数功能仍然保持完整,从而使开发人员能够获得增强的性能。

面向 .NET 框架压缩版的好处很多,其中包括:可以针对 Pocket PC 和其他 Windows CE .NET 设备进行单一的二进制部署,提高开发人员的工作效率,以及加快产品投放市场的速度。

在本白皮书中,我将讨论编写面向小型设备的游戏的主要要求,并且说明 .NET 框架压缩版能够轻松地满足这些要求。我还将讨论一些高级性能优化技术,您可以使用这些技术来突破游戏的极限。总之,您将了解到使用 .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 和 OnPaintBackground

典型的游戏应用程序会对窗体内容进行自定义绘制。它采取的办法是重写控件的 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 的大小关系将有很大好处,尤其是在屏上 和离屏 之间变换子图形的坐标时。

// 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 个像素单位。现在,我们可以将帧速率固定为 10 fps,并且将子图形每帧纵向移动 10 个像素,以便达到上述净速度,或者我们还可以将帧速率增加到 20 fps,并且使子图形的每帧纵向移动距离下降至 5 个像素。

采用任一种方法都可以达到相同的净速度,区别在于:在前一种情形下,子图形的移动看起来可能有一点跳跃性,因为与后一种情形相比,它移动相同距离所用的刷新周期要短一些。但是,在后一种情形中,我们依赖于游戏以 20 fps 的帧速率渲染。因此,在决定使用哪一种方法之前,我们需要绝对确定硬件、系统和 .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) a“
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() 事件处理程序中。

无效-更新

另一种使游戏前进的方式是在特别 的基础上渲染它。每当游戏逻辑检测到屏幕需要刷新时,我们可以请求系统使屏幕的相应部分无效并对其进行刷新。

这一技术是最简单的,并且最适合基于用户交互前进的游戏。这是指那些不是持续不断地走时和渲染(如游戏循环、计时器回调中那样)的游戏,而是仅当用户与其交互时才前进的游戏,例如智力测验游戏。

我们可以通过调用 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 笔针

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 这样的小型设备中。因此,应该使用前面讨论的所有绘图优化技术(如脏区计算),以便节省每帧的绘图时间。

帧速率是另一个需要记住的重要因素,请基于目标设备的功能明智地加以选择。


版权所有:UML软件工程组织