Perl Page #6: Creating 2D Video Games in SDL_perl / SDLx


There's strictly no warranty for the correctness of this text. You use any of the information provided here at your own risk.

The page's not finished yet, it's a work in progress.


Contents:

  1. About SDL_perl and SDLx
  2. Moving Sprites
  3. More on Keyboard Input
  4. The First Example Once More, But With Changes
  5. Displaying Text
  6. Controlling the Speed
  7. Doing Things More the SDLx-Way (Including Keyboard Input)
  8. Scrolling the Background


1. About SDL_perl and SDLx

"SDL_perl" is a set of Perl modules to create video games. The modules are bindings to the C-library "SDL" ("Simple Direct Layer").

It seems, the original SDL_perl modules were not so easy to use. To make things more convenient, a further layer called "SDLx" was created. Convenience isn't necessarily a good thing, but it seems, SDLx did quite a good job in assisting the programmer.
In general, I'll explain things the SDLx way, and fall back to the SDL modules, where I think it's important.

There's an official tutorial as a pdf file here. There are also a few code examples. Useful perldocs are:


2. Moving Sprites

Let's get right into it. I'm creating a sprite with a black background and red circle on it, and move it from left to right using mostly SDLx. I'm also putting this script on my GitHub page:

#!/usr/bin/perl

use warnings;
use strict;

# SDL_perl example 1

# Copyright (C) 2021 hlubenow
# License: GNU GPL 3.

use SDLx::App;
use SDLx::Surface;
use SDLx::Rect;
use SDLx::FPS;

my $RESX  = 800;
my $RESY  = 600;
my $BLACK = [0, 0, 0, 255];
my $BLUE  = [0, 0, 189, 255];
my $RED   = [220, 0, 0, 255];
my $SIZE  = 50;
my $STEP  = 5;

package GameWindow {

    use SDL::Event;

    sub new {
        my $classname = shift;
        my $self = {};
        $self->{addon} = AddOn->new();
        return bless($self, $classname);
    }    

    sub start {
        my $self = shift;
        SDL::putenv("SDL_VIDEO_WINDOW_POS=130,18");
        $self->{app} = SDLx::App->new(w            => $RESX,
                                      h            => $RESY,
                                      title        => 'Moving Circle',
                                      exit_on_quit => 1);

        $self->{screen} = SDLx::Surface::display();
        $self->{fps}    = SDLx::FPS->new(fps => 50);
        $self->{event}  = SDL::Event->new();

        $self->{red_circle} = RedCircle->new($self->{addon});
        $self->{red_circle}->createSurface();

        $self->{running} = 1;

        # Main Loop:
        while ($self->{running}) {
            $self->{fps}->delay();
            if ($self->handleEvents() eq "quit") {
                $self->{running} = 0;
            }
            $self->{red_circle}->moveRight();

            $self->{addon}->fill($self->{screen}, $BLUE);
            $self->{red_circle}->draw($self->{screen});
            $self->{screen}->flip(); # or "update()"
        }
    }

    sub handleEvents {
        my $self = shift;
        SDL::Events::pump_events();
        if (SDL::Events::poll_event($self->{event})) {
            if ($self->{event}->type == SDL_KEYDOWN ) {
                if ($self->{event}->key_sym == SDLK_SPACE) {
                    $self->{red_circle}->startStopMoving();
                }
                if ($self->{event}->key_sym == SDLK_q) {
                    return "quit";
                }
            }
        }
        return 0;
    }
}

package AddOn {

    # My own additions to SDLx.

    sub new {
        my $classname = shift;
        my $self = {};
        return bless($self, $classname);
    }    

    sub get_rect {
        my ($self, $surface) = @_;
        return SDLx::Rect->new(0, 0, $surface->w, $surface->h);
    }

    sub fill {
        my ($self, $surface, $colorref) = @_;
        $surface->draw_rect( [ 0, 0, $surface->w, $surface->h ], $colorref);
    }
}


package RedCircle {

    sub new {
        my $classname = shift;
        my $self = {moving => 1}; 
        $self->{addon} = shift;
        return bless($self, $classname);
    }    

    sub createSurface {
        my $self = shift;
        $self->{surface} = SDLx::Surface->new(w => $SIZE, h => $SIZE);
        $self->{addon}->fill($self->{surface}, $BLACK);
        $self->{surface}->draw_circle_filled( [$SIZE / 2, $SIZE / 2], ($SIZE - 10) / 2, $RED);
        $self->{rect} = $self->{addon}->get_rect($self->{surface});
        # Set start-position. Values for "->topleft()" are in the order of y, x:
        $self->{rect}->topleft(300 - $SIZE, 0);
    }

    sub startStopMoving {
        my $self = shift;
        $self->{moving} = 1 - $self->{moving};
    }

    sub moveRight {
        my $self = shift;
        if ($self->{moving} == 0) {
            return;
        }
        $self->{rect}->x($self->{rect}->x + $STEP);
        if ($self->{rect}->right > 800) {
            $self->{rect}->x(0);
        }
    }

    sub draw {
        my ($self, $screen) = @_;
        $screen->blit_by($self->{surface}, undef, $self->{rect});
    }
}

my $app = GameWindow->new();
$app->start();

There's already quite a lot going on here.


3. More on Keyboard Input

What kind of keyboard I/O routine needs to be used, depends on what kind of keyboard input you want to achieve:

  1. Getting just a single key press, and then wait, until the key is relased and another key is pressed (blocking I/O):
    package GameWindow {
        ...
        use SDL::Event;
        ...
        sub start {
            ...
            $self->{event} = SDL::Event->new();
            while ($self->{running}) {
                if ($self->processEvents() eq "quit") {
                    $self->{running} = 0;
                }
        }
    
        sub processEvents {
            my $self = shift;
            SDL::Events::pump_events();
            if (SDL::Events::poll_event($self->{event})) {
                if ($self->{event}->type == SDL_KEYDOWN ) {
                    if ($self->{event}->key_sym == SDLK_SPACE) {
                        # Do something, when the space-bar is pressed.
                    }
                    if ($self->{event}->key_sym == SDLK_q) {
                        return "quit";
                    }
                }
            }
            return 0;
        }
    }
  2. Non-blocking keyboard input. The user can press several keys at once.
    A variable is set up, and it has to be checked each loop for "SDL_KEYDOWN" and "SDL_KEYUP" events:
    package GameWindow {
        ...
        use SDL::Event;
        ...
        sub start {
            ...
            $self->{event}        = SDL::Event->new();
            $self->{keystates}    = ();
            $self->{keystatekeys} = [SDLK_LEFT, SDLK_RIGHT, SDLK_UP, SDLK_DOWN, SDLK_q];
            my $i;
            for $i (@{ $self->{keystatekeys} }) {
                $self->{keystates}{$i} = 0;
            }
            ...
            while ($self->{running}) {
                if ($self->processEvents() eq "quit") {
                    $self->{running} = 0;
                }
        }
    
        sub processEvents {
            my $self = shift;
            my ($i, $num);
            SDL::Events::pump_events();
            if (SDL::Events::poll_event($self->{event})) {
                if ($self->{event}->type == SDL_KEYDOWN ) {
                    for $i (@{ $self->{keystatekeys} }) {
                        if ($self->{event}->key_sym == $i) {
                            $self->{keystates}{$i} = 1;
                        }
                    }
                }
                if ($self->{event}->type == SDL_KEYUP ) {
                    for $i (@{ $self->{keystatekeys} }) {
                        if ($self->{event}->key_sym == $i) {
                            $self->{keystates}{$i} = 0;
                        }
                    }
                }
            }
            $num = SDLK_LEFT;
            if ($self->{keystates}{$num}) {
                # Left
            }
            $num = SDLK_RIGHT;
            if ($self->{keystates}{$num}) {
                # Right
            }
            $num = SDLK_UP;
            if ($self->{keystates}{$num}) {
                # Up
            }
            $num = SDLK_DOWN;
            if ($self->{keystates}{$num}) {
                # Down
            }
            $num = SDLK_q;
            if ($self->{keystates}{$num}) {
                # 'q' pressed.
                return "quit";
            }
            return 0;
        }
    }

It is also possible to get non-blocking keyboard input in example (a) by setting:

SDL::Events::enable_key_repeat(1, 30);

But to process several key presses at once, construction (b) is still needed.


4. The First Example Once More, But With Changes

Let's rewrite the first example script above in a way, that you can move the circle around on the screen with the cursor keys, and that you can press multiple keys at once, so that you can also go diagonally.

In the first example, I filled the circle-surface's background with a black color, so that you could see, that Surface objects are in the form of a rectangle. This time, I leave that step out. SDLx makes the background of surfaces transparent by default it seems (which is a reasonable choice in my opinion). Therefore, only the red circle will be visible (which would probably be, what you wanted in a game).

As the code of the second example is a bit longer, I put it on my GitHub page.
If you want to download it, you have to go a few directories up, to here. There you can download the whole "project", using the button labeled "Code".


5. Displaying Text

Displaying Text in a SDL_Perl game by rendering a given TrueType font is rather straightforward (script on GitHub):

#!/usr/bin/perl

use warnings;
use strict;

# SDL_perl Example 3

# Displaying text by rendering a font.

# Copyright (C) 2021 hlubenow
# License: GNU GPL 3.

use SDLx::App;
use SDLx::Text;
use SDL::Event;

my $FONTFILE = "FreeSans.ttf";

if (! -e $FONTFILE) {
    print "\nError: Font-file '$FONTFILE' not found in the script directory.\n";
    print "       '$FONTFILE' can be downloaded at:\n";
    print "       http://ftp.gnu.org/gnu/freefont/freefont-ttf.zip\n\n";
    exit 1;
}

SDL::putenv("SDL_VIDEO_WINDOW_POS=320,100");
my $app  = SDLx::App->new(w => 640,
                          h => 480,
                          title => "Text example",
                          eoq   => 1);
my $event = SDL::Event->new();

my $text = SDLx::Text->new(font  => $FONTFILE,
                           color => [220, 50, 50, 255],
                           size    => 32, 
                           h_align => 'center');

while (1) {
    processEvents($event);
    $text->write_xy($app, 300, 200, "Hello World!");
    $app->flip();
}

sub processEvents {
    my $event = shift;
    SDL::Events::pump_events();
    if (SDL::Events::poll_event($event)) {
        if ($event->type == SDL_KEYDOWN ) {
            if ($event->key_sym == SDLK_q) {
                exit;
            }
        }
    }
}

The question is more, where to get good TrueType fonts for your game without getting into copyright trouble.
For the examples on this page, I used the font "FreeSans" (in the file "FreeSans.ttf") from the GNU FreeFont package, which can be downloaded here. So that is easily available and should be safe to use. (Although "FreeSans" is a (very good) general purpose font, that's not really made for being used in games.)


6. Controlling the Speed

To control the general speed of the application, "SDLx::App" provides a useful mechanism, that is described in the next chapter.
This chapter deals with an older method, that is probably deprecated. You may skip to the next chapter.

One (older) way to control the application's speed would be to use "SDLx::FPS". You can then make the application run at a certain "fps" ("frames per second"), like in the following example:

#!/usr/bin/perl

use warnings;
use strict;

use SDLx::App;
use SDLx::Surface;
use SDLx::Rect;
use SDLx::FPS;

package Main {

    sub new { ... }

    sub start {
        $self->{app} = SDLx::App->new( ... );
        ...
        $self->{fps} = SDLx::FPS->new(fps => 50);
        while (1) {
            $self->{fps}->delay();
            ...
            $self->{screen}->flip(); # or "update()"
        }
    }
}

A more detailed speed control is needed, when some objects on the screen are supposed to move faster or slower than others. Which is probably the case in almost every game.
"$self->{app}->ticks()" returns the number of milliseconds that have passed, since the application started. With this, you can set up a delay time in a certain function, as shown in this example:

#!/usr/bin/perl

use warnings;
use strict;

use SDLx::App;
use SDLx::Surface;
use SDLx::Rect;
use SDLx::FPS;

my $SPEEDSETTING = 50;

package Spaceship {

    sub new {
        my $self = {...
                    timer     => shift,
                    delaytime => shift};
        return bless($self, $classname);
    }

    sub move {
        my ($self, $currenttime) = @_;
        if ($currenttime - $self->{timer} <= $self->{delaytime}) {
            return;
        }
        # Doing something slowed down:
        ...
        $self->{rect}->x($self->{rect}->x + 1);
        ...
        # This line is necessary at the end to make it work:
        $self->{timer} = $currenttime;
    }

    sub draw {
        my ($self, $screen) = @_;
        ...
    }
}

package Main {

    sub new { ... }    

    sub start {
        my $self = shift;
        $self->{app} = SDLx::App->new( ... );
        $self->{fps} = SDLx::FPS->new(fps => 50);
        $self->{spaceship} = Spaceship->new(...,
                                            $self->{app}->ticks(),
                                            $SPEEDSETTING * 2);
        while (1) {
            $self->{fps}->delay();
            $self->{timer} = $self->{app}->ticks();
            ...
            $self->{spaceship}->move($self->{timer});
            $self->{spaceship}->draw($self->{screen});
            $self->{screen}->flip(); # or "update()"
        }
    }
    ...
}

So the timer of the class is updated by the timer of the main loop, each time the delaytime has been reached.

It's a good idea to use a global variable, to which all "$self->{delaytime}" variables relate, like "$SPEEDSETTING" in the example above. This way, the speed of all objects can be set in relation to one global application speed.
With the newer method of SDLx::App, described in the following chapter, you just store the individual speeds of each game object. That is a more elegant (and probably more reliable) solution.


7. Doing Things More the SDLx-Way (Including Keyboard Input)

What I explained up to now works, so I'll leave the first chapters as they are. But I found, that in SDLx, you're supposed to write code a bit differently. And it makes sense to me, so I'll follow that route.

The reason for the changes is the control of speed of the application in general and of single objects (see above). When you use a FPS-limiter like "SDLx::FPS", the game will take more ressources from the system than needed. So people thought about a solution to make the game run at the same speed on all PCs without slowing down fast systems. In "SDLx::Controller" an "industry-proven standard" (as its perldoc states) was created for controlling the application speed independently from the FPS.
"SDLx::Controller" is automatically loaded, when a "SDLx::App" object is created.

To make it work, you're supposed to store the speed of your game objects. Separated into a positive or negative x-speed and a positive or negative y-speed. In the "Pong" example of the SDL_Manual these are called "v_x" and "v_y", as "velocity" just means the same as "speed" (it's just about speed, not about acceleration).
And for the main loop, you're supposed to use callback handlers. Instead of writing your own "while (1) { ... }"-loop, you'll set up three types of handlers and then call "$self->{app}-run();". The handlers are set up like this:

        $self->{app}->add_event_handler(sub {
                                              my ( $event, $app ) = @_; 
                                              $self->processEvents($event); }); 
        $self->{app}->add_move_handler( sub {
                                              my ( $step, $app ) = @_; 
                                              $self->{gameobject}->move($step) } );

        $self->{app}->add_show_handler( sub { $self->showScreen(); });

        $self->{app}->run();

Alright. That was a lot of theory. I created two example scripts, so you can see how it all comes together in practice. The

first example script

seems to do the same thing as the other one before: You can move a red circle around on a blue background using the cursor keys. But as you see, the code to achieve this is quite different.

At first, I wanted to use the keyboard input routine of the "SDLx Pong" example in the SDL manual. Although it can manage keys being pressed over a longer time, and also multiple key presses at once (making diagonal movement possible), it can become a bit unreliable, when left/right or up/down keys are pressed closely after another.
So I changed it to my own keyboard routine instead, which works a lot better for this. Let me just show again, how this keyboard routine is used together with the SDLx callback handlers:

#!/usr/bin/perl

use warnings;
use strict;

use SDLx::App;
use SDLx::Rect;

package GameWindow {
    ...
    use SDL::Event;
    ...
    sub start {
        ...
        $self->{keystates}    = ();
        $self->{keystatekeys} = [SDLK_LEFT, SDLK_RIGHT, SDLK_UP, SDLK_DOWN, SDLK_q];
        for my $i (@{ $self->{keystatekeys} }) {
            $self->{keystates}{$i} = 0;
        }
        $self->{pressed_x}    = 0;
        $self->{pressed_y}    = 0;

        $self->{gameobject}   = GameObject->new();

        $self->{app}->add_event_handler(sub {
                                              my ( $event, $app ) = @_;
                                              $self->processEvents($event); });
        $self->{app}->add_move_handler( sub {
                                              my ( $step, $app ) = @_;
                                              $self->{gameobject}->move($step) } );
        $self->{app}->add_show_handler( ... );
        $self->{app}->run();
    }

    ...

    sub processEvents {
        my ($self, $event) = @_; 
        my ($i, $num);
        if ($event->type == SDL_KEYDOWN) {
            for $i (@{ $self->{keystatekeys} }) {
                if ($event->key_sym == $i) {
                    $self->{keystates}{$i} = 1;
                }
            }
        }
        if ($event->type == SDL_KEYUP ) {
            for $i (@{ $self->{keystatekeys} }) {
                if ($event->key_sym == $i) {
                    $self->{keystates}{$i} = 0;
                }
            }
        }
        $self->{pressed_x} = 0;
        $self->{pressed_y} = 0;
        $num = SDLK_LEFT;
        if ($self->{keystates}{$num}) {
            $self->{pressed_x} = 1;
            $self->{gameobject}->startMoving("left");
        }
        $num = SDLK_RIGHT;
        if ($self->{keystates}{$num}) {
            $self->{pressed_x} = 1;
            $self->{gameobject}->startMoving("right");
        }
        $num = SDLK_UP;
        if ($self->{keystates}{$num}) {
            $self->{pressed_y} = 1;
            $self->{gameobject}->startMoving("up");
        }
        $num = SDLK_DOWN;
        if ($self->{keystates}{$num}) {
            $self->{pressed_y} = 1;
            $self->{gameobject}->startMoving("down");
        }
        $num = SDLK_q;
        if ($self->{keystates}{$num}) {
            $self->{app}->stop();
        }
        if ($self->{pressed_x} == 0) {
            $self->{gameobject}->stopMoving("x");
        }
        if ($self->{pressed_y} == 0) {
            $self->{gameobject}->stopMoving("y");
        }
    }   
}

package GameObject {

    sub new {
        my $classname = shift;
        my $self = {speedx => 0,
                    speedy => 0};
        $self->{speed} = 50;
        return bless($self, $classname);
    }    

    sub startMoving {
        my ($self, $direction) = @_;
        if ($direction eq "left") {
            $self->{speedx} = -$self->{speed};
        }
        if ($direction eq "right") {
            $self->{speedx} = $self->{speed};
        }
        if ($direction eq "up") {
            $self->{speedy} = -$self->{speed};
        }
        if ($direction eq "down") {
            $self->{speedy} = $self->{speed};
        }
    }

    sub stopMoving {
        my ($self, $axis) = @_;
        if ($axis eq "x") {
            $self->{speedx} = 0;
        } else {
            $self->{speedy} = 0;
        }
    }

    sub move {
        my ($self, $step) = @_;
        $self->{rect}->x(int($self->{rect}->x + $self->{speedx} * $step));
        $self->{rect}->y(int($self->{rect}->y + $self->{speedy} * $step));
    }

    sub draw {
        my ($self, $screen) = @_;
        $screen->blit_by($self->{surface}, undef, $self->{rect});
    }
}

my $app = GameWindow->new();
$app->start();

As a second script, I rewrote the "SDLx Pong" example of "SDL_Manual.pdf".
To this day (08-25-2021) there are a few problems with this example in the manual - which after all is the first game in SDLx, that is presented to the reader:

The reason for the last issue (being way too slow) may be a mistake in the manual: It says, the default value for "dt" is 0.01, but in fact it's 0.1 (so it's higher by factor 10). Accordingly, in the "SDLx Pong" example, "dt" is set to 0.02. Which is way too low, slowing down the application. That's not necessary.
My rewritten version of the "SDLx Pong" example can be found

here.

I didn't bother to fix the problem with the AI. May not be so easy to do.
The original example draws the objects to the screen directly each frame (using "->draw_rect()"). I changed that to a surface for each game object, that is blitted to the screen. That's the more common way of generating and moving sprites. So in my version each game object has its own class, its own "surface" and its own "rect", like already explained in the chapters above.
To be able to set the speeds of the game objects in the script directly and to be sure, that they are displayed that way on every user's system (because this is calculated by "SDLx::Controller"), is not only convenient. It's actually really nice.


8. Scrolling the Background

Scrolling the background is an essential programming technique, that is used in many 2D games. Think of classic games like "River Raid" or "Giana Sisters". The background is scrolled, while the player avatar is animated on a spot. This gives the illusion of movement.

To be scrolled, the background has to be a surface, that is larger than the screen surface. The screen then only displays a part of the background surface at a time. In this context, the background is also called the "stage".

The functions "->blit()" and "->blit_by()" of SDLx make it possible, to blit just a part of a surface onto another surface. So you can do something like this:

my $app                = SDLx::App->new(w => 640,
                                        h => 480);
my $screen             = SDLx::Surface::display();

# Way larger than $screen:
my $backgroundsurface  = SDLx::Surface->new(w => 8192,
                                            h => 1024);
# $arearect: x_position => 1024, 
#            y_position => 0,
#            width      => 640,
#            height     => 480:
my $arearect           = SDLx::Rect->new(1024, 0, 640, 480);
my $positionrect       = SDLx::Rect->new(0, 0, 0, 0);
$screen->blit_by($backgroundsurface , $arearect, $positionrect);

This would blit a part of 640x480 of the background (that's size is all in all 8192x1024) to the screen at [0, 0]. Where the background part that's displayed starts at [1024, 0] of the background.

Again, I created a

larger script

that shows, how this can be done in practice.
For the background, I drew 3x3 "checkerboards" of 9x9 tiles each onto the surface of an object called "map".

I encountered a problem (with perl_SDL) here: At first, I wanted to create a surface for each checkerboard, and then blit these 9 smaller surfaces onto the large background surface. It turned out, it wasn't possible to display the background surface then (with the other surfaces blitted onto it). I don't know why (in Pygame this is absolutely possible). I worked around this then, by drawing everything right onto the large background surface. This worked.

Well, this has become already a more complex script. I'm quite pleased with the result, although there sometimes is still a little flickering during the movement. But the scrolling technique should be good enough for my games.

To be continued ...



Email: hlubenow2 {at-symbol} gmx.net
Back