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:
+41
-55
@@ -1,10 +1,13 @@
|
|||||||
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,
|
||||||
|
flow_field_active: bool,
|
||||||
bounds_active: bool,
|
bounds_active: bool,
|
||||||
camera_active: bool,
|
camera_active: bool,
|
||||||
camera: rl.Camera2D,
|
camera: rl.Camera2D,
|
||||||
@@ -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
@@ -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
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user