fuzzix.org

Jaws was never my scene and I don't like Star Wars
Subscribe

Repliconz - dev log 2, Collisions Revisited, Sound

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.

Collision (or miss) illustration
The arrows coming from our things in Frame 1 (roughly) represent the vector they'll be travelling along. Between frames the bullet passes through the baddie but since collision detection occurs as "snapshots" of each frame, the baddie escapes, then mocks us.

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?

Youtube link

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.

by fuzzix on Tue, 28 Jan 2014 18:30.

Comments

Comment on this post

Text only, no HTML, * denotes a required field.

Name *
Email Address *
Website
Mystery box, leave it alone!