PVG
35. Spike Tool — Program Video Games

35. Spike Tool

[[programvideogames]]Let's make a spike placement and removal tool.

We have different parameters for placing spikes.

  1. Spikes can only be placed in a row or column fashion - an area that's 1 tile wide or tall.
  2. Spikes need to be adjacent to solid tiles.
  3. 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
}