Introduction
Continuing my adventure in XNA after porting Arkanoid for Windows Phone 7, I’ve been busy during the last days in an attempt to re-create the famous Windows Mobile 6.x game called “Bubble Breaker” for Windows Phone 7. This is a great game to spend your time, and I find it highly addictive. I had two choices to implement it; either Silverlight or XNA. I chose XNA for this example, but I promise to also create a Silverlight version! If you don’t want to read more and want to get your hands dirty playing with the source code, check here: http://sdrv.ms/XK7D9g
Game Description
The game is rather simple. There are some balls on screen, using different colors. User can touch one ball, and if one or more of its ‘neighbours’ (vertically or horizontally, *not* diagonally) are the same color, they get ‘selected’. The process is repeated, recursively, for the selected balls (having, of course, the same color) until there are no neighboring balls with the same color as the originally selected. The selected balls all get a new (in our example, white) color, and the user can re-select them, to make them disappear. If there are blank positions vertically or horizontally, the game moves the remaining balls either down or right (imagine having a ‘gravity’ system pulling objects both from the bottom and from the right side). Score is easily calculated by the formula
score = score + (number of ellipses removed) * (number of ellipses removed – 1)
which is the same formula used in the original Windows Mobile game. If there are no more ellipses that can be selected for removal (i.e. there are no ‘neighboring’ ellipses carrying the same color), the game is over. User can also reset the game by touching the bottom of the game area.
Screenshot:
Diving into the code
First of all, the game itself is played only on portrait mode. This is enforced by adding the following two lines to the constructor
1: graphics.PreferredBackBufferHeight = 800;
2: graphics.PreferredBackBufferWidth = 480;
The game uses a two dimensional board to display the ellipses. It also uses some class variables to hold important information for our game. Here is the code for the InitializeGame method, that initializes the game and is called by the Initialize XNA method (when the XNA framework loads the game), and when the user wants to reset the game.
1: private void InitializeGame()
2: {
3: AreEllipsesSelected = false;
4: IsGameOver = false;
5: score = 0;
6: ellipses = new Ellipse[EllipsesColumns, EllipsesRows];
7: for (int column = 0; column < EllipsesColumns; column++)
8: {
9: for (int row = 0; row < EllipsesRows; row++)
10: {
11: Ellipse el = new Ellipse(this.Content.Load<Texture2D>("ellipse"), GetRandomEllipseColor());
12: el.Location = new Vector2(column * EllipseWidth, row * EllipseHeight);
13: ellipses[column, row] = el;
14: }
15: }
16: }
The code for the ellipse class is rather simple, too.
1: class Ellipse
2: {
3: public Texture2D Texture {get;set;}
4: public Color EllipseColor { get; set; }
5: public Vector2 Location { get; set; }
6: public Color OriginalEllipseColor { get; private set; }
7:
8:
9:
10: public Ellipse(Texture2D texture, Color color)
11: {
12: this.Texture = texture;
13: Location = Vector2.Zero;
14: OriginalEllipseColor = EllipseColor = color;
15: }
16:
17: public void Update()
18: { }
19:
20:
21: public void Draw(SpriteBatch spriteBatch, Rectangle bounds)
22: {
23: spriteBatch.Draw(Texture, Location, EllipseColor);
24: }
25:
26:
27: public static List<Color> AvailableColors { get; private set; }
28: static Ellipse()
29: {
30: AvailableColors = new List<Color>();
31: AvailableColors.Add(Color.Blue);
32: AvailableColors.Add(Color.Purple);
33: AvailableColors.Add(Color.Green);
34: AvailableColors.Add(Color.Yellow);
35: AvailableColors.Add(Color.Red);
36:
37: }
38: }
Each ellipse gets a reference to the same Texture object loaded at the beginning of the game, and has two members carrying information about its color. The OriginalEllipseColor variable is used to hold a ‘backup’ of the ellipse’s original color, since if it is selected and not chosen for removal, it should return to its original color. The Draw method just draws the ellipse on the screen, and the AvailableColors static field is used to list the available colors for the ellipses, with each ellipse getting assigned a randomly selected color at the beginning of the game.
The Update method in the main code file first calls a method to get touch input, called HandledTouchInput. In this method, we first check to see if the user has touched the bottom 50 pixels of the screen, so as to reset the game
1: //check if the user wants to reset the game
2: if (touchLocation.Position.Y > 750)
3: {
4: InitializeGame();
5: return;
6: }
Then, we parse the point that the user touched into a respective ellipse
1: int column = (int)(touchLocation.Position.X / EllipseWidth);
2: int row = (int)(touchLocation.Position.Y / EllipseHeight);
3:
4: //out of ellipses bounds
5: if (column >= EllipsesColumns || row >= EllipsesRows) return;
6:
7: Ellipse selectedEllipse = ellipses[column, row];
Then, we have two choices, depending on whether there are selected ellipses or not. If there are no ellipses selected, then we have to check the ‘neighboring’ ones (to the one chosen by the user) for the same color. We create a SelectedEllipses list to hold a reference to the selected ellipses, and we also check if the total ellipses about to be selected are fewer than our threshold, which is held in the minEllipsesToRemove static variable). We also call the MarkEllipses method, which has the task to mask the ‘neighboring’ ellipses with the same color as the one selected by the user.
1: if (!AreEllipsesSelected)//user selects an ellipse
2: {
3: if (selectedEllipse != null)
4: {
5: SelectedEllipses = new List<Ellipse>();
6: MarkEllipses(selectedEllipse, column, row, selectedEllipse.EllipseColor);
7: if (SelectedEllipses.Count < minEllipsesToRemove) //not enough selected ellipses
8: {
9: //reset the selected
10: foreach (Ellipse el in SelectedEllipses)
11: el.EllipseColor = el.OriginalEllipseColor;
12:
13: return;
14: }
15: AreEllipsesSelected = true;
16: selectedSoundEffect.Play();
17: }
18: }
The MarkEllipses method is recursively called 4 times (maximum) for each ellipse it scans, it order to check left, right, top and bottom.
1: private void MarkEllipses(Ellipse ellipse, int column, int row, Color colorToCompare)
2: {
3: if (ellipse != null)
4: {
5: if (ellipse.EllipseColor == colorToCompare)
6: {
7: ellipse.EllipseColor = selectedEllipseColor;
8: SelectedEllipses.Add(ellipse);
9:
10: //check left
11: if (column != 0)
12: MarkEllipses(ellipses[column - 1, row], column - 1, row, colorToCompare);
13: if (row != 0) //check top
14: MarkEllipses(ellipses[column, row - 1], column, row - 1, colorToCompare);
15: if (row != EllipsesRows - 1) //check bottom
16: MarkEllipses(ellipses[column, row + 1], column, row + 1, colorToCompare);
17: if (column != EllipsesColumns - 1) //check top
18: MarkEllipses(ellipses[column + 1, row], column + 1, row, colorToCompare);
19: }
20: else
21: return;
22: }
23: }
However, if ellipses are selected, and the ellipse that just got touched is contained in the SelectedElllipses list, we first make them disappear, calculate the new score and then reallocate the remaining ellipses. This gets performed in the ReallocateEllipses method. If the ellpse that just got touched is not contained in the SelectedEllipses list, we just revert the selected ellipses to their original color.
1: else if (AreEllipsesSelected) //ellipses are already selected
2: {
3: if (SelectedEllipses.Contains(selectedEllipse) && SelectedEllipses.Count >= minEllipsesToRemove)//let's disappear them!
4: {
5: foreach (Ellipse el in SelectedEllipses)
6: {
7: ellipses[(int)(el.Location.X / EllipseWidth), (int)(el.Location.Y / EllipseHeight)] = null;
8: }
9:
10: score += SelectedEllipses.Count * (SelectedEllipses.Count - 1);
11:
12: //let's deorganize the rest of the ellipses
13: ReallocateEllipses();
14: deletedSoundEffect.Play();
15: }
16: else
17: {
18: foreach (Ellipse el in SelectedEllipses)
19: {
20: el.EllipseColor = el.OriginalEllipseColor;
21: }
22: }
23: AreEllipsesSelected = false;
24: }
In the ReallocateEllipses method, clears the rows of any ‘blank’ spaces and the columns of any empty ones (tries to collapse the ellipses both vertically – bottom – and horizontally – right). Disclaimer: Yeah, I am pretty sure that the algorithm can become much faster (e.g. scan only rows and columns containing info from the SelectedEllipses list) but I’d rather leave it to you as an exercise!
1: private void ReallocateEllipses()
2: {
3: //first, let's clear the empty spaces in the rows
4: for (int column = 0; column < EllipsesColumns; column++)
5: for (int row = EllipsesRows - 1; row >= 0; row--)
6: {
7: //
8: for (int l = 1; l <= row; l++)
9: {
10: if (ellipses[column, l] == null && ellipses[column, l - 1] != null)
11: {
12: ellipses[column, l] = ellipses[column, l - 1];
13: ellipses[column, l - 1] = null;
14: ellipses[column, l].Location += new Vector2(0, EllipseHeight);
15: }
16: }
17:
18: }
19:
20: //now, we'll check for empty columns
21: for (int column = EllipsesColumns - 1; column >= 0; column--)
22: {
23: for (int row = 1; row <= column; row++)
24: {
25: //we'll check the bottom element
26: //if it's null, then the whole row is null
27: if (ellipses[row, EllipsesRows - 1] == null && ellipses[row - 1, EllipsesRows - 1] != null)
28: {
29: //copy entire column...
30: for (int k = 0; k < EllipsesRows; k++)
31: {
32: if (ellipses[row - 1, k] == null) continue;
33: ellipses[row, k] = ellipses[row - 1, k];
34: ellipses[row - 1, k] = null;
35: ellipses[row, k].Location += new Vector2(EllipseWidth, 0);
36: }
37: }
38: }
39: }
40: }
The CheckIsGameOver method checks if there are any ‘neighboring’ ellipses with the same color
1: private bool CheckIsGameOver()
2: {
3: //if there are any ellipses selected, there's no point in checking as it's definitely not game over
4: if (AreEllipsesSelected) return false;
5:
6: for (int column = 0; column <= EllipsesColumns - 1; column++)
7: {
8: for (int row = 0; row < EllipsesRows - 1; row++)
9: {
10: //we are comparing each ellipse with the ones located below and right from it
11: if (ellipses[column, row] == null) continue;
12:
13:
14: if (ellipses[column, row].EllipseColor == ellipses[column, row + 1].EllipseColor)
15: return false;
16:
17: if(column<EllipsesColumns)
18: { if (ellipses[column + 1, row] == null) continue;
19: if (ellipses[column, row].EllipseColor == ellipses[column + 1, row].EllipseColor)
20: return false; }
21: }
22: }
23:
24: return true;
25:
26: }
UPDATE: Above method slightly updated, many thanks to Daniel Janik Microsoft PFE for correcting a bug and letting me know about it!
Draw just delegates to the Draw method of each Ellipse instance, and DrawScore draws the score using the arial font (and the game over message if IsGameOver has returned true).
1: protected override void Draw(GameTime gameTime)
2: {
3: //get a black background
4: GraphicsDevice.Clear(Color.Black);
5:
6: spriteBatch.Begin();
7:
8: //draw all the ellipses
9: foreach (Ellipse e in ellipses)
10: {
11: if (e != null)
12: e.Draw(spriteBatch, graphics.GraphicsDevice.Viewport.Bounds);
13: }
14:
15:
16: DrawScore(spriteBatch);
17:
18: spriteBatch.End();
19: base.Draw(gameTime);
20: }
21:
22: private void DrawScore(SpriteBatch spriteBatch)
23: {
24: Vector2 textLocation = new Vector2(10, 750);
25:
26: spriteBatch.DrawString(font, string.Format(messageScore, score), textLocation, Color.White);
27:
28: if (IsGameOver)
29: {
30: Vector2 msgSize = font.MeasureString(messageScore);
31: spriteBatch.DrawString(font, string.Format(messageGameOver, score), new Vector2(textLocation.X + msgSize.X + 10, textLocation.Y), Color.White);
32: }
33: }
You can download the source code for free by visiting my skydrive folder: http://sdrv.ms/XK7D9g
Stay tuned for more Windows Phone 7 tutorials!
[…] – Bubble Breaker Clone -> https://dgkanatsios.com/2010/07/28/bubble-breaker-in-windows-phone-7-using-xna-3/ […]
LikeLike