In part 1 we got some basic collision detection by checking for overlapping rectangles in each move call.
Using positions of Things at a given frame for collision detection presents a problem. If an object is small and fast moving, like a bullet, it will have fairly long vectors to travel and can appear to pass through objects if the delay between frames is long enough.
What we need is something that will calculate if two objects of given dimensions travelling along a given vector over a given time-frame will collide. As usual, CPAN provides. Collision::2D is a module which offers us this kind of continuous collision detection.
So what has changed? Well, we now keep track of the previous move to pass to
Collision::2D::dynamic_collision
as we also want to perform and show the move
(to avoid dodgy looking collision detection where things look like they hit when they merely
get close).
Game::Repliconz::Thing
now has a move
method which stores the parameters of
the move for later calculation, then applies it to x
and y
(performs the move).
sub move {
my ($self) = @_;
$self->last_x = $self->x;
$self->last_y = $self->y;
$self->last_dt = $self->dt;
$self->last_v_x = $self->v_x;
$self->last_v_y = $self->v_y;
$self->x += $self->v_x * $self->dt;
$self->y += $self->v_y * $self->dt;
}
The collision detection itself is now fairly simple:
sub collision {
my ( $self, $thing ) = @_;
return 0 if (!$self->alive || !$thing->alive);
my @rect = map { hash2rect( {
x => $_->last_x,
y => $_->last_y,
h => $_->h,
w => $_->w,
xv => $_->last_v_x,
yv => $_->last_v_y,
} ) } ( $self, $thing );
return dynamic_collision ($rect[0], $rect[1], interval => $self->last_dt);
}
sub check_collides_with {
my ( $self, $things ) = @_;
my $check_distance = 50;
for my $thing (@{$things}) {
next if (abs($self->x - $thing->x > $check_distance) ||
abs($self->y - $thing->y > $check_distance));
if ($self->collision( $thing )) {
$thing->hit;
$self->hit;
return 1;
}
}
}
The collision
function generates a pair of rectangles (rects) for use by Collision::2D.
We then check if a collision occurred at any time between the last frame and
the current one with dynamic_collision
.
We can then use check_collides_with
on our Things, pass in another set of
Things and find out if there was a hit. It only performs the expensive
detection if the objects in question are within a certain distance
So in our move
handler callback in Game::Repliconz
we simply:
$self->{hero}->check_collides_with($self->{baddies});
for my $bullet (@{$self->{hero}->{bullets}}) {
$bullet->check_collides_with($self->{baddies});
}
Then we can filter the lists of Things (bullets, baddies, whatever) by
their alive
method which simply checks remaining lives - bullets have
"lives" to end their path when they hit an enemy. You could make stronger
bullets as a bonus by modifying this value, of course. So how does this
all look?
Note: there was a small bug present in the code this video was captured from, so the collision might not be 100% represented here, but it's close enough.
If you watch this video, you might notice another addition - sound.
This is done pretty simply. There has been code present since the beginning to initialise the SDL audio subsystem and load up a few samples:
sub _init_audio {
my ( $opts ) = @_;
SDL::init(SDL_INIT_AUDIO);
if ( SDL::Mixer::open_audio( 44100, SDL::Constants::AUDIO_S16, 2, 4096) == 0 ) {
$opts->{audio} = 1;
}
else {
$opts->{audio} = 0;
carp "Audio disabled : " . SDL::get_error();
return 0;
}
SDL::Mixer::Channels::allocate_channels(4);
@{$opts->{samples}}{ qw/
bonus_sweeps
laser
explosion
/ } = (
SDL::Mixer::Samples::load_WAV("$opts->{working_dir}/sound/bonus_sweeps.wav"),
SDL::Mixer::Samples::load_WAV("$opts->{working_dir}/sound/laser.wav"),
SDL::Mixer::Samples::load_WAV("$opts->{working_dir}/sound/explosion.wav"),
)
}
This allocates four channels, which we can use for each event type we want to make noise for, shooting, explosions, bonuses and so on. Using a single channel means sounds won't "overlap", so for lasers and such it gives a classic "pew-pew-pewww" effect rather than an echoey/overlaid one. Hope I got my onomatopoeia right there.
The sample, channel and audio subsystem status are passed into the given Thing on instantiation.
$self->{hero} = Game::Repliconz::Guy->new( {
field_width => $self->{w},
field_height => $self->{h},
shoot_noise => $self->{samples}->{laser},
shoot_channel => 1,
audio => $self->{audio},
} );
...which then plays the sound when the given event occurs:
sub shoot {
...
SDL::Mixer::Channels::play_channel( $self->{shoot_channel}, $self->{shoot_noise}, 0 ) if $self->{audio};
...
}
The samples themselves were generated by sfxr, an excellent tool to generate old school FX for games. Each sample was created by hitting the "Randomize" button until a sound I liked played, though there is plenty of scope for controlling and fine tuning the sounds yourself.
That's it for now. As always, comments, criticism and contributions welcome.
Code for Repliconz is on Github.
Text only, no HTML, * denotes a required field.