DCSIMG
Simple Puzzle Game for Windows Phone 7 using XNA - Scenes From A Developer Memory - Site Root - StudentGuru

Simple Puzzle Game for Windows Phone 7 using XNA

Introduction

In this tutorial blog post we will discover how easy it is to implement a simple puzzle game in Windows Phone 7 using the XNA 4.0 platform. The game consists of a single image, split randomly in 15 tiles (each of them having the same dimensions). The player has to move the tiles one by one (using the “empty” tile), attempting to recreate the original image. The starting screen is

image

If the player touches the screen, the game begins, the tiles are randomly placed on screen.

image

The black tile that you can see in the bottom-right corner is the “empty” one, the one that the player can move neighboring tiles to, in his attempts to solve the puzzle.

Game Components

If you open the game solution and check the startup object/game loop called GameManager (in the corresponding GameManager.cs codefile) you will not see much code, apart from the classic methods that the default XNA game template provides. Why is that? Where is the code for the game? Take a look at the start of the file, and you will see a reference to a PuzzleGame class. If you check the PuzzleGame.cs file, you can see that PuzzleGame is a class that inherits DrawableGameComponent. What is a (Drawable)GameComponent, then?

In order to better isolate different parts of your game, XNA framework provides the GameComponent class. The GameComponent class enables you to override various methods, like the ones found in the original Game class: Initialize, Update, LoadContent etc. It does not include Draw, however, but you can easily “obtain” a Draw method by inheriting from DrawableGameComponent (which inherits GameComponent, for that matter).

Onr can add a component to ones game class by simply calling the Game.AddComponent instance method. Then, during the game initialization and the game loop, the XNA framework calls the relevant methods in the GameComponent class before it calls the corresponding methods in the main game class, i.e. for our game, it first calls the Update method in all the PuzzleGame class, and then the Update method in the GameManager class. Most importantly, classes inheriting from GameComponent have an Enabled property, which (most surprisingly) enables or disables the GameComponent, which modifies accordingly its participation (or not) in the game loop.

Let’s examine the pieces that form our simple game.

The Tile class

The Tile class is responsible for keeping info regarding each tile. Let’s look at its code

 1:     class Tile
 2:     {
 3:         public Vector2 TextureLocationInSpriteSheet;
 4:         /// <summary>
 5:         /// This contains a number from 1-15 containing the 
 6:         /// original location of the tile tile, 
 7:         /// in order to determine of the player has won
 8:         /// </summary>
 9:         public int OriginalLocationInPuzzle;
 10:     }

The secondly listed variable, OriginalLocationInPuzzle, will keep the initial number of the tile (first row will have tiles with numbers 1,2,3, the second 4,5,6 and so forth) in order to be able to do comparisons of the current tile position during the game, to determine whether the player has won. The first variable, TextureLocationInSpriteSheet, will be used to contain the sprite’s texture location in the spritesheet. You don’t know what a spritesheet is? Please read on.

Spritesheet

Loading a file from disk takes time. Loading many files from disk takes more time. In our game, we need to display the whole image, and then “break” the image into 15 parts – tiles. So, we could choose to load 16 (!) images in the game, however this would take lots of time. Instead, we chose to implement the picture as a spritesheet. A spritesheet is a useful technique to have many images/sprites inside one, to minimize load times. This technique is really useful for all kinds of games such as arcade, shooter etc. However, in our game, supposed we created a Texture2D object referencing the entire picture. How should we draw just a portion of the image? The answer lies to a specific overload of the SpriteBatch.Draw method. More or that later. Just remember that the TextureLocationInSpriteSheet carries the location of the sprite in the entire image.

The PuzzleGame class

Let’s look at the private variables in the class

 1:     class PuzzleGame : DrawableGameComponent
 2:     {
 3:         GameState gameState;
 4:  
 5:         SpriteFont font;
 6:         Texture2D fullPicture;
 7:         SpriteBatch spriteBatch;
 8:  
 9:         const int Columns = 3;
 10:         const int Rows = 5;
 11:  
 12:         const int TileWidth = 160;
 13:         const int TileHeight = 160;
 14:  
 15:         Tile[,] tilesArray;
 16:         Tile[,] tilesArrayTemp;
 17:         Random random = new Random();
 18:         public PuzzleGame(Game game) : base(game) { }

The gameState variable holds information regarding the state of the game, described by the GameState enumeration (StartScreen, Playing, Winner). The other stuff is mostly explanatory, with the game having 5x3 tiles. The two two-dimensional arrays will be used to hold references to the image tiles.

The LoadTilesToTheirInitialLocation method initializes the tilesArray array

 1:         private void LoadTilesToTheirInitialLocation()
 2:         {
 3:             tilesArray = new Tile[Columns, Rows];
 4:             for (int column = 0; column < Columns; column++)
 5:                 for (int row = 0; row < Rows; row++)
 6:                 {
 7:                     Tile t = new Tile();
 8:                     t.TextureLocationInSpriteSheet.X = TileWidth * column;
 9:                     t.TextureLocationInSpriteSheet.Y = TileHeight * row;
 10:                     t.OriginalLocationInPuzzle = row * Columns + column + 1;
 11:                     tilesArray[column, row] = t;
 12:                 }
 13:         }

We initialize the tilesArray, and then, via 2 for-loops, create a new Tile, assign its relevant coordinates in the spritesheet (described before), and then assign a value to the OriginalLocationInPuzzle variable.

 1:         public override void Update(GameTime gameTime)
 2:         {
 3:             TouchLocation? tl = null;
 4:             TouchCollection tc = TouchPanel.GetState();
 5:             if (tc.Count > 0 && tc[0].State == TouchLocationState.Released)
 6:             {
 7:                 tl = tc[0];
 8:             }
 9:  
 10:             switch (gameState)
 11:             {
 12:                 case GameState.StartScreen:
 13:                     if (tl != null)
 14:                     {
 15:                         LoadTilesIntoRandomLocations();
 16:                         gameState = GameState.Playing;
 17:                     }
 18:                     break;
 19:                 case GameState.Playing:
 20:                     if (tl != null)
 21:                     {
 22:                         int hitTileColumn = (int)(tl.Value.Position.X / TileWidth);
 23:                         int hitTileRow = (int)(tl.Value.Position.Y / TileHeight);
 24:                         CheckAndSwap(hitTileColumn, hitTileRow);
 25:                         if (CheckIfPlayerWins())
 26:                             gameState = GameState.Winner;
 27:                     }
 28:                     else if (tl == null) //user has not touched the screen, so don't bother drawing
 29:                         Game.SuppressDraw();
 30:                     break;
 31:                 case GameState.Winner:
 32:                     if (tl != null)
 33:                         gameState = GameState.StartScreen;
 34:                     break;
 35:             }
 36:  
 37:             base.Update(gameTime);
 38:         }

In the Update method, we first check whether the player has touched the screen. Then, we check the gameState variable.

  • If we are in the starting screen, we will randomize the tiles’ positions, and begin playing.
  • If we are in the playing state, we get the coordinates of the touch input point, then we check if the tile that was touched is a neighboring to the “empty” one (so to move it), and, finally, check if the player has solved the puzzle, in order to go to the Winner state.
    • An interesting point here is the call to the Game.SuppressDraw method. If the user has not touched the screen, we will call this method to prevent any calls to Draw till the next Update call. So, the screen will not be updated (it will continue to show whatever was drawn in the last Draw call) and (surprise, surprise) we will conserve battery. This will surely please our user
  • Last but not least, if the player touches the screen when we are in the Winner state, we will transfer him to the initial state.
 1:         private void LoadTilesIntoRandomLocations()
 2:         {
 3:             tilesArrayTemp = new Tile[Columns, Rows];
 4:             for (int column = 0; column < Columns; column++)
 5:                 for (int row = 0; row < Rows; row++)
 6:                 {
 7:                     if (column == Columns - 1 && row == Rows - 1) break;
 8:  
 9:                     int newColumn; // = random.Next(0, Columns);
 10:                     int newRow; // = random.Next(0, Rows);
 11:                     do
 12:                     {
 13:                         newColumn = random.Next(0, Columns);
 14:                         newRow = random.Next(0, Rows);
 15:                     } while (tilesArrayTemp[newColumn, newRow] != null || (newColumn == Columns - 1 && newRow == Rows - 1));
 16:                     tilesArrayTemp[newColumn, newRow] = tilesArray[column, row];
 17:                 }
 18:             tilesArray = tilesArrayTemp;
 19:         }

In order to load the tiles randomly we use a temp two-dimensional array (tilesArrayTemp) and a random generator to assign random positions to all initial tiles (apart from the bottom-right one). Furthermore, we will not place a tile in the bottom-right corner, to leave a place for the “empty” tile. The algorithm assigns a new column and row to each tile. If it’s already occupied (from a previous tile), then the algorithm tries again (via the do…while loop). NOTE: This is NOT efficient, I’d rather use something like “Assign random positions to the first half tiles, and position the rest sequentially”. I also need to note that “Oh, it’s been long since I last needed a do…while loop!

 1:         private void CheckAndSwap(int column, int row)
 2:         {
 3:             if (row > 0 && tilesArray[column, row - 1] == null)
 4:             {
 5:                 tilesArray[column, row - 1] = tilesArray[column, row];
 6:                 tilesArray[column, row] = null;
 7:             }
 8:             else if (column > 0 && tilesArray[column - 1, row] == null)
 9:             {
 10:                 tilesArray[column - 1, row] = tilesArray[column, row];
 11:                 tilesArray[column, row] = null;
 12:             }
 13:             else if (row < Rows - 1 && tilesArray[column, row + 1] == null)
 14:             {
 15:                 tilesArray[column, row + 1] = tilesArray[column, row];
 16:                 tilesArray[column, row] = null;
 17:             }
 18:             else if (column < Columns - 1 && tilesArray[column + 1, row] == null)
 19:             {
 20:                 tilesArray[column + 1, row] = tilesArray[column, row];
 21:                 tilesArray[column, row] = null;
 22:             }
 23:         }

The CheckAndSwap method takes the column and row of the currently user-touched tile. In the if-else branches, it checks for out-of-bounds column and row, for the “empty” tile (the value for the “empty” tile in the tilesArray is null) and, if the tile touched is neighboring the “empty” tile, they will swap.

 1:         private bool CheckIfPlayerWins()
 2:         {
 3:             bool playerWins = true;
 4:  
 5:             for (int column = 0; column < Columns; column++)
 6:                 for (int row = 0; row < Rows; row++)
 7:                 {
 8:                     if (tilesArray[column, row] == null) continue; //if we are at the empty tile, just continue
 9:                     if (tilesArray[column, row].OriginalLocationInPuzzle != row * Columns + column + 1)
 10:                     {
 11:                         playerWins = false;
 12:                         break;
 13:                     }
 14:                 }
 15:  
 16:             return playerWins;
 17:         }

The CheckIfPlayerWins method owes its simplicity to the fact that in order to decide on winning, it just compares the current LocationInPuzzle of each tile to its original one. If all of them return true, then we have a winner!

 1:         private void DrawTiles()
 2:         {
 3:             for (int column = 0; column < Columns; column++)
 4:             {
 5:                 for (int row = 0; row < Rows; row++)
 6:                 {
 7:                     if (tilesArray[column, row] == null) continue;
 8:  
 9:                     spriteBatch.Draw(fullPicture,
 10:                         new Vector2(column * TileWidth, row * TileHeight),
 11:                         new Rectangle((int)tilesArray[column, row].TextureLocationInSpriteSheet.X,
 12:                             (int)tilesArray[column, row].TextureLocationInSpriteSheet.Y,
 13:                             TileWidth, TileHeight),
 14:                         Color.White,
 15:                         0f, //rotation
 16:                         Vector2.Zero,//origin
 17:                         1,//scale
 18:                         SpriteEffects.None,
 19:                         0);
 20:                 }
 21:             }
 22:         }

Remember our talk about the spritesheet? The above is the way to do it, the third argument of the SpriteBatch.Draw overload that we are using takes the Rectangle in the spritesheet that surrounds the sprite we want to draw. Easy enough?

I will not list the other Draw-related methods here, because they are plain simple. I’ll discuss one more thing though, before the end of this blog post.

SpriteBatch.DrawLine

Nah, there is no SpriteBatch.DrawLine method in XNA. So, what do you do if you need to draw a line in XNA? Return to the start of this blog post, and check the white lines in the second image. In order to draw these, I had to import a white 1x1 image in my project (pixel.png file). By using the second argument in the SpriteBatch.Draw method, we can adjust the Rectangle that will be drawn with our current texture/sprite. So, for the game purposes, we will use 2 vertical and 4 horizontal “lines”.

 1:         private void DrawLines()
 2:         {
 3:             //draw first vertical line
 4:             spriteBatch.DrawLine(new Vector2(TileWidth - 1, 0), 2, Game.GraphicsDevice.Viewport.Height,
 5:                 Color.White, 100, 0, SpriteEffects.None, 0);
 6:             //draw second vertical line
 7:             spriteBatch.DrawLine(new Vector2(2 * TileWidth - 1, 0), 2, Game.GraphicsDevice.Viewport.Height,
 8:                 Color.White, 100, 0, SpriteEffects.None, 0);
 9:             //draw first horizontal line
 10:             spriteBatch.DrawLine(new Vector2(0,TileHeight - 1),  Game.GraphicsDevice.Viewport.Width, 2,
 11:                 Color.White, 100, 0, SpriteEffects.None, 0);
 12:             //draw second horizontal line
 13:             spriteBatch.DrawLine(new Vector2(0, 2* TileHeight - 1), Game.GraphicsDevice.Viewport.Width, 2,
 14:                Color.White, 100, 0, SpriteEffects.None, 0);
 15:             //draw third horizontal line
 16:             spriteBatch.DrawLine(new Vector2(0, 3 * TileHeight - 1), Game.GraphicsDevice.Viewport.Width, 2,
 17:                Color.White, 100, 0, SpriteEffects.None, 0);
 18:             //draw fourth horizontal line
 19:             spriteBatch.DrawLine(new Vector2(0, 4 * TileHeight - 1), Game.GraphicsDevice.Viewport.Width, 2,
 20:                Color.White, 100, 0, SpriteEffects.None, 0);
 21:         }

But, we just said that SpriBatch does not have a DrawLine method. Where did this came up? The answer lies to a magic C# feature called extension methods. We will not list the details of this C# feature here, but we will briefly mention that it’s a great feature if you want to add more functionality to an existing class, without (and this is really important) breaking encapsulation. Let’s take a look at the DrawLine extension method implementation

 1:     static class Extensions
 2:     {
 3:  
 4:         public static Texture2D LineTexture { get; set; }
 5:  
 6:         public static void DrawLine(this SpriteBatch spriteBatch, Vector2 position, int width, int height,
 7:             Color color, byte opacity, float rotation, SpriteEffects effects, float layerDepth)
 8:         {
 9:             color.A = opacity;
 10:             spriteBatch.Draw(LineTexture, 
 11:                 new Rectangle((int)position.X,(int)position.Y,width,height),
 12:                 null,
 13:                 color,
 14:                 rotation,
 15:                 Vector2.Zero,
 16:                 effects,
 17:                 layerDepth);
 18:         }
 19:  
 20:                 
 21:     }

The LineTexture static reference is being set at the LoadContent method of the PuzzleGame class. The DrawLine method just calls the SpriBatch.Draw method, with the second argument being used to encapsulate the space that the drawn object will occupy.

Update: As Simon Jackson correctly points out, we do not need to have a 1x1 pixel file, we might as well programmatically create a new Texture2D object and use it to draw our lines! Sample source:

BlankTexture = new Texture2D(GraphicsDevice, 1, 1);

BlankTexture.SetData(new Color[] { Color.White });

Source code

Free, as always! Link: https://skydrive.live.com/?cid=10E568ADBB498DC8&id=10E568ADBB498DC8%211600&sc=documents#cid=10E568ADBB498DC8&id=10E568ADBB498DC8%21365&sc=documents

Enjoy!