DEV Community

Cover image for Space Shooter Game with SDL2 and Odin - Part 8 - Enemies Attack!
Patrick O'Dacre
Patrick O'Dacre

Posted on

Space Shooter Game with SDL2 and Odin - Part 8 - Enemies Attack!

In this guide we'll go through the following steps:

  1. Create && Fire Drone Lasers
  2. Show Entity hitboxes

Create && Fire Drone Lasers

We'll create a fixed number of lasers for the drones to use. Much like with our own laser, a drone laser won't fire unless it is available -- health == 0.

We need to implement two separate timers -- one master timer, and another for each individual drone. These two timers help us control the number of drone lasers firing at the player.

We'll add a ready field to the Entity struct to control the laser fire-rate of individual drones, and the drone_laser_cooldown field to the Game struct as our master timer.


NUM_OF_DRONE_LASERS :: 5

Game :: struct
{
    /// Other stuff ^^^
    /// new
    drone_laser_tex: ^SDL.Texture,
    drone_lasers: [NUM_OF_DRONE_LASERS]Entity,
    drone_laser_cooldown : f64,
}

Entity :: struct
{
    source: SDL.Rect,
    dest: SDL.Rect,
    dx: f64,
    dy: f64,
    health: int,
    ready: f64,
}

create_entities :: proc()
{
    /// create drones ^^

    // drone lasers
    game.drone_laser_tex = SDL_Image.LoadTexture(game.renderer, "assets/drone_laser_1.png")
    assert(game.drone_laser_tex != nil, SDL.GetErrorString())
    drone_laser_w : i32
    drone_laser_h : i32
    SDL.QueryTexture(game.drone_laser_tex, nil, nil, &drone_laser_w, &drone_laser_h)

    for _, idx in 1..=NUM_OF_DRONE_LASERS
    {

        game.drone_lasers[idx] = Entity{
            dest = SDL.Rect{
                x = -100,
                y = -100,
                w = drone_laser_w / 8,
                h = drone_laser_h / 6,
            },
            dx = DRONE_LASER_SPEED,
            dy = DRONE_LASER_SPEED,
            health = 0,
        }
    }


    ///
}

Enter fullscreen mode Exit fullscreen mode

The drone lasers aren't shaped quite like I want, so I skew the image by changing the w and h a bit to get it as close to round as I can.

Fire Lasers

We'll allow drones to fire when within certain boundaries and the cooldown timer has expired:


if drone.dest.x > 30 &&
drone.dest.x < (WINDOW_WIDTH - 30) &&
drone.ready <= 0 &&
game.drone_laser_cooldown < 0
{
    // fire
}

Enter fullscreen mode Exit fullscreen mode

We check this each time we render a drone.

If the drone's individual timer has expired, ie: drone.ready < 0 then we'll look for the first available drone laser to "fire":

// find a drone laser:
fire_drone_laser : for laser, idx in &game.drone_lasers
{

    // find the first one available
    if laser.health == 0
    {
        // fire
    }
}

Enter fullscreen mode Exit fullscreen mode

Drones need to shoot towards our player, so we'll have to calculate a dx and dy value that will lead the laser from the position of the drone to the current position of the player:

// fire from the drone's position
laser.dest.x = drone.dest.x
laser.dest.y = drone.dest.y
laser.health = 1

new_dx, new_dy := calc_slope(
    laser.dest.x,
    laser.dest.y,
    game.player.dest.x,
    game.player.dest.y,
    )

if !game.is_paused
{
    laser.dx = new_dx * get_delta_motion(drone.dx + 150)
    laser.dy = new_dy * get_delta_motion(drone.dx + 150)
}

// reset the cooldown to prevent firing too rapidly
drone.ready = DRONE_LASER_COOLDOWN_TIMER_SINGLE
game.drone_laser_cooldown = DRONE_LASER_COOLDOWN_TIMER_ALL

Enter fullscreen mode Exit fullscreen mode

Of course, we're also careful to reset our timers.

Now, each time this active laser is rendered, it will move according to the calculated dx and dy:

// Render Drone Lasers -- check collisions -> render
for laser, idx in &game.drone_lasers
{

    if laser.health == 0 do continue

    // check collision based on previous frame's rendered position
    // check player health to make sure drone lasers don't explode
    // while we're rendering our stage_reset() scenes after a player
    // has already died.
    if game.player.health > 0
    {
        hit := collision(
            game.player.dest.x,
            game.player.dest.y,
            game.player.dest.w,
            game.player.dest.h,

            laser.dest.x,
            laser.dest.y,
            laser.dest.w,
            laser.dest.h,
            )

        if hit
        {
            laser.health = 0

            if !game.is_invincible
            {
                explode_player(&game.player)
            }
        }
    }

    if !game.is_paused
    {
        laser.dest.x += i32(laser.dx)
        laser.dest.y += i32(laser.dy)
    }

    // reset laser if it's offscreen
    // checking x and y b/c these drone
    // lasers go in different directions
    if laser.dest.x <= 0 ||
    laser.dest.x >= WINDOW_WIDTH ||
    laser.dest.y <= 0 ||
    laser.dest.y >= WINDOW_HEIGHT
    {
        laser.health = 0
    }

    if laser.health > 0
    {
        when HITBOXES_VISIBLE do render_hitbox(&laser.dest)
        SDL.RenderCopy(game.renderer, game.drone_laser_tex, &laser.source, &laser.dest)
    }

}

Enter fullscreen mode Exit fullscreen mode

Show Entity hitboxes

To help with visualizing how our collisions are working, let's add a little utility to outline our collision areas.

We'll add a flag that will be checked at compile time:


HITBOXES_VISIBLE :: false

Enter fullscreen mode Exit fullscreen mode

Next, we'll add this helper function to render a red rectangle at a given destination SDL.Rect:


render_hitbox :: proc(dest: ^SDL.Rect)
{
    r := SDL.Rect{ dest.x, dest.y, dest.w, dest.h }

    SDL.SetRenderDrawColor(game.renderer, 255, 0, 0, 255)
    SDL.RenderDrawRect(game.renderer, &r)
}

Enter fullscreen mode Exit fullscreen mode

Now we can pass in any entity SDL.Rect to get an outline of their collision boundaries:


if laser.health > 0
{
    when HITBOXES_VISIBLE do render_hitbox(&laser.dest)
    SDL.RenderCopy(game.renderer, game.laser_tex, nil, &laser.dest)
}

Enter fullscreen mode Exit fullscreen mode

Top comments (0)