35. Spike Tool
[[programvideogames]]Let's make a spike placement and removal tool.
We have different parameters for placing spikes.
- Spikes can only be placed in a row or column fashion - an area that's 1 tile wide or tall.
- Spikes need to be adjacent to solid tiles.
- Spikes cannot overlap with tiles.
Given these parameters, our code will be similar, yet different.
First thing's first: let's move from gs.spikes to gs.level.spikes as we did with colliders.
Go through the code and change anywhere that uses gs.spikes to gs.level.spikes.
Next, we'll add a Spike option to our Editor_Tool enum in editor.odin:
Editor_Tool :: enum {
None,
Tile,
Spike, // New
}
Which we will turn on with S, in the same way we do with tiles:
if rl.IsKeyPressed(.T) {
es.tool = .Tile
}
if rl.IsKeyPressed(.S) {
es.tool = .Spike
}
And we'll add our case .Spike: block to the switch:
case .Spike:
if rl.IsMouseButtonPressed(.LEFT) || rl.IsMouseButtonPressed(.RIGHT) {
es.area_begin = coords
es.area_end = coords
}
if rl.IsMouseButtonDown(.LEFT) || rl.IsMouseButtonDown(.RIGHT) {
rect := rect_from_coords_any_orientation(es.area_begin, coords)
if rect.width > rect.height {
es.area_end.x = coords.x
es.area_end.y = es.area_begin.y
}
if rect.height > rect.width {
es.area_end.x = es.area_begin.x
es.area_end.y = coords.y
}
}
if rl.IsMouseButtonReleased(.LEFT) {
rect := rect_from_coords_any_orientation(es.area_begin, es.area_end)
editor_place_spikes(rect, gs.level)
}
if rl.IsMouseButtonReleased(.RIGHT) {
rect := rect_from_coords_any_orientation(es.area_begin, es.area_end)
editor_remove_spikes(rect, gs.level)
}
All pretty familiar - the code when the mouse button is held down is to ensure that the Rect is a single row or column.
When we place Tiles, we also want to remove Spikes. So, update the editor_place_tile proc to this new version:
editor_place_tile :: proc(coords: Vec2i, l: ^Level) {
if !is_tile_at(coords, l) {
append(&l.tiles, Tile{pos = pos_from_coords(coords)})
slice.sort_by(l.tiles[:], proc(a, b: Tile) -> bool {
if a.pos.y != b.pos.y do return a.pos.y < b.pos.y
return a.pos.x < b.pos.x
})
// New
pos := pos_from_coords(coords)
rect := Rect{pos.x, pos.y, TILE_SIZE, TILE_SIZE}
editor_remove_spikes(rect, l)
recreate_level_colliders(l)
}
}
All the new Spike procedures are reproduced below:
editor_place_spikes :: proc(rect: Rect, l: ^Level) {
coords := coords_from_pos({rect.x, rect.y})
cols := int(rect.width) / TILE_SIZE
rows := int(rect.height) / TILE_SIZE
for y in 0 ..< rows {
for x in 0 ..< cols {
editor_remove_tile(coords + {i32(x), i32(y)}, l)
}
}
editor_remove_spikes(rect, l)
spike: Spike
spike.collider = rect
if facing, ok := determine_spike_facing(rect, l); ok {
spike.facing = facing
switch facing {
case .Up:
spike.collider.y += SPIKE_DIFF
spike.collider.height = SPIKE_DEPTH
case .Right:
spike.collider.width = SPIKE_DEPTH
case .Down:
spike.collider.height = SPIKE_DEPTH
case .Left:
spike.collider.width = SPIKE_DEPTH
spike.collider.x += SPIKE_DIFF
}
append(&l.spikes, spike)
}
}
determine_spike_facing :: proc(rect: Rect, l: ^Level) -> (facing: Direction, ok: bool) {
begin_coords := coords_from_pos({rect.x, rect.y})
end_coords := coords_from_pos({rect.x + rect.width, rect.y + rect.height})
// Check Below, Facing Up
if is_area_tiled(begin_coords + {0, 1}, end_coords + {0, 1}, l) {
return .Up, true
}
// Check Above, Facing Down
if is_area_tiled(begin_coords + {0, -1}, end_coords + {0, -1}, l) {
return .Down, true
}
// Check Right, Facing Left
if is_area_tiled(begin_coords + {1, 0}, end_coords + {1, 0}, l) {
return .Left, true
}
// Check Left, Facing Right
if is_area_tiled(begin_coords + {-1, 0}, end_coords + {-1, 0}, l) {
return .Right, true
}
return .Up, false
}
editor_remove_spikes :: proc(rect: Rect, l: ^Level) {
#reverse for spike, i in l.spikes {
if rl.CheckCollisionRecs(rect, spike.collider) {
ordered_remove(&l.spikes, i)
}
}
}
And finally, a utility proc that I've left in editor.odin:
is_area_tiled :: proc(begin: Vec2i, end: Vec2i, l: ^Level) -> bool {
for y in 0 ..< end.y - begin.y {
for x in 0 ..< end.x - begin.x {
if !is_tile_at(begin + {x, y}, l) {
return false
}
}
}
return true
}