Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 360 lines (266 sloc) 14.731 kb
f4298ed unix linefeeds instead of windows CRLF
Tobias Leich authored
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<SDLx::Surface>, but we're going to do it the libsdl way with C<SDL::Image> 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<SDL::Image::load> will return false if there was an error, so we want to discard it when that happens. If we used C<SDLx::Surface> 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<my $i = SDL::Image::load($_)> 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<SDL::GFX::Rotozoom> makes this possible. The two Rotozoom functions that are the most useful are C<surface> and C<surface_xy>. 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<SMOOTHING_ON> or C<SMOOTHING_OFF> (which can be exported by C<SDL::GFX::Rotozoom>) 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<use SDL::Events> 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<warn> 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<x> component is first and the second is C<y>. 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<x> and C<y> positions of the piece, the C<x> and C<y> 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<draw_rect>, so letting it use the defaults is good. Next we iterate through a C<y> and C<x> 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<flip>.
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<x> and C<y> 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<blit_by> 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<blit_by> works as opposed to C<blit>:
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<List::Util::shuffle>. This array is then arranged into a 2D grid with a C<map> 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<new_grid> to replace the grid initialisation we had before. We put C<won> into the event handler to make click call C<new_grid> if you have won. Finally, C<won> 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<SDL::GFX::Rotozoom>, C<SDL::Image::load> and C<blit_by>.
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
Something went wrong with that request. Please try again.