65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (97投票s)

2004年6月3日

CPOL

10分钟阅读

viewsIcon

272242

downloadIcon

2738

一篇关于 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 上发布的一系列游戏文章

结束语

还有几件事我想提一下,但为了及时提交这篇文章/游戏以参与比赛,我真的需要现在完成它。

本文的第二部分将在 X 天/月/年/或可能永远不会发布。游戏远未完成,但目前是可以玩的。我希望您下载并试一试。如果您觉得该项目有趣且有前途,我鼓励您加入 http://workspaces.gotdotnet.com/pocket1945 上的工作区,并参与该游戏的持续开发。工作区也将是获取最新游戏版本的地方。

对本文和游戏的任何评论都非常感谢。

© . All rights reserved.