Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
Fetching contributors…

Octocat-spinner-32-eaf2f5

Cannot retrieve contributors at this time

file 360 lines (266 sloc) 14.706 kb

Abstract

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.

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.

The Window

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.

       use strict;
       use warnings;

       use SDL;
       use SDLx::App;

       my $App = SDLx::App->new(w => 400, h => 400, t => 'Puzz');

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.

    my @Grid;
    my @Img;
    my $CurrentImg;
    my %Move;

For now, lets fill in @Grid with what it's going to look like:

    @Grid = (
       [0,  1,  2,  3],
       [4,  5,  6,  7],
       [8,  9,  10, 11],
       [12, 13, 14, 15],
    );

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.

Loading the images

To load the images, we would normally use SDLx::Surface, but we're going to do it the libsdl way with SDL::Image because we need to do our own error handling.

    use SDL::Image;
    use SDL::GFX::Rotozoom 'SMOOTHING_ON';

    while(<./*>) {
        if(-f and my $i = SDL::Image::load($_)) {
            $i = SDL::GFX::Rotozoom::surface_xy($i, 0, 400 / $i->w, 400 / $i->h, SMOOTHING_ON);
            push @Img, $i;
        }
        else
        {   
            warn "Cannot Load $_: " . SDL::get_error() if $_ =~ /jpg|png|bmp/;
        }
    }
    $CurrentImg = $Img[rand @Img];
    
    die "Please place images in the Current Folder" if $#Img < 0;

We just go through every file in the current directory, and try to load it as an image. SDL::Image::load will return false if there was an error, so we want to discard it when that happens. If we used 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 my $i = SDL::Image::load($_) is just an idiom for setting a var and checking it for truth at the same time.

We want the image to be 400x400, and SDL::GFX::Rotozoom makes this possible. The two Rotozoom functions that are the most useful are surface and surface_xy. They work like this:

    $zoomed_src = SDL::GFX::Rotozoom::surface($src, $angle, $zoom, $smoothing)
    $zoomed_src = SDL::GFX::Rotozoom::surface_xy($src, $angle, $x_zoom, $y_zoom, $smoothing)

The zoom values are the multiplier for that component, or for both components at once as with $zoom. $angle is an angle of rotation in degrees. $smoothing should be SMOOTHING_ON or SMOOTHING_OFF (which can be exported by SDL::GFX::Rotozoom) or just 1 or 0.

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.

Handling Events

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 use SDL::Events for the constants.

    use SDL::Events;

    sub on_event {
        my ($e) = @_;
        if($e->type == SDL_QUIT or $e->type == SDL_KEYDOWN and $e->key_sym == SDLK_ESCAPE) {
            $App->stop;
        }
        elsif($e->type == SDL_MOUSEBUTTONDOWN and $e->button_button == SDL_BUTTON_LEFT) {
            ...
        }
    }

    $App->add_event_handler(\&on_event);
    # $App->add_move_handler(\&on_move); 
    # $App->add_show_handler(\&on_show);
    $App->run;

Filling the Grid

Once we have something like this, it's a good time to put some warn messages in to make sure the inputs are working correctly. Once they are, it's time to fill it in.

    my $x = int($e->button_x / 100);
    my $y = int($e->button_y / 100);
    if(!%Move and $Grid[$y][$x]) {`
        ...
    }

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 (%Move is false), and the piece clicked isn't the blank piece (0).

    for([-1, 0], [0, -1], [1, 0], [0, 1]) {
        my $nx = $x + $_->[0];
        my $ny = $y + $_->[1];
        if($nx >= 0 and $nx < 4 and $ny >= 0 and $ny < 4 and !$Grid[$ny][$nx]) {
            ...
        }
    }

Moving the Pieces

We check that the blank piece is in the 4 surrounding places by constructing 4 vectors. These will take us to those squares. The x component is first and the second is y. We iterate through them, setting $nx and $ny to the new position. Then if both $nx and $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.

    %Move = (
        x      => $x,
        y      => $y,
        x_dir  => $_->[0],
        y_dir  => $_->[1],
        offset => 0,
    );

To make a piece move, we construct the move hash with all the information it needs to move the piece. The x and y positions of the piece, the x and 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.

The Move Handler Callback

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).

    sub on_move {
        if(%Move) {
            $Move{offset} += 30 * $_[0];
            if($Move{offset} >= 100) {
                $Grid[$Move{y} + $Move{y_dir}][$Move{x} + $Move{x_dir}] = $Grid[$Move{y}][$Move{x}];
                $Grid[$Move{y}][$Move{x}] = 0;
                undef %Move;
            }
        }
    }

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 $_[0] so that the animation moves in correct time with the updating.

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 %Move is deleted.

Rendering the Game

Now that we have all the functionality we need it's finally time to see the game.

    sub on_show {
        $App->draw_rect( [0,0,$App->w,$App->h], 0 );
        for my $y (0..3) {
            for my $x (0..3) {
                ...
            }
        }
        $App->flip;
    }

We start the show handler by drawing a black rect over the entire app. Entire surface and black are the defaults of draw_rect, so letting it use the defaults is good. Next we iterate through a y and 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 flip.

    next unless my $val = $Grid[$y][$x];
    my $xval = $val % 4;
    my $yval = int($val / 4);
    my $move = %Move && $Move{x} == $x && $Move{y} == $y;
    ...

Inside the two loops we put this. First we set $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 x and 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. $move is set with a bool of whether it is this piece that is moving, if there is a piece moving at all.

    $App->blit_by(
        $CurrentImg,
        [$xval * 100, $yval * 100, 100, 100],
        [$x * 100 + ($move ? $Move{offset} * $Move{x_dir} : 0),
         $y * 100 + ($move ? $Move{offset} * $Move{y_dir} : 0)]
    );

Now that we have all of this, we can blit the portion of the current image we need to the app. We use 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 blit_by works as opposed to blit:

    $src->blit($dest, $src_rect, $dest_rect)
    $dest->blit_by($src, $src_rect, $dest_rect)

The portion we need is from the $xval and $yval, and where it needs to go to is from $x and $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.

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.

    use List::Util 'shuffle';

    sub new_grid {
        my @new = shuffle(0..15);
        @Grid = map { [@new[ $_*4..$_*4+3 ]] } 0..3;
        $CurrentImg = $Img[rand @Img];
    }

We will replace the grid initialising we did with this sub. First it shffles the numbers 0 through 15 with List::Util::shuffle. This array is then arranged into a 2D grid with a map and put in to @Grid. Setting the current image is also put into this sub.

    sub won {
        my $correct = 0;
        for(@Grid) {
            for(@$_) {
                return 0 if $correct != $_;
                $correct++;
            }
        }
        return 1;
    }

This sub returns whether the grid is in the winning configuration, that is, all piece values are in order from 0 to 15.

Now we put a call to new_grid to replace the grid initialisation we had before. We put won into the event handler to make click call new_grid if you have won. Finally, won is put into the show handler to show the blank piece if you have won.

Complete Code

Here is the finished code:

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 SDL::GFX::Rotozoom, SDL::Image::load and blit_by.

Activities

  1. Make the blank piece the bottom right piece instead of the top left piece.
  2. Make the grid dimensions variable by getting the value from $ARGV[0]. The grid will then be 5x5 if $ARGV[0] is 5 and so on.

Author

This chapter's content graciously provided by Blaizer.

POD ERRORS

Hey! The above document had some coding errors, which are explained below:

Around line 2:

Unknown directive: =head0

Something went wrong with that request. Please try again.