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

Bubble.NET 游戏

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.51/5 (40投票s)

2004年5月25日

CPOL

11分钟阅读

viewsIcon

260774

downloadIcon

3261

一款使用 .NET 紧凑型框架开发的 Pocket PC 版 Puzzle Bobble (又名 Bust-A-Move) 克隆游戏。

引言

这是我对 CodeProject 紧凑型框架竞赛的贡献。(也是我的第一篇 CodeProject 文章!)

我一直很喜欢原版的 Puzzle Bobble 游戏。它简单却令人上瘾。许多变体可以在各种平台上玩,但我看到的 Pocket PC 版克隆游戏都不是免费的。那么为什么不创建一款免费的、开源的 .Net 紧凑型框架版的这款著名游戏呢?我从四月初开始了这个项目,那时我看到 CodeProject 组织了一个竞赛,但到四月底还没有完成。它功能可用,但没有任何图形,只有一个随机关卡生成器… 幸运的是,他们延长了截止日期,所以我有多余的时间来改进整个项目:)。总之,还有很多地方可以进一步改进。

  • 更好的图形 (启动器还没有真正完成,泡泡也不是很好看,背景…)
  • 一些声音
  • 分数管理
  • 一个关卡编辑器
  • 更多关卡 (示例关卡是用记事本创建的… 一旦关卡编辑器完成,就会容易得多!)
  • 添加一些特殊泡泡 (爆炸泡泡、多色泡泡等等…)
  • 或许使用 GAPI 提高动画速度 (我在模拟器上没有机会这样做…)
  • 一个很酷的网络多人游戏变体… (也许是 v2.0,或者 v3.0 吧 ;))

值得注意的是,我目前没有 Pocket PC,我在项目开始时借用了我老板的 ppc (谢谢 Patrick!),只是为了看看动画速度是否可以接受,但之后我只使用了模拟器,所以我希望它在“真正的”pocket pc 上能运行得很好!在游戏屏幕的“菜单”按钮中,你会找到一个“恒定帧率”条目,如果游戏太慢,请取消选中它…

游戏规则

核心思想基于著名的 Puzzle Bobble 游戏:游戏的目标是清除棋盘上的所有泡泡。你有一个有限的时间将一个彩色泡泡发射到棋盘上。泡泡会“粘”在遇到的第一个泡泡上。如果棋盘上有三个以上相同颜色的泡泡,它们就会爆炸,任何未连接的泡泡也会随之落下。每发射 8 个泡泡,天花板就会下降一层。当一个泡泡到达截止线(红线)时,你就失去一次生命。

使用代码

游戏中的每个重要部分都有一个类

  • BubbleGame:这是游戏的主类,它包含所有高级功能和游戏逻辑(大部分在 `Go` 方法中)
  • Bubble:这个类代表了内存中的游戏棋盘上的一个泡泡,它保存了一些有用的信息,如泡泡的类型(颜色)、泡泡在游戏棋盘上的位置,以及屏幕上泡泡的矩形(泡泡精灵在屏幕上的位置)
  • BubbleImages:在这个类中,你会发现一些静态方法和字段,它们充当泡泡图像的 Bitmap 缓存。
  • LauncherImages:就像 `BubbleImages` 类一样,这个类充当发射器位图的 Bitmap 缓存。
  • BubbleSprite:代表一个移动的泡泡(玩家泡泡、下落和爆炸的泡泡),这个类跟踪每个泡泡的方向和速度,以及它在屏幕和游戏棋盘上的位置。
  • MovingSprites:这个类将保存所有被摧毁和下落的泡泡,同时它们在屏幕上移动。
  • GameBoard:这个类代表内存中的游戏棋盘。它有加载关卡到内存、检测玩家泡泡碰撞、检测泡泡邻居和下落泡泡、以及检测泡泡是否到达截止线的方法。它有一个名为 `CreateBoardBitmap` 的方法,该方法可以从内存中的游戏棋盘创建一个位图(包含完整的背景、墙壁、天花板和固定泡泡)。这个位图被缓存,并在游戏期间(由 `BubbleGame` 类)用作背景,因此我们只需要绘制移动的泡泡,而不是固定的泡泡。
  • AngleHelper:这个辅助类用于根据玩家泡泡精灵的角度和距离来计算其移动。
  • GraphicsHelper:这个辅助类包含由各种 GDI+ 方法使用的静态成员(其中一些仅用于测试目的…)
  • GXInput:这个类来自一个 MSDN 示例游戏项目,对于注册 pocket pc 的所有硬件按键非常有用。否则,当你按下硬件按键时,一个应用程序可能会启动并出现在游戏窗口之上…

关注点

这个项目早期遇到的一个问题是,比 winforms 的按键相关事件更准确地处理按键按下。

幸运的是,'coredll.dll' 中有一个函数满足了这个需求。

[DllImport("coredll.dll")]
public static extern int GetAsyncKeyState(int vkey);

我们可以在主游戏循环(`'BubbleGame'` 类中的 `'Go'` 方法)中使用这个函数。

if ((GetAsyncKeyState((int)System.Windows.Forms.Keys.Left) & 0x8000) !=0)
    angle+=2;    //x--;
if ((GetAsyncKeyState((int)System.Windows.Forms.Keys.Right) & 0x8000) !=0)
    angle-=2;    //x++;

这种技术是在这个项目刚开始时使用的,但在此期间,我阅读了一系列关于使用 .Net Compact Framework 在 Pocket PC 上进行游戏开发的优秀 MSDN 文章,作者还解决了另一个问题:拦截硬件按键。这些按键就像热键:它们直接启动或激活一个应用程序。这个功能在日常使用中很有用,但对游戏来说非常烦人,因为有时我们想用这些按键执行特定任务。但我们无法做到,除非进行一些复杂的 `DllImport` 和 P/Invoke… 这就是 GXInput 有用的地方:它来自 MSDN 文章中的一个示例游戏(http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnnetcomp/html/WrapGAPI3.asp),它使我们能够注册硬件按键,并在我们的应用程序中使用它们!事实上,GXInput 不仅仅是注册按键,但由于我这个库添加得非常晚,所以我没有使用它的其他任何功能…

此外,我不得不绕过一个奇怪的问题:在主游戏窗体中,我希望菜单出现在左下角。窗体本身被配置为填充整个屏幕(`MinimizeBox=false, MaximizeBox= false, WindowState= Maximized)。没有菜单时,窗体按预期显示,但一旦我添加了菜单,窗口就不再是全屏模式了(即使我将 `ControlBox` 设置为 `false`,顶部标题栏和开始菜单也会出现…)。我通过在窗体左下角添加一个按钮并为其分配一个在单击按钮时显示的上下文菜单来解决这个问题…

关卡设计

关卡设计以纯文本文件的形式保存(我创建了一个包含十个关卡的 'codeproject.lvl' 示例文件),你可以通过编辑示例文件或创建新文件(这些文件必须具有 .LVL 扩展名)来创建自己的关卡,并将它们复制到游戏目录中。之后,你可以在主菜单中选择关卡文件。

文件的结构非常简单:这是第一个关卡

[Level]
06,06,08,08,02,02,03,03
06,06,08,08,02,02,03,00
02,02,03,03,06,06,08,08
02,03,03,06,06,08,08,00

每个关卡都以 '[Level]' 开始,之后是一系列数字,表示每个单元格的泡泡颜色。

00 = no bubble
01 = Black
02 = Blue
03 = Green
04 = Magenta
05 = Orange
06 = Red
07 = White
08 = Yellow

每行最多可以有 8 个数字,每个关卡最多可以有 10 行(这样的关卡很难完成,因为最后一行非常靠近截止线!!!)。在未来的版本中,将有一个关卡编辑器,让你更轻松地创建关卡…

它是如何实现的?

`GameBoard` 类包含一个 `Bubble` 数组:

private Bubble[][] gameBoard = new Bubble[GRID_X_SIZE][];

每个 Bubble 都有一个类型(颜色)和网格上的位置,这些 Bubble 是在 `GameBoard` 类的 `LoadLevel` 方法中创建的。

int[,] BubbleData= this.GetLevelData(LevelFile,LevelNum);

for (int x=0;x<GRID_X_SIZE;x++)
  for (int y=0;y<GRID_Y_SIZE;y++)
  {
    if (x!=GRID_X_SIZE-1 || y%2==0) 
    {  
      if (BubbleData[x,y]!=0)
        this.gameBoard[x][y] = new Bubble(x,y,(BubbleKind)BubbleData[x,y]);
    }
  }

每次创建一个 Bubble 时,构造函数中都会指定它在网格上的位置以及它的类型。根据它在网格上的位置(`GameBoard`),我们可以计算出它在屏幕上的位置(包含 Bubble 在游戏屏幕上的矩形)。Bubble 类的 `UpdateBubbleRectangle` 方法会在需要时更新这个 Rectangle。

public void UpdateBubbleRectangle()
{ this.bubbleRectangle = new Rectangle(
    (int)((this.gridX*this.bubbleBitmap.Width)+ 
    GameBoard.X_POS_OFFSET +((this.bubbleBitmap.Width/2) 
    * (this.gridY%2))),
    (int)(this.gridY*(this.bubbleBitmap.Height*GameBoard.ROW_DIST)
    +GameBoard.Y_POS_OFFSET),
    this.bubbleBitmap.Width, this.bubbleBitmap.Height); }

Bubble 的屏幕上位置存储在一个 Rectangle 中,其边界如下:

  • X 位置的计算方法是:将 Bubble 的 X 网格位置 (gridX) 乘以 Bubble 的宽度 (bubbleBitmap.Width),然后加上 GameBoard 的 X 偏移量(GameBoard 在屏幕上的位置),最后如果这个 Bubble 在奇数行,则加上一个 Bubble 宽度的二分之一。
  • Y 位置的计算方法是:将 Bubble 的 Y 网格位置 (gridY) 乘以 Bubble 的高度除以一个常数因子 (bubbleBitmap.Height*GameBoard.ROW_DIST),这样每一行都会与前一行稍微“重叠”,最后加上 GameBoard 的 Y 偏移量(当天花板每下降一行——即每发射 8 个泡泡——时,这个偏移量都会改变)。
  • 矩形的宽度和高度就是 Bubble 位图的宽度和高度。
加载关卡并初始化游戏棋盘后,将调用 `GameBoard` 类的 `CreateBoardBitmap()` 方法,并返回一个 `Bitmap` 对象。这个 Bitmap 被用作游戏的“完整背景”,因为它由“简单背景”(天花板、地板、墙壁和背景图像)加上当前关卡中剩余泡泡的图像组成。这个“完整背景”只有在玩家泡泡撞击另一个泡泡并粘在棋盘上,或者在碰撞后某些泡泡弹出或下落时才会刷新。

public Bitmap CreateBoardBitmap()
...
  for (int x=0;x<GRID_X_SIZE;x++)
    for (int y=0;y<GRID_Y_SIZE;y++)
    {
      oneBubble= this.gameBoard[x][y];
      if (oneBubble!=null)
      {
        oneBubble.UpdateBubbleRectangle(); 
  // update the bubble rectangle in case of board shift down
        gameBoardGraphics.DrawImage(oneBubble.BubbleBitmap, 
           oneBubble.BubbleRectangle,0,0,
           oneBubble.BubbleBitmap.Width, oneBubble.BubbleBitmap.Height ,
           GraphicsUnit.Pixel,
           BubbleImages.GetTranspImageAttr());
      }
    }
...
return gameBoardBitmap

在这个“完整背景”上,绘制发射器和玩家精灵(以及下落和爆炸的泡泡)。玩家精灵由 `BubbleSprite` 类管理。这个类有一个 `BubbleRectangle` 属性,返回包含 Bubble 在屏幕上位置的矩形。(就像 `Bubble` 类一样,这个属性用于知道在哪里将精灵绘制到屏幕上)。它还有一个 `PositionOnGrid` 属性,返回一个 `Point`,表示精灵在 GameBoard 上的位置。(这是 `Bubble` 类 `UpdateBubbleRectangle` 方法的逆操作,主要用于碰撞检测例程)。

这种检测发生在玩家泡泡移动的每一次,但分为两个阶段。

  • 首先,我们需要知道玩家泡泡附近是否有一个或多个泡泡。这可以通过获取棋盘上泡泡的邻居来完成。这就是使用 `PositionOnGrid` 方法以及 `GameBoard` 类中名为 `GetNeighbors` 的辅助方法的地方,它返回邻居泡泡的 `ArrayList`。
  • 其次,如果玩家泡泡附近至少有一个邻居,我们需要对玩家泡泡及其邻居进行更精确的命中测试。这是 `GameBoard` 类 `CheckSpriteCollision` 方法的作用。

    这个测试通过计算玩家精灵与其邻居之间的距离来完成。

    dist = (Sprite.BubbleRectangle.X-OneBubble.BubbleRectangle.X)
     *(Sprite.BubbleRectangle.X-OneBubble.BubbleRectangle.X)+
     (Sprite.BubbleRectangle.Y-OneBubble.BubbleRectangle.Y)*
     (Sprite.BubbleRectangle.Y-OneBubble.BubbleRectangle.Y);
    如果距离小于某个特定值,则必须将玩家泡泡添加到游戏棋盘上。

将玩家泡泡添加到游戏棋盘后,我们需要测试该泡泡是否被至少两个相同类型的泡泡包围,如果是,则必须销毁这些泡泡。这个测试由 `GameBoard` 类的 `GetSameKindNeighbors` 方法完成,通过为与玩家精灵相同类型的每个泡泡调用 `GetNeighbors` 方法。一旦找到玩家泡泡的所有相同类型的邻居泡泡,它们就会从 `GameBoard` 中移除,并添加到 `MovingSprites` 集合中。这个集合用于跟踪下落或爆炸的泡泡,并对它们进行动画处理。

如果某些泡泡被摧毁,我们需要测试它们是否在游戏棋盘上留下了一些“悬空”的泡泡。通过递归调用第一行每个泡泡的 `GetNeighbors` 来检测“悬空”泡泡,并将其标记为“FallChecked”,这样我们就知道它们必须保留在游戏棋盘上。这是 `GameBoard` 类 `GetFallingBubbles()` 方法的作用。所有其他泡泡都会从 `GameBoard` 中移除,并添加到 `MovingSprites` 集合中,以实现下落动画效果。

除此之外,没有什么特别之处:使用了双缓冲来防止闪烁(有很多文章详细介绍了双缓冲技术的使用,所以我不深入探讨…),并使用 GDI+ 将精灵绘制到屏幕上…

最后的话…

我希望我能有一些时间来完成这款游戏,并添加一些很酷的功能,比如网络多人模式,或者特殊类型的泡泡… 我不知道 v2.0 版本何时会发布,如果会有的话!我用模拟器开发了这款游戏,因为我没有真正的 Pocket PC。这就是为什么我参加了这个竞赛,希望能有机会获胜…(看到其他竞争对手的精彩作品,这将会非常困难!)。这就是为什么我在 http://workspaces.gotdotnet.com/bobblenet 创建了一个 GotDotNet 工作区。如果你对此感兴趣,并且有很棒的想法、图形或代码认为对这个项目有用,请加入我!

请,如果你在真正的 Pocket PC 上测试过这款游戏,请给我一些反馈,这样我就可以优化代码了… 谢谢!

有用链接

历史

  • 2004年6月13日:文章更新,提供关于核心游戏功能的更详细信息。
  • 2004年5月21日:v1.0 - 首次发布。
© . All rights reserved.