PVG
12. Attacking — Program Video Games

12. Attacking

[[programvideogames]]Let's get the attack working.

We're going to need two new features to get this working in a reasonable way.

  1. Non-looping Animations
  2. Animation Events

Let's start with Non-looping Animations.

Update the animation type, creating a new Animation_Flags and Animation_Event type:

Animation :: struct {
    size:         Vec2,
    offset:       Vec2,
    start:        int,
    end:          int,
    row:          int,
    time:         f32,
    flags:        bit_set[Animation_Flags],
    on_finish:    proc(gs: ^Game_State, entity: ^Entity),
    timed_events: [dynamic]Animation_Event,
}

Animation_Flags :: enum {
    // Default is to play once then stop
    Loop,
    Ping_Pong, // Loop + Ping_Pong will play forwards, then backwards, then forwards...
}

Animation_Event :: struct {
    timer:    f32,
    duration: f32,
    callback: proc(gs: ^Game_State, entity: ^Entity),
}

We'll make our Animations stop by default, which means we need to update the animation code in src/entity.odin:

entity_update :: proc(gs: ^Game_State, dt: f32) {
    for &e in gs.entities {
        if e.health == 0 && .Immortal not_in e.flags {
            e.flags += {.Dead}
        }

        if len(e.animations) > 0 {
            anim := e.animations[e.current_anim_name]

            // Switch frames?
            e.animation_timer -= dt
            if e.animation_timer <= 0 {
                e.current_anim_frame += 1
                e.animation_timer = anim.time

                if .Loop in anim.flags {
                    if e.current_anim_frame > anim.end {
                        e.current_anim_frame = anim.start
                    }
                } else {
                    if e.current_anim_frame > anim.end {
                        e.current_anim_frame -= 1
                        if anim.on_finish != nil {
                            anim.on_finish(gs, &e)
                        }
                    }
                }
            }

            // Events?
            for &event in anim.timed_events {
                if event.timer > 0 {
                    event.timer -= dt
                    if event.timer <= 0 {
                        event.callback(gs, &e)
                    }
                }
            }
        }
    }
}

With our new entity_update proc signature, we need to update the call site in src/main.odin:

player_update(&gs, dt)
entity_update(&gs, dt)
physics_update(gs.entities[:], gs.solid_tiles[:], dt)
// ...

We want to make sure our looping animations have the new flag:

player_anim_idle := Animation {
    // ...
    flags  = {.Loop},
}

player_anim_fall := Animation {
    // ...
    flags  = {.Loop},
}

player_anim_run := Animation {
    // ...
    flags  = {.Loop},
}

The Attack Animation will use the new callbacks:

    player_anim_attack := Animation {
        size         = {120, 80},
        offset       = {52, 42},
        start        = 0,
        end          = 3,
        row          = 3,
        time         = 0.15,
        on_finish    = player_on_finish_attack,
        timed_events = {{timer = 0.15, duration = 0.15, callback = player_attack_callback}},
    }

![[Pasted image 20240922120628.png]]

Don't forget to add Attack to the player loading code:

gs.player_id = entity_create(
    {
        // ...
        animations = {
            // ...
            "attack" = player_anim_attack,
        },
        current_anim_name = "idle",
    },
)

Now I've already figured out what I think is a pretty decent attack area, and it's using a circle.

Drawing that to the screen, where we were drawing the safe_position:

debug_draw_circle(
    {player.collider.x, player.collider.y} +
    {.Left in player.flags ? -30 + player.collider.width : 30, 20},
    25,
    rl.GREEN,
)

Before we jump into the player code, I'm going to assume we want an entity_damage proc. From experience this is one premature mini-abstraction thing that's good to have so we can add effects and stuff later.

entity_damage :: proc(id: Entity_Id, amount: int) {
    entity := entity_get(id)
    entity.health -= amount
    if entity.health <= 0 {
        entity.flags += {.Dead}
    }
}

While we are in src/entity.odin, let's update the animation switching code to restart event timers:

switch_animation :: proc(entity: ^Entity, name: string) {
    entity.current_anim_name = name
    anim := &entity.animations[name] // Get Pointer to reset event timers
    entity.animation_timer = anim.time
    entity.current_anim_frame = anim.start

    for &event in anim.timed_events {
        event.timer = event.duration
    }
}

Okay we are ready to update our player's code and add the Attack. In src/player.odin:

Player_Movement_State :: enum {
    Uncontrollable,
    Attacking, // I know, it seems weird. It'll make sense shortly
    Idle,
    Run,
    Jump,
    Fall,
}

player_update :: proc(gs: ^Game_State, dt: f32) {
    // ...

    case .Attacking:
        if .Grounded in player.flags {
            player.vel.x = 0
        }
    case .Idle:
        // ...
        try_attack(gs, player)
    case .Run:
        // ...
        try_attack(gs, player)
    case .Jump:
        // ...
        try_attack(gs, player)
    case .Fall:
        // ...
        try_attack(gs, player)
}
        
try_attack :: proc(gs: ^Game_State, player: ^Entity) {
    if rl.IsMouseButtonPressed(.LEFT) {
        switch_animation(player, "attack")
        gs.player_movement_state = .Attacking
    }
}

player_on_finish_attack :: proc(gs: ^Game_State, player: ^Entity) {
    switch_animation(player, "idle")
    gs.player_movement_state = .Fall
}

player_attack_callback :: proc(gs: ^Game_State, player: ^Entity) {
    center := Vec2{player.x, player.y}
    center += {.Left in player.flags ? -30 + player.collider.width : 30, 20}

    for &e, i in gs.entities {
        id := Entity_Id(i)
        if id == gs.player_id do continue
        if .Dead in e.flags do continue
        if .Immortal in e.flags do continue

        if rl.CheckCollisionCircleRec(center, 25, e.collider) {
            entity_damage(Entity_Id(i), 1)
        }
    }
}

Finally, I want to fix a little bug we had in src/behavior.odin:

for &e in entities {
    if .Dead in e.flags do continue // Don't run behavior code for dead entities
    // ...
}