1 Feb 2010 ruoso   » (Journeyer)

Writing games in Perl - part 1 - Bouncing Ball

I've been saying I will write a lengthy description of my experiments with SDL in Perl for a while, today kthakore uploaded a SDL experimental version with all the code involved in this experiments. So I decided to start, but I decided to make it a serial post. I'm still unsure about how the format will look like for the rest of the posts, but I certainly would love any comment with ideas of where I should go next.

So at the first post, I'll try to make a very simple application: "bouncing ball". It is simply a ball bouncing in the screen, where the left and right cursors will accelerate them to each side.

Starting the environment

Well, at first you need version 2.3_5 or greater of the SDL module available here. Building the SDL perl binding will require the headers for most of the sdl related libraries. If you're in a Debian machine (or most debian-based distros) you can simply install the regular version of the SDL perl bindings. You can also do:

# apt-get build-dep libsdl-perl

This line requires you to have a deb-src line in your /etc/apt/sources.list. Mine looks like:

deb-src http://ftp.br.debian.org/debian lenny main

In the case you change your sources.list file, remember you need to call apt-get update for it to get the new info.

After that you can simply download SDL 2.3_5 from CPAN and follow the README instructions, which basically means (note that this version is experimental so you might notice some failing tests):

# perl Build.PL
# ./Build
# ./Build test
# ./Build install

You might setup a local::lib if you still want to run other applications that require the older and stable version of SDL (yes, there are a lot of API changes). I'm not a Windows user, but it has been reported that it works like charm when using Strawberry Perl.

If everything is fine, you should be able to get "2.35" when you do:

$ perl -MSDL -E 'say $SDL::VERSION'

You might always get into #sdl@irc.perl.org if you run into trouble.

The way I write games

Alright, before getting into the technicalities of how to write a game with SDL in Perl, let's think a bit on the mechanics of how a game works.

The first thing to realize is that a game needs to simulate some universe, and that this universe needs to have some universal rules like the physics you apply. For instance, it is very important that you decide, from the beggining, if you're going to have gravity in your game.

The second thing to realize is that, even if we have the impression that the time is a continuum, time is actually a sequence of events, like the shutter of a camera in burst mode (okay, for the not interested in photography, think in stop motion). That basically means: in frame A the ball was at position 10,10 and in frame B it was at 20,30. There's no in-between, you don't have to worry about it. You might be wondering if a collision might be lost in that move, but the point is, if the machine can't evaluate enough frames per second as to avoid that, it probably wouldn't be able to evaluate a more ellaborated calculation of the tragetories to see if they would have collided.

This provides an important simplification on how to think your game model. In this first example I'm going to overlook the modelling for interaction with different objects, since we have just a bouncing ball.

One last thing before we go on. You might be tempted to use pixels as your measuring unit, there's one important aspect to keep in mind. A Pixel is an integer value, which means that you'll need to do roundings for each frame. And if you need to store it as a integer value, you're going to accumulate imprecision, which might lead to weird effects, specially when the fps rate changes dramatically. My suggestion is to stick with the good old international measuring system and just use meters, a simple calculation can covert from/to pixels.

package Util;
use strict;
use warnings;
our $DPI = 96; # I think there's a sane way to fetch this value
sub m2px { int(((shift) * ($DPI / 0.0254)) + 0.5) }
sub px2m { (shift) / ($DPI / .0254) }

Modelling the ball

I think there probably isn't a better fit for Object Orientation than games, since you're actually dealing with simulated objects, and the most obvious choice is to use object orientation to work with it. So that's the attributes I can think right now to our object.

package Ball;
use Moose;

use constant g => 1.5;
use Util;
use SDL::Rect;

# how much space does it take
has radius => (is => 'rw', isa => 'Num', default => 0.005);

# Position - vertical and horizontal
has pos_v => (is => 'rw', isa => 'Num', default => 0.1);
has pos_h => (is => 'rw', isa => 'Num', default => 0.04);

# Velocty - vertical and horizontal
has vel_v => (is => 'rw', isa => 'Num', default => 0);
has vel_h => (is => 'rw', isa => 'Num', default => 0);

# Current acceleration - vertical and horizontal
# gravity is added later
has acc_v => (is => 'rw', isa => 'Num', default => 0);
has acc_h => (is => 'rw', isa => 'Num', default => 0);

With our virtual ball defined, we need to implement the simulation of time. And again, we need to think that time is not a continuum, so what we do is providing a "time_lapse" method that will recalculate the attributes of our object according to the ammount of time past.

sub time_lapse {
  my ($self, $old_time, $new_time, $height, $width) = @_;
  my $elapsed = ($new_time - $old_time)/1000; # convert to seconds...

  # now simple mechanics...
  $self->vel_h( $self->vel_h + $self->acc_h * $elapsed );
  # and add gravity for vertical velocity.
  $self->vel_v( $self->vel_v + ($self->acc_v - g) * $elapsed );

  # finally get the new position
  $self->pos_v( $self->pos_v + $self->vel_v * $elapsed );
  $self->pos_h( $self->pos_h + $self->vel_h * $elapsed );

  # this ball is supposed to bounce, so let's check $width and $height
  # if we're out of bounds, we assume a 100% efficient bounce.
  if ($self->pos_v pos_v($self->pos_v * -1);
    $self->vel_v($self->vel_v * -1);
  } elsif ($self->pos_v > $height) {
    $self->pos_v($height - ($self->pos_v - $height));
    $self->vel_v($self->vel_v * -1);
  }

  if ($self->pos_h pos_h($self->pos_h * -1);
    $self->vel_h($self->vel_h * -1);
  } elsif ($self->pos_h > $width) {
    $self->pos_h($width - ($self->pos_h - $width));
    $self->vel_h($self->vel_h * -1);
  }


}

From the simulated universe to pixels

You probably noticed that we haven't talked much about the game development per se, so let's start thinking about it

The first difference from the simulated universe and the actual game is the coordinate system. In our universe the vertical position 0 is near the floor, but in the screen, the vertical position 0 is at the top of the screen.

The other difference, which I already mentioned, is that our measures are in meters, and the screen is measured in pixels. The SDL library provides an important class to interact with this issue, the SDL::Rect.

That way, we're going to have a method to return a rect - but we need to remember to only return a rect that is inside the expected bounds, otherwise SDL will throw an exception.

sub get_rect {
  my ($self, $height, $width) = @_;

  my $inverted_v = $height - $self->pos_v;

  my $x = Util::m2px( $self->pos_h - $self->radius );
  my $y = Util::m2px( $inverted_v - $self->radius );
  my $h = Util::m2px( $self->radius * 2 );
  my $w = Util::m2px( $self->radius * 2 );

  my $screen_w = Util::m2px( $width );
  my $screen_h = Util::m2px( $height );

  if ($x  $screen_w) {
    $w -= ($x + $w) - $screen_w;
  }

  if ($y  $screen_h) {
    $h -= ($y + $h) - $screen_h;
  }

  return SDL::Rect->new( $x, $y, $w, $h );
}

Painting

The last part is actually drawing the ball, which involves painting the ball in the correct place. In our first version, our ball will be a square, since it's the most primitive drawing we have and I don't want to get into that specifics.

my $color;
sub draw {
  my ($self, $surface, $height, $width) = @_;
  unless ($color) {
    $color = SDL::Video::map_RGB
      ( $surface->format(),
        0, 0, 255 ); # blue
  }
  SDL::Video::fill_rect
      ( $surface,
        $self->get_rect($height, $width),
        $color );
}

Yeah, I know, it's an ugly ball, but it bounces... ;)

Putting the pieces together

Now we just need the actual application to use our Ball. Note that the cycle is basically:

  • Listen for events
  • Evaluete the time_lapse
  • Draw the background
  • Draw the ball
  • Update the parts of the screen that where changed
  • Ask for a delay to keep the desired fps rate

Note that I could have asked for it to update the entire screen instead of only the parts that were changed, but that particular operation is actually the most expensive thing you can have in your app, so we try to update as few parts of the screen as possible.

#!/usr/bin/perl

use 5.10.0;
use strict;
use warnings;

use SDL;
use SDL::Video;
use SDL::App;
use SDL::Events;
use SDL::Event;
use SDL::Time;

use lib 'lib';
use Util;
use Ball;

my $ball = Ball->new;
my $width = 0.2;
my $height = 0.15;
my $fps = 60;

my $app = SDL::App->new
  ( -title => 'Bouncing Ball',
    -width => Util::m2px($width),
    -height => Util::m2px($height));

my $black = SDL::Video::map_RGB
  ( $app->format(),
    0, 0, 0 ); # black

my $event = SDL::Event->new();
my $time = SDL::get_ticks;
my $app_rect = SDL::Rect->new(0, 0, $app->w, $app->h);
my $ball_rect = $ball->get_rect($height, $width);

while (1) {
  my $oldtime = $time;
  my $now = SDL::get_ticks;

  while (SDL::Events::poll_event($event)) {
    exit if $event->type == SDL_QUIT;
  }

  my $old_ball_rect = $ball_rect;

  $ball->time_lapse($oldtime, $now, $height, $width);

  $ball_rect = $ball->get_rect($height, $width);

  SDL::Video::fill_rect
      ( $app,
        $app_rect,
        $black );

  $ball->draw($app, $height, $width);

  SDL::Video::update_rects
      ( $app,
        $old_ball_rect, $ball_rect );

  $time = SDL::get_ticks;
  if (($time - $oldtime) < (1000/$fps)) {
    SDL::delay((1000/$fps) - ($time - $oldtime));
  }
}

Syndicated 2010-02-01 12:54:20 from Daniel Ruoso

Latest blog entries     Older blog entries

New Advogato Features

New HTML Parser: The long-awaited libxml2 based HTML parser code is live. It needs further work but already handles most markup better than the original parser.

Keep up with the latest Advogato features by reading the Advogato status blog.

If you're a C programmer with some spare time, take a look at the mod_virgule project page and help us with one of the tasks on the ToDo list!