20. Hazards and Obstacles
First, I'll cover the changes in main.odin.
Remove this import (if you still have it):
import "core:fmt"
We add a new field to the LDtk_Field_Instance_Value union:
LDtk_Field_Instance_Value :: union {
// ... other fields remain ...
string,
}
This allows for string values in LDtk field instances.
We update the Game_State struct:
// ... other fields remain ...
spikes: [dynamic]Spike,
falling_logs: [dynamic]Falling_Log,
We're changing spikes from a map to a dynamic array and adding a new dynamic array for falling logs.
Why are we are changing how Spikes work?
Having everything be a generic Entity type means adding more and more special-case code to Entity.
For example, how do we know an Entity is a Spike?
What if we want the Enemies to make sure they don't walk into Spikes?
We could do round-about checks on a generic Entity.
Or, we just add Spike as a type.
It's what I wanted to do in the beginning, but misguidedly made it more generic which has just caused more awkward code.
We define two new structs:
Spike :: struct {
collider: Rect,
facing: Direction,
}
Falling_Log :: struct {
collider: Rect,
rope_height: f32,
state: enum {
Default,
Falling,
Settled,
},
}
We update the background color:
BG_COLOR :: rl.Color{50, 44, 67, 255}
This changes the background to a dark purple color.
We delete the spike_on_enter procedure as we'll use something specific to the player, instead.
In the main procedure, we update the level loading code:
case "Player":
gs.player_id = entity_create(
{
x = entity.__worldX,
y = entity.__worldY,
// ... other fields ...
},
)
gs.safe_position = {entity.__worldX, entity.__worldY}
We're now setting the safe position to the player's initial position - just making sure we always have one.
We add handling for spikes and falling logs in the level loading:
case "Spikes":
facing := Direction.Right
x, y := entity.__worldX, entity.__worldY
width, height := entity.width, entity.height
// We know the shape of our data - we don't have to pretend we don't
switch entity.fieldInstances[0].__value {
case "Up":
facing = .Up
y += SPIKE_DIFF
height = SPIKE_DEPTH
case "Right":
width = SPIKE_DEPTH
case "Down":
facing = .Down
height = SPIKE_DEPTH
case "Left":
facing = .Left
width = SPIKE_DEPTH
x += SPIKE_DIFF
}
append(
&gs.spikes,
Spike{collider = {x, y, width, height}, facing = facing},
)
case "Falling_Log":
append(
&gs.falling_logs,
Falling_Log {
collider = {
entity.__worldX,
entity.__worldY,
entity.width,
entity.height,
},
},
)
This code creates spike and falling log entities based on their placement in the LDtk level.
We update the background tile creation to avoid placing tiles where spikes are:
// When drawing the background - don't draw behind spikes
// This prevents trees and bushes from spawning behind spikes
// It's why we changed the BG_COLOR
for spike in gs.spikes {
if !rl.CheckCollisionRecs(
spike.collider,
{auto_tile.px.x, auto_tile.px.y, 16, 16},
) {
append(
&gs.bg_tiles,
Tile{auto_tile.px, auto_tile.src, auto_tile.f},
)
}
}
After loading the layers, we calculate the rope height for falling logs:
for layer in level.layerInstances {
// ... layer loading code ...
}
for &falling_log in gs.falling_logs {
center := rect_center(falling_log.collider)
hits, hits_ok := raycast(
center,
UP * (gs.level_max.y - gs.level_min.y),
gs.colliders[:],
)
if hits_ok {
slice.sort_by(hits, proc(a, b: Vec2) -> bool {
return a.y > b.y || a.y == b.y
})
falling_log.rope_height =
center.y - hits[0].y - falling_log.collider.height / 2
}
}
In the main game loop, we update the physics call and add falling log behaviour:
player_update(&gs, dt)
entity_update(&gs, dt)
physics_update(gs.entities[:], gs.colliders[:], gs.falling_logs[:], dt)
behavior_update(gs.entities[:], gs.colliders[:], dt)
for &falling_log in gs.falling_logs {
if falling_log.state == .Falling {
falling_log.collider.y += dt * 600
for collider in gs.colliders {
if rl.CheckCollisionRecs(collider, falling_log.collider) {
if collider.y <= falling_log.collider.y + falling_log.collider.height {
falling_log.state = .Settled
append(&gs.colliders, falling_log.collider)
break
}
}
}
for entity, i in gs.entities {
if rl.CheckCollisionRecs(entity.collider, falling_log.collider) {
entity_damage(Entity_Id(i), 999)
}
}
}
}
This code handles the falling and settling of logs, as well as damaging entities they collide with.
We update the safe position check to include spikes:
// Without this check there's a chance to set the safe position to inside spikes on the ground
for spike in gs.spikes {
if rl.CheckCollisionRecs(spike.collider, player.collider) {
break safety_check
}
}
In the drawing section, we add code to draw spikes and falling logs:
for spike in gs.spikes {
rl.DrawRectangleLinesEx(spike.collider, 1, rl.YELLOW)
}
for falling_log in gs.falling_logs {
center := rect_center(falling_log.collider)
if falling_log.state == .Default {
rope_pos := Vec2{center.x, center.y - falling_log.collider.height / 2}
rl.DrawLineEx(rope_pos, rope_pos - {0, falling_log.rope_height}, 1, rl.BROWN)
}
rl.DrawRectangleLinesEx(falling_log.collider, 1, rl.BROWN)
}
We also draw the safe position (again):
rl.DrawRectangleLinesEx({gs.safe_position.x, gs.safe_position.y, 16, 16}, 1, rl.BLUE)
Moving on to the changes in physics.odin.
We update the physics_update procedure signature to take the logs:
physics_update :: proc(
entities: []Entity,
static_colliders: []Rect,
logs: []Falling_Log,
dt: f32,
)
We add falling log collision checks in the Y and X axis updates:
for log in logs {
if rl.CheckCollisionRecs(entity.collider, log.collider) {
resolve_entity_vs_static_y(&entity, log.collider)
break
}
}
// ... and similarly for X axis
We add two new procedures:
resolve_entity_vs_static_y :: proc(entity: ^Entity, static: Rect) {
if entity.vel.y > 0 {
entity.y = static.y - entity.height
entity.flags += {.Grounded}
} else {
entity.y = static.y + static.height
}
entity.vel.y = 0
}
resolve_entity_vs_static_x :: proc(entity: ^Entity, static: Rect) {
if entity.vel.x > 0 {
entity.x = static.x - entity.width
} else {
entity.x = static.x + static.width
}
entity.vel.x = 0
}
These procedures handle collision resolution for entities against static objects.
Finally, we'll look at the changes in player.odin:
We add spike collision handling in the player_update procedure:
for spike in gs.spikes {
if rl.CheckCollisionRecs(spike.collider, player.collider) {
player.x = gs.safe_position.x
player.y = gs.safe_position.y
player.vel = 0
gs.safe_reset_timer = PLAYER_SAFE_RESET_TIME
gs.player_movement_state = .Uncontrollable
switch_animation(player, "idle")
}
}
We update the player_attack_callback procedure to include falling log interaction:
for &falling_log in gs.falling_logs {
if falling_log.state != .Default do continue
log_center := rect_center(falling_log.collider)
rope_pos := Vec2{log_center.x, log_center.y - falling_log.collider.height / 2}
rect := Rect {
rope_pos.x - 1,
rope_pos.y - falling_log.rope_height,
2,
falling_log.rope_height - TILE_SIZE,
}
if rl.CheckCollisionCircleRec(center, 25, rect) {
falling_log.state = .Falling
}
}