Writing games in Perl - Part 3 - Collision detection

| No Comments | No TrackBacks

Following
posts 1
and 2
on the subject of writing games in Perl, now we are going to add
colision.

The idea is quite simple, we are going to add another square to the
game, and when the ball hits it, it will change direction. Following
the way we were working, I'm going to add another object, called
Wall.

The first thing is modelling our wall, which will be a rectangle,
so it has the following attributes.


package Wall;
use Moose;
use Util;
use SDL::Rect;

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

# Width and height
has width => (is => 'rw', isa => 'Num', default => 0.005);
has height => (is => 'rw', isa => 'Num', default => 0.4);

Unlike the ball, a wall doesn't move, so we don't need a time_lapse
method, but we still have the get_rect and draw methods.


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

my $inverted_v = ($height - ($self->pos_v + $self->height));

my $x = Util::m2px( $self->pos_h );
my $y = Util::m2px( $inverted_v );
my $h = Util::m2px( $self->height );
my $w = Util::m2px( $self->width );

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 );
}

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

See
the first
post
for more details on the get_rect and draw codes.

Now we need to add our wall to the game, that will mean a simple
change in our main code, first we need to load the Wall module, then
initialize the Wall just after initializing the ball, and finally
calling the draw method just after calling the same method on
ball.


use Wall;


my $wall = Wall->new;


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

If you tried to run the code at this point, you'll notice you won't
see any wall. That happens because the application is only updating
the screen where the ball is passing. The Wall needs to be drawn a
first time, and the screen needs to be updated at that position. This
prevents us from re-updating the wall rect everytime, which is
pointless, since the wall is static - that code goes right before the
main loop.


# let's draw the wall for the first time.
$wall->draw($app, $height, $width);
SDL::Video::update_rects
( $app,
$wall->get_rect($height, $width) );

Now we need to check for a collision. This should happen in the
place of the time_lapse call. Note that while I neglected math in the
movement part, here it's more complicated because I need to react in a
reasonable manner depending on how the collision happened. But as
we're working in Perl and we have CPAN, I can just use Collision::2D
(zpmorgan++ for working on this and pointing me in the correct
direction)

If you don't have the Collision::2D module installed, just call


# cpan Collision::2D

If you're not sure wether you have it or not, just try installing
it anyway, it will suceed if the module is already installed.


use Collision::2D ':all';
sub collide {
my ($ball, $wall, $time) = @_;
my $rect = hash2rect({ x => $wall->pos_h, y => $wall->pos_v,
h => $wall->height, w => $wall->width });
my $circ = hash2circle({ x => $ball->cen_h, y => $ball->cen_v,
radius => $ball->radius,
xv => $ball->vel_h,
yv => $ball->vel_v });
return dynamic_collision($circ, $rect, interval => $time);
}

I assumed an API that wasn't currently implemented in our Ball
object, so I changed the ball so that pos_v, pos_h, width and height
return the bounding dimensions for the ball I won't put the code in
the post, but you can check at
the github repo.

Okay, now it's time to check for collisions and act
accordingly. Again, we'll assume an 100% efficient collision, so the
code looks like:


my $frame_elapsed_time = ($now - $oldtime)/1000;
if (my $coll = Util::collide($ball, $wall, $frame_elapsed_time)) {
# need to place the ball in the result after the bounce given
# the time elapsed after the collision.
my $collision_remaining_time = $frame_elapsed_time - $coll->time;
my $movement_before_collision_h = $ball->vel_h * $coll->time;
my $movement_before_collision_v = $ball->vel_v * $coll->time;
my $movement_after_collision_h = $ball->vel_h * $collision_remaining_time;
my $movement_after_collision_v = $ball->vel_v * $collision_remaining_time;
if ($coll->axis eq 'x') {
$ball->cen_h(($ball->cen_h + $movement_before_collision_h) +
($movement_after_collision_h * -1));
$ball->cen_v($ball->cen_v +
$movement_before_collision_v +
$movement_after_collision_v);
$ball->vel_h($ball->vel_h * -1);
} elsif ($coll->axis eq 'y') {
$ball->cen_v(($ball->cen_v + $movement_before_collision_v) +
($movement_after_collision_v * -1));
$ball->cen_h($ball->cen_h +
$movement_before_collision_h +
$movement_after_collision_h);
$ball->vel_v($ball->vel_v * -1);
} elsif (ref $coll->axis eq 'ARRAY') {
my ($xv, $yv) = @{$coll->bounce_vector};
$ball->cen_h(($ball->cen_h + $movement_before_collision_h) +
($xv * $collision_remaining_time));
$ball->vel_h($xv);
$ball->cen_v(($ball->cen_v + $movement_before_collision_v) +
($yv * $collision_remaining_time));
$ball->vel_v($yv);
} else {
warn 'BAD BALL!';
$ball->cen_h(($ball->cen_h + $movement_before_collision_h) +
($movement_after_collision_h * -1));
$ball->cen_v(($ball->cen_v + $movement_before_collision_v) +
($movement_after_collision_v * -1));
$ball->vel_h($ball->vel_h * -1);
$ball->vel_v($ball->vel_v * -1);
}
} else {
$ball->time_lapse($oldtime, $now, $height, $width);
}

Okay, the above code was a bit complicated, let's brake it down...


my $frame_elapsed_time = ($now - $oldtime)/1000;

Collision::2D works with time in seconds, it calculates if the two
objects would have collided during the duration of this frame.


if (my $coll = Util::collide($ball, $wall, $frame_elapsed_time)) {
...
} else {
$ball->time_lapse($oldtime, $now, $height, $width);
}

Now we check if there was a collision. If not, we just proceed to
the regular code that calculates the new position for the ball.


my $collision_remaining_time = $frame_elapsed_time - $coll->time;
my $movement_before_collision_h = $ball->vel_h * $coll->time;
my $movement_before_collision_v = $ball->vel_v * $coll->time;
my $movement_after_collision_h = $ball->vel_h * $collision_remaining_time;
my $movement_after_collision_v = $ball->vel_v * $collision_remaining_time;

In the case we have a collision, Collision::2D tells us when and
how it happened. In order to implement the bouncing, I also calculate
how far they would have been proceeded before and after the collision.



if ($coll->axis eq 'x') {
...
} elsif ($coll->axis eq 'y') {
...
} elsif (ref $coll->axis eq 'ARRAY') {
...
} else {
...
}

The method that describes how the collision happened is "axis". If
it was a purely horizontal colision, it will return 'x', if it was
purely vertical, it will return 'y', if it was mixed, it will return a
vector that describes it. In the case of a bug, it will return undef.


$ball->cen_h(($ball->cen_h + $movement_before_collision_h) +
($movement_after_collision_h * -1));
$ball->cen_v($ball->cen_v +
$movement_before_collision_v +
$movement_after_collision_v);
$ball->vel_h($ball->vel_h * -1);

In the case of perfect horizontal or vertical collision (or bug),
we reposition the ball by first calculating where it would be at the
time of the collision and then bounce it away - depending on how the
collision occurred.


my ($xv, $yv) = @{$coll->bounce_vector};
$ball->cen_h(($ball->cen_h + $movement_before_collision_h) +
($xv * $collision_remaining_time));
$ball->vel_h($xv);
$ball->cen_v(($ball->cen_v + $movement_before_collision_v) +
($yv * $collision_remaining_time));
$ball->vel_v($yv);

This last part of the code uses a cool feature for Collision::2D,
which returns a bounce information for that collision, which we then
use to figure out the correct position after the bounce.

And now we can run our code. I have made some other changes not
explained here, because they are just settings that control the
behavior. Remember to access
the github repo for
more details.

Now a small video of the game running.

No TrackBacks

TrackBack URL: http://daniel.ruoso.com/cgi-bin/mt/mt-tb.cgi/161

Leave a comment

About this Entry

This page contains a single entry by Daniel Ruoso published on March 20, 2010 11:03 PM.

Introducing the Null CMS [Perl] was the previous entry in this blog.

Writing Games in Perl - Part 4 - Implementing a Camera is the next entry in this blog.

Find recent content on the main index or look in the archives to find all content.