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!