Export (0) Print
Expand All

Focus Point: An Image Scaling Game for Smartphones

.NET Compact Framework 1.0
 

Rob Miles
Department of Computer Science, University of Hull

June 2005

Applies to:
   Windows Mobile-based Smartphones
   Microsoft .NET Compact Framework
   Microsoft Visual Studio .NET 2003

Summary: This article discusses how to manage image animation, scaling, and loading on Smartphones. The description is illustrated by the development of an image scaling game for Smartphone. (33 printed pages)


Download Focus Point for Smartphone.msi from the Microsoft Download Center.

Contents

Introduction
What You Will Need to Get Started
Describing the Focus Point Game
Generating the Tile Grid
Selecting the Tiles to Move
An Auto Solve Method
Simple Game
Using a Back Buffer to Improve the Display
Animating the Cursor
Animating the Tile Movement
Loading Pictures from Files
Image Scaling
Final Refinements
Complete Game
Conclusion

Introduction

Sliding puzzle games that use images are very popular. The display and processing power of Smartphones make them capable of successfully running such games. This article describes the creation of an image manipulation game called Focus Point in which players must unscramble an image that has been scrambled. During the development, you will learn how to break an image into a number of tiles, animate their movements, and load and scale images for use in the game.

What You Will Need to Get Started

You will need a copy of Visual Studio .NET 2003. You will also need the Smartphone 2003 SDK.

Describing the Focus Point Game

The idea behind Focus Point, like many good ideas, is simple. An image is divided into a number of smaller tiles. The images on the tiles are then scrambled, and the player must rebuild the picture by swapping the position of pairs of tiles.

Figure 1 shows the initial image and the tiles.

Click here for larger image

Figure 1. The initial image Click on thumbnail for a larger image.

Figure 2 shows the image after the tiles have been scrambled.

Click here for larger image

Figure 2. The scrambled image. Click on thumbnail for a larger image.

The player moves the cursor (which outlines a tile in yellow) by using the joystick on the Smartphone. The player selects the first item to be swapped by moving the yellow selection box by using the joystick and pressing the joystick. When the player moves to and selects the second item, the images on the two tiles are interchanged, and the game continues. During the game, a timer tracks the amount of time the player takes to solve the puzzle.

Generating the Tile Grid

The first step is to generate the grid that you will use for the tiles in the game. The grid is expressed as a list of rectangles, each of which covers one part of the screen.

Dividing the Screen into Four

One way to divide the screen into rectangles is to divide the screen into four with two lines, as shown in Figure 3.

Click here for larger image

Figure 3. Divide the screen into four. Click on thumbnail for a larger image.

The code to take a rectangle and divide it into four is simple. First, the application must indicate where the two dividing lines are to be drawn, as shown in the following code example.

int xSplit = (sourceRectangle.Width / 4) + 
    random.Next(sourceRectangle.Width / 2);
int ySplit = (sourceRectangle.Height / 4) + 
    random.Next(sourceRectangle.Height /2);

The code picks the two axes that will divide the rectangle. The random number generator is given a range to work with that makes sure that the positions do not result in rectangles that are too narrow or too flat. After the two axes have been selected, you can use the axes to specify the four rectangles, as shown in the following code example.

Rectangle[] fourSplit = new Rectangle[ 4 ];
fourSplit[ 0 ] = new Rectangle(
    sourceRectangle.Left, sourceRectangle.Top, 
    xSplit, ySplit);
fourSplit[ 1 ] = new Rectangle(
    sourceRectangle.Left + xSplit, sourceRectangle.Top, 
    sourceRectangle.Width - xSplit, ySplit);
fourSplit[ 2 ] = new Rectangle(
    sourceRectangle.Left, sourceRectangle.Top + ySplit, 
    xSplit, sourceRectangle.Height - ySplit);
fourSplit[ 3 ] = new Rectangle(
    sourceRectangle.Left + xSplit, sourceRectangle.Top + ySplit,
    sourceRectangle.Width - xSplit, sourceRectangle.Height - ySplit);

The fourSplit array is populated with the four rectangles that make up the divided rectangle.

Dividing the Screen Further by Using Recursion

You might think that you should create more rectangles by defining further axes and then extracting the rectangles that are formed by them. This is one way to do it, but it would be tiresome to write and would restrict the number of rectangles that could be created. A better way to produce more rectangles is to use recursion. If you look at the grid in Figure 1, you can see that it is made up of four rectangles, each of which has been divided into four additional rectangles.

In other words, after you have a way of dividing a rectangle into four rectangles, you can use this method for each of the rectangles it produces to further divide them again. This is a good example of recursion, in that the divide-into-four method will call itself. Remember that the recursion must stop at some point; otherwise, the method will never finish. The number of times the method calls itself determines the number of rectangles that are ultimately produced, hence determining the difficulty of the puzzle.

The following code example controls the recursion.

private ArrayList DivideRect
         (Rectangle sourceRectangle, int cIterations)
{
    ArrayList rectangleList = new ArrayList();
    Rectangle[] fourSplit = new Rectangle[ 4 ];

    int xSplit = 
        (sourceRectangle.Width / 4) +  
        random.Next(sourceRectangle.Width / 2);
    int ySplit = 
        (sourceRectangle.Height / 4) + 
        random.Next(sourceRectangle.Height /2);
    fourSplit[ 0 ] = new Rectangle(
        sourceRectangle.Left,
        sourceRectangle.Top, 
        xSplit, 
        ySplit);
    fourSplit[ 1 ] = new Rectangle(
        sourceRectangle.Left + xSplit,
        sourceRectangle.Top, 
        sourceRectangle.Width - xSplit, 
        ySplit);
    fourSplit[ 2 ] = new Rectangle(
        sourceRectangle.Left, 
        sourceRectangle.Top + ySplit, 
        xSplit, 
        sourceRectangle.Height - ySplit);
    fourSplit[ 3 ] = new Rectangle(
        sourceRectangle.Left + xSplit, 
        sourceRectangle.Top + ySplit, 
        sourceRectangle.Width - xSplit, 
        sourceRectangle.Height - ySplit);

    if (--cIterations > 0)
        foreach (Rectangle rect in fourSplit)
            foreach (Rectangle rc in 
                    DivideRect(rect, cIterations))
                    rectangleList.Add(rc);
    else
        foreach     (Rectangle rc in fourSplit)
            rectangleList.Add(rc);

    return(rectangleList);
}

The rectangles are assembled into a collection of rectangles called rectangleList. The cIterations variable determines the depth of the iterations. If the method has not finished splitting rectangles (that is, if cIterations is greater than 0), it calls itself and then adds the result that it returns to the list. If has reached the end of the split process, it adds the four rectangles that it produced to the list. Figure 4 shows the outcome of this method.

Click here for larger image

Figure 4. A screen divided into four rectangles three times. Click on thumbnail for a larger image.

The iterations value has been set to three, which means that the initial four rectangles have been divided into four, and each of those rectangles has been divided into four rectangles again. If you examine the lines carefully, you can see the initial dividing lines and the lines that specify each of the four rectangles inside the other rectangles at each level.

After the rectangles are divided, the application creates a list of rectangles that represents the tiles that you'll use as the basis of the game.

Shuffling the Puzzle Grid

At the start of the game, the images on the tiles are shuffled; the purpose of the game is for the player to put the images back into their correct places. The tiles are shuffled by changing the order of the draw process. When the puzzle is drawn, parts of the image are copied from an original onto a shuffled destination. The copy is performed by reading a rectangular area from the original and writing it into a rectangular area on the destination. If the same rectangle is used for the read as is used for the write, the image appears in the correct location. If a different destination rectangle is used, the image is in the incorrect location.

In Figure 5, an image from the bottom right tile has been swapped with one from the upper left tile. Note that the pieces have been scaled during the draw. Scaling makes the puzzle even more interesting.

Click here for larger image

Figure 5. Two tiles are swapped. Click on thumbnail for a larger image.

The shuffle information is held in an array of integers, one for each tile. Initially, the array is set so that the elements hold the values that match their subscripts, that is, element 0 holds the value 0, and so on. The draw action will read the image from rectangle 0 and write it on to rectangle 0 (that is, the image will not be shuffled).

To shuffle the picture, random locations in this array are swapped a number of times. For example, after a shuffle, the image read from rectangle 0 may be written into rectangle 5, and the image in rectangle 5 will be drawn in rectangle 0,

for ( int i=0; i < shuffledOrder.Length; i++ ) 
{
    shuffledOrder[i] = i;
}

int r1, r2, temp;
for ( int i=0 ; i < shuffledOrder.Length ; i++ ) 
{
    r1 = random.Next(shuffledOrder.Length);
    r2 = random.Next(shuffledOrder.Length);
    temp = shuffledOrder[r1];
    shuffledOrder[r1]=shuffledOrder[r2];
    shuffledOrder[r2]=temp;
}

The previous code performs the shuffle. The first part of the shuffle copies the original values into the shuffledOrder array. Then, the locations are shuffled by generating two random numbers from zero to one less than the size of the array and by swapping the location values stored in the two shuffledOrder array elements that are indexed by those random numbers. At the end of the shuffle process, you can use the shuffledOrder array to control the draw process.

for ( int i=0; i < puzzleRectangles.Count ; i++ ) 
{
    g.DrawImage(image, 
        (Rectangle) puzzleRectangles[i], 
        (Rectangle) puzzleRectangles[shuffledOrder[i]], 
        System.Drawing.GraphicsUnit.Pixel);
}

The previous code performs the shuffled draw. The image variable refers to the bitmap that is the source of the picture. The DrawImage method makes use of a source and destination rectangle. The source rectangle is read and copied into the shuffled destination. The image is automatically scaled by the DrawImage method. Note that the elements in puzzleRectangles have to be cast to the Rectangle type because puzzleRectangles is an ArrayList, and it manages everything in terms of references to Object.

Selecting the Tiles to Move

The next stage of developing the game is to provide a way that the player can select the two tiles that are to be swapped. The best way to select the tile is to use the joystick to move a cursor around the screen. This action is slightly tricky because the tile arrangement will be different each time the tiles are shuffled. The move method must intelligently determine which tile is the target for a move in a particular direction.

Figure 6. Selecting a tile with the cursor

In Figure 6, the cursor is on the rectangle that is outlined in yellow. If the player moves the joystick down, the cursor must move to a rectangle below the current cursor. There are two rectangles below the current cursor, but the left one is the best destination for the move.

It is possible to calculate the Y coordinate of the new cursor rectangle by adding the height of the cursor rectangle to the current cursor rectangle Y coordinate. Then each of the rectangles with this Y coordinate can be examined to see if it is a suitable destination for the cursor move. The best destination rectangle is the one with an X coordinate closest to the original cursor position. The code to make this move is as follows.

int diff = this.Width;
int cursorIndex;
int newY = cursorRect.Y + cursorRect.Height;
foreach ( Rectangle rc in puzzleRectangles) 
{
    if ( rc.Y != newY ) 
    {
        continue;
    }
    if ( diff > Math.Abs(rc.X-cursorRect.X)) 
    {
        diff = Math.Abs(rc.X-cursorRect.X);
        cursorIndex = puzzleRectangles.IndexOf(rc);
    }
}

The previous code calculates the Y position of the new cursor rectangle and stores it in newY. It then goes through each rectangle in the puzzleRectangles collection and looks for ones that have the same y coordinate as newY. If the target rectangle is on the correct level, the code then looks for the rectangle that has an x coordinate closest to the present rectangle's x coordinate. At the end of this loop, the cursorIndex variable holds the offset in the puzzleRectangles collection of the rectangle that defines the new cursor position . One useful side effect of this design is that the player is not able to move the cursor off the edge of the screen because there is not a destination rectangle in that location.

In the finished program, you have to implement a construction (like the previously mentioned one) for the player to move the cursor in each of the four possible directions. This code will be executed in response to a KeyDown event.

Displaying a Cursor

When the game is running, the player will select two tiles on the puzzle. One is the tile that is currently being selected as the "cursor." The other is the tile that the player selects for swapping. The first time the player selects a tile, this tile is marked as the "source" of the swap. The second time the player selects a tile, this tile is the "destination" of the swap.

Each time the player presses a key to move the cursor to a different tile, the cursor must be redrawn around the newly selected tile. The code to display the cursor simply draws the rectangles in the appropriate places, as shown in the following code.

private void drawCursors (Graphics g) 
{
    outlines.Color = cursorColor;
    g.DrawRectangle(outlines,(Rectangle)puzzleRectangles[cursorIndex]);
    if ( sourceIndex >= 0 ) 
    {
        outlines.Color=sourceColor;
        g.DrawRectangle(outlines, 
            (Rectangle) puzzleRectangles[sourceIndex]);
    }
}

The drawCursors method draws the cursor rectangle on the Graphics object that is supplied as a parameter. In this version of the game, where the screen is redrawn when the Paint method is called, it will draw using the graphics object supplied to the Paint method. Later in this article, you will draw the cursors on a back buffer bitmap. The offset of source and destination cursors in the puzzleRectangles collection are held in the variables sourceIndex and cursorIndex respectively. The colors of the rectangles that are used to display the cursors are set when the program starts. The source cursor is only shown if it has been selected. The value -1 in sourceIndex is used to indicate that no source has been selected yet. The drawCursors method is called each time that the player moves the cursor.

Selecting the Tiles to Swap

The first time the player selects a tile this position must be marked as one of the tiles to be moved. The second time a player selects a tile, the two tiles must be "swapped" and redrawn. When the player presses the joystick to select a tile, the keycode for carriage return is generated. The event handler for the keyPressed event checks for this character and calls the method returnPressed, as shown in the following code example.

private void returnPressed () 
{
    if ( sourceIndex < 0 ) 
    {
        sourceIndex = cursorIndex;
    }
    else 
    {
        swapTiles( sourceIndex, cursorIndex);
        sourceIndex = -1;
    }
}

The first time returnPressed is called, the array offset of the currently selected tile is recorded in the variable sourceIndex. The second time returnPressed is called, the swapTiles method is called to perform the swap, and sourceIndex is set to indicate that the source tile has not been selected yet.

The swapTiles method is simple. It swaps the two locations in the shuffled array and then requests that the screen be redrawn, as shown in the following code example.

private void swapTiles ( int src, int dest) 
{
    int temp = shuffledOrder[src];
    shuffledOrder[src] = shuffledOrder[dest];
    shuffledOrder[dest] = temp;
    this.Refresh();
}

Note that the Refresh method is used to request a screen redisplay — rather than the Invalidate method. Refresh causes the Form to redrawn itself the instant the method is called. Invalidate requests that the Form be redrawn at a later time. When the puzzle is solving itself, it performs a sequence of swaps and pauses between each swap so the player can view the changes. To allow the changes to be visible to the player, a call to the Refresh method must be used to cause the redraw action.

An Auto Solve Method

Solving the puzzle automatically is not difficult. The elements in the shuffledOrder array need to be rearranged so that each element holds the same value as the subscript of that element. One way to arrange the elements is to perform a sort of the array. The first instance of the autoSolve method works in exactly this way, as shown in the following code example.

private void autoSolve () 
{
    bool swaps ;
    do 
    {
        swaps = false ;
        for ( int i=0 ; i < shuffledOrder.Length-1 ; i++ ) 
        {
            if ( shuffledOrder[i] > shuffledOrder [i+1] ) 
            {
                swapTiles ( i, i+1) ;
                System.Threading.Thread.Sleep(750);
                swaps = true;
            }
        }
    } while (swaps);
}

This is a simple bubble sort. It makes repeated passes through the array swapping tiles that are out of order. After each swap, it pauses for three quarters of a second so that the player can see what is happening. Note that because the swapTiles method causes the display to refresh, the player can see the tiles change places.

Simple Game

The Simple Game project, which is a part of this article's download sample, contains a complete implementation of this game. The player can press the Shuffle soft key to draw a new tile grid and shuffle the tiles. The player can then use the cursor to select tiles for swapping. The AutoSolve command solves the puzzle.

Using a Back Buffer to Improve the Display

Thus far, the game draws the screen in a very simple way. Each time that the Paint event is raised, the entire screen is recreated. First, the tiles are drawn, and then, the grid lines and the cursors are placed on top. This results in a lot of work, and each time the cursor is moved, the screen flickers. The flicker is especially noticeable when the AutoSolve command is operating.

To greatly improve the appearance of the game, you can use a back buffer. With a back buffer, all of the drawing operations are performed on a bitmap in memory. When the Paint event is raised, the bitmap is copied onto the screen buffer. Using a back buffer has two advantages: it makes the amount of work performed in response to a Paint event a simple block copy of the bitmap onto the screen buffer and changes to the display can be achieved by drawing only the effected part of the bitmap — and not the whole screen. The paint method looks like the following code example.

private void FocusPointForm_Paint(
    object sender, System.Windows.Forms.PaintEventArgs e)
{
    e.Graphics.DrawImage(bufferImage,0,0);
}

The bufferImage variable is an instance of the BitMap class that holds the image maintained by the program. When the Paint event occurs, this image is copied into the display buffer.

Preventing the Background Redraw

When Microsoft Windows performs a paint operation, it first clears the form, filling it with the background color. Because you are doing all of the drawing, however, you don't need to clear the form. In fact, clearing the form before the back buffer is copied onto it causes a very noticeable flicker. The solution to this problem is to override the method in the Form, which performs the clear, as shown in the following code example.

protected override void OnPaintBackground(PaintEventArgs e)
{
    // Don't let the background get painted
}

The background paint method is now empty, removing the flicker.

Drawing the Game with a Back Buffer

When you use a back buffer, the drawing is slightly more complex, but it has the advantage that only the changes in the display need to be drawn. As an example, when the player moves the cursor, the program only needs to draw the new cursor and replace the old one, as shown in the following code example.

private int oldCursorIndex = -1;
private void drawCursors (Graphics g) 
{
    if ( oldCursorIndex >= 0 ) 
    {
        outlines.Color = outlineColor;
        g.DrawRectangle(outlines,
            (Rectangle)puzzleRectangles[oldCursorIndex]);
    }
    outlines.Color = cursorColor;
    g.DrawRectangle(outlines,(Rectangle)puzzleRectangles[cursorIndex]);
    oldCursorIndex = cursorIndex;
}

The drawCursors method draws the cursors on the Graphics object g. When a back buffer is used, the graphics object will be one obtained from that buffer bitmap. The drawCursors method keeps track of the previous cursor position and overwrites the old cursor rectangle before drawing the new one. The value of -1 is used to indicate when there is no original position, such as at the start of a puzzle. The position of the source tile is managed in the same way, as are the tiles that are swapped.

Simple Game with Back Buffer

The Back Buffer project, which is a part of this article's download sample, contains an implementation of the simple game that uses a back buffer for all of the drawing operations. You will find that this project redraws a lot more smoothly.

Animating the Cursor

The present game is quite usable, but it is hard to see the cursor. One way to make the game easier to play would be to animate the cursor. Many games make use of animation to allow the player to identify the objects on the screen. You can make the cursor change pulse with a changing color. To do this, you need a sequence of colors that the cursor tile outline can change through, as shown in the following code example.

private Color [] cursorGlowColors = 
{
    Color.FromArgb(255,252,31),
    Color.FromArgb(255,252,31),
    Color.FromArgb(255,252,0),
    Color.FromArgb(247,244,0),
    Color.FromArgb(229,226,0),
    Color.FromArgb(198,196,0),
    Color.FromArgb(170,168,0),
    Color.FromArgb(140,138,0),
    Color.FromArgb(140,138,0),
    Color.FromArgb(170,168,0),
    Color.FromArgb(198,196,0),
    Color.FromArgb(229,226,0),
    Color.FromArgb(247,244,0),
    Color.FromArgb(255,252,0),
    Color.FromArgb(255,252,31),
    Color.FromArgb(255,252,31)
};    

The cursorGlowColors array holds a range of color values that cycle from bright yellow to dark yellow and back again. If each time the cursor is redrawn, the next color in the sequence is chosen, the cursor will appear to pulse. The method that redraws the cursor simply steps onto the next color in turn, as shown in the following code example.

private Pen outlines = new Pen(Color.Gray); 
int cursorColorPos;
private void glowCursors (Graphics g) 
{
    outlines.Color = cursorGlowColors[cursorColorPos];
    g.DrawRectangle(outlines,(Rectangle)puzzleRectangles[cursorIndex]);
    cursorColorPos++;
    if ( cursorColorPos == cursorGlowColors.Length ) 
    {
        cursorColorPos=0;
    }

    if ( sourceIndex >= 0 ) 
    {
        outlines.Color=sourceColor;
        g.DrawRectangle(outlines, 
            (Rectangle) puzzleRectangles[sourceIndex]);
    }
    this.Invalidate();
}

The glowCursors method is called on a timer that ticks 50 times a second and causes the cursor to pulse gently. Because the program is now drawing onto a back buffer, it does not need to draw any other part of the image — it only needs to redraw the cursor outlines. After the glowing cursor has been drawn, the source cursor rectangle is drawn (if there is one). This action ensures if the cursor is moved onto the source rectangle, it is still shown as the source.

private void timer1_Tick(object sender, System.EventArgs e)
{
    using (Graphics g = Graphics.FromImage(bufferImage)) 
    {
        glowCursors(g);    
    }
}

The timer1_tick method gets the graphics context from the back buffer and then calls the glowCursors method to update the cursor glow status.

Simple Game with Glowing Cursor

The Glow Cursor project, which is a part of this article's download sample, contains an implementation of the simple game that implements the glowing cursor effect.

Animating the Tile Movement

The game is now quite presentable, but it would be much improved if the tile swapping action was more attractive. Currently, it is hard for the player to see the tiles that have been changed because they change places so quickly. The program needs a way to move the content of the two tiles being swapped from one location to the other in a smoothly animated transition.

Calculating the Tile Trajectory

To implement this smooth transitioning effect, the program must plot a path between the start and end points of each location and then animate the movement of the tiles from one location to the other. The effect is shown in Figure 7.

Click here for larger image

Figure 7. The animated transfer. Click on thumbnail for a larger image.

During the swap, an image must slide over the screen and change its dimensions to that of the destination, as shown in Figure 8. This swap must occur over a given number of steps.

Click here for larger image

Figure 8. Changes in dimension during transfer. Click on thumbnail for a larger image.

The program performs the move by translating the upper-left corner of the tile into the new position. At the same time, the width and height are updated with change values for each redraw operation. When the swap begins, the program must calculate the size of the changes in X and Y for each step in the transfer. It also needs to work out the change in the size of the tile. In the Figure 8, the image must be made to get narrower and taller with each step.

As shown in the following code example, the two rectangles, destRect and srcRect, define the two tiles that are being moved. The variable stepSize determines the number of steps over which the move is going to take place. The smaller this number, the more jerky the move appears. If the number is made larger, however, the transfer may take too long. I found that 50 steps is a reasonable compromise between speed and smoothness.

Note   All ofthe values that are calculated in the following code example are of the float type. If integers are used, the rounding errors in the divisions result in incorrect final values for position and size.
xpos1 = srcRect.X;
ypos1 = srcRect.Y;
xstep1 = (destRect.X - xpos1) / (float) stepSize;
ystep1 = (destRect.Y - ypos1) / (float) stepSize;
width1 = srcRect.Width;
height1 = srcRect.Height;
widthStep1 = (destRect.Width - srcRect.Width) / (float)stepSize;
heightStep1 = (destRect.Height - srcRect.Height) / (float)stepSize;

Each time the rectangle is drawn, the position and size values are updated, as shown in the following code example.

move1DestRect.X=(int)xpos1;
move1DestRect.Y=(int)ypos1;
move1DestRect.Width=(int)width1;
move1DestRect.Height=(int)height1;
xpos1 += xstep1;
ypos1 += ystep1;
width1 += widthStep1;
height1 += heightStep1;

The values that are being used are cast to integers before being assigned to the properties of the rectangle, which will be used to determine the destination of the draw operation. A similar set of values needs to be calculated and managed for the movement of the other tile.

Drawing the Moving Tiles

The tiles must be drawn each time their positions are updated. The tiles must also be erased from their previous positions. One way to erase the old tiles would be to redraw the entire bitmap. This way would slow the movement down, however, and result in a lot of extra work for the program. The best way to erase the tiles from their previous positions is to keep a copy of the empty background and then use this copy as a "clean" source for the redraw. The program uses slightly more memory, but the size of the screen is small, so this is a reasonable price to pay. The final version of the updateMove method for one of the tiles is shown in the following code example.

// Erase the old tile
g.DrawImage(drawBuffer, 
    move1DestRect, move1DestRect, System.Drawing.GraphicsUnit.Pixel);
// Update the position of the rectangle
move1DestRect.X=(int)xpos1;
move1DestRect.Y=(int)ypos1;
move1DestRect.Width=(int)width1;
move1DestRect.Height=(int)height1;
// Draw the tile at the new position
g.DrawImage(rawImage, 
    move1DestRect,
    (Rectangle) puzzleRectangles[shuffledOrder[source1]], 
    System.Drawing.GraphicsUnit.Pixel);
// Advance the position and size values
xpos1 += xstep1;
ypos1 += ystep1;
width1 += widthStep1;
height1 += heightStep1;

The first call to the DrawImage method draws from the drawBuffer bitmap. This holds the shuffled image (that is, the picture that the tile is moving over). This DrawImage call has the effect of erasing the previous image in the moving sequence.

The second call to the DrawImage method draws from the rawImage bitmap. This holds the original picture. This draw uses the position in the shuffledOrder array to determine the rectangle to be drawn from the puzzle picture.

Each time the update is performed, move1DestRect holds the destination of the previous draw, which is the tile that needs to be erased. This code is repeated for the other tile that is being moved.

Managing the Tile Movement

The moving tile positions need to be updated at regular intervals. The best way to get regular inputs of this kind is to use the timer. The timer is already being used in the program to manage the glowing cursor; the game must also use it to control the movement of the tiles. To do this, the timer method needs to know the state of the game when the timer event occurs. A variable is required that will manage the state of the game. The game contains an enumerated type that manages the state of the game, as shown in the following code example.

private enum GameMode 
{
    moveCursor,
    tileMove,
    autoSolve
}

During the game, the player is either moving the cursor to select a tile, a tile is being animated in movement, or the game is in the process of solving itself. The enumerated type can hold a value to represent each of these states.

When the game is in tileMove mode, at the end of a move it returns to moveCursor mode. When the game is in autoSolve mode, it remains in that mode until the tiles are in their correct positions. The state of the game controls the behavior of the timer method, as shown in the following code example.

private void timer1_Tick(object sender, System.EventArgs e)
{
    using (Graphics g = Graphics.FromImage(bufferImage)) 
    {
        switch ( mode ) 
        {
            case GameMode.moveCursor:
                glowCursors(g);    
                break ;
            case GameMode.tileMove:
                updateSwap(g);
                break;
            case GameMode.autoSolve:
                updateSolve(g);
                break;
        }
    }
}

If the tiles are being moved, the timer calls the updateSwap method for each timer event. If the game is in the moveCursor mode, the glowCursors method is invoked. A timer interval of 20 milliseconds provides a good speed for the cursor glow and also for the movement of the tiles.

The updateSwap method updates the tile movement. It checks to see if the animation is complete. If it is (that is, the move has reached the end of all the animation steps), the puzzle is redrawn (this puts back the grid and the cursors) and the game returns to the moveCursor mode. If the animation is not complete, the next animation step is performed and the display is invalidated so that the changed version of the bitmap will be drawn, as shown in the following code example.

private void updateSwap (Graphics g)
{
    if ( steps > stepSize ) 
    {
        drawPuzzle(g);
        mode = GameMode.moveCursor;
    }
    else 
    {
        updateMove (g);
        Invalidate();
    }
}

The Auto Solve Behavior

When the mode is set to autoSolve, the updateSolve method is called for each timer tick, as shown in the following code example. This method is similar to the updateSwap method

private void updateSolve ( Graphics g ) 
{
    if ( steps > stepSize ) 
    {
        drawPuzzle(g);
        sortTile(g);
    }
    else 
    {
        updateMove (g);
        Invalidate();
    }
}

The updateMove method is called to perform the animation of the movement of a pair of tiles. When a move completes, the sortTile method is called to continue the puzzle solving.

An Improved Auto Solve Method

Earlier versions of the program used a very simple auto solve behavior in which the shuffled array was sorted by using a bubble sort. This way works, but it results in a large number of visible tile swaps. When the swaps are animated, the automatic solver takes a long time to complete. The program, therefore, needs a faster sorting method, as shown in the following code example.

private void sortTile ( Graphics g ) 
{
    int src=0;
    int dest=0;
    bool sortComplete = true;
        
    for ( int i=0 ; i < puzzleRectangles.Count ; i++ ) 
    {
        if ( i == shuffledOrder[i] ) 
        {
            continue;
        }
        sortComplete = false;
        src = i;
        for ( int j=i ; j < puzzleRectangles.Count ; j++ ) 
        {
            if ( shuffledOrder[j] == src ) 
            {
                dest = j;
                break;
            }
        }
        break;
    }
    if ( !sortComplete ) 
    {
        startSwapTiles ( shuffledOrder[src], shuffledOrder[dest], g);
    }
    else
    {
        drawPuzzle(g);
        mode = GameMode.moveCursor;
    }
}

This way solves the puzzle for a single tile. It performs a kind of insertion sort. First, it scans down the shuffled list until it finds an element that is "wrong". (Remember that if all of the elements in the shuffle array line up, that is, shuffledOrder[0] contains 0, shuffledOrder[1] contains 1, and so on, the picture is not shuffled when it is drawn.) The first part of this way, therefore, searches for the first wrong value, that is,, the first location in the array whose value does not match the subscript. Second, it searches down the array for the position of the right version to go in that location. Third, having found that version, it initiates a swap of the two positions.

If it reaches the end of the array without finding any wrong locations, the puzzle has been completely solved, and the game returns to the moveCursor mode.

If all of the elements in the puzzle are shuffled, the sortTile method needs to be called for each of the elements to put them in the right places. The movement of the tiles is animated if the player has selected this option. It is rather pleasant to watch the puzzle solve itself.

Loading Pictures from Files

The early versions of the program only used the single image that was embedded into the project. The game is much more fun, however, if players can load images of their own to scramble and solve. Many Smartphones have built-in cameras, which mean that players could take their own pictures and use them as the basis for puzzles. The only problem is that, unlike Pocket PCs, Smartphones do not have a File Open dialog box that can be used to locate and load image files. To enable the player to load a picture, you will have to create your own File Open dialog box.

Building a List of File names to Load

Fortunately, building a list of file names to load is easy to do. You can use the ListView control to display a list of files and enable the player to select the one that is required. It is even possible to create icons for the file types. The following code example shows how a list of file names can be loaded into a ListView control.

private string [] searchPatterns = 
    new string [] {
                        @"*.jpg",
                        @"*.gif"
                    };

private System.Windows.Forms.ListView fileList;

foreach ( string searchPattern in searchPatterns ) 
{
    string [] files = 
        System.IO.Directory.GetFiles (rootPath, searchPattern );
    foreach ( string file in files ) 
    {
        ListViewItem item = new ListViewItem (splitFilename(file) );
        fileList.Items.Add( item );
    }
}

The searchPatterns array contains a list of the file extensions that are to be found by the GetFiles method. In the case of this game, the program must look for .gif and .jpeg images. The rootPath variable holds the path to the current directory. The GetFiles method returns a list of the file names that match the search pattern. These file names are used to create ListViewItem instances that you will add to the ListView control to be displayed for the player The splitFilename method removes the name from the full file name, ready for display to the player When the list is displayed, the player can select this item.

Adding Images to ListView Items

The user interface (UI) is greatly improved if you add images to the items that are displayed. You can assign a ListView control an ImageList. This ImageList is a set of images that you can use to adorn the list items that are displayed. An ImageList is managed within Visual Studio by use of the Image Collection Editor. You create an image list by dragging it from the toolbox, as shown in Figure 9.

Figure 9. Adding an ImageList

You can drag the ImageList onto the component tray next to the Timer and menu items. You can manage the ImageList properties using the Properties pane, as shown in Figure 10.

Figure 10. ImageList properties

You manage the actual images in the ImageList in the Collection property. If you click the Ellipses button to the right of the collection, the Image Collection Editor opens and enables you to manage the images in the list, as shown in Figure 11.

Click here for larger image

Figure 11. Image Collection Editor. Click on thumbnail for a larger image.

The Add and Remove buttons in the Image Collection Editor let you manage the images in the list. For the file selection task, you only need two images for the items in the list. One picture in the ImageList will be used to represent an image file, and the other will represent a directory in the Smartphone file store. These files are created as bitmaps and then added to the ImageList. There are a number of predefined images in Program Files\Microsoft Visual Studio .NET 2003\Common7\Graphics, which you can use in this way.

The numbers down the left side of the list of images are the numbers that will represent the images on the elements in the control, that is, if you select image number 0 from the ImageList, the folder icon appears. Image number 1 will display the image file icon. The image to be used for a particular list view item is set by using the ImageIndex property, as shown in the following code example.

private const int FOLDER_IMAGE = 0 ;
private const int FILE_IMAGE = 1 ;

item.ImageIndex = FOLDER_IMAGE;

To make the code more clear and to allow for the possibility that the image numbers may change if you use a different ImageList, you can create constants to represent the image offsets as shown in the previous code example.

You can use the Properties pane in Visual Studio to assign the image list to the ListView control. You can assign two image lists to a ListView control: one for small icons and the other for large icons. Two image lists are needed because two forms of list view can be displayed, and it may be necessary to have differently sized icons. For simplicity, this example uses the same image list for each list view, as shown in Figure 12.

Figure 12. Assigning ImageLists to the ListView control

Visual Studio incorporates the ImageList into the completed program when it is built.

Navigating Down the Directory Hierarchy

For a file selection tool to be useful, it must be possible for the player to move up and down the directory tree. This is achieved by adding all of the directories to the ListView before you add the files, as shown in the following code example.

// Now add all of the directories
string [] dirs = System.IO.Directory.GetDirectories(rootPath);

foreach ( string dir in dirs ) 
{
    ListViewItem item = new ListViewItem(splitFilename (dir) );
    item.ImageIndex = FOLDER_IMAGE;
    fileList.Items.Add( item );
}

The rootPath variable holds the current path that is being browsed. Each of the directories in that path is added to the list view. Note that the ImageIndex property is set to the directory image, so the player can recognize that it is a directory and not an image file.

Navigating Up the Directory Hierarchy

The player will also need a way of navigating up a directory hierarchy into the parent directory. The most common way to allow for this ability is to provide a directory icon with the name ".." that refers to the parent directory. A list item to denote the parent directory is added to the ListView if the directory has a parent.

The root of the Smartphone file system is the path "\". If the path has reached that level, no parent to the directory exists. If you are browsing a directory below that level, however, a parentFolder ListView item needs to be produced. The following code example makes this test and adds the item if required.

// If there is a parent - allow the player to climb up
if ( rootPath != @"\" ) 
{
    // Contains a path delimiter - have a parent
    parentFolder = new ListViewItem(".." ) ;
    parentFolder.ImageIndex = FOLDER_IMAGE ;
    fileList.Items.Add(parentFolder);
}

At the moment, this ListView item is assigned the same image as a folder item. A possible enhancement to the program would be to use another image (perhaps an arrow pointing upwards) to indicate the function of this item.

Figure 13 shows a directory being navigated. The parent directory is presently selected. Pressing the joystick will navigate up one level, in this case to the root. By selecting the FilesToBeDeleted folder, the player can navigate into that directory. Note the horizontal scroll bar. This bar indicates that more image files can be found across the list.

Figure 13. The ListView display

Item Selection

When the player selects an item in the ListView, an event is produced. If the selection is a file, the selection window can close and the file must be opened and used for the game. If the selection is a directory, the directory must be opened and selected as the current directory. If the selection is the parent directory, the file selector must navigate up to the parent directory. If the selection is a file, the selection window must return the file name and close itself as shown in the following code example.

private void listItemSelect (object sender, System.EventArgs e)
{
    System.Windows.Forms.ListViewItem selected = 
        fileList.Items[fileList.SelectedIndices[0]];
    if ( selected == parentFolder ) 
    {
        climbPath() ;
        buildFileList();
        return ;
    }
    string filename = fileList.Items[fileList.SelectedIndices[0]].Text;
    if ( rootPath == @"\" ) 
    {
        // Starting from the root level - everything is a directory 
        rootPath = rootPath + filename ;
        buildFileList();
        return ;
    }
    // Build the full name
    string fullname = rootPath + @"\" + filename ;
    if ( System.IO.Directory.Exists(fullname) )
    {
        // It is a directory or a device
        rootPath = fullname;
        buildFileList();
        return ;
    }
    // If you get here it really is a picture file - use it
    filenameValue=fullname;
    Close();
}

The first part of the method gets the currently selected item. This is checked to determine if it is the item that refers to the parent directory. If this is the case, the method navigates up the tree and rebuilds the file list. Then, the root directory is handled specially because it does not have a parent. Next, the file name is pulled off the selected item, and this path is tested to see if it refers to a directory. If the path is a directory, this directory name is added to the path, and the file list is rebuilt.

Finally, if the file name is a picture file, the file name is used to set the filenameValue property of the form, and then the form is closed. Note that there is also a cancel behavior that sets the return value to an empty string before closing the form. This is attached to the Cancel command on the form.

Using the PictureSelector Form

The PictureSelector form can be used to select pictures. You can use the form without ever worrying about how it works, as shown in the following code example.

private PictureSelector pictureSelector = null;

private void loadImage () 
{
    if ( pictureSelector == null ) 
    {
        pictureSelector = new PictureSelector ( this, @"\" ) ;
    }
    Cursor.Current = Cursors.WaitCursor;
    pictureSelector.ShowDialog();
    if ( pictureSelector.Filename.Length==0)
    {
        return;
    }
    Bitmap newFileImage;
    try 
    {
        newFileImage = new Bitmap( pictureSelector.Filename );
        scaleImage ( rawImage, newFileImage);
    }
    catch 
    {
        return ;
    }
    newGame();
}

The loadImage method is called when the player selects the menu command to load a new image. Only one PictureSelector instance is created when the program runs. There are two reasons why this only one instance is a good idea. The first reason is that after the PictureSelector has been created, it is much quicker to reuse on the created instance rather than recreate the form each time. The second reason is that if the player navigates to a particular directory, the form is positioned at this directory for next time; the player is not repeatedly returned to the root of the filestore each time. The first time the loadImage method is called, the PictureSelector is created. In subsequent calls, the same instance is used each time.

Note that the PictureSelector constructor accepts two parameters. The first is a reference to the form from which it is being used. This allows the PictureSelector can hide the parent form when it opens and show the parent when it closes. The second parameter is the path to the initial directory. This has been set to "\", the root of the Smartphone filestore.

The form is created and then displayed by using its ShowDialog method. This is modal, in that the calling method will pause until the form is closed. When the method resumes, it checks the FileName property to see if the player did select a file. If the name is an empty string, this means that the player selected the Cancel command, and so the method returns at that point. Otherwise, a new bitmap is created, which is then scaled and copied into the rawImage for use in a puzzle.

Image Scaling

The camera on a Smartphone can take pictures that are of a much higher resolution than the screen on the phone. In addition the aspect ratio (the ratio of the width to the height) for pictures taken with the camera is different from that of the puzzle display. The camera pictures are landscape (that is, wider than they are high); the puzzle pictures are portrait (that is, higher than they are wide). If the program does not take this difference into account, the puzzle images will look distorted.

The solution is to perform scaling on the loaded image to make sure that the picture fits the display and looks correct. It may also mean that a border needs to be added.

As shown in the following code example, the scaleImage method calculates the aspect ratio of the loaded image and then scales it as required. It draws a black background first, so that if the image doesn't fill the screen, the image will have a border. This action ensures that as much of the screen is used as possible. The Smartphone screen aspect ratio is 0.8. If the ratio of the loaded image is greater than that, the image must be scaled to fit in the width of the screen. If the ratio is less, it must be scaled to fit the height of the screen. Note that if you are writing programs for the QVGA Smartphones, the ratio is 0.75.

private void scaleImage ( Bitmap dest, Bitmap src ) 
{
    using ( Graphics g = Graphics.FromImage(dest) ) 
    {
        Brush b = new SolidBrush(Color.Black);
        g.FillRectangle(b,rawRectangle);
        Rectangle srcRect = new Rectangle (0,0,src.Width, src.Height) ;
        Rectangle destRect= new Rectangle(0,0,1,1);
        float aspect = (float) src.Width / (float) src.Height;
        if ( aspect > 0.8 ) 
        {
            // Wider than high
            destRect.Width = screenWidth;
            destRect.Height = (int) ((float) screenWidth / aspect) ;
            destRect.X=0;
            destRect.Y = (screenHeight - destRect.Height) / 2;
        }
        else 
        {
            // Higher than wide
            destRect.Height = screenHeight;
            destRect.Width = (int) ( (float) screenHeight * aspect ) ;
            destRect.X = (screenWidth - destRect.Width) / 2;
            destRect.Y = 0;
        }
    g.DrawImage(src,destRect,srcRect,System.Drawing.GraphicsUnit.Pixel);
    }
}

Final Refinements

A final couple of refinements add sound and a timer to the program.

Sound

The sound is created by instances of the class Sound, which is shown in the following code example.

private Sound moveSound;
private Sound winSound;

moveSound = new Sound("FocusPoint.sounds.move.wav");
winSound = new Sound("FocusPoint.sounds.win.wav");

The moveSound instance is played to produce a swooshing noise when tiles are animated. The winSound is played to produce a happy jingle when the puzzle is completed.

winSound.Play();    // Player has completed the puzzle

Timer

The timer is updated each second. It is active when the player is selecting a tile to move. The timer count is shown on the title line of the form. The following code example shows how to add a timer.

DateTime lastTick = DateTime.Now;
DateTime nowTick;
private void updateClock () 
{
    nowTick = DateTime.Now;
    if ( nowTick.Second != lastTick.Second ) 
    {
        secondCounter++;
        this.Text = "Time : " + secondCounter.ToString();
        lastTick=nowTick;
    }
}

The clock keeps track of the last time it was called. When the seconds value is different, it increases the second counter. The updateClock method is called each time the timer ticks and the game is in the moveCursor state. It is possible for the timer to not always be updated correctly, but fortunately extremely unlikely, the timer will not get called for more than a second.

Complete Game

The complete game project holds a full implementation of the game. Playing this game can be quite compulsive.

Conclusion

In the game, you can use recursion to repeat a behavior and also performed sprite scaling and movement. Finally, you have added file loading to create a complete working game.

Show:
© 2014 Microsoft