Pocket 1945 - C# .NET CF 射击游戏






4.90/5 (97投票s)
一篇关于 Pocket PC 游戏开发的文章。
引言
Pocket 1945 是一款受经典游戏 1942 启发的经典射击游戏。该游戏使用 C# 编写,面向 .NET Compact Framework。本文是我首次提交给 Code Project 的作品,也是我对正在进行的 .NET CF 比赛的贡献。
除了是我的第一篇文章,这也是我的第一个游戏。我的日常工作是构建以数据为中心的业务应用程序,所以游戏开发是完全不同的事情。所以,请对我温柔点。
开始这个项目时,我的目标之一是制作一个游戏,其他开发人员在进入 C# 游戏开发时可以将其作为起点。我专注于使代码尽可能清晰和简单。我还想在不引入任何第三方组件(如游戏应用程序接口 (GAPI))的情况下构建这个游戏。我这样做的原因是我想看看我能用核心框架做什么。另一个目标是将这个游戏/示例比大多数井字游戏示例更进一步,实际构建一个有趣、有挑战性且外观精美的游戏。
在做这个项目时,我意识到的一件事是游戏需要时间,无论它们多么简单。游戏仍未达到 1.0 版本,但它目前是可以玩的。我已经将游戏上传到 GotDotNet 工作区,我鼓励所有觉得游戏有趣的人加入工作区,帮助我为 Pocket PC 平台构建一款有趣的射击游戏。
如何安装/玩 Pocket 1945
要玩游戏,您需要一台启用 Pocket PC 的设备,并安装了 .NET Compact Framework 1.1。要安装,只需将 Pocket1945.exe 文件和级别 XML 文件复制到设备上的新文件夹中。无需安装。
要玩游戏,您可以使用设备上的方向键。要退出,请点击日历按钮(第一个硬件按钮)。要开火,请点击第二个硬件按钮。由于我没有真正的设备,我不确定这些按钮的“名称”是什么。但是,试试看吧!
当前的游戏远未“完成”,但运行代码是安全的,而且可以玩。游戏包含 4 个级别。要添加自己的级别,只需制作新的级别 XML 文件并将其复制到设备上的游戏文件夹中。由于我还没有级别编辑器,我建议您根据现有级别构建您的新级别。如果您制作了任何有趣的级别,请与我们分享。
游戏设计
游戏包含一个名为 Pocket1945 的 Visual Studio .NET 解决方案。该项目包含 13 个类、3 个接口、1 个结构和 7 个枚举器。我提供了一张 VS.NET 中类视图的屏幕截图,以说明游戏中的类设计。

GameForm
类是应用程序的主类。此功能负责绘制和运行实际游戏。Level 类加载一个级别 XML 文件并将 XML 解析为对象。Background 类将地图和背景元素绘制到屏幕上。Player 类定义游戏中的玩家角色。Enemy 类定义游戏中使用的所有敌机。Bonus 类用于额外生命或护盾升级等奖励元素。LevelGui
类用于绘制一个简单的游戏内用户界面。此功能用于向玩家显示有关生命值、进度、分数等信息。
Input 和 StopWatch
类取自 MSND 文章之一,为您提供对检测到的硬件按钮和高性能计数器的访问。这些类与 MSDN 示例中的完全相同。
IArmed
接口在可以发射子弹并被其他子弹击中的游戏对象上实现。ICollidable
接口由可以碰撞的物品(例如奖励物品)实现。IDrawable
接口由游戏过程中可以绘制到屏幕上的所有对象实现。
不同的枚举用于诸如奖励类型、武器、敌人、移动等属性。
关卡设计
每个级别都是一个 XML 文件,其中包含一个带有级别地图的 ASCII 表,以及用于背景元素(如岛屿)、敌人和奖励元素的 XML 节点。Level 类在构造函数中接受一个路径,并加载传入构造函数的 XML 文件,然后根据节点构建对象。
ASCII 表包含一个有 8 列和未知行数的表。表中的每个字符代表一个 32x32 像素的背景图块。因此,如果 ASCII 表高 56 行,宽 8 列,则背景大小将为 1792x256 像素。级别文件还有一个名为 speed 的设置,它以像素/秒为单位设置速度。平均地图高 1800 像素,以每秒 15 像素的速度滚动,提供大约 2 分钟的游戏时间。ASCII 地图的示例
<Map>
<![CDATA[AAAAAAAA
BBBBBBBB
AAAAAAAA
CCCCCCCC
CCCCCCCC
AAAAAAAA
BBBBBBBB
BBBCCCBB
ABCDABCD]]>
</Map>
敌人节点包含每个敌人的所有设置。Y 属性告诉游戏引擎敌人何时出现。例如,一个 Y=1500 的敌人在玩家滚动到地图上的 1500 点时开始移动。敌人和奖励元素都以这种方式定位。一个示例敌人节点看起来像这样
<Enemy X="140" Y="1700" Speed="80" MovePattern="1"
EnemyType="1" BulletType="1" BulletPower="5" BulletSpeed="150"
BulletReloadTime="1000" Power="10" Score="100" />
X
= 此敌人的水平起始位置。Y
= 此敌人的垂直起始位置(当它获得焦点时)。Speed
= 敌人移动的速度(像素/秒)。MovePattern
= 敌人移动的方式。目前只支持直线前进。我将添加诸如 Z 字形、扫射、神风特攻队和简单 AI 等模式。MovePattern
枚举定义了不同的移动模式。EnemyType
= 敌人的类型。目前支持 6 种敌人。EnemyType
枚举定义了不同类型的敌人。BulletType
= 此敌人发射的子弹类型。BulletType
枚举定义了不同类型的子弹。BulletPower
= 此敌人发射的子弹威力。这表示玩家被击中后受到的伤害程度。BulletSpeed
= 子弹的飞行速度(像素/秒)。BulletReloadTime
= 敌人重新加载所需的时间(毫秒)。Power
= 敌方护盾的厚度(杀死它的难度)。Score
= 杀死此敌人后获得的分数。
正如您可能猜到的,我正计划编写一个基于 XML 的关卡设计器来为游戏构建新关卡。我还考虑制作一个基于 XML Web Service 的游戏服务器,您可以在其中上传和下载新的关卡集。XML 格式还需要通过制作模式来规范化。所有这些都在待办事项列表中。
奖励元素的实现方式与敌人几乎相同,因此我不会详细介绍奖励节点。此处可下载示例关卡文件:Level1.xml(已压缩)
兴趣点 - 精灵列表
一个可能有用之处在于我如何实现精灵。所有游戏图形都是嵌入式位图资源。起初我只使用一个带有所有精灵的位图,但我很快意识到这会使文件难以维护,并且添加新精灵会导致精灵索引问题。我将图像分成了逻辑部分,如子弹、敌人、玩家、图块和奖励。
所有精灵都由 SpriteList
类管理。该类实现了单例模式,以确保在整个游戏中只有一个此类实例。该类由一个名为 LoadSprites()
的公共方法和几个保存每个精灵的公共位图数组组成。LoadSprites()
方法读取嵌入式资源并调用一个名为 ParseSpriteStrip()
的私有方法,该方法读取一个精灵条(一个包含多个精灵的大位图)并将其拆分为一个 Bitmap
数组。每个游戏对象(如奖励物品、子弹或敌人)通过从公共 Bitmap
数组之一读取位图来绘制自身。
通过这种方式处理精灵,您可以以一致的方式访问您的图形资源。通过将类设为单例,您可以确保应用程序中只有一个类实例。所有加载都在游戏初始化时完成,这是一种快速读取精灵的方式。
以下代码展示了 LoadSprites()
方法和 ParseSpriteStrip()
方法。
/// <summary>
/// Metod loading the sprites from the assembly resource files
/// into the public bitmap array. To be sure the sprites are only loaded
/// once a private bool is set to true/false indicating if the sprites
/// have been loaded or not.
/// </summary>
public void LoadSprites()
{
if(!doneLoading)
{
//Accessing the executing assembly to read embeded resources.
Assembly asm = Assembly.GetExecutingAssembly();
//Reads the sprite strip containing the sprites you want to "parse".
Bitmap tiles = new Bitmap(asm.GetManifestResourceStream(
"Pocket1945.Data.Sprites.Tiles.bmp"));
Bitmap bonuses = new Bitmap(asm.GetManifestResourceStream(
"Pocket1945.Data.Sprites.Bonuses.bmp"));
Bitmap bullets = new Bitmap(asm.GetManifestResourceStream(
"Pocket1945.Data.Sprites.Bullets.bmp"));
Bitmap smallPlanes = new Bitmap(asm.GetManifestResourceStream(
"Pocket1945.Data.Sprites.SmallPlanes.bmp"));
Bitmap smallExplotion = new Bitmap(asm.GetManifestResourceStream(
"Pocket1945.Data.Sprites.SmallExplotion.bmp"));
Bitmap bigBackgroundElements = new Bitmap(asm.GetManifestResourceStream(
"Pocket1945.Data.Sprites.BigBackgroundElements.bmp"));
Bitmap bigExplotion = new Bitmap(asm.GetManifestResourceStream(
"Pocket1945.Data.Sprites.BigExplotion.bmp"));
Bitmap bigPlanes = new Bitmap(asm.GetManifestResourceStream(
"Pocket1945.Data.Sprites.BigPlanes.bmp"));
//Parse the sprite strips into bitmap arrays.
Tiles = ParseSpriteStrip(tiles);
Bullets = ParseSpriteStrip(bullets);
Bonuses = ParseSpriteStrip(bonuses);
SmallPlanes = ParseSpriteStrip(smallPlanes);
SmallExplotion = ParseSpriteStrip(smallExplotion);
BigBackgroundElements = ParseSpriteStrip(bigBackgroundElements);
BigExplotion = ParseSpriteStrip(bigExplotion);
BigPlanes = ParseSpriteStrip(bigPlanes);
//Clean up.
tiles.Dispose();
bullets.Dispose();
bonuses.Dispose();
smallPlanes.Dispose();
smallExplotion.Dispose();
bigBackgroundElements.Dispose();
bigExplotion.Dispose();
bigPlanes.Dispose();
doneLoading = true;
}
}
/// <summary>
/// Method parsing a sprite strip into a bitmap array.
/// </summary>
/// <param name="destinationArray">
/// The destination array for the sprites.</param>
/// <param name="spriteStrip">The sprite strip to
/// read the sprites from.</param>
private Bitmap[] ParseSpriteStrip(Bitmap spriteStrip)
{
Rectangle spriteRectangle = new Rectangle(1, 1,
spriteStrip.Height - 2, spriteStrip.Height - 2);
Bitmap[] destinationArray = new Bitmap[(spriteStrip.Width - 1)
/ (spriteStrip.Height - 1)];
//Loop drawing the sprites into the bitmap array.
for(int i = 0; i < destinationArray.Length; ++i)
{
destinationArray[i] = new Bitmap(spriteRectangle.Width, spriteRectangle.Height);
Graphics g = Graphics.FromImage(destinationArray[i]);
spriteRectangle.X = i * (spriteRectangle.Width + 2) - (i - 1);
g.DrawImage(spriteStrip, 0, 0, spriteRectangle, GraphicsUnit.Pixel);
g.Dispose();
}
return destinationArray;
}
兴趣点 - 双缓冲
另一个值得一提的是我如何绘制每个游戏帧。我使用了一种称为双缓冲的常见技术。基本上,这意味着我在内存中绘制整个帧,然后再将其移动到屏幕上。通过这样做,我避免了不必要的闪烁。我没有真正的 Pocket PC,但我被告知游戏在上面运行得非常好。我希望能赢得一台 Pocket PC,这样我就可以亲自测试一下。
GameForm
类(游戏的主类)有三个用于绘图的私有字段
private Bitmap offScreenBitmap;
private Graphics offScreenGraphics;
private Graphcis onScreenGraphics;
offScreenBitmap
是用于保存每个游戏帧内存版本的位图。offScreenGraphics
是一个 Graphics
对象,用于绘制到内存中的位图。onScreenGraphics
是一个 Graphics
对象,用于在每个游戏循环结束时将内存中的位图绘制到屏幕上。所有可绘制的游戏元素都实现了 IDrawable
接口,该接口有一个名为 Draw(Graphics g)
的方法,用于将自身绘制到游戏窗体上。在游戏循环中,您调用 player.Draw(offScreenGraphic)
使玩家将自身绘制到离屏位图上。这是级别循环的示例,显示了如何将 offScreenGraphcis
对象传递给游戏对象,并在循环结束时将 offScreenBitmap
移动到屏幕上
private void DoLevel(string filename)
{
CurrentLevel = new Level(GetFullPath(filename));
StopWatch sw = new StopWatch();
bool levelCompleted = false;
bool displayMenu = false;
while((playing) && (!levelCompleted))
{
// Store the tick at which this frame started
Int64 startTick = sw.CurrentTick();
input.Update();
//Update the rownumber.
TickCount++;
//Draw the background map.
CurrentLevel.BackgroundMap.Draw(offScreenGraphics);
//Update bullets, enemies and bonuses.
HandleBonuses();
HandleBullets();
HandleEnemies();
//Update and draw the player.
Player.Update(input);
Player.Draw(offScreenGraphics);
playing = (Player.Status != PlayerStatus.Dead);
//Draw in-game user interface
levelGui.Draw(offScreenGraphics);
//Move the offScreenBitmap buffer to the screen.
onScreenGraphics.DrawImage(offScreenBitmap, 0, 0,
this.ClientRectangle, GraphicsUnit.Pixel);
//Process all events in the event que.
Application.DoEvents();
}
}
待办事项
在它被认为是一个“真正”有趣和令人兴奋的游戏之前,还有很多事情要做。但是,我们正在接近目标。我不会详细介绍所有需要完成的事情,但我会添加一些重要的点
- 一个好的关卡编辑器。
- 一组 XML Web Services,用于上传和下载关卡以及发布分数。
- 新的移动模式(敌人如何移动)。
- 一个游戏 GUI(主菜单、标题屏幕、高分列表等)。
- 定义关卡文件规则的 XML Schemas。
- 设计更精良、平衡且具有挑战性的关卡。
- Boss。我们需要强大的 Boss。
- 还有更多。
任何建议都非常感谢,无论是在 Code Project 上还是在工作区网站上。
资源
在构建这个游戏时,我使用了几个在线资源。首先,我必须感谢 Ari Feldman 在游戏中使用的出色图形。Ari 在他的网站上发布了几个精灵集,遵循 SpriteLib GPL 协议。精灵可以在 http://www.arifeldman.com/games/spritelib.html 上找到。
我还要感谢 EFNet 上 #ms.net 的所有人。特别感谢 ^CareBear 对游戏在真实设备上表现的即时反馈。
其他使用的资源是 MSDN 上发布的一系列游戏文章
- 使用 Microsoft .NET Compact Framework 编写移动游戏 (http://msdn.microsoft.com/mobility/default.aspx?pull=/library/en-us/dnnetcomp/html/netcfgaming.asp)。
- 使用 .NET Compact Framework 进行游戏 (http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnnetcomp/html/GManGame.asp)。
- 使用 .NET Compact Framework 进行游戏:一个简单示例 (http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnnetcomp/html/BustThisGame.asp)。
结束语
还有几件事我想提一下,但为了及时提交这篇文章/游戏以参与比赛,我真的需要现在完成它。
本文的第二部分将在 X 天/月/年/或可能永远不会发布。游戏远未完成,但目前是可以玩的。我希望您下载并试一试。如果您觉得该项目有趣且有前途,我鼓励您加入 http://workspaces.gotdotnet.com/pocket1945 上的工作区,并参与该游戏的持续开发。工作区也将是获取最新游戏版本的地方。
对本文和游戏的任何评论都非常感谢。