PVG
9. Getting Hurt and Safe Position — Program Video Games

9. Getting Hurt and Safe Position

[[programvideogames]]In this lecture we're adding damage and a "safe zone".

The safe zone is somewhere safe to be transported to when the player hit spikes.

Add three new fields to the Entity struct.

health:                     int,
max_health:                 int,
on_hit_damage:              int,

Add this flag to Entity_Flags - it'll make sense later.

Entity_Flags :: enum {
    // other flags remain the same
    Immortal,
}

First, we'll tackle hurting the player entering enemy colliders.

I've put this proc in main.odin for now but we'll probably want to create a player.odin as we build out more player code.

player_on_enter :: proc(self_id, other_id: Entity_Id) {
    player := entity_get(self_id)
    other := entity_get(other_id)

    if other.on_hit_damage > 0 {
        player.health -= other.on_hit_damage
    }
}

Before heading down to attach this callback to the player, let's move the player_id variable into Game_State so we can easily access it from anywhere. This will be handy shortly.

Game_State {
    player_id: Entity_Id,
    safe_position:         Vec2,
    safe_reset_timer:      f32,
    player_uncontrollable: bool,
    // other fields remain the same
}

We'll also add some convenience constants now, as well as a safe reset time, which is how long the player is uncontrollable for:

UP :: Vec2{0, -1}
RIGHT :: Vec2{1, 0}
DOWN :: Vec2{0, 1}
LEFT :: Vec2{-1, 0}

PLAYER_SAFE_RESET_TIME :: 1

Delete the player_id: Entity_Id line in main and now we can update the loading code.

case 'P':
    gs.player_id = entity_create( // Assign to gs.player_id
        {
            // other fields remain the same
            on_enter = player_on_enter,
            health = 5,
            max_health = 5,
        },
    )
case '^':
    id := entity_create(
        Entity {
            // other fields remain the same
            flags = {.Kinematic, .Debug_Draw, .Immortal}, // new flag
            on_hit_damage = 1,
        },
    )
    gs.spikes[id] = .Up

Make sure to add the same Immortal flag and on_hit_damage field to the other spikes.

Just after we update dt, we want to tick down this timer:

// Start of the main loop
dt := rl.GetFrameTime()

gs.safe_reset_timer -= dt

if gs.safe_reset_timer <= 0 {
    gs.player_uncontrollable = false
}

Then we wrap our player input in an if statement:

if !gs.player_uncontrollable {
    input_x: f32
    if rl.IsKeyDown(.T) do input_x += 1
    if rl.IsKeyDown(.R) do input_x -= 1

    if rl.IsKeyPressed(.SPACE) && .Grounded in player.flags {
        player.vel.y = -player.jump_force
        player.flags -= {.Grounded}
    }

    player.vel.x = input_x * player.move_speed
}

We'll add this to our update procs in main.odin

entity_update(gs.entities[:], dt)
physics_update(gs.entities[:], gs.solid_tiles[:], dt)
behavior_update(gs.entities[:], gs.solid_tiles[:], dt)

We aren't using dt yet but it will be useful later when we do animations.

Now just after those proc calls we are adding a pretty big chunk of code:

if .Grounded in player.flags {
    pos := Vec2{player.x, player.y}
    size := Vec2{player.width, player.height}

    targets := make([dynamic]Rect, context.temp_allocator)
    for e, i in gs.entities {
        if Entity_Id(i) == player_id do continue
        if .Dead not_in e.flags {
            append(&targets, e.collider)
        }
    }

    safety_check: {
        _, hit_ground_left := raycast(pos + {0, size.y}, DOWN * 2, gs.solid_tiles[:])
        if !hit_ground_left do break safety_check

        _, hit_ground_right := raycast(pos + size, DOWN * 2, gs.solid_tiles[:])
        if !hit_ground_right do break safety_check

        _, hit_entity_left := raycast(pos, LEFT * TILE_SIZE, targets[:])
        if hit_entity_left do break safety_check

        _, hit_entity_right := raycast(pos + {size.x, 0}, RIGHT * TILE_SIZE, targets[:])
        if hit_entity_right do break safety_check

        gs.safe_position = pos
    }
}

Down where we debug draw our entities, we'll add a new check for the Dead flag and draw the safe position as a blue rectangle.

for e in gs.entities {
    if .Debug_Draw in e.flags && .Dead not_in e.flags {
        rl.DrawRectangleLinesEx(e.collider, 1, e.debug_color)
    }
}

debug_draw_rect(gs.safe_position, {player.width, player.height}, 1, rl.BLUE)

To get our safe position working, we add this to the spike_on_enter callback.

if other_id == gs.player_id {
    other.x = gs.safe_position.x
    other.y = gs.safe_position.y
    other.vel = 0
    gs.safe_reset_timer = PLAYER_SAFE_RESET_TIME
    gs.player_uncontrollable = true
}

Finally, to add dying, we'll want to update entity.odin with a new proc.

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

That's it!