Initial working implementation of flow field.

Debug methods to visualize the flow field
Player updates in game update on tile change
Map does the actual looping and setting of values
This commit is contained in:
2026-07-31 07:11:08 +02:00
parent e0c53290d7
commit 09c3e34c6b
4 changed files with 193 additions and 70 deletions
+46 -60
View File
@@ -1,13 +1,16 @@
package game package game
import "core:math"
import "core:fmt"
import rl "vendor:raylib" import rl "vendor:raylib"
Debug :: struct { Debug :: struct {
active: bool, active: bool,
grid_active: bool, grid_active: bool,
bounds_active: bool, flow_field_active: bool,
camera_active: bool, bounds_active: bool,
camera: rl.Camera2D, camera_active: bool,
camera: rl.Camera2D,
} }
debug_init :: proc(game: ^Game, camera_enabled: bool) { debug_init :: proc(game: ^Game, camera_enabled: bool) {
@@ -112,75 +115,58 @@ debug_grid_draw :: proc(camera: ^rl.Camera2D, active: bool) {
px := x * TILE_SIZE px := x * TILE_SIZE
rl.DrawLine(px, start_y * TILE_SIZE, px, end_y * TILE_SIZE, rl.Fade(rl.WHITE, 0.2)) rl.DrawLine(px, start_y * TILE_SIZE, px, end_y * TILE_SIZE, rl.Fade(rl.WHITE, 0.2))
} }
for y in start_y..=end_y { for y in start_y..=end_y {
py := y * TILE_SIZE py := y * TILE_SIZE
rl.DrawLine(start_x * TILE_SIZE, py, end_x * TILE_SIZE, py, rl.Fade(rl.WHITE, 0.2)) rl.DrawLine(start_x * TILE_SIZE, py, end_x * TILE_SIZE, py, rl.Fade(rl.WHITE, 0.2))
} }
// per-cell triangle
// pad: f32 = TILE_SIZE * 0.2
// for x in start_x..<end_x {
// for y in start_y..<end_y {
// cell_x := f32(x * TILE_SIZE)
// cell_y := f32(y * TILE_SIZE)
// // raylib expects counter-clockwise winding
// top := rl.Vector2{ cell_x + TILE_SIZE/2, cell_y + pad }
// left := rl.Vector2{ cell_x + pad, cell_y + TILE_SIZE - pad }
// right := rl.Vector2{ cell_x + TILE_SIZE - pad, cell_y + TILE_SIZE - pad }
// rl.DrawTriangle(top, left, right, rl.Fade(rl.YELLOW, 0.3))
// }
// }
} }
} }
// draw_direction_triangle :: proc(center: rl.Vector2, direction: rl.Vector2, size: f32, color: rl.Color) { // @TODO make this clamped to the camera view only
// if direction == {0, 0} { debug_draw_flow_field :: proc(game: ^Game, active: bool) {
// return // no direction to draw (e.g. goal cell, or unreachable cell) when DEBUG_MODE {
// } if !active {
return
}
// angle := math.atan2(direction.y, direction.x) for value, index in game.current_map.data {
row := index / game.current_map.width
column := index % game.current_map.width
flat_index := (row * game.current_map.width) + column
flow_weight := game.current_map.cost_field[flat_index]
// // base triangle pointing right (angle = 0), relative to center str := fmt.ctprintf("%d", flow_weight)
// p0 := rl.Vector2{ size * 0.6, 0 } rect := grid_to_world(column, row)
// p1 := rl.Vector2{ -size * 0.4, size * 0.4 } rl.DrawText(str, i32(rect.x), i32(rect.y), 1, rl.BLACK)
// p2 := rl.Vector2{ -size * 0.4, -size * 0.4 }
// rl.DrawTriangle( dir := game.current_map.flow_field[flat_index]
// center + rl.Vector2Rotate(p0, angle), draw_direction_triangle(row, column, dir, 8)
// center + rl.Vector2Rotate(p1, angle), }
// center + rl.Vector2Rotate(p2, angle), }
// color, }
// )
// }
// debug_flow_field_draw :: proc(camera: ^rl.Camera2D, active: bool, field: ^Flow_Field) { draw_direction_triangle :: proc(row, col: int, dir: rl.Vector2, size: f32) {
// when DEBUG_MODE { if dir == {0,0} do return
// if !active {
// return
// }
// view_width := GAME_WIDTH / camera.zoom center := grid_to_world(col, row) + {TILE_SIZE * 0.5, TILE_SIZE * 0.5}
// view_height := GAME_HEIGHT / camera.zoom
// top_left := camera.target - rl.Vector2{ f32(view_width)/2, f32(view_height)/2 }
// start_x := max(i32(top_left.x / TILE_SIZE), 0) color := rl.Color{ 253, 249, 0, 100 } // Yellow
// start_y := max(i32(top_left.y / TILE_SIZE), 0)
// end_x := min(i32((top_left.x + view_width) / TILE_SIZE) + 1, field.width)
// end_y := min(i32((top_left.y + view_height) / TILE_SIZE) + 1, field.height)
// for x in start_x..<end_x { // Rotate dir 90 degrees to get a "sideways" vector — used to push the
// for y in start_y..<end_y { // two base corners left/right away from the center line
// cell := field.cells[y * field.width + x] perp := rl.Vector2{-dir.y, dir.x}
// center := rl.Vector2{
// f32(x) * TILE_SIZE + TILE_SIZE/2, // Tip: pushed forward from center along dir
// f32(y) * TILE_SIZE + TILE_SIZE/2, tip := center + dir * size
// }
// draw_direction_triangle(center, cell.direction, TILE_SIZE * 0.35, rl.Fade(rl.SKYBLUE, 0.6)) // Base corners: pulled slightly backward along -dir, then spread
// } // apart sideways using perp, so the base is centered behind the tip
// } left := center - dir * (size * 0.5) + perp * (size * 0.5)
// } right := center - dir * (size * 0.5) - perp * (size * 0.5)
// }
rl.DrawTriangle(tip, right, left, color)
}
// Collision stuff // Collision stuff
+12 -6
View File
@@ -33,16 +33,20 @@ game_init :: proc() -> Game {
// ! game setup // ! game setup
game := Game{} game := Game{}
game.assets = asset_manager_init() game.assets = asset_manager_init()
game.player = player_init(&game.assets.player) game.player = player_init(&game.assets.player)
game.camera = camera_init(&game.player, 1) game.camera = camera_init(&game.player, 1)
game.world = rl.LoadRenderTexture(GAME_WIDTH, GAME_HEIGHT) game.world = rl.LoadRenderTexture(GAME_WIDTH, GAME_HEIGHT)
game.current_map = map_init() game.current_map = map_init()
game.current_map.player_grid_pos = world_to_grid(game.player.position)
// ! init timers // ! init timers
game.enenmy_spawn_timer = init_timer(0.1, true) game.enenmy_spawn_timer = init_timer(0.1, true)
// flow field on boot
compute_flow_field(&game.current_map, game.player.position)
compute_flow_directions(&game.current_map)
// @ DEBUG // @ DEBUG
debug_init(&game, false) debug_init(&game, false)
@@ -55,6 +59,7 @@ game_update :: proc(game: ^Game) {
dt := rl.GetFrameTime() dt := rl.GetFrameTime()
update_player(&game.player, &game.assets.player, game.camera, game.current_map, &game.enemies, game.enemy_count, dt) update_player(&game.player, &game.assets.player, game.camera, game.current_map, &game.enemies, game.enemy_count, dt)
update_player_flow_field(&game.player, &game.current_map)
update_camera(&game.player, &game.camera, game.current_map.width_px, game.current_map.height_px) update_camera(&game.player, &game.camera, game.current_map.width_px, game.current_map.height_px)
if update_timer(&game.enenmy_spawn_timer, dt) { if update_timer(&game.enenmy_spawn_timer, dt) {
@@ -74,17 +79,18 @@ game_draw :: proc(game: ^Game) {
rl.BeginTextureMode(game.world) rl.BeginTextureMode(game.world)
rl.ClearBackground(rl.BLACK) rl.ClearBackground(rl.BLACK)
rl.BeginMode2D(cam) rl.BeginMode2D(cam)
draw_map(game.current_map) draw_map(game.current_map)
// draw objects
draw_enemies(&game.enemies, game.enemy_count)
draw_player(&game.player)
// @ DEBUG // @ DEBUG
debug_bounds(game) debug_bounds(game)
debug_camera_draw(&game.camera, game.debug.camera_active) debug_camera_draw(&game.camera, game.debug.camera_active)
debug_grid_draw(&game.debug.camera, game.debug.grid_active) debug_grid_draw(&game.debug.camera, game.debug.grid_active)
debug_draw_flow_field(game, game.debug.flow_field_active)
// draw objects
draw_enemies(&game.enemies, game.enemy_count)
draw_player(&game.player)
rl.EndMode2D() rl.EndMode2D()
rl.EndTextureMode() rl.EndTextureMode()
+110 -3
View File
@@ -36,17 +36,21 @@ tile_defs: [Tile_Type]Tile_Def = {
}, },
} }
Grid_Pos :: [2]int
Game_Map :: struct { Game_Map :: struct {
tile_size: int, tile_size: int,
width: int, width: int,
height: int, height: int,
width_px: f32, width_px: f32,
height_px: f32, height_px: f32,
data: [400]int data: [400]int,
// Flow Field props
cost_field: [400]int,
flow_field: [400]rl.Vector2,
player_grid_pos: Grid_Pos
} }
Grid_Pos :: [2]int
map_init :: proc() -> Game_Map { map_init :: proc() -> Game_Map {
// @TODO: store maps and load them later // @TODO: store maps and load them later
// NOTE: flat, row-major — data[row * width + col] // NOTE: flat, row-major — data[row * width + col]
@@ -213,3 +217,106 @@ draw_map :: proc(current_map: Game_Map) {
} }
} }
} }
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)
}
}
}
+24
View File
@@ -148,6 +148,29 @@ update_player :: proc(
animation_update(&player.animation, dt) animation_update(&player.animation, dt)
} }
update_player_flow_field :: proc(player: ^Player, game_map: ^Game_Map) {
// Use the collider's center (not the sprite anchor/top-left) so the
// grid cell reflects where the player physically is
collider := get_player_collider(player, player.position)
center := rl.Vector2{
collider.x + collider.width * 0.5,
collider.y + collider.height * 0.5,
}
player_grid_pos := world_to_grid(center)
// only recompute when the player has actually moved to a new tile
// @ PERF: recomputes the whole map every tile crossing — fine for
// small maps, but revisit (e.g. dirty region / partial update)
// if the map grows or this shows up in profiling
if game_map.player_grid_pos != player_grid_pos {
game_map.player_grid_pos = player_grid_pos
compute_flow_field(game_map, center)
compute_flow_directions(game_map)
}
}
draw_player :: proc(player: ^Player) { draw_player :: proc(player: ^Player) {
clip := player.animation.clip clip := player.animation.clip
src_rect := animation_frame_rect(&player.animation) src_rect := animation_frame_rect(&player.animation)
@@ -165,3 +188,4 @@ draw_player :: proc(player: ^Player) {
rl.DrawTexturePro(clip.texture, src_rect, dst_rect, rl.Vector2{}, 0, rl.WHITE) rl.DrawTexturePro(clip.texture, src_rect, dst_rect, rl.Vector2{}, 0, rl.WHITE)
} }