Repliconz - dev log 0 (Perl and SDL)

I posted this yesterday on my personal site and realised it might just act as a reasonable "getting started" type article for SDL with Perl. Further articles in the series probably won't have this level of detail, so I'll keep them on my own site. Anyway, on with the show.

So I decided to play around with Perl's SDL bindings. A game appears to be happening, so let's play around with this dev log idea too, documenting the process and pitfalls of making some stuff move around on screen.

It turns out you don't need to know a whole lot to make this happen, in the simplest cases at least. Let's see what we have so far:

Youtube link

OK, so we're not going to set the world alight just yet. Anyway, we have a guy and some bullets. We are missing enemies, scoring, action, pew pew noises and any incentive to play. These come later, I hope.

Controls are somewhere between Robotron 2084 or Smash TV (both made by the same legendary game developer) and Abuse. WASD / Arrows move the guy, aim and shoot with the mouse.

Perl's SDL distribution comes with a convenient extension called SDLx::App which is built on SDLx::Controller. This makes creating game loops simple. You set a delay between iterations, handler callbacks for the things a game loop usually contains (input events, drawing and so on) and then run it. Each callback gets a delta-T value (the time that has passed since the previous iteration) so you can tie this value to your physics model and get consistent movement even if you are occasionally starved of resources or change your frame rate (delay).

The three classes of handler we need to define are event, move and show. For those of you familiar with MVC style frameworks, these handlers are (very) roughly analogous: Model ~= move, View ~= show, Controller ~= event.

It should be noted that any number of callbacks for each handler type can be added, though this code currently has just one for each.

Event processing is triggered on input, mouse movements, container changes and so on. Our callback function receives an SDL::Event instance containing the queued events which need handling. Let's take a look at the event handler in Repliconz:

sub events {
    my ( $self, $event, $app ) = @_;
    return $app->stop if $event->type == SDL_QUIT;

    if ($event->type == SDL_KEYDOWN) {
        $self->{keys}->{$event->key_sym} = 1;
    }
    if ($event->type == SDL_KEYUP) {
        $self->{keys}->{$event->key_sym} = 0;
    }

    if ($event->type == SDL_MOUSEMOTION) {
        $self->{mouse}->{x} = $event->motion_x;
        $self->{mouse}->{y} = $event->motion_y;
    }

    if ($event->type == SDL_MOUSEBUTTONDOWN && $event->button_button == SDL_BUTTON_LEFT) {
            $self->{mouse}->{firing} = 1;
    }
    if ($event->type == SDL_MOUSEBUTTONUP && $event->button_button == SDL_BUTTON_LEFT) {
            $self->{mouse}->{firing} = 0;
    }
}

Simple enough, we store the key up/down states in $self->{keys} and mouse position / button state in $self->{mouse}. So what do we now that we know what our player is doing? We move...

How does our move handler look?

sub move {
    my ( $self, $dt, $app, $t ) = @_;
    my $v_x = 0;
    my $v_y = 0;
    my $bomb = 0;

    for (grep { $self->{keys}->{$_} } keys %{$self->{keys}}) {
        when ($self->{controls}->{keyboard}->{u}) { $v_y += -1 }
        when ($self->{controls}->{keyboard}->{d}) { $v_y += 1 }
        when ($self->{controls}->{keyboard}->{l}) { $v_x += -1 }
        when ($self->{controls}->{keyboard}->{r}) { $v_x += 1 }
        when ($self->{controls}->{keyboard}->{b}) { $bomb = 1 }
    }

    $self->{hero}->move( $v_x, $v_y, $dt, $app );

    $self->{hero}->self_destruct if ($bomb);

    $self->{hero}->shoot( $dt, $self->{mouse}->{x}, $self->{mouse}->{y} ) if ($self->{mouse}->{firing});

    for my $bullet (@{$self->{hero}->{bullets}}) { $bullet->move( $dt, $app ) }
}

There's a little more going on here. First we go through the keys which were set in the event handler and add 1 (since we end up manipulating this unit vector with an actual velocity later) to X/Y velocity for each control. Simply setting them to 1/-1 means that the hero will still move when opposite controls are being pressed, so we add them to prevent this: 1 and -1 being added will cancel each other out. Ignore the $bomb stuff, that's in the "notion" stage of development.

$self->{hero} is an instance of Game::Repliconz::Guy. Let's take a look at its move method:

sub move {
    my ( $self, $v_x, $v_y, $dt, $app ) = @_;

    ($v_x, $v_y) = constrain_velocity_xy($v_x, $v_y);

    $self->{x} += $v_x * $self->{velocity} * $dt;
    $self->{y} += $v_y * $self->{velocity} * $dt;

    ($self->{x} < 0) && ($self->{x} = 0);
    ($self->{x} > ($app->w - $self->{w})) && ($self->{x} = $app->w - $self->{w});
    ($self->{y} < 0) && ($self->{y} = 0);
    ($self->{y} > ($app->h - $self->{h})) && ($self->{y} = $app->h - $self->{h});
}

The moving itself is taken care of by setting our new coords to our unit vector coords multiplied by our velocity and delta-T, the time that has passed. This is the key to getting consistent movement even if timing / framerate changes. You can check this out by reducing the delay in app setup to effectively increase framerate. You can also manipulate $dt in your callbacks to speed up or slow down the action. For example, to add a slo-mo mode you could divide $dt by 2.

The last four lines reset our position if the guy tries to move outside the bounds of the play field. What is constrain_velocity_xy up to?

sub constrain_velocity_xy {
    my ( $v_x, $v_y ) = @_;

    ($v_y > 0) && ($v_y = 1);
    ($v_y < 0) && ($v_y = -1);
    ($v_x > 0) && ($v_x = 1);
    ($v_x < 0) && ($v_x = -1);

    # Moving diagonally, moderate eventual velocity
    if ( $v_y != 0 && $v_x != 0 ) {
        $v_y *= 0.707; # sin(45 degrees)
        $v_x *= 0.707;
    }

    return ($v_x, $v_y);
}

There are a couple of things going on here. The first is we reset the bounds of our unit vector set in the move handler callback. Since there are two sets of controls and we used an 'additive' approach to setting the movement, if you press two buttons for the same direction, you end up with twice the required vector length and, eventually, twice the velocity. If any direction is set, we reset it to 1 or -1.

The other thing is, if we are moving diagonally, our vector is no longer length 1, it's the length required to go from corner to corner of a square with sides of length 1. Pythagoras tells us this is ~1.41. To correct this and set our vector length back to 1, we reduce the coords of our vector end point to ~0.7 (sine of 45°) of their original value. This diagram on Mathematics For Blondes illustrates quite effectively why this works, I think.

If you watched the video above, you saw that the guy wasn't the only thing moving. We also have bullets. Bullets are instances of Game::Repliconz::Bullet, which are pushed in and out of a queue (currently arbitrarily limited to 20 items) in our instance of Game::Repliconz::Guy when shoot is triggered by the fire button:

sub shoot {
    my ( $self, $dt, $mouse_x, $mouse_y ) = @_;
    state $total_dt = 0;
    $total_dt += $dt;
    return unless ($total_dt >= $self->{cooling_time});
    $total_dt -= $self->{cooling_time};

    push @{$self->{bullets}}, Game::Repliconz::Bullet->new( {
        guy => $self,
        target_x => $mouse_x,
        target_y => $mouse_y
    });

    shift @{$self->{bullets}} if (scalar @{$self->{bullets}} > $self->{max_bullets});
}

We track time elapsed by adding delta-T values until our gun cooling time is reached. This allows us to easily control rate-of-fire at any stage, to implement a high ROF bonus by just reducing the cooling time, for example.

We tell the bullet about the guy and mouse position, but how do bullets aim in the mouse cursor direction? Another unit vector is created on instantiation, based on the guy's position and the mouse cursor position:

sub new {
    my ( $class, $opts ) = @_;

    $opts->{x} = $opts->{guy}->{x} + ($opts->{guy}->{w} / 2);
    $opts->{y} = $opts->{guy}->{y} + ($opts->{guy}->{h} / 2);
    $opts->{w} = 4;
    $opts->{h} = 4;

    # normalise vector : guy position -> target position
    $opts->{v_y} = $opts->{target_y} - $opts->{y};
    $opts->{v_x} = $opts->{target_x} - $opts->{x};
    my $v_len = sqrt($opts->{v_y} ** 2 + $opts->{v_x} ** 2);
    $opts->{v_y} /= $v_len;
    $opts->{v_x} /= $v_len;

    $opts->{colour} = 0xFFFFFFFF;
    $opts->{velocity} = 70;

    bless $opts, $class;
}

So we set the initial x/y of our bullet to be the centre of the guy, along with width and height for drawing later. We then make use of Pythagoras' theorem again to convert the difference between the start point and mouse position into a unit vector. If you consider the X and Y axes of our coords to be the sides of a right triangle, adding their squares will give us the length of the vector guy -> cursor. We just need to divide our coords by this value to wind up with a vector of length 1.

Once you have all this information, the move method becomes very simple:

sub move {
    my ( $self, $dt, $app ) = @_;

    $self->{x} += $self->{v_x} * $self->{velocity} * $dt;
    $self->{y} += $self->{v_y} * $self->{velocity} * $dt;
}

Now that we know where everything is (or is going to be), the next step is drawing. Our show handler is pretty naive, simply blanking the screen and calling draw for each of our objects:

sub show {
    my ( $self, $dt, $app ) = @_;
    SDL::Video::fill_rect( $app, SDL::Rect->new(0, 0, $app->w, $app->h), 0 );

    $self->{hero}->draw($app);
    for my $bullet (@{$self->{hero}->{bullets}}) { $bullet->draw($app) }

    $app->update();
}

The draw method is inherited from Game::Repliconz::Thing:

sub draw {
    my ( $self, $app ) = @_;
    $app->draw_rect( [ $self->{x}, $self->{y}, $self->{w}, $self->{h} ], $self->{colour} );
}

We simply draw a rect at x, y, of size w, h of the given colour for each instance directly onto a passed SDLx::App instance.

Doesn't get simpler than that, I think. I hope you found this interesting and/or useful. Comments, criticism and contributions welcome. You can check out the code for Repliconz on Github.

The Wolfire Games Blog features a couple of cool posts on linear algebra with a focus specifically on game development:

Linear algebra for game developers ~ part 1

Linear algebra for game developers ~ part 2

Leave a comment

About fuzzix

user-pic Software Developer (mostly Perl) in Dublin.