Introduction
This is my contribution to the CodeProject compact framework contest. (and my first CodeProject article !)
I always loved the original Puzzle Bobble game. It's simple, yet addictive. Many variants are playable on many platforms, but the only clones I saw for Pocket Pc were not free. So why not create a free and open source .Net compact framework variant of this famous game ? I started this project from scratch at the beginning of April, when I saw that CodeProject organized a contest, but at the end of April, it wasn't finished. It was functional, but did not have any graphics, and only had a random level generator... Happily, they extended the deadline, so I had time to improve the whole thing :). Anyway, a lot of things could be enhanced further more:
- Better graphics (the launcher is not really finished, the bubbles are not so nice, the background...)
- Some sound
- Score management
- A level editor
- More levels (the sample levels were created with Notepad... it will be much more easy once the level editor is finished !)
- Add some special bubbles (exploding bubbles, multi-color bubble, and so on...)
- Perhaps use GAPI to improve animation speed (I did not have this opportunity with the emulator ...)
- A cool network multi-player variant... (perhaps for v2.0, or v3.0 ;))
It's worth to be noted that I currently do not own a Pocket PC, I borrowed my boss' ppc at the beginning of the project (thank Patrick !) just to see if the animation speed would be decent, but after that I only used the emulator, so I hope that it will perform nicely on a 'real' pocket pc ! In the 'Menu' button on the game screen, you'll find a 'Constant frame rate' entry, uncheck it if the game is too slow...
Game rules
The main idea is based on the famous Puzzle Bobble game: the goal of the game is to destroy every bubble on the board. You have a limited time to launch a colored bubble on the board. The bubble will 'stick' on the first encountered bubble. If there's more than three bubble of the same color on the board, they explore, and every bubble that's not attached will fall as well. The ceiling will get down every 8 launched bubbles. You lose a live when a bubble reach the deadline (the red line).
Using the code
There's a class for each important part of the game:
BubbleGame
: This is the main class of the game, it contain all the high-level features and game logic (most of it in the 'Go
' method)
Bubble
: This class represents a bubble on the in-memory game board, it hold some useful info like the kind of bubble (its color), the position of the bubble on the game board, and the rectangle of the bubble on the screen (the position of the bubble sprite on the screen)
BubbleImages
: In this class, you'll find some static methods and fields that act like a Bitmap cache of the Bubble images.
LauncherImages
: Just like the BubbleImages
class, this class act as a Bitmap cache for the Launcher bitmaps.
BubbleSprite
: Represents a moving bubble (player bubble, falling and exploding bubbles), this class keeps track of the direction and speed of each bubble, as well as its position on screen and on the game board.
MovingSprites
: This class will hold all the destroyed and falling bubbles while they are moving on screen.
GameBoard
: This class represents the in-memory game board. It has methods for loading a level in memory, for detecting Player bubble collisions, detecting bubble neighbors and falling bubbles, as well as detecting if a bubble has reached the deadline. It has a method called 'CreateBoardBitmap
' that will create a bitmap from the in-memory game board (with complete background, walls, ceiling, and fixed bubbles). This bitmap is cached and used during the game (by the BubbleGame
class) as the background, so we only have to draw the moving bubbles, not the fixed ones.
AngleHelper
: This helper class is used to calculate the movement of the player bubble sprite from its angle and distance.
GraphicsHelper
: This helper class contains static members used by various GDI+ methods (some of them are only used for testing purpose...)
GXInput
: This class come from a MSDN sample game project, and is very useful to register all the hardware keys of the pocket pc. Otherwise, when you press an hardware key, an application could be started and appears on top of the game window...
Points of Interest
One of the first problem I had on this project, was to handle the key presses more accurately than the key related events of the winforms...
Happily, there's a function in 'coredll.dll' that satisfied this need:
[DllImport("coredll.dll")]
public static extern int GetAsyncKeyState(int vkey);
We can use this function in the main game loop (the 'Go'
method of the 'BubbleGame'
class):
if ((GetAsyncKeyState((int)System.Windows.Forms.Keys.Left) & 0x8000) !=0)
angle+=2; if ((GetAsyncKeyState((int)System.Windows.Forms.Keys.Right) & 0x8000) !=0)
angle-=2;
This technique was used at the very beginning of this project, but meanwhile, I read a series of excellent articles on MSDN that talked about game development on Pocket PC with the .Net Compact Framework, and the author addressed another issue: intercepting the hardware keys. Those keys are like hot keys: they launch or activate an application directly. The feature is useful in day-to-day use, but is very annoying for games, because sometimes we want to use those keys for specific tasks. But we can't without some cumbersome DllImport
and P/Invoke ... That's where the GXInput is useful: it comes from the sample game of a MSDN article (http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnnetcomp/html/WrapGAPI3.asp) , and enable us to register the hardware keys, and use them in our application ! In fact the GXInput do more than registering keys, but since I added this library very lately in my project I did not use any other feature...
Otherwise, I had to work around a strange problem: in the Main game form, I wanted a menu to appears at the bottom left corner. The form itself was configured to fill the whole screen (MinimizeBox=false, MaximizeBox= false, WindowState= Maximized
). Without the menu, the form was displayed as expected, but as soon as I added a menu, the window was no longer in fullscreen mode (the top title, and the Start menu appeared, even if I set the ControlBox
as false
...) I worked around this problem by adding a Button at the bottom-left corner of the form and by assigning a context menu that is displayed when we click on the button...
Level Design
The level design is saved as plain text file (I created a 'codeproject.lvl' sample file with ten levels), you can create your own levels by editing the sample file, or by creating new files (those files must have a .LVL extension), and they must be copied in the game directory. After that you can select the level file on the main menu.
The structure of the file is very simple: here is the first level:
[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
Each level begins with '[Level]', after that, we have a series of numbers indicating the bubble color of each cell:
00 = no bubble
01 = Black
02 = Blue
03 = Green
04 = Magenta
05 = Orange
06 = Red
07 = White
08 = Yellow
Each line can have up to 8 numbers, and each level can have up to 10 lines (such a level is hard to finish, because the last line is very near the deadline !!!). In future releases, there will be a level editor that will enable you to create levels more easily...
How is it done ?
The GameBoard
class contains an array of Bubble:
private Bubble[][] gameBoard = new Bubble[GRID_X_SIZE][];
Each Bubble has a kind (a color) and a position on grid, those bubbles are created in the LoadLevel
method of the GameBoard
class.
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]);
}
}
Each time a bubble is created, its position on grid is specified in the constructor, as well as its kind. From its position on the grid (the GameBoard
), we can calculate its position on screen (the Rectangle
containing the bubble on the game screen). The UpdateBubbleRectangle
method of the Bubble class update this Rectangle if needed:
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); }
The on-screen position of the bubble is stored in a Rectangle with the following bounds:
- The X position is calculated by multiplying the X GridPosition of the bubble (gridX) by the Width of the bubble (
bubbleBitmap.Width
), then adding the GameBoard X offset (the position of the gameboard on the screen), and finally adding half the width of a bubble only if this bubble is on an odd row.
- The Y position is calculated by multiplying the Y GridPosition of the bubble (gridY) by the Height of the Bubble divided by a constant factor (
bubbleBitmap.Height*GameBoard.ROW_DIST
), so each row 'overlaps' a little bit the preceding row, and finally adding the GameBoard Y offset (this offset will change each time the ceiling fall by one row - that is, every 8 fired bubbles).
- The width and height of the rectangle is simply the width and height of the bubble bitmap.
After a level is loaded and the gameboard is initialized, the
CreateBoardBitmap()
of the
GameBoard
class is called and returns a
Bitmap
object. This Bitmap is used as the 'full background' of the game, since it's made up of the 'simple background' (the ceiling, floor, walls, and background image) plus the images of the remaining bubbles of the current level. This 'full background' will only be refreshed if the player bubble hit another bubble and stick on the board, or if, after a collision, some bubbles pops out or fall.
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();
gameBoardGraphics.DrawImage(oneBubble.BubbleBitmap,
oneBubble.BubbleRectangle,0,0,
oneBubble.BubbleBitmap.Width, oneBubble.BubbleBitmap.Height ,
GraphicsUnit.Pixel,
BubbleImages.GetTranspImageAttr());
}
}
...
return gameBoardBitmap
On this 'full background' the launcher and the player sprite are drawn (as well as falling and destroyed bubbles). The player sprite is managed by the BubbleSprite
class. This class has a BubbleRectangle
property that returns the rectangle containing the bubble on screen. (just like the Bubble
class, this property is used to know where to paint the sprite on screen). It also has a PositionOnGrid
property that returns a Point
which represents the position of the sprite on the GameBoard. (it's the opposite of the UpdateBubbleRectangle
method of the Bubble
class, and is primarily used in the collision detection routine)
This detection occurs each time the player bubble moves, but is done in two phases:
- First, we need to know if there's one or more bubbles near the player bubble. This is done by obtaining the neighbors of the on-board bubble. This is where the
PositionOnGrid
method is used, as well as an helper method named GetNeighbors
, from the GameBoard
class, which returns an ArrayList
of the neighbors bubbles.
- Secondly, if there's at least one neighbor near the player bubble, we need to do a more accurate hit test between the player bubble and its neighbors. This is the role of the
CheckSpriteCollision
method of the GameBoard
class.
This test is done by calculating the distance between the player sprite and its neighbors:
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);
If the distance is less than a specific amount, the player bubble must be added on the gameboard.
After the player bubble is added to the gameboard, we need to test if this bubble is surrounded by at least two bubble of the same kind, if it's the case those bubbles must be destroyed. This test is done by the GetSameKindNeighbors
method of the GameBoard
class, by calling the GetNeighbors
method for each bubble of the same kind as the player sprite. Once all the same kind neighbors bubbles of the player bubble are found, they are removed from the GameBoard
and added to the MovingSprites
collection. This collection is used to keep track of the falling or exploded bubbles, and to animate them.
If some bubbles were destroyed, we need to test if they leaved some 'floating' bubbles on the GameBoard. The 'floating' bubbles are detected by recursively calling GetNeighbors
for each bubble of the first row and marking them as 'FallChecked', so we know that they must remain on the GameBoard. This is the role of the GetFallingBubbles()
method of the GameBoard
class. Every other bubble is removed from the GameBoard
, and added to the MovingSprites
collection, for the falling animation effect.
Beside this, there's nothing really special: double-buffer was used to prevent flicker (there's a lot of articles that detail the use of double-buffering technique, so I won't dive into it...), and GDI+ was used to draw the sprites on screen...
Last words...
I hope that i will have some time to finish this game, and add some cool features, like network multiplayer mode, or special kind of bubbles... I don't know when the v2.0 version will be out, if there's any! I developed this game with the emulator, as i don't own a real Pocket PC. That's why i entered this contest, hoping i would have a chance to win... (Seeing the awesome work of some other competitors, it will be very hard !). That's why i created a GotDotNet workspace at
http://workspaces.gotdotnet.com/bobblenet. If you are interested and you have cool ideas, some graphics or code that you think useful to this project, join me !
And please, give me some feedback if you tested this game on a real Pocket PC, so i can fine-tune the code... Thanks !
Useful Links
History
- 13/June/2004: Article updated to provide more detailed information about the core game functionalities.
- 21/May/2004 : v1.0 - First release.