Files
raylib_reference/src/map.odin
T

327 lines
10 KiB
Odin

package game
import "core:math"
import rl "vendor:raylib"
Tile_Type :: enum {
// TEMP light gray
FLOOR,
// TEMP DARK gray
WALL,
// TEMP gray
LOW_WALL,
// TREMP GREEN
PICKUP,
}
Tile_Def :: struct {
solid: bool,
collider_offset_x, collider_offset_y: f32,
collider_w, collider_h: f32
}
// awesome odin sets the fixed size to the ammount of enum options
tile_defs: [Tile_Type]Tile_Def = {
.FLOOR = { solid = false },
.PICKUP = { solid = false },
.WALL = {
solid = true,
collider_offset_x = 0, collider_offset_y = 0,
collider_w = TILE_SIZE, collider_h = TILE_SIZE
},
.LOW_WALL = {
solid = true,
collider_offset_x = 0, collider_offset_y = TILE_SIZE/2,
collider_w = TILE_SIZE, collider_h = TILE_SIZE/2
},
}
Grid_Pos :: [2]int
Game_Map :: struct {
tile_size: int,
width: int,
height: int,
width_px: f32,
height_px: f32,
data: [400]int,
// Flow Field props
cost_field: [400]int,
flow_field: [400]rl.Vector2,
player_grid_pos: Grid_Pos
}
map_init :: proc() -> Game_Map {
// @TODO: store maps and load them later
// NOTE: flat, row-major — data[row * width + col]
map_data := [400]int{
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 1,
0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1,
1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1,
1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1,
1, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
}
game := Game_Map{
width = 20,
height = 20,
tile_size = TILE_SIZE,
data = map_data,
}
game.width_px = f32(game.width * game.tile_size)
game.height_px = f32(game.height * game.tile_size)
return game
}
// requires raw window-space
// i.e mouse position clicked in window get that space in screen
// returns screen-space pos, scaled to game resolution.
window_to_screen :: proc(window_pos: rl.Vector2) -> rl.Vector2 {
// window_width / game_width will give the scale back
// invert it to scale down back to the render size
screen_x := window_pos.x / (WINDOW_WIDTH / GAME_WIDTH)
screen_y := window_pos.y / (WINDOW_HEIGHT / GAME_HEIGHT)
return rl.Vector2{screen_x, screen_y}
}
// requires screen-space pos i.e mouse pos, scaled from window -> render size
// returns world-space position (accounts for camera and its zoom)
// remember uesd with raw input coordinates, not something already in world space (like player.pos).
screen_to_world :: proc(screen_pos: rl.Vector2, camera: rl.Camera2D) -> rl.Vector2 {
return rl.GetScreenToWorld2D(screen_pos, camera)
}
// @TODO: Do I need this?? Maybe for debug view??
world_to_screen :: proc(world_pos: rl.Vector2, camera: rl.Camera2D) -> rl.Vector2 {
return rl.GetWorldToScreen2D(world_pos, camera)
}
// requires world-space position i.e player.pos, or output of screen_to_world
// returns grid col/row not considering camera
world_to_grid :: proc(world_pos: rl.Vector2) -> Grid_Pos {
col := int(math.floor(world_pos.x / TILE_SIZE))
row := int(math.floor(world_pos.y / TILE_SIZE))
return Grid_Pos{col, row}
}
// returns world-space position grid pos top-left corner opposite of world_to_grid.
grid_to_world :: proc(col, row: int) -> rl.Vector2 {
world_x := f32(col * TILE_SIZE)
world_y := f32(row * TILE_SIZE)
return rl.Vector2{world_x, world_y}
}
grid_to_index :: proc(col, row, width: int) -> int {
return (row * width) + col
}
// requires world x/y and then gives the rectangle in pixels that matches that
get_world_tile_rect :: proc(gx, gy, offset_x, offset_y, w, h: f32) -> rl.Rectangle {
return rl.Rectangle {
(gx * TILE_SIZE) + offset_x,
(gy * TILE_SIZE) + offset_y,
w, h
}
}
// Simple check for now
is_tile_solid :: proc(tile_type: Tile_Type) -> bool {
return tile_type == .WALL
}
// takes in the next position/rect for check compares it
check_map_collision :: proc(rect: rl.Rectangle, game_map: Game_Map) -> bool {
// collider top left point
min_grid := world_to_grid(rl.Vector2{rect.x, rect.y})
// collider bot right point
max_grid := world_to_grid(rl.Vector2{rect.x + rect.width, rect.y + rect.height})
// check all points along the x axis per y axis 'row'
for gy := min_grid[1]; gy <= max_grid[1]; gy += 1 {
for gx := min_grid[0]; gx <= max_grid[0]; gx += 1 {
// bounds check
if gx < 0 || gx >= game_map.width || gy < 0 || gy >= game_map.height do continue
idx := (gy * game_map.width) + gx
tile := Tile_Type(game_map.data[idx])
def := tile_defs[tile]
if def.solid {
tile_rect := get_world_tile_rect(
f32(gx), f32(gy),
def.collider_offset_x, def.collider_offset_y,
def.collider_w, def.collider_h
);
if rl.CheckCollisionRecs(rect, tile_rect) {
return true
}
}
}
}
return false
}
// true when collision found
check_enemy_collision :: proc(rect_a: rl.Rectangle, active_enemies: int, enemies: ^[MAX_ENEMIES]Enemy) -> bool {
for idx in 0..<active_enemies {
e := enemies[idx]
enemy_rect := get_enemy_collider(e, e.position)
if rl.CheckCollisionRecs(rect_a, enemy_rect) {
return true
}
}
return false
}
draw_map :: proc(current_map: Game_Map) {
for value, index in current_map.data {
row := index / current_map.width
column := index % current_map.width
tile := Tile_Type(value)
color: rl.Color
rect := grid_to_world(column, row)
// base floor color, draw full tile first to avoid background bleeding
rl.DrawRectangle(i32(rect.x), i32(rect.y), TILE_SIZE, TILE_SIZE, rl.LIGHTGRAY)
// anything that isn't plain floor draws on top of the base
#partial switch tile {
case .WALL:
rl.DrawRectangle(i32(rect.x), i32(rect.y), TILE_SIZE, TILE_SIZE, rl.DARKGRAY)
case .LOW_WALL:
rl.DrawRectangle(i32(rect.x), i32(rect.y + TILE_SIZE / 2), TILE_SIZE, TILE_SIZE / 2, rl.GRAY)
case .PICKUP:
rl.DrawRectangle(i32(rect.x), i32(rect.y), TILE_SIZE, TILE_SIZE, rl.GREEN)
}
}
}
reset_cost_array :: proc(cost_arr: ^[400]int) {
for val, idx in cost_arr {
cost_arr[idx] = -1
}
}
compute_flow_field :: proc(game_map: ^Game_Map, player_pos: rl.Vector2) {
reset_cost_array(&game_map.cost_field)
player_grid_pos := world_to_grid(player_pos)
player_idx := grid_to_index(player_grid_pos.x, player_grid_pos.y, game_map.width)
game_map.cost_field[player_idx] = 0
flow_field_queue: [dynamic]Grid_Pos
defer delete(flow_field_queue)
append_elem(&flow_field_queue, player_grid_pos)
for len(flow_field_queue) != 0 {
current := pop_front(&flow_field_queue)
current_idx := grid_to_index(current.x, current.y, game_map.width)
current_cost := game_map.cost_field[current_idx]
// Check all 8 neighbors (3x3 grid minus center):
// (-1,-1) (0,-1) (1,-1)
// (-1, 0) X (1, 0)
// (-1, 1) (0, 1) (1, 1)
for loop_y in -1..=1 {
for loop_x in -1..=1 {
if loop_x == 0 && loop_y == 0 do continue
nx := current.x + loop_x
ny := current.y + loop_y
get_neighbor_cost({nx, ny}, &flow_field_queue, game_map, current_cost)
}
}
}
}
get_neighbor_cost :: proc(grid_pos: Grid_Pos, queue: ^[dynamic]Grid_Pos, game_map: ^Game_Map, current_cost: int) {
if grid_pos.x >= 0 && grid_pos.x < game_map.width && grid_pos.y >= 0 && grid_pos.y < game_map.height {
grid_pos_index := grid_to_index(grid_pos.x, grid_pos.y, game_map.width)
if game_map.cost_field[grid_pos_index] == -1 {
tile_t := Tile_Type(game_map.data[grid_pos_index])
def_t := tile_defs[tile_t]
if !def_t.solid {
game_map.cost_field[grid_pos_index] = current_cost + 1
append_elem(queue, grid_pos)
}
}
}
}
compute_flow_directions :: proc(game_map: ^Game_Map) {
// walk every tile, look at neighbor costs in cost_field,
// write a direction vector into direction_field[index]
for value, index in game_map.data {
row := index / game_map.width
column := index % game_map.width
current_index := grid_to_index(column, row, game_map.width)
// skip unreachable tiles
if game_map.cost_field[current_index] == -1 do continue
current_cost := game_map.cost_field[current_index]
lowest_cost := current_cost
// x = column, y = row — matches grid_to_world / screen space
a := rl.Vector2{f32(column), f32(row)}
b := a
// Check all 8 neighbors (3x3 grid minus center):
// (-1,-1) (0,-1) (1,-1)
// (-1, 0) X (1, 0)
// (-1, 1) (0, 1) (1, 1)
for loop_y in -1..=1 {
for loop_x in -1..=1 {
if loop_x == 0 && loop_y == 0 do continue
ncolumn := column + loop_x
nrow := row + loop_y
if ncolumn >= 0 && ncolumn < game_map.width && nrow >= 0 && nrow < game_map.height {
cost_index := grid_to_index(ncolumn, nrow, game_map.width)
neighbor_cost := game_map.cost_field[cost_index]
if neighbor_cost != -1 && neighbor_cost < lowest_cost {
lowest_cost = neighbor_cost
b = rl.Vector2{f32(ncolumn), f32(nrow)}
}
}
}
}
// point from current tile toward the lower-cost neighbor
delta := b - a
if delta == {0, 0} {
game_map.flow_field[current_index] = {0, 0}
} else {
game_map.flow_field[current_index] = rl.Vector2Normalize(delta)
}
}
}