Animating a Series of Images
The next example animates a series of images stored in an array. The application uses the same technique to load and display Images as shown in Fig. 17.23.
The animation in Fig. 17.24 uses a PictureBox, which contains the images that we animate. We use a Timer to cycle through the images and display a new image every 50 milliseconds. Variable count keeps track of the current image number and increases by one every time we display a new image. The array includes 30 images (numbered 029); when the application reaches image 29, it returns to image 0. The 30 images are located in the images folder inside the project's bin/Debug and bin/Release directories.
Figure 17.24. Animation of a series of images.
1 // Fig. 17.24: LogoAnimator.cs
2 // Program that animates a series of images.
3 using System;
4 using System.Drawing;
5 using System.Windows.Forms;
6
7 // animates a series of 30 images
8 public partial class LogoAnimator : Form
9 {
10 private Image[] images = new Image[ 30 ];
11 private int count = -1;
12
13 // LogoAnimator constructor
14 public LogoAnimator()
15 {
16 InitializeComponent();
17
18 for ( int i = 0; i < 30; i++ )
19 images[ i ] = Image.FromFile( @"imagesdeitel" + i + ".gif" );
20
21 logoPictureBox.Image = images[ 0 ]; // display first image
22
23 // set PictureBox to be the same size as Image
24 logoPictureBox.Size = logoPictureBox.Image.Size;
25 } // end LogoAnimator constructor
26
27 // event handler for timer's Tick event
28 private void timer_Tick( object sender, EventArgs e )
29 {
30 count = ( count + 1 ) % 30; // increment counter
31 logoPictureBox.Image = images[ count ]; // display next image
32 } // end method timer_Tick
33 } // end class LogoAnimator
|
Lines 1819 load each of 30 images and place them in an array of Images. Line 21 places the first image in the PictureBox. Line 24 modifies the size of the PictureBox so that it is equal to the size of the Image it is displaying. The event handler for timer's Tick event (line 2832) responds to each event by displaying the next image from the array.
Chess Example
The following chess example demonstrates techniques for two-dimensional collision detection, selecting single frames from a multiframe image, and regional invalidation, refreshing only the parts of the screen that have changed, to increase performance. Two-dimensional collision detection enables a program to detect whether two shapes overlap or whether a point is contained within a shape. In the next example, we demonstrate the simplest form of collision detection, which determines whether a point (the mouse-click location) is contained within a rectangle (a chess-piece image).
Class ChessPiece (Fig. 17.25) represents the individual chess pieces. Lines 1018 define a public enumeration of constants that identify each chess-piece type. The constants also serve to identify the location of each piece in the chess-piece image file. Rectangle object targetRectangle (lines 2425) identifies the image location on the chessboard. The x and y properties of the rectangle are assigned in the ChessPiece constructor, and all chess-piece images have a width and height of 75 pixels.
Figure 17.25. Class that represents chess piece attributes.
(This item is displayed on pages 845 - 846 in the print version)
1 // Fig. 17.25 : ChessPiece.cs 2 // Class that represents chess piece attributes. 3 using System; 4 using System.Drawing; 5 6 // represents a chess piece 7 class ChessPiece 8 { 9 // define chess-piece type constants 10 public enum Types 11 { 12 KING, 13 QUEEN, 14 BISHOP, 15 KNIGHT, 16 ROOK, 17 PAWN 18 } // end enum Types 19 20 private int currentType; // this object's type 21 private Bitmap pieceImage; // this object's image 22 23 // default display location 24 private Rectangle targetRectangle = 25 new Rectangle( 0, 0, 75, 75 ); 26 27 // construct piece 28 public ChessPiece( int type, int xLocation, 29 int yLocation, Bitmap sourceImage ) 30 { 31 currentType = type; // set current type 32 targetRectangle.X = xLocation; // set current x location 33 targetRectangle.Y = yLocation; // set current y location 34 35 // obtain pieceImage from section of sourceImage 36 pieceImage = sourceImage.Clone( 37 new Rectangle( type * 75, 0, 75, 75 ), 38 System.Drawing.Imaging.PixelFormat.DontCare ); 39 } // end method ChessPiece 40 41 // draw chess piece 42 public void Draw( Graphics graphicsObject ) 43 { 44 graphicsObject.DrawImage( pieceImage, targetRectangle ); 45 } // end method Draw 46 47 // obtain this piece's location rectangle 48 public Rectangle GetBounds() 49 { 50 return targetRectangle; 51 } // end method GetBounds 52 53 // set this piece's location 54 public void SetLocation( int xLocation, int yLocation ) 55 { 56 targetRectangle.X = xLocation; 57 targetRectangle.Y = yLocation; 58 } // end method SetLocation 59 } // end class ChessPiece |
The ChessPiece constructor (lines 2839) receives the chess-piece type, its x and y location and the Bitmap containing all chess-piece images. Rather than loading the chesspiece image within the class, we allow the calling class to pass the image. This increases the flexibility of the class by allowing the user to change images. Lines 3638 extract a subimage that contains only the current piece's bitmap data. Our chess-piece images are defined in a specific manner: One image contains six chess-piece images, each defined within a 75-pixel block, resulting in a total image size of 450-by-75. We obtain a single image via Bitmap's Clone method, which allows us to specify a rectangle image location and the desired pixel format. The location is a 75-by-75 pixel block with its upper-left corner x equal to 75 * type and the corresponding y equal to 0. For the pixel format, we specify constant DontCare, causing the format to remain unchanged.
Method Draw (lines 4245) causes the ChessPiece to draw pieceImage in the targetRectangle using the Graphics object passed as Draw's argument. Method GetBounds (lines 4851) returns the targetRectangle object for use in collision detection, and method SetLocation (lines 5458) allows the calling class to specify a new piece location.
Class ChessGame (Fig. 17.26) defines the game and graphics code for our chess game. Lines 1115 define instance variables the program requires. ArrayList chessTile (line 11) stores the board tile images. ArrayList chessPieces (line 12) stores all active ChessPiece objects, and intselectedIndex (line 13) identifies the index in chessPieces of the currently selected piece. The board (line 14) is an 8-by-8, two-dimensional int array corresponding to the squares of a chess board. Each board element is an integer from 0 to 3 that corresponds to an index in chessTile and is used to specify the chessboard-square image. const TILESIZE (line 15) defines the size of each tile in pixels.
Figure 17.26. Chess-game code.
1 // Fig. 17.26: ChessGame.cs
2 // Chess Game graphics code.
3 using System;
4 using System.Collections;
5 using System.Drawing;
6 using System.Windows.Forms;
7
8 // allows 2 players to play chess
9 public partial class ChessGame : Form
10 {
11 private ArrayList chessTile = new ArrayList(); // for tile images
12 private ArrayList chessPieces = new ArrayList(); // for chess pieces
13 private int selectedIndex = -1; // index for selected piece
14 private int[ , ] board = new int[ 8, 8 ]; // board array
15 private const int TILESIZE = 75; // chess tile size in pixels
16
17 // default constructor
18 public ChessGame()
19 {
20 // Required for Windows Form Designer support
21 InitializeComponent();
22 } // end constructor
23 24 // load tile bitmaps and reset game 25 private void ChessGame_Load( object sender, EventArgs e ) 26 { 27 // load chess board tiles 28 chessTile.Add( Bitmap.FromFile( @"imageslightTile1.png" ) ); 29 chessTile.Add( Bitmap.FromFile( @"imageslightTile2.png" ) ); 30 chessTile.Add( Bitmap.FromFile( @"imagesdarkTile1.png" ) ); 31 chessTile.Add( Bitmap.FromFile( @"imagesdarkTile2.png" ) ); 32 33 ResetBoard(); // initialize board 34 Invalidate(); // refresh form 35 } // end method ChessGame_Load 36 37 // initialize pieces to start and rebuild board 38 private void ResetBoard() 39 { 40 int current = -1; 41 ChessPiece piece; 42 Random random = new Random(); 43 bool light = false; 44 int type; 45 46 chessPieces.Clear(); // ensure empty arraylist 47 48 // load whitepieces image 49 Bitmap whitePieces = 50 ( Bitmap ) Image.FromFile( @"imageswhitePieces.png" ); 51 52 // load blackpieces image 53 Bitmap blackPieces = 54 ( Bitmap ) Image.FromFile( @"imageslackPieces.png" ); 55 56 // set whitepieces to be drawn first 57 Bitmap selected = whitePieces; 58 59 // traverse board rows in outer loop 60 for ( int row = 0; row <= board.GetUpperBound( 0 ); row++ ) 61 { 62 // if at bottom rows, set to black pieces images 63 if ( row > 5 ) 64 selected = blackPieces; 65 66 // traverse board columns in inner loop 67 for ( int column = 0; 68 column <= board.GetUpperBound( 1 ); column++ ) 69 { 70 // if first or last row, organize pieces 71 if ( row == 0 || row == 7 ) 72 { 73 switch ( column ) 74 {
75 case 0: 76 case 7: // set current piece to rook 77 current = ( int ) ChessPiece.Types.ROOK; 78 break; 79 case 1: 80 case 6: // set current piece to knight 81 current = ( int ) ChessPiece.Types.KNIGHT; 82 break; 83 case 2: 84 case 5: // set current piece to bishop 85 current = ( int ) ChessPiece.Types.BISHOP; 86 break; 87 case 3: // set current piece to king 88 current = ( int ) ChessPiece.Types.KING; 89 break; 90 case 4: // set current piece to queen 91 current = ( int ) ChessPiece.Types.QUEEN; 92 break; 93 } // end switch 94 95 // create current piece at start position 96 piece = new ChessPiece( current, 97 column * TILESIZE, row * TILESIZE, selected ); 98 99 chessPieces.Add( piece ); // add piece to arraylist 100 } // end if 101 102 // if second or seventh row, organize pawns 103 if ( row == 1 || row == 6 ) 104 { 105 piece = new ChessPiece( 106 ( int ) ChessPiece.Types.PAWN, 107 column * TILESIZE, row * TILESIZE, selected ); 108 chessPieces.Add( piece ); // add piece to arraylist 109 } // end if 110 111 type = random.Next( 0, 2 ); // determine board piece type 112 113 if ( light ) // set light tile 114 { 115 board[ row, column ] = type; 116 light = false; 117 } 118 else // set dark tile 119 { 120 board[ row, column ] = type + 2; 121 light = true; 122 } 123 } // end for loop for columns 124 125 light = !light; // account for new row tile color switch 126 } // end for loop for rows 127 } // end method ResetBoard
128 129 // display board in form OnPaint event 130 private void ChessGame_Paint( object sender, PaintEventArgs e ) 131 { 132 Graphics graphicsObject = e.Graphics; // obtain graphics object 133 graphicsObject.TranslateTransform( 0, 24 ); // adjust origin 134 135 for ( int row = 0; row <= board.GetUpperBound( 0 ); row++ ) 136 { 137 for ( int column = 0; 138 column <= board.GetUpperBound( 1 ); column++) 139 { 140 // draw image specified in board array 141 graphicsObject.DrawImage( 142 ( Image ) chessTile[ board[ row, column ] ], 143 new Point( TILESIZE * column, ( TILESIZE * row ) ) ); 144 } // end for loop for columns 145 } // end for loop for rows 146 } // end method ChessGame_Paint 147 148 // return index of piece that intersects point 149 // optionally exclude a value 150 private int CheckBounds( Point point, int exclude ) 151 { 152 Rectangle rectangle; // current bounding rectangle 153 154 for ( int i = 0; i < chessPieces.Count; i++ ) 155 { 156 // get piece rectangle 157 rectangle = GetPiece( i ).GetBounds(); 158 159 // check if rectangle contains point 160 if ( rectangle.Contains( point ) && i != exclude ) 161 return i; 162 } // end for 163 164 return -1; 165 } // end method CheckBounds 166 167 // handle pieceBox paint event 168 private void pieceBox_Paint( 169 object sender, System.Windows.Forms.PaintEventArgs e ) 170 { 171 // draw all pieces 172 for ( int i = 0; i < chessPieces.Count; i++ ) 173 GetPiece( i ).Draw( e.Graphics ); 174 } // end method pieceBox_Paint 175 176 // handle pieceBox MouseDown event 177 private void pieceBox_MouseDown( 178 object sender, System.Windows.Forms.MouseEventArgs e ) 179 {
180 // determine selected piece 181 selectedIndex = CheckBounds( new Point( e.X, e.Y ), -1 ); 182 } // end method pieceBox_MouseDown 183 184 // if piece is selected, move it 185 private void pieceBox_MouseMove( 186 object sender, System.Windows.Forms.MouseEventArgs e ) 187 { 188 if ( selectedIndex > -1 ) 189 { 190 Rectangle region = new Rectangle( 191 e.X - TILESIZE * 2, e.Y - TILESIZE * 2, 192 TILESIZE * 4, TILESIZE * 4 ); 193 194 // set piece center to mouse 195 GetPiece( selectedIndex ).SetLocation( 196 e.X - TILESIZE / 2, e.Y - TILESIZE / 2 ); 197 198 pieceBox.Invalidate( region ); // refresh region 199 } // end if 200 } // end method pieceBox_MouseMove 201 202 // on mouse up deselect piece and remove taken piece 203 private void pieceBox_MouseUp( object sender, MouseEventArgs e ) 204 { 205 int remove = -1; 206 207 // if chess piece was selected 208 if ( selectedIndex > -1 ) 209 { 210 Point current = new Point( e.X, e.Y ); 211 Point newPoint = new Point( 212 current.X - ( current.X % TILESIZE ), 213 current.Y - ( current.Y % TILESIZE ) ); 214 215 // check bounds with point, exclude selected piece 216 remove = CheckBounds( current, selectedIndex ); 217 218 // snap piece into center of closest square 219 GetPiece( selectedIndex ).SetLocation( newPoint.X, newPoint.Y ); 220 selectedIndex = -1; // deselect piece 221 222 // remove taken piece 223 if ( remove > -1 ) 224 chessPieces.RemoveAt( remove ); 225 } // end if 226 227 pieceBox.Invalidate(); // ensure artifact removal 228 } // end method pieceBox_MouseUp 229
230 // helper function to convert 231 // ArrayList object to ChessPiece 232 private ChessPiece GetPiece( int i ) 233 { 234 return ( ChessPiece ) chessPieces[ i ]; 235 } // end method GetPiece 236 237 // handle NewGame menu option click 238 private void newGameItem_Click( 239 object sender, System.EventArgs e ) 240 { 241 ResetBoard(); // reinitialize board 242 Invalidate(); // refresh form 243 } // end method newGameItem_Click 244 } // end class ChessGame
|
The chess game GUI consists of Form ChessGame, the area in which we draw the tiles; Panel pieceBox, the area in which we draw the pieces (note that pieceBox's background color is set to "TRansparent"); and a Menu that allows the user to begin a new game. Although the pieces and tiles could have been drawn on the same form, doing so would decrease performance. We would be forced to refresh the board and all the pieces every time we refreshed the control.
The ChessGame_Load event handler (lines 2535) loads four tile images into chessTiletwo light tiles and two dark tiles for variety. It then calls method ResetBoard to refresh the Form and begin the game. Method ResetBoard (lines 38127) clears chessPieces, loads images for both the black and the white chess-piece sets and creates Bitmap selected to define the currently selected Bitmap set. Lines 60126 loop through the board's 64 positions, setting the tile color and piece for each tile. Lines 6364 cause the currently selected image to switch to the blackPieces after the fifth row. If the row counter is on the first or last row, lines 71100 add a new piece to chessPieces. The type of the piece is based on the current column we are initializing. Pieces in chess are positioned in the following order, from left to right: rook, knight, bishop, queen, king, bishop, knight and rook. Lines 103109 add a new pawn at the current location if the current row is second or seventh.
A chessboard is defined by alternating light and dark tiles across a row in a pattern where the color that starts each row is equal to the color of the last tile of the previous row. Lines 113122 assign the current board-tile color to an element in the board array. Based on the alternating value of bool variable light and the results of the random operation on line 111, we assign an int to the board to determine the color of that tile0 and 1 represent light tiles; 2 and 3 represent dark tiles. Line 125 inverts the value of light at the end of each row to maintain the staggered effect of a chessboard.
Method ChessGame_Paint (lines 130146) handles the Form's Paint event and draws the tiles according to their values in the board array. Since the default height of a MenuStrip is 24 pixels, we use the TranslateTransform method of class Graphics to shift the origin of the Form down 24 pixels (line 133). This shift prevents the top row of tiles from being hidden behind the MenuStrip. Method pieceBox_Paint (lines 168174), which handles the Paint event for the pieceBox Panel, iterates through each element of the chessPiece ArrayList and calls its Draw method.
The pieceBox MouseDown event handler (lines 177182) calls CheckBounds (lines 150165) with the location of the mouse to determine whether the user selected a piece.
The pieceBox MouseMove event handler (lines 185200) moves the selected piece with the mouse. Lines 190192 define a region of the Panel that spans two tiles in every direction from the pointer. As mentioned previously, Invalidate is slow. This means that the pieceBox MouseMove event handler might be called several times before the Invalidate method completes. If a user working on a slow computer moves the mouse quickly, the application could leave behind artifacts. An artifact is any unintended visual abnormality in a graphical program. By causing the program to refresh a two-square rectangle, which should suffice in most cases, we achieve a significant performance enhancement over an entire component refresh during each MouseMove event. Lines 195196 set the selected piece location to the mouse-cursor position, adjusting the location to center the image on the mouse. Line 198 invalidates the region defined in lines 190192 so that it will be refreshed.
Lines 203228 define the pieceBox MouseUp event handler. If a piece has been selected, lines 208225 determine the index in chessPieces of any piece collision, remove the collided piece, snap (align) the current piece to a valid location and deselect the piece. We check for piece collisions to allow the chess piece to "take" other chess pieces. Line 216 checks whether any piece (excluding the currently selected piece) is beneath the current mouse location. If a collision is detected, the returned piece index is assigned to remove. Lines 211213 determine the closest valid chess tile and "snaps" the selected piece to that location. If remove contains a positive value, line 224 removes the object at that index from the chessPieces ArrayList. Finally, the entire Panel is invalidated in line 227 to display the new piece location and remove any artifacts created during the move.
Method CheckBounds (lines 150165) is a collision-detection helper method; it iterates through ArrayList chessPieces and returns the index of any piece's rectangle that contains the point passed to the method (the mouse location, in this example). CheckBounds uses Rectangle method Contains to determine whether a point is in the Rectangle. Method CheckBounds optionally can exclude a single piece index (to ignore the selected index in the pieceBox MouseUp event handler, in this example).
Lines 232235 define helper function GetPiece, which simplifies the conversion from objects in ArrayList chessPieces to ChessPiece types. Method newGameItem_Click (lines 238243) handles the NewGame menu item click event, calls RefreshBoard to reset the game and invalidates the entire form.