# PerlGameDev/SDL_Manual

### Subversion checkout URL

You can clone with HTTPS or Subversion.

 f4298ed unix linefeeds instead of windows CRLF Tobias Leich authored Dec 28, 2010 1 2 =head0 Puzz! A puzzle game 3 4 =head1 Abstract 5 6 We are now ready to write another complete game. Instead of listing the code and then explaining it, I will go through the process of how I might write it. 7 8 Puzz is a simple rearrangment puzzle. A random image from the folder Puzz is in is chosen and broken into a 4x4 grid. The top left corner piece is then taken away, and every other piece is then moved to a random position, scrambling the image up. The goal is then to move pieces which are in the 4 squares adjacent to the empty square on to the empty square, and eventually restore the image. 9 10 =for figure 11 \includegraphics[width=0.5\textwidth]{../src/images/puzz1.png} 12 \caption{Credits to Sebastian Riedel (kraih.com) for the Perl6 logo used with permission in the application.} 13 \label{fig:puzz} 14 15 =head1 The Window 16 17 So, first thing we do is create the window. I've decided I want each piece to be 100x100, so the window needs to be 400x400. 18 19 use strict; 20 use warnings; 21 22 use SDL; 23 use SDLx::App; 24 25 my $App = SDLx::App->new(w => 400, h => 400, t => 'Puzz'); 26 27 Next thing we usually do is figure out what global vars we will be needing. As with$App, I like to name my globals with title case, so they are easily distinguishable from lexical vars. The globals we need are the grid (the positions of the pieces), the images we have to use, the current image, and a construct that will give us piece movement, along with an animation. 28 29 my @Grid; 30 my @Img; 31 my $CurrentImg; 32 my %Move; 33 34 For now, lets fill in @Grid with what it's going to look like: 35 36 @Grid = ( 37 [0, 1, 2, 3], 38 [4, 5, 6, 7], 39 [8, 9, 10, 11], 40 [12, 13, 14, 15], 41 ); 42 43 C<0> will be our blank piece, but we could have chosen it to be any other number. When the grid looks like this, it's solved, so eventually we will need a way to scramble it. It's good enough for now, though. 44 45 =head1 Loading the images 46 47 To load the images, we would normally use C, but we're going to do it the libsdl way with C because we need to do our own error handling. 48 49 50 use SDL::Image; 51 use SDL::GFX::Rotozoom 'SMOOTHING_ON'; 52 53 while(<./*>) { 54 if(-f and my$i = SDL::Image::load($_)) { 55$i = SDL::GFX::Rotozoom::surface_xy($i, 0, 400 /$i->w, 400 / $i->h, SMOOTHING_ON); 56 push @Img,$i; 57 } 58 else 59 { 60 warn "Cannot Load $_: " . SDL::get_error() if$_ =~ /jpg|png|bmp/; 61 } 62 } 63 $CurrentImg =$Img[rand @Img]; 64 65 die "Please place images in the Current Folder" if $#Img < 0; 66 67 We just go through every file in the current directory, and try to load it as an image. C will return false if there was an error, so we want to discard it when that happens. If we used C to load the images, we would get a warning every time a file fails to load as an image, which we don't want. The C is just an idiom for setting a var and checking it for truth at the same time. 68 69 We want the image to be 400x400, and C makes this possible. The two Rotozoom functions that are the most useful are C and C. They work like this: 70 71$zoomed_src = SDL::GFX::Rotozoom::surface($src,$angle, $zoom,$smoothing) 72 $zoomed_src = SDL::GFX::Rotozoom::surface_xy($src, $angle,$x_zoom, $y_zoom,$smoothing) 73 74 The zoom values are the multiplier for that component, or for both components at once as with C<$zoom>. C<$angle> is an angle of rotation in degrees. C<$smoothing> should be C or C (which can be exported by C) or just 1 or 0. 75 76 Once the image is zoomed, it is added to the image array. The current image is then set to a random value of the array. 77 78 =head1 Handling Events 79 80 The next part I like to write is the events. We're going to make Escape quit, and left click will move the pieces around. We C for the constants. 81 82 use SDL::Events; 83 84 sub on_event { 85 my ($e) = @_; 86 if($e->type == SDL_QUIT or$e->type == SDL_KEYDOWN and $e->key_sym == SDLK_ESCAPE) { 87$App->stop; 88 } 89 elsif($e->type == SDL_MOUSEBUTTONDOWN and$e->button_button == SDL_BUTTON_LEFT) { 90 ... 91 } 92 } 93 94 $App->add_event_handler(\&on_event); 95 #$App->add_move_handler(\&on_move); 96 # $App->add_show_handler(\&on_show); 97$App->run; 98 99 =head1 Filling the Grid 100 101 Once we have something like this, it's a good time to put some C messages in to make sure the inputs are working correctly. Once they are, it's time to fill it in. 102 103 my $x = int($e->button_x / 100); 104 my $y = int($e->button_y / 100); 105 if(!%Move and $Grid[$y][$x]) {` 106 ... 107 } 108 109 From the pixel coordinates of the click (0 to 399), we want to find out the grid coordinates (0 to 3), so we divide both components by 100 and round them down. Then, we only want to continue on to see if that piece can move if no other piece is moving (C<%Move> is false), and the piece clicked isn't the blank piece (0). 110 111 for([-1, 0], [0, -1], [1, 0], [0, 1]) { 112 my$nx = $x +$_->[0]; 113 my $ny =$y + $_->[1]; 114 if($nx >= 0 and $nx < 4 and$ny >= 0 and $ny < 4 and !$Grid[$ny][$nx]) { 115 ... 116 } 117 } 118 119 =head1 Moving the Pieces 120 121 We check that the blank piece is in the 4 surrounding places by constructing 4 vectors. These will take us to those squares. The C component is first and the second is C. We iterate through them, setting C<$nx> and C<$ny> to the new position. Then if both C<$nx> and C<$ny> are within the grid (0 to 3), and that position in the grid is 0, we can move the piece to the blank square. 122 123 %Move = ( 124 x => $x, 125 y =>$y, 126 x_dir => $_->[0], 127 y_dir =>$_->[1], 128 offset => 0, 129 ); 130 131 To make a piece move, we construct the move hash with all the information it needs to move the piece. The C and C positions of the piece, the C and C directions it will be moving (the vector), and it's current pixel offset from it's position (for the moving animation), which starts at 0. 132 133 =head2 The Move Handler Callback 134 135 Next we will write the move handler. All it needs to do is move any moving piece along by updating the offset, and click it in to where it's being moved to when it has moved the whole way (offset is 100 or more). 136 137 sub on_move { 138 if(%Move) { 139 $Move{offset} += 30 *$_[0]; 140 if($Move{offset} >= 100) { 141$Grid[$Move{y} +$Move{y_dir}][$Move{x} +$Move{x_dir}] = $Grid[$Move{y}][$Move{x}]; 142$Grid[$Move{y}][$Move{x}] = 0; 143 undef %Move; 144 } 145 } 146 } 147 148 30 has been arbitrarily chosen as the speed of the move, as it felt the best after a little playing and tweaking. Always remember to multiply things like this by the step value in C<$_[0]> so that the animation moves in correct time with the updating. 149 150 Once the offset is 100 or more, the grid place that the piece is moving to is set to the value of the piece, and the piece is set to the blank value. The move is then finished, so C<%Move> is deleted. 151 152 =head1 Rendering the Game 153 154 Now that we have all the functionality we need it's finally time to see the game. 155 156 sub on_show { 157$App->draw_rect( [0,0,$App->w,$App->h], 0 ); 158 for my $y (0..3) { 159 for my$x (0..3) { 160 ... 161 } 162 } 163 $App->flip; 164 } 165 166 We start the show handler by drawing a black rect over the entire app. Entire surface and black are the defaults of C, so letting it use the defaults is good. Next we iterate through a C and C of 0 to 3 so that we can go through each piece of the grid. At the end of the handler we update the app with a call to C. 167 168 next unless my$val = $Grid[$y][$x]; 169 my$xval = $val % 4; 170 my$yval = int($val / 4); 171 my$move = %Move && $Move{x} ==$x && $Move{y} ==$y; 172 ... 173 174 Inside the two loops we put this. First we set C<$val> to the grid value at the current position, and we skip to the next piece if it's the blank piece. We have the C and C coordinates of where that piece is on the board, but we need to figure out where it is on the image. If you refer back to the initialisation of the grid, the two operations to find the values should make sense. C<$move> is set with a bool of whether it is this piece that is moving, if there is a piece moving at all. 175 176 $App->blit_by( 177$CurrentImg, 178 [$xval * 100,$yval * 100, 100, 100], 179 [$x * 100 + ($move ? $Move{offset} *$Move{x_dir} : 0), 180 $y * 100 + ($move ? $Move{offset} *$Move{y_dir} : 0)] 181 ); 182 183 Now that we have all of this, we can blit the portion of the current image we need to the app. We use C because the image we're blitting isn't an SDLx::Surface (because we didn't load it as one), but the app is. Here's how C works as opposed to C: 184 185 $src->blit($dest, $src_rect,$dest_rect) 186 $dest->blit_by($src, $src_rect,$dest_rect) 187 188 The portion we need is from the C<$xval> and C<$yval>, and where it needs to go to is from C<$x> and C<$y>. All are multiplied by 100 because we're dealing with 0 to 300, not 0 to 3. If the piece is moving, the offset multiplied by the diretion is added to the position. 189 190 When the code is run with all 3 handlers, we have a fully working game. The pieces move around nicely when clicked. The only things it still needs are a shuffled grid and a way to check if the player has won. To imlement these two things, we will make two more functions. 191 192 use List::Util 'shuffle'; 193 194 sub new_grid { 195 my @new = shuffle(0..15); 196 @Grid = map { [@new[ $_*4..$_*4+3 ]] } 0..3; 197 $CurrentImg =$Img[rand @Img]; 198 } 199 200 We will replace the grid initialising we did with this sub. First it shffles the numbers 0 through 15 with C. This array is then arranged into a 2D grid with a C and put in to @Grid. Setting the current image is also put into this sub. 201 202 sub won { 203 my $correct = 0; 204 for(@Grid) { 205 for(@$_) { 206 return 0 if $correct !=$_; 207 $correct++; 208 } 209 } 210 return 1; 211 } 212 213 This sub returns whether the grid is in the winning configuration, that is, all piece values are in order from 0 to 15. 214 215 Now we put a call to C to replace the grid initialisation we had before. We put C into the event handler to make click call C if you have won. Finally, C is put into the show handler to show the blank piece if you have won. 216 217 =head1 Complete Code 218 219 Here is the finished code: 220 221 =begin programlisting 222 223 use strict; 224 use warnings; 225 226 use SDL; 227 use SDLx::App; 228 use SDL::Events; 229 use SDL::Image; 230 use SDL::GFX::Rotozoom 'SMOOTHING_ON'; 231 use List::Util 'shuffle'; 232 233 my$App = SDLx::App->new(w => 400, h => 400, t => 'Puzz'); 234 235 my @Grid; 236 my @Img; 237 my $CurrentImg; 238 my %Move; 239 240 while(<./*>) { 241 if(-f and my$i = SDL::Image::load($_)) { 242$i = SDL::GFX::Rotozoom::surface_xy($i, 0, 400 /$i->w, 400 / $i->h, SMOOTHING_ON); 243 push @Img,$i; 244 } 245 else 246 { 247 warn "Cannot Load $_: " . SDL::get_error() if$_ =~ /jpg|png|bmp/; 248 } 249 250 } 251 252 die "Please place images in the Current Folder" if $#Img < 0; 253 254 new_grid(); 255 256 sub on_event { 257 my ($e) = @_; 258 if($e->type == SDL_QUIT or$e->type == SDL_KEYDOWN and $e->key_sym == SDLK_ESCAPE) { 259$App->stop; 260 } 261 elsif($e->type == SDL_MOUSEBUTTONDOWN and$e->button_button == SDL_BUTTON_LEFT) { 262 my($x,$y) = map { int($_ / 100) }$e->button_x, $e->button_y; 263 if(won()) { 264 new_grid(); 265 } 266 elsif(!%Move and$Grid[$y][$x]) { 267 for([-1, 0], [0, -1], [1, 0], [0, 1]) { 268 my($nx,$ny) = ($x +$_->[0], $y +$_->[1]); 269 if($nx >= 0 and$nx < 4 and $ny >= 0 and$ny < 4 and !$Grid[$ny][$nx]) { 270 %Move = ( 271 x =>$x, 272 y => $y, 273 x_dir =>$_->[0], 274 y_dir => $_->[1], 275 offset => 0, 276 ); 277 } 278 } 279 } 280 } 281 } 282 283 sub on_move { 284 if(%Move) { 285$Move{offset} += 30 * $_[0]; 286 if($Move{offset} >= 100) { 287 $Grid[$Move{y} + $Move{y_dir}][$Move{x} + $Move{x_dir}] =$Grid[$Move{y}][$Move{x}]; 288 $Grid[$Move{y}][$Move{x}] = 0; 289 undef %Move; 290 } 291 } 292 } 293 294 sub on_show { 295$App->draw_rect( [0,0,$App->w,$App->h], 0 ); 296 for my $y (0..3) { 297 for my$x (0..3) { 298 next if not my $val =$Grid[$y][$x] and !won(); 299 my $xval =$val % 4; 300 my $yval = int($val / 4); 301 my $move = %Move &&$Move{x} == $x &&$Move{y} == $y; 302$App->blit_by( 303 $CurrentImg, 304 [$xval * 100, $yval * 100, 100, 100], 305 [$x * 100 + ($move ?$Move{offset} * $Move{x_dir} : 0), 306$y * 100 + ($move ?$Move{offset} * $Move{y_dir} : 0)] 307 ); 308 } 309 } 310$App->flip; 311 } 312 313 sub new_grid { 314 my @new = shuffle(0..15); 315 @Grid = map { [@new[ $_*4..$_*4+3 ]] } 0..3; 316 $CurrentImg =$Img[rand @Img]; 317 } 318 319 sub won { 320 my $correct = 0; 321 for(@Grid) { 322 for(@$_) { 323 return 0 if $correct !=$_; 324 $correct++; 325 } 326 } 327 return 1; 328 } 329 330$App->add_event_handler(\&on_event); 331 $App->add_move_handler(\&on_move); 332$App->add_show_handler(\&on_show); 333 $App->run; 334 335 =end programlisting 336 337 You now hopefully know more of the process that goes in to creating a simple game. The process of creating a complex game is similar, it just requires more careful planning. You should have also picked up a few other tricks, like with C, C and C. 338 339 =head1 Activities 340 341 =over 342 343 =item 1 344 345 Make the blank piece the bottom right piece instead of the top left piece. 346 347 =item 2 348 349 Make the grid dimensions variable by getting the value from C<$ARGV[0]>. The grid will then be 5x5 if C<\$ARGV[0]> is 5 and so on. 350 351 =back 352 353 354 =head1 Author 355 356 This chapter's content graciously provided by Blaizer. 357 358 =for vim: spell 359