From 09c3e34c6bea8d3972abff02bff2e42f54229745 Mon Sep 17 00:00:00 2001 From: Jason Hilder Date: Fri, 31 Jul 2026 07:11:08 +0200 Subject: [PATCH] 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 --- src/debug.odin | 106 ++++++++++++++++++++------------------------- src/game.odin | 20 ++++++--- src/map.odin | 113 ++++++++++++++++++++++++++++++++++++++++++++++-- src/player.odin | 24 ++++++++++ 4 files changed, 193 insertions(+), 70 deletions(-) diff --git a/src/debug.odin b/src/debug.odin index 5f39cdd..59ef601 100644 --- a/src/debug.odin +++ b/src/debug.odin @@ -1,13 +1,16 @@ package game +import "core:math" +import "core:fmt" import rl "vendor:raylib" Debug :: struct { - active: bool, - grid_active: bool, - bounds_active: bool, - camera_active: bool, - camera: rl.Camera2D, + active: bool, + grid_active: bool, + flow_field_active: bool, + bounds_active: bool, + camera_active: bool, + camera: rl.Camera2D, } 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 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 { py := y * TILE_SIZE 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.. Game { // ! game setup game := Game{} - game.assets = asset_manager_init() game.player = player_init(&game.assets.player) game.camera = camera_init(&game.player, 1) game.world = rl.LoadRenderTexture(GAME_WIDTH, GAME_HEIGHT) game.current_map = map_init() + game.current_map.player_grid_pos = world_to_grid(game.player.position) // ! init timers 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_init(&game, false) @@ -55,6 +59,7 @@ game_update :: proc(game: ^Game) { dt := rl.GetFrameTime() 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) if update_timer(&game.enenmy_spawn_timer, dt) { @@ -74,18 +79,19 @@ game_draw :: proc(game: ^Game) { rl.BeginTextureMode(game.world) rl.ClearBackground(rl.BLACK) rl.BeginMode2D(cam) + draw_map(game.current_map) - // @ DEBUG - debug_bounds(game) - debug_camera_draw(&game.camera, game.debug.camera_active) - debug_grid_draw(&game.debug.camera, game.debug.grid_active) - - // draw objects draw_enemies(&game.enemies, game.enemy_count) draw_player(&game.player) + // @ DEBUG + debug_bounds(game) + debug_camera_draw(&game.camera, game.debug.camera_active) + debug_grid_draw(&game.debug.camera, game.debug.grid_active) + debug_draw_flow_field(game, game.debug.flow_field_active) + rl.EndMode2D() rl.EndTextureMode() diff --git a/src/map.odin b/src/map.odin index a1911c0..fdffe17 100644 --- a/src/map.odin +++ b/src/map.odin @@ -36,17 +36,21 @@ tile_defs: [Tile_Type]Tile_Def = { }, } +Grid_Pos :: [2]int + Game_Map :: struct { tile_size: int, width: int, height: int, width_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 { // @TODO: store maps and load them later // 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) + } + } +} diff --git a/src/player.odin b/src/player.odin index 310bcf4..3c33fa9 100644 --- a/src/player.odin +++ b/src/player.odin @@ -148,6 +148,29 @@ update_player :: proc( 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) { clip := player.animation.clip 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) } +