From cb11496752ede6dab15d7ae60e0005e78b77e5bb Mon Sep 17 00:00:00 2001 From: Xander Swan Date: Fri, 5 Dec 2025 21:31:14 -0500 Subject: actual physics system --- src/aabb.odin | 13 ---- src/main.odin | 50 +++++++++++-- src/phys/body.odin | 65 +++++++++++++++++ src/phys/world.odin | 205 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/platform.odin | 9 ++- src/player.odin | 91 ++++++++++------------- 6 files changed, 361 insertions(+), 72 deletions(-) delete mode 100644 src/aabb.odin create mode 100644 src/phys/body.odin create mode 100644 src/phys/world.odin (limited to 'src') diff --git a/src/aabb.odin b/src/aabb.odin deleted file mode 100644 index 9637ec9..0000000 --- a/src/aabb.odin +++ /dev/null @@ -1,13 +0,0 @@ -package demonchime - -aabb_hori :: proc(a: Rect, b: Rect) -> bool { - return a.start.x < b.start.x + b.size.x && b.start.x < a.start.x + a.size.x -} - -aabb_vert :: proc(a: Rect, b: Rect) -> bool { - return a.start.y < b.start.y + b.size.y && b.start.y < a.start.y + a.size.y -} - -aabb :: proc(a: Rect, b: Rect) -> bool { - return aabb_hori(a, b) && aabb_vert(a, b) -} diff --git a/src/main.odin b/src/main.odin index 9a9c91c..48bf454 100644 --- a/src/main.odin +++ b/src/main.odin @@ -15,6 +15,7 @@ import stime "shared:sokol/time" import slog "shared:sokol/log" import "draw" +import "phys" Vec2 :: [2]f32 @@ -29,6 +30,7 @@ state: struct { platform_list: Entity_List(Platform), renderer: draw.Renderer, + physics_world: phys.World, input: Input, } @@ -59,23 +61,61 @@ init :: proc "c" () { make_platform(Rect{ start = {50, 50}, - size = {100, 20} + size = {64, 20} + }) + + i := f32(20) + for i < draw.SCREEN_WIDTH - 20 - 64 { + make_platform(Rect{ + start = {i, 340}, + size = {64, 20} + }) + i += 65 + } + // make_platform(Rect{ + // start = {i, 340}, + // size = {20, 20} + // }) + + make_platform(Rect{ + start = {160, 320}, + size = {20, 20} }) make_platform(Rect{ - start = {20, 340}, - size = {440, 20} + start = {240, 280}, + size = {40, 20} }) make_platform(Rect{ - start = {140, 320}, + start = {260, 260}, size = {20, 20} }) make_platform(Rect{ - start = {240, 280}, + start = {300, 220}, + size = {40, 20} + }) + + make_platform(Rect{ + start = {240, 180}, + size = {40, 20} + }) + + make_platform(Rect{ + start = {200, 140}, + size = {40, 20} + }) + + make_platform(Rect{ + start = {180, 100}, size = {20, 20} }) + + make_platform(Rect{ + start = {140, 100}, + size = {16, 16} + }) } frame :: proc "c" () { diff --git a/src/phys/body.odin b/src/phys/body.odin new file mode 100644 index 0000000..fcae697 --- /dev/null +++ b/src/phys/body.odin @@ -0,0 +1,65 @@ +package phys + +Vec2 :: [2]f32 + +Rect :: struct { + start: Vec2, + size: Vec2, +} + +Layer :: enum(u16) { + DEFAULT, + HARD, // hard collisions; don't let bodies intersect at all + SOFT, // soft collisions; push away other bodies with a force + ENEMY, // enemy hitboxes + PLAYER, // player hitboxes +} + +Collision_Type :: enum(u16) { + UP, + DOWN, + RIGHT, + LEFT, + HORIZONTAL, + VERTICAL, +} + +Body :: struct { + handle: Body_Handle, + bin_idx: i32, + rect: Rect, + active: bool, + pos: Vec2, + vel: Vec2, + collisions: bit_set[Collision_Type; u16], + layers: bit_set[Layer; u16], + mask: bit_set[Layer; u16], +} + +make_body :: proc( + w: ^World, + rect: Rect, + layers := bit_set[Layer; u16]{.DEFAULT}, + mask := bit_set[Layer; u16]{.DEFAULT}, +) -> (Body_Handle, ^Body) { + b := Body { + rect = rect, + layers = layers, + mask = mask, + active = true, + } + return add_body(w, b) +} + +aabb_hori :: proc(a: Rect, b: Rect) -> bool { + return a.start.x < b.start.x + b.size.x && b.start.x < a.start.x + a.size.x +} + +aabb_vert :: proc(a: Rect, b: Rect) -> bool { + return a.start.y < b.start.y + b.size.y && b.start.y < a.start.y + a.size.y +} + +aabb :: proc(a: Rect, b: Rect) -> bool { + return aabb_hori(a, b) && aabb_vert(a, b) +} + diff --git a/src/phys/world.odin b/src/phys/world.odin new file mode 100644 index 0000000..eac3d5c --- /dev/null +++ b/src/phys/world.odin @@ -0,0 +1,205 @@ +package phys + +import "core:log" +import "core:math" + +import sapp "shared:sokol/app" + +BIN_COUNT :: 2056 +BIN_SIZE :: 64 + +Body_Handle :: u32 + +World :: struct { + handles: [dynamic]u32, + unused_handles: [dynamic]Body_Handle, + bodies: [dynamic]Body, + bins: [BIN_COUNT][dynamic]Body_Handle, +} + +destroy_world :: proc(w: World) { + for bin in w.bins { + delete(bin) + } +} + +@(private="file") +hash_bin :: proc(x: i32, y: i32) -> u32 { + return transmute(u32)((x * 73856093) ~ (y * 19349663)) +} + +@(private="file") +world_to_bin :: proc(point: Vec2) -> (i32, i32) { + return \ + i32(math.floor(point.x / BIN_SIZE)), + i32(math.floor(point.y / BIN_SIZE)) +} + +@(private="file") +get_surrounding_bins :: proc( + w: ^World, + pos: Vec2, + allocator := context.temp_allocator, +) -> []^[dynamic]Body_Handle { + neighbors := make([]^[dynamic]Body_Handle, 9, allocator) + + center_x, center_y := world_to_bin(pos) + + idx := 0 + + for offset_x in -1..=1 { + for offset_y in -1..=1 { + bin_idx := hash_bin(center_x + i32(offset_x), center_y + i32(offset_y)) + bin := &w.bins[bin_idx % BIN_COUNT] + neighbors[idx] = bin + idx += 1 + } + } + + return neighbors +} + +@(private="file") +find_bin :: proc(w: ^World, b: Body) -> ^[dynamic]Body_Handle { + bin_x, bin_y := world_to_bin(b.pos + b.rect.start) + return &w.bins[hash_bin(bin_x, bin_y) % BIN_COUNT] +} + +@(private="file") +add_to_bins :: proc(w: ^World, b: Body) { + bin := find_bin(w, b) + idx := i32(len(bin)) + append(bin, b.handle) + w.bodies[bin[idx]].bin_idx = idx +} + +@(private="file") +remove_from_bins :: proc(w: ^World, b: Body) { + bin := find_bin(w, b) + + assert(bin[b.bin_idx] == b.handle) + + last := pop(bin) + if last != b.handle { + bin[b.bin_idx] = last + w.bodies[last].bin_idx = b.bin_idx + } +} + +get_body :: proc(w: World, h: Body_Handle) -> ^Body { + return &w.bodies[w.handles[h]] +} + +add_body :: proc(w: ^World, b: Body) -> (Body_Handle, ^Body) { + handle: Body_Handle + + if b.rect.size.x > BIN_SIZE || b.rect.size.y > BIN_SIZE { + log.warnf("Body size is too big (%vx%v)", b.rect.size.x, b.rect.size.y) + } + + if len(w.unused_handles) > 0 { + handle = pop(&w.unused_handles) + } else { + handle = cast(Body_Handle)len(w.handles) + append(&w.handles, 0) + } + + w.handles[handle] = u32(len(w.bodies)) + append(&w.bodies, b) + + body := &w.bodies[w.handles[handle]] + body.handle = handle + + add_to_bins(w, body^) + + return handle, body +} + +remove_body :: proc(w: ^World, h: Body_Handle) { + b := get_body(w^, h) + + remove_from_bins(w, b^) + + last := pop(&w.bodies) + if last.handle != h { + w.bodies[h] = last + w.handles[last.handle] = w.handles[h] + } + + append(&w.unused_handles, b.handle) +} + +update_body :: proc(w: ^World, h: Body_Handle) { + dt := f32(sapp.frame_duration()) + + b := get_body(w^, h) + + res_pos := b.pos + b.vel * dt + + rect := b.rect + rect.start += b.pos + + res_rect := b.rect + res_rect.start += res_pos + + bin_list := get_surrounding_bins(w, res_rect.start) + + b.collisions = {} + + total_len := 0 + + for bin in bin_list { + total_len += len(bin) + + for c_h in bin { + if c_h == h { + continue + } + + c := get_body(w^, c_h) + + c_rect := c.rect + c_rect.start += c.pos + + if aabb(res_rect, c_rect) { + if aabb_hori(rect, c_rect) { + if b.vel.y > 0 { + res_pos.y = c_rect.start.y - b.rect.size.y - b.rect.start.y + b.collisions += {.DOWN} + } else { + res_pos.y = c_rect.start.y + c_rect.size.y - b.rect.start.y + b.collisions += {.UP} + } + b.collisions += {.VERTICAL} + } else if aabb_vert(rect, c.rect) { + if b.vel.x > 0 { + res_pos.x = c_rect.start.x - b.rect.size.x - b.rect.start.x + b.collisions += {.LEFT} + } else { + res_pos.x = c_rect.start.x + c_rect.size.x - b.rect.start.x + b.collisions += {.RIGHT} + } + b.collisions += {.HORIZONTAL} + } + } + } + } + + if .HORIZONTAL in b.collisions { + b.vel.x = 0 + } + if .VERTICAL in b.collisions { + b.vel.y = 0 + } + + b.pos = res_pos + + prev_bin := hash_bin(world_to_bin(b.pos + b.rect.start)) + res_bin := hash_bin(world_to_bin(res_pos + b.rect.start)) + + if prev_bin != res_bin { + remove_from_bins(w, b^) + add_to_bins(w, b^) + } +} + diff --git a/src/platform.odin b/src/platform.odin index b837355..150d96c 100644 --- a/src/platform.odin +++ b/src/platform.odin @@ -1,21 +1,24 @@ package demonchime import "draw" +import "phys" Platform :: struct { handle: Entity_Handle, - rect: Rect, + body: phys.Body_Handle, } make_platform :: proc(rect: Rect) -> (Entity_Handle, ^Platform) { + handle, body := phys.make_body(&state.physics_world, transmute(phys.Rect)rect) return make_entity(&state.platform_list, Platform { - rect = rect, + body = handle, }) } draw_platforms :: proc() { iter := iter_entity_list(state.platform_list) for p in entity_list_iter(&iter) { - draw.rect(&state.renderer, cast(draw.Rect)p.rect) + body := phys.get_body(state.physics_world, p.body) + draw.rect(&state.renderer, cast(draw.Rect)body.rect) } } diff --git a/src/player.odin b/src/player.odin index 1dd79ae..f204126 100644 --- a/src/player.odin +++ b/src/player.odin @@ -6,22 +6,25 @@ import "core:math" import sapp "shared:sokol/app" import "draw" +import "phys" Player :: struct { - pos: Vec2, - vel: Vec2, + body_handle: phys.Body_Handle, anim: draw.Animation, sprite: draw.Sprite, - on_floor: bool + + jump_buffer: f32, + cyote_time: f32, } PLAYER_SPEED :: 100 PLAYER_ACCEL :: 10 PLAYER_JUMP_FORCE :: 350 -init_player :: proc(p: ^Player) { - p.pos = Vec2{50, 50} +JUMP_BUFFERING :: 0.07 +CYOTE_TIME :: 0.05 +init_player :: proc(p: ^Player) { anim_ok := draw.init_anim_data(&p.anim, "res/robot2.json") if !anim_ok { fmt.println("coult not load animation") @@ -29,6 +32,14 @@ init_player :: proc(p: ^Player) { return } + handle, body := phys.make_body( + &state.physics_world, + phys.Rect{{-8, -16}, {16, 16}}, + ) + p.body_handle = handle + + body.pos = Vec2{50, 50} + draw.init_sprite(&p.sprite, p.anim.image_path, p.anim) p.sprite.offset = Vec2{ @@ -38,6 +49,8 @@ init_player :: proc(p: ^Player) { } deinit_player :: proc(p: ^Player) { + phys.remove_body(&state.physics_world, p.body_handle) + draw.delete_anim_data(p.anim) } @@ -51,6 +64,10 @@ update_player :: proc(p: ^Player, dt: f32) { input += 1 } + if is_keybind_down(state.input, state.input.jump) { + p.jump_buffer = JUMP_BUFFERING + } + if input != 0 { draw.set_sprite_active_tag(&p.sprite, "down_run") p.sprite.scale.x = math.sign(input) @@ -58,60 +75,32 @@ update_player :: proc(p: ^Player, dt: f32) { draw.set_sprite_active_tag(&p.sprite, "down_idle") } - if is_keybind_down(state.input, state.input.jump) && p.on_floor { - p.vel.y = -PLAYER_JUMP_FORCE + body := phys.get_body(state.physics_world, p.body_handle) + + if .DOWN in body.collisions { + p.cyote_time = CYOTE_TIME } - p.vel.x = math.lerp( - p.vel.x, + if p.jump_buffer > 0 && p.cyote_time > 0 { + p.jump_buffer = 0 + p.cyote_time = 0 + body.vel.y = -PLAYER_JUMP_FORCE + } + + body.vel.x = math.lerp( + body.vel.x, input * PLAYER_SPEED, math.pow(0.5, dt * PLAYER_ACCEL), ) - p.vel.y = math.min(p.vel.y + GRAVITY * dt, TERMINAL_VELOCITY) - - res_pos := p.pos + p.vel * dt + body.vel.y = math.min(body.vel.y + GRAVITY * dt, TERMINAL_VELOCITY) - rect_size := Vec2{f32(p.sprite.width), f32(p.sprite.height)} - rect_offset := Vec2{-rect_size.x / 2, -rect_size.y} - - rect := Rect{ - res_pos + rect_offset, - rect_size, - } - - p.on_floor = false - - iter := iter_entity_list(state.platform_list) - for plat in entity_list_iter(&iter) { - if aabb(rect, plat.rect) { - prev_rect := Rect{ - p.pos + rect_offset, - rect_size, - } - if aabb_hori(prev_rect, plat.rect) { - if p.vel.y > 0 { - res_pos.y = plat.rect.start.y - rect.size.y - rect_offset.y - p.on_floor = true - } else { - res_pos.y = plat.rect.start.y + plat.rect.size.y - rect_offset.y - } - p.vel.y = 0 - } else if aabb_vert(prev_rect, plat.rect) { - if p.vel.x > 0 { - res_pos.x = plat.rect.start.x - rect.size.x - rect_offset.x - } else { - res_pos.x = plat.rect.start.x + plat.rect.size.x - rect_offset.x - } - p.vel.x = 0 - } - } - } - - p.pos = res_pos - - p.sprite.pos = p.pos + phys.update_body(&state.physics_world, p.body_handle) + p.sprite.pos = body.pos draw.update_sprite(&p.sprite, dt) + + p.jump_buffer -= dt + p.cyote_time -= dt } draw_player :: proc(p: Player) { -- cgit v1.3-2-g0d8e