gutter_runner/game/game.odin
2025-07-25 00:38:24 +04:00

1556 lines
39 KiB
Odin

// This file is compiled as part of the `odin.dll` file. It contains the
// procs that `game_hot_reload.exe` will call, such as:
//
// game_init: Sets up the game state
// game_update: Run once per frame
// game_shutdown: Shuts down game and frees memory
// game_memory: Run just before a hot reload, so game.exe has a pointer to the
// game's memory.
// game_hot_reloaded: Run after a hot reload so that the `g_mem` global variable
// can be set to whatever pointer it was in the old DLL.
//
// Note: When compiled as part of the release executable this whole package is imported as a normal
// odin package instead of a DLL.
package game
import "assets"
import "common:name"
import "core:fmt"
import "core:log"
import "core:math"
import "core:math/linalg"
import "game:physics"
import "game:render"
import rl "libs:raylib"
import "libs:raylib/rlgl"
import "libs:tracy"
import "ui"
PIXEL_WINDOW_HEIGHT :: 360
Track :: struct {
points: [dynamic]rl.Vector3,
}
copy_track :: proc(dst, src: ^Track) {
resize(&dst.points, len(src.points))
copy(dst.points[:], src.points[:])
}
destroy_track :: proc(track: ^Track) {
delete(track.points)
}
Debug_Draw_State :: struct {
show_menu: bool,
draw_physics_scene: bool,
}
Scene :: struct {
scene_desc_path: name.Name,
modtime: i64,
level_geoms: []physics.Level_Geom_Handle,
}
scene_destroy :: proc(world: ^World, scene: ^Scene) {
scene.scene_desc_path = name.NONE
sim_state := physics.get_sim_state(&world.physics_scene)
for handle in scene.level_geoms {
physics.remove_level_geom(sim_state, handle)
}
delete(scene.level_geoms)
scene.level_geoms = nil
}
immediate_scene :: proc(world: ^World, scene: ^Scene, path: cstring) {
tracy.Zone()
path_name := name.from_cstring(path)
desc, modtime := assets.get_scene_desc(&g_mem.assetman, path_name)
if scene.scene_desc_path != path_name || scene.modtime != modtime {
scene_destroy(world, scene)
scene.scene_desc_path = path_name
scene.modtime = modtime
scene.level_geoms = make([]physics.Level_Geom_Handle, len(desc.instances))
sim_state := physics.get_sim_state(&world.physics_scene)
for inst, i in desc.instances {
scene.level_geoms[i] = physics.add_level_geom(
sim_state,
physics.Level_Geom_Config {
position = inst.pos,
rotation = inst.rot,
source = physics.Level_Geometry_Asset(inst.model),
},
)
}
}
}
scene_copy :: proc(dst, src: ^Scene) {
tracy.Zone()
dst.scene_desc_path = src.scene_desc_path
if len(dst.level_geoms) != len(src.level_geoms) {
delete(dst.level_geoms)
dst.level_geoms = make([]physics.Level_Geom_Handle, len(src.level_geoms))
}
copy(dst.level_geoms, src.level_geoms)
}
scene_draw :: proc(scene: ^Scene) {
desc, _ := assets.get_scene_desc(&g_mem.assetman, scene.scene_desc_path, true)
for geo in desc.instances {
render.draw_model(
assets.get_model(&g_mem.assetman, name.to_cstring(geo.model)),
{},
auto_cast linalg.matrix4_from_trs(geo.pos, geo.rot, 1),
)
}
}
World :: struct {
player_pos: rl.Vector3,
track: Track,
physics_scene: physics.Scene,
pause: bool,
car_com: rl.Vector3,
car_handle: physics.Body_Handle,
engine_handle: physics.Engine_Handle,
debug_state: Debug_Draw_State,
main_scene: Scene,
}
world_init :: proc(world: ^World) {
physics.scene_init(&world.physics_scene, &g_mem.assetman)
}
copy_world :: proc(dst, src: ^World) {
if dst == src {
return
}
copy_track(&dst.track, &src.track)
physics.copy_physics_scene(&dst.physics_scene, &src.physics_scene)
scene_copy(&dst.main_scene, &src.main_scene)
dst.player_pos = src.player_pos
dst.pause = src.pause
dst.car_com = src.car_com
dst.car_handle = src.car_handle
dst.engine_handle = src.engine_handle
dst.debug_state = src.debug_state
}
destroy_world :: proc(world: ^World) {
destroy_track(&world.track)
scene_destroy(world, &world.main_scene)
physics.scene_destroy(&world.physics_scene)
}
Runtime_World :: struct {
world_snapshots: []World,
current_world_index: int,
camera_yaw_pitch: rl.Vector2,
camera_speed: f32,
camera: rl.Camera3D,
orbit_camera: Orbit_Camera,
camera_mode: Camera_Mode,
dt: f32,
rewind_simulation: bool,
commit_simulation: bool,
single_step_simulation: bool,
}
runtime_world_init :: proc(runtime_world: ^Runtime_World, num_snapshots: int = 2) {
runtime_world.world_snapshots = make([]World, num_snapshots)
world := runtime_world_current_world(runtime_world)
physics.scene_init(&world.physics_scene, &g_mem.assetman)
world.debug_state.show_menu = true
world.debug_state.draw_physics_scene = true
}
runtime_world_current_world :: proc(runtime_world: ^Runtime_World) -> ^World {
return &runtime_world.world_snapshots[runtime_world.current_world_index]
}
runtime_world_next_world :: proc(runtime_world: ^Runtime_World) -> ^World {
next_world_index :=
(runtime_world.current_world_index + 1) %% len(runtime_world.world_snapshots)
return &runtime_world.world_snapshots[next_world_index]
}
runtime_world_destroy :: proc(runtime_world: ^Runtime_World) {
for &world in runtime_world.world_snapshots {
destroy_world(&world)
}
delete(runtime_world.world_snapshots)
}
Car :: struct {
pos: rl.Vector3,
}
SOLVER_CONFIG :: physics.Solver_Config {
timestep = 1.0 / 60,
gravity = rl.Vector3{0, -9.8, 0},
substreps_minus_one = 4 - 1,
}
Game_Memory :: struct {
name_container: name.Container,
assetman: assets.Asset_Manager,
runtime_world: Runtime_World,
es: Editor_State,
ui_context: ui.Context,
default_font: rl.Font,
editor: bool,
preview_bvh: int,
preview_node: int,
physics_pause: bool,
mouse_captured: bool,
free_cam: bool,
}
Camera_Mode :: enum {
Orbit,
Hood,
}
Track_Edit_State :: enum {
// Point selection
Select,
// Moving points
Move,
}
Move_Axis :: enum {
None,
X,
Y,
Z,
XZ,
XY,
YZ,
}
// For undo/redo
World_Stack :: struct {
worlds: []World,
head, tail: int,
}
world_stack_init :: proc(stack: ^World_Stack, num_snapshots: int, allocator := context.allocator) {
assert(num_snapshots > 0)
stack.worlds = make([]World, num_snapshots, allocator)
world_stack_push(stack)
world_init(world_stack_current(stack))
}
world_stack_destroy :: proc(stack: ^World_Stack, allocator := context.allocator) {
for &world in stack.worlds {
destroy_world(&world)
}
delete(stack.worlds, allocator)
}
world_stack_len :: proc(stack: ^World_Stack) -> int {
if stack.tail >= stack.head {
return stack.tail - stack.head
} else {
return (stack.tail + len(stack.worlds)) - stack.head
}
}
world_stack_current :: proc(stack: ^World_Stack) -> ^World {
if world_stack_len(stack) > 0 {
return &stack.worlds[(stack.tail - 1) %% len(stack.worlds)]
}
return nil
}
world_stack_push :: proc(stack: ^World_Stack) {
stack_len := world_stack_len(stack)
assert(stack_len <= len(stack.worlds))
if stack_len == len(stack.worlds) {
stack.head = (stack.head + 1) %% len(stack.worlds)
}
assert(world_stack_len(stack) < len(stack.worlds))
prev_world := world_stack_current(stack)
stack.tail = (stack.tail + 1) %% len(stack.worlds)
new_world := world_stack_current(stack)
if prev_world != nil {
copy_world(new_world, prev_world)
}
}
world_stack_pop :: proc(stack: ^World_Stack) {
if world_stack_len(stack) > 1 {
stack.tail = (stack.tail - 1) %% len(stack.worlds)
}
}
Editor_State :: struct {
world_stack: World_Stack,
mouse_captured: bool,
point_selection: map[int]bool,
track_edit_state: Track_Edit_State,
move_axis: Move_Axis,
prev_mouse_pos: rl.Vector2,
move_initial_pos: rl.Vector3,
total_movement_world: rl.Vector3,
camera: Free_Camera,
}
editor_state_init :: proc(es: ^Editor_State, num_snapshots: int) {
world_stack_init(&es.world_stack, num_snapshots)
world_stack_current(&es.world_stack).debug_state = {
show_menu = true,
draw_physics_scene = true,
}
}
editor_state_destroy :: proc(es: ^Editor_State) {
world_stack_destroy(&es.world_stack)
}
g_mem: ^Game_Memory
get_runtime_world :: proc() -> ^Runtime_World {
return &g_mem.runtime_world
}
get_world :: proc() -> ^World {
return(
g_mem.editor ? world_stack_current(&g_mem.es.world_stack) : &g_mem.runtime_world.world_snapshots[g_mem.runtime_world.current_world_index] \
)
}
get_editor_state :: proc() -> ^Editor_State {
return &g_mem.es
}
game_camera :: proc() -> rl.Camera2D {
w := f32(rl.GetScreenWidth())
h := f32(rl.GetScreenHeight())
return {
zoom = h / PIXEL_WINDOW_HEIGHT,
target = get_world().player_pos.xy,
offset = {w / 2, h / 2},
}
}
camera_rotation_matrix :: proc() -> matrix[3, 3]f32 {
return linalg.matrix3_from_euler_angles_xy(
get_runtime_world().camera_yaw_pitch.x,
get_runtime_world().camera_yaw_pitch.y,
)
}
camera_forward_vec :: proc() -> rl.Vector3 {
rotation_matrix := camera_rotation_matrix()
return rotation_matrix * rl.Vector3{0, 0, 1}
}
game_camera_3d :: proc() -> rl.Camera3D {
if g_mem.editor || g_mem.free_cam {
return free_camera_to_rl(&g_mem.es.camera)
}
return get_runtime_world().camera
}
ui_camera :: proc() -> rl.Camera2D {
return {zoom = 1}
}
select_track_point :: proc(index: int) {
clear(&g_mem.es.point_selection)
g_mem.es.point_selection[index] = true
}
is_point_selected :: proc() -> bool {
return len(g_mem.es.point_selection) > 0
}
add_track_spline_point :: proc() {
forward := -free_camera_rotation(g_mem.es.camera)[2]
append(&get_world().track.points, g_mem.es.camera.pos + forward)
select_track_point(len(&get_world().track.points) - 1)
}
get_movement_axes :: proc(
axis: Move_Axis,
out_axes: ^[2]rl.Vector3,
out_colors: ^[2]rl.Color,
) -> (
axes: []rl.Vector3,
colors: []rl.Color,
) {
switch axis {
case .None:
return out_axes[0:0], {}
case .X:
out_axes[0] = {1, 0, 0}
out_colors[0] = rl.RED
return out_axes[0:1], out_colors[0:1]
case .Y:
out_axes[0] = {0, 1, 0}
out_colors[0] = rl.GREEN
return out_axes[0:1], out_colors[0:1]
case .Z:
out_axes[0] = {0, 0, 1}
out_colors[0] = rl.BLUE
return out_axes[0:1], out_colors[0:1]
case .XZ:
out_axes[0] = {1, 0, 0}
out_axes[1] = {0, 0, 1}
out_colors[0] = rl.RED
out_colors[1] = rl.BLUE
return out_axes[:], out_colors[:]
case .XY:
out_axes[0] = {1, 0, 0}
out_axes[1] = {0, 1, 0}
out_colors[0] = rl.RED
out_colors[1] = rl.GREEN
return out_axes[:], out_colors[:]
case .YZ:
out_axes[0] = {0, 1, 0}
out_axes[1] = {0, 0, 1}
out_colors[0] = rl.GREEN
out_colors[1] = rl.BLUE
return out_axes[:], out_colors[:]
}
return out_axes[0:0], out_colors[0:0]
}
World_Update_Config :: struct {
single_step_physics: bool,
commit_physics: bool,
}
update_world :: proc(world: ^World, dt: f32, config: World_Update_Config) {
if !world.pause {
car_model := assets.get_model(&g_mem.assetman, "assets/ice_cream_truck.glb")
car_bounds := rl.GetModelBoundingBox(car_model)
world.car_com = (car_bounds.min + car_bounds.max) / 2
if true {
physics.immediate_body(
&world.physics_scene,
#hash("floor", "fnv32a"),
physics.Body_Config {
name = name.from_string("Floor"),
initial_pos = {0, -0.5, 0},
initial_rot = linalg.QUATERNIONF32_IDENTITY,
shapes = {
{
rel_q = linalg.QUATERNIONF32_IDENTITY,
inner_shape = physics.Shape_Box{size = {100, 1, 100}},
},
},
},
)
physics.immediate_body(
&world.physics_scene,
#hash("ramp", "fnv32a"),
physics.Body_Config {
name = name.from_string("Ramp"),
initial_pos = {0, 0, 0},
initial_rot = linalg.quaternion_from_euler_angle_x_f32(-10 * math.RAD_PER_DEG),
shapes = {
{
rel_q = linalg.QUATERNIONF32_IDENTITY,
inner_shape = physics.Shape_Box{size = {5, 1, 100}},
},
},
},
)
}
body_low_convex := assets.get_convex(
&g_mem.assetman,
"assets/ice_cream_truck_body_low_convex.obj",
)
body_high_convex := assets.get_convex(
&g_mem.assetman,
"assets/ice_cream_truck_body_high_convex.obj",
)
cone_convex := assets.get_convex(&g_mem.assetman, "assets/cone_convex.obj")
left_fence_convex := assets.get_convex(&g_mem.assetman, "assets/left_fence_convex.obj")
right_fence_convex := assets.get_convex(&g_mem.assetman, "assets/right_fence_convex.obj")
world.car_handle = physics.immediate_body(
&world.physics_scene,
#hash("car", "fnv32a"),
physics.Body_Config {
name = name.from_string("Car"),
initial_pos = {0, 4, -10},
initial_rot = linalg.QUATERNIONF32_IDENTITY,
// initial_rot = linalg.quaternion_angle_axis(
// math.RAD_PER_DEG * 180,
// rl.Vector3{0, 0, 1},
// ) *
// linalg.quaternion_angle_axis(math.RAD_PER_DEG * 30, rl.Vector3{1, 0, 0}),
initial_ang_vel = {0, 0, 0},
shapes = {
{
rel_q = linalg.QUATERNIONF32_IDENTITY,
inner_shape = physics.Shape_Convex {
mesh = body_low_convex.mesh,
center_of_mass = body_low_convex.center_of_mass,
inertia_tensor = auto_cast body_low_convex.inertia_tensor,
total_volume = body_low_convex.total_volume,
},
},
{
rel_q = linalg.QUATERNIONF32_IDENTITY,
inner_shape = physics.Shape_Convex {
mesh = body_high_convex.mesh,
center_of_mass = body_high_convex.center_of_mass,
inertia_tensor = auto_cast body_high_convex.inertia_tensor,
total_volume = body_high_convex.total_volume,
},
},
{
rel_q = linalg.QUATERNIONF32_IDENTITY,
density_minus_one = 0.2 - 1,
inner_shape = physics.Shape_Convex {
mesh = cone_convex.mesh,
center_of_mass = cone_convex.center_of_mass,
inertia_tensor = auto_cast cone_convex.inertia_tensor,
total_volume = cone_convex.total_volume,
},
},
{
rel_q = linalg.QUATERNIONF32_IDENTITY,
density_minus_one = 0.2 - 1,
inner_shape = physics.Shape_Convex {
mesh = left_fence_convex.mesh,
center_of_mass = left_fence_convex.center_of_mass,
inertia_tensor = auto_cast left_fence_convex.inertia_tensor,
total_volume = left_fence_convex.total_volume,
},
},
{
rel_q = linalg.QUATERNIONF32_IDENTITY,
density_minus_one = 0.2 - 1,
inner_shape = physics.Shape_Convex {
mesh = right_fence_convex.mesh,
center_of_mass = right_fence_convex.center_of_mass,
inertia_tensor = auto_cast right_fence_convex.inertia_tensor,
total_volume = right_fence_convex.total_volume,
},
},
},
mass = 1000,
com_shift = physics.Vec3{0, 0, -0.5},
},
)
if true {
for x in 0 ..< 10 {
for y in 0 ..< 10 {
box_name := name.from_string(fmt.tprintf("box[{},{}]", x, y))
physics.immediate_body(
&world.physics_scene,
u32(box_name),
physics.Body_Config {
name = box_name,
initial_pos = {-5, 0.5 + f32(y) * 1.1, f32(x) * 3 + 10},
initial_rot = linalg.QUATERNIONF32_IDENTITY,
shapes = {
{
rel_q = linalg.QUATERNIONF32_IDENTITY,
inner_shape = physics.Shape_Box{size = 1},
},
},
mass = 10,
},
)
}
}
}
// car_body := physics.get_body(&world.physics_scene, runtime_world.car_handle)
// camera := &runtime_world.camera
// camera.up = rl.Vector3{0, 1, 0}
// camera.fovy = 60
// camera.projection = .PERSPECTIVE
// camera.position = physics.body_local_to_world(
// car_body,
// physics.body_world_to_local(
// car_body,
// physics.body_local_to_world(car_body, rl.Vector3{1, 0, -2}),
// ),
// )
// camera.target = physics.get_body(&world.physics_scene, runtime_world.car_handle).x
// if runtime_world.camera.position == {} {
// runtime_world.camera.position = runtime_world.camera.target - rl.Vector3{10, 0, 10}
// }
sim_state := physics.get_sim_state(&world.physics_scene)
wheel_extent_x_front := f32(2) / 2
wheel_extent_x_back := f32(2) / 2
wheel_y := f32(0.5)
rest := f32(0.7)
natural_frequency := f32(0.4)
damping := f32(0.06)
radius := f32(0.737649) / 2
wheel_front_z := f32(1.6)
wheel_back_z := f32(-1.63)
wheel_mass := f32(14)
pacejka_long := physics.slice_to_pacejka94_long(
assets.get_curve_1d(&g_mem.assetman, "assets/tyre_longitudinal.csv"),
)
pacejka_lat := physics.slice_to_pacejka94_lat(
assets.get_curve_1d(&g_mem.assetman, "assets/tyre_lateral.csv"),
)
wheel_fl := physics.immediate_suspension_constraint(
&world.physics_scene,
#hash("FL", "fnv32a"),
{
rel_pos = {-wheel_extent_x_front, wheel_y, wheel_front_z},
rel_dir = {0, -1, 0},
radius = radius,
rest = rest,
natural_frequency = natural_frequency,
damping = damping,
body = world.car_handle,
mass = wheel_mass,
pacejka_lat = pacejka_lat,
pacejka_long = pacejka_long,
},
)
wheel_fr := physics.immediate_suspension_constraint(
&world.physics_scene,
#hash("FR", "fnv32a"),
{
rel_pos = {wheel_extent_x_front, wheel_y, wheel_front_z},
rel_dir = {0, -1, 0},
radius = radius,
rest = rest,
natural_frequency = natural_frequency,
damping = damping,
body = world.car_handle,
mass = wheel_mass,
pacejka_lat = pacejka_lat,
pacejka_long = pacejka_long,
},
)
wheel_rl := physics.immediate_suspension_constraint(
&world.physics_scene,
#hash("RL", "fnv32a"),
{
rel_pos = {-wheel_extent_x_back, wheel_y, wheel_back_z},
rel_dir = {0, -1, 0},
radius = radius,
rest = rest,
natural_frequency = natural_frequency,
damping = damping,
body = world.car_handle,
mass = wheel_mass,
pacejka_lat = pacejka_lat,
pacejka_long = pacejka_long,
},
)
wheel_rr := physics.immediate_suspension_constraint(
&world.physics_scene,
#hash("RR", "fnv32a"),
{
rel_pos = {wheel_extent_x_back, wheel_y, wheel_back_z},
rel_dir = {0, -1, 0},
radius = radius,
rest = rest,
natural_frequency = natural_frequency,
damping = damping,
body = world.car_handle,
mass = wheel_mass,
pacejka_lat = pacejka_lat,
pacejka_long = pacejka_long,
},
)
world.engine_handle = physics.immediate_engine(
&world.physics_scene,
#hash("engine", "fnv32a"),
physics.Engine_Config {
rpm_torque_curve = assets.get_curve_2d(
&g_mem.assetman,
"assets/ae86_rpm_torque.csv",
),
lowest_rpm = 1200,
rev_limit_rpm = 7800,
rev_limit_interval = 0.01,
inertia = 0.264 * 0.5,
internal_friction = 0.005,
gear_ratios = []f32{3.48, 3.587, 2.022, 1.384, 1, 0.861},
axle = physics.Drive_Axle_Config {
wheels = {wheel_rl, wheel_rr},
wheel_count = 2,
diff_type = .Fixed,
final_drive_ratio = 4.1,
},
},
)
drive_wheels := []physics.Suspension_Constraint_Handle{wheel_rl, wheel_rr}
turn_wheels := []physics.Suspension_Constraint_Handle{wheel_fl, wheel_fr}
front_wheels := turn_wheels
back_wheels := drive_wheels
DRIVE_IMPULSE :: 3000
BRAKE_IMPULSE :: 10
TURN_ANGLE :: -f32(50) * math.RAD_PER_DEG
// 68% front, 32% rear
BRAKE_BIAS :: f32(0.68)
gas_input := rl.GetGamepadAxisMovement(0, .RIGHT_TRIGGER) * 0.5 + 0.5
brake_input := rl.GetGamepadAxisMovement(0, .LEFT_TRIGGER) * 0.5 + 0.5
if rl.IsKeyDown(.S) && !g_mem.free_cam {
brake_input = 1
}
if rl.IsKeyDown(.W) && !g_mem.free_cam {
gas_input = 1
}
for wheel_handle in front_wheels {
wheel := physics.get_suspension_constraint(sim_state, wheel_handle)
wheel.brake_impulse = brake_input * BRAKE_BIAS * BRAKE_IMPULSE
}
for wheel_handle in back_wheels {
wheel := physics.get_suspension_constraint(sim_state, wheel_handle)
wheel.brake_impulse = brake_input * (1.0 - BRAKE_BIAS) * BRAKE_IMPULSE
}
engine := physics.get_engine(sim_state, world.engine_handle)
engine.throttle = gas_input
if rl.IsKeyPressed(.LEFT_SHIFT) || rl.IsGamepadButtonPressed(0, .RIGHT_FACE_DOWN) {
engine.gear += 1
}
if rl.IsKeyPressed(.LEFT_CONTROL) || rl.IsGamepadButtonPressed(0, .RIGHT_FACE_LEFT) {
engine.gear -= 1
}
car_body := physics.get_body(sim_state, world.car_handle)
turn_vel_correction := clamp(4.0 / linalg.length(car_body.v), 0, 1)
turn_input := rl.GetGamepadAxisMovement(0, .LEFT_X)
if abs(turn_input) < GAMEPAD_DEADZONE {
turn_input = 0
}
if rl.IsKeyDown(.A) && !g_mem.free_cam {
turn_input = -1
}
if rl.IsKeyDown(.D) && !g_mem.free_cam {
turn_input = 1
}
for wheel_handle in turn_wheels {
wheel := physics.get_suspension_constraint(sim_state, wheel_handle)
wheel.turn_angle = TURN_ANGLE * turn_vel_correction * turn_input
}
immediate_scene(world, &world.main_scene, "assets/blender/test_level_blend/Scene.scn")
if len(world.track.points) > 1 {
interpolated_points := calculate_spline_interpolated_points(
world.track.points[:],
context.temp_allocator,
)
track_verts, track_inds := spline_generate_mesh(
interpolated_points,
context.temp_allocator,
)
physics.immediate_level_geom(
&world.physics_scene,
#hash("track", "fnv32a"),
{
rotation = linalg.QUATERNIONF32_IDENTITY,
source = physics.Level_Geometry_Mesh {
vertices = track_verts,
indices = track_inds,
},
},
)
}
physics.simulate(
&world.physics_scene,
SOLVER_CONFIG,
dt,
commit = config.commit_physics,
step_mode = config.single_step_physics ? physics.Step_Mode.Single : physics.Step_Mode.Accumulated_Time,
)
}
{
engine_loop := assets.get_music(&g_mem.assetman, "assets/engine_loop.wav")
if !rl.IsMusicStreamPlaying(engine_loop) {
rl.PlayMusicStream(engine_loop)
}
rl.UpdateMusicStream(engine_loop)
sim_state := physics.get_sim_state(&world.physics_scene)
engine := physics.get_engine(sim_state, world.engine_handle)
rpm_percent :=
max(physics.angular_velocity_to_rpm(engine.w) - engine.lowest_rpm, 0.0) /
(engine.rev_limit_rpm - engine.lowest_rpm)
pitch := linalg.lerp(f32(1.0), f32(3.0), rpm_percent)
rl.SetMusicPitch(engine_loop, pitch)
}
}
update_runtime_world :: proc(runtime_world: ^Runtime_World, dt: f32) {
cur_world := runtime_world_current_world(runtime_world)
runtime_world.dt = dt
should_single_step := rl.IsKeyPressed(.PERIOD)
runtime_world.rewind_simulation = rl.IsKeyPressed(.COMMA)
runtime_world.commit_simulation = !g_mem.physics_pause || should_single_step
runtime_world.single_step_simulation = should_single_step
if !runtime_world.rewind_simulation {
next_world := runtime_world_next_world(runtime_world)
copy_world(next_world, cur_world)
config := World_Update_Config {
commit_physics = true,
single_step_physics = should_single_step,
}
update_world(next_world, dt, config)
if runtime_world.commit_simulation {
runtime_world.current_world_index =
(runtime_world.current_world_index + 1) %% len(runtime_world.world_snapshots)
}
} else {
runtime_world.current_world_index =
(runtime_world.current_world_index - 1) %% len(runtime_world.world_snapshots)
}
}
Free_Camera :: struct {
pos: rl.Vector3,
yaw, pitch: f32,
speed: f32,
}
Orbit_Camera :: struct {
target: rl.Vector3,
yaw, pitch: f32,
distance: f32,
}
GAMEPAD_DEADZONE :: f32(0.07)
collect_camera_input :: proc() -> (delta: rl.Vector2, sense: f32) {
gamepad_delta := rl.Vector2 {
rl.GetGamepadAxisMovement(0, .RIGHT_X),
rl.GetGamepadAxisMovement(0, .RIGHT_Y),
}
if abs(gamepad_delta.x) < GAMEPAD_DEADZONE {
gamepad_delta.x = 0
}
if abs(gamepad_delta.y) < GAMEPAD_DEADZONE {
gamepad_delta.y = 0
}
should_capture_mouse := rl.IsMouseButtonDown(.RIGHT)
if g_mem.mouse_captured != should_capture_mouse {
if should_capture_mouse {
rl.DisableCursor()
} else {
rl.EnableCursor()
}
g_mem.mouse_captured = should_capture_mouse
}
mouse_delta := g_mem.mouse_captured ? rl.GetMouseDelta() : 0
MOUSE_SENSE :: 0.005
GAMEPAD_SENSE :: 1
if linalg.length2(mouse_delta) > linalg.length2(gamepad_delta) {
sense = MOUSE_SENSE
delta = mouse_delta
} else {
sense = GAMEPAD_SENSE * rl.GetFrameTime()
delta = gamepad_delta
}
return
}
orbit_camera_update :: proc(camera: ^Orbit_Camera) {
world := runtime_world_current_world(get_runtime_world())
camera.target =
physics.get_body(physics.get_sim_state(&world.physics_scene), world.car_handle).x
delta, sense := collect_camera_input()
camera.yaw += delta.x * sense
camera.pitch += delta.y * sense
camera.pitch = math.clamp(camera.pitch, -math.PI / 2.0 + 0.0001, math.PI / 2.0 - 0.0001)
}
orbit_camera_to_rl :: proc(camera: Orbit_Camera) -> rl.Camera3D {
result: rl.Camera3D
result.target = camera.target
rotation :=
linalg.matrix3_rotate(-camera.yaw, rl.Vector3{0, 1, 0}) *
linalg.matrix3_rotate(-camera.pitch, rl.Vector3{1, 0, 0})
// rotation = linalg.transpose(rotation)
position := rotation * rl.Vector3{0, 0, 1}
position *= camera.distance
result.position = result.target + position
result.up = rl.Vector3{0, 1, 0}
result.fovy = 60
return result
}
Matrix3 :: # row_major matrix[3, 3]f32
free_camera_rotation :: proc(camera: Free_Camera) -> linalg.Matrix3f32 {
return(
linalg.matrix3_rotate_f32(camera.yaw, {0, 1, 0}) *
linalg.matrix3_rotate_f32(camera.pitch, {1, 0, 0}) \
)
}
free_camera_update :: proc(camera: ^Free_Camera) {
delta, sense := collect_camera_input()
camera.yaw -= delta.x * sense
camera.pitch -= delta.y * sense
camera.pitch = math.clamp(camera.pitch, -math.PI / 2.0 + 0.0001, math.PI / 2.0 - 0.0001)
input: rl.Vector2
if rl.IsKeyDown(.UP) || rl.IsKeyDown(.W) {
input.y += 1
}
if rl.IsKeyDown(.DOWN) || rl.IsKeyDown(.S) {
input.y -= 1
}
if rl.IsKeyDown(.LEFT) || rl.IsKeyDown(.A) {
input.x -= 1
}
if rl.IsKeyDown(.RIGHT) || rl.IsKeyDown(.D) {
input.x += 1
}
camera.speed += rl.GetMouseWheelMove() * 0.01
camera.speed = linalg.clamp(camera.speed, 0.01, 10)
rotation := free_camera_rotation(camera^)
forward := -rotation[2]
right := rotation[0]
input = linalg.normalize0(input)
camera.pos += (input.x * right + input.y * forward) * camera.speed
}
free_camera_to_rl :: proc(camera: ^Free_Camera) -> (result: rl.Camera3D) {
rotation := free_camera_rotation(camera^)
forward := -rotation[2]
result.position = camera.pos
result.target = camera.pos + forward
result.up = rl.Vector3{0, 1, 0}
result.fovy = 60
return result
}
update :: proc() {
tracy.Zone()
ui.rl_update_inputs(&g_mem.ui_context)
ui.begin(&g_mem.ui_context)
if rl.IsKeyPressed(.TAB) {
g_mem.editor = !g_mem.editor
// When switching from editor to game, copy editor world into game world
if !g_mem.editor {
es := &g_mem.es
editor_world := world_stack_current(&es.world_stack)
copy_world(runtime_world_current_world(&g_mem.runtime_world), editor_world)
}
}
if rl.IsKeyPressed(.F1) {
g_mem.free_cam = !g_mem.free_cam
// g_mem.es.world.player_pos = g_mem.runtime_world.camera.position
}
if rl.IsKeyPressed(.F2) && !g_mem.free_cam {
cam_mode := &get_runtime_world().camera_mode
switch cam_mode^ {
case .Orbit:
cam_mode^ = .Hood
case .Hood:
cam_mode^ = .Orbit
}
}
dt := rl.GetFrameTime()
// Debug BVH traversal
// mesh_bvh := assets.get_bvh(&g_mem.assetman, "assets/toyota_corolla_ae86_trueno.glb")
// if rl.IsKeyDown(.LEFT_SHIFT) {
// if g_mem.preview_bvh >= 0 && g_mem.preview_bvh < len(mesh_bvh.bvhs) {
// b := mesh_bvh.bvhs[g_mem.preview_bvh]
// node := &b.nodes[g_mem.preview_node]
// if !bvh.is_leaf_node(node^) {
// if rl.IsKeyPressed(.LEFT_BRACKET) {
// g_mem.preview_node = int(node.child_or_prim_start)
// } else if rl.IsKeyPressed(.RIGHT_BRACKET) {
// g_mem.preview_node = int(node.child_or_prim_start + 1)
// } else if rl.IsKeyPressed(.P) {
// g_mem.preview_node = 0
// }
// }
// }
// } else {
// if rl.IsKeyPressed(.LEFT_BRACKET) {
// g_mem.preview_bvh -= 1
// g_mem.preview_node = 0
// }
// if rl.IsKeyPressed(.RIGHT_BRACKET) {
// g_mem.preview_bvh += 1
// g_mem.preview_node = 0
// }
// }
if rl.IsKeyPressed(.SPACE) {
g_mem.physics_pause = !g_mem.physics_pause
}
if g_mem.editor {
update_editor(get_editor_state(), dt)
} else {
update_runtime_world(get_runtime_world(), dt)
world := runtime_world_current_world(get_runtime_world())
if g_mem.free_cam {
free_camera_update(&g_mem.es.camera)
} else {
switch get_runtime_world().camera_mode {
case .Orbit:
orbit_camera_update(&get_runtime_world().orbit_camera)
get_runtime_world().camera = orbit_camera_to_rl(get_runtime_world().orbit_camera)
case .Hood:
car := physics.get_body(
physics.get_sim_state(&world.physics_scene),
world.car_handle,
)
cam: rl.Camera3D
cam.position = physics.body_local_to_world(car, physics.Vec3{0, 0.9, 2})
cam.target =
cam.position + physics.body_local_to_world_vec(car, physics.Vec3{0, 0, 1})
cam.up = physics.body_local_to_world_vec(car, physics.Vec3{0, 1, 0})
cam.fovy = 60
cam.projection = .PERSPECTIVE
get_runtime_world().camera = cam
}
}
}
}
catmull_rom_coefs :: proc(
v0, v1, v2, v3: rl.Vector3,
alpha, tension: f32,
) -> (
a, b, c, d: rl.Vector3,
) {
t01 := math.pow(linalg.distance(v0, v1), alpha)
t12 := math.pow(linalg.distance(v1, v2), alpha)
t23 := math.pow(linalg.distance(v2, v3), alpha)
m1 := (1.0 - tension) * (v2 - v1 + t12 * ((v1 - v0) / t01 - (v2 - v0) / (t01 + t12)))
m2 := (1.0 - tension) * (v2 - v1 + t12 * ((v3 - v2) / t23 - (v3 - v1) / (t12 + t23)))
a = 2.0 * (v1 - v2) + m1 + m2
b = -3.0 * (v1 - v2) - m1 - m1 - m2
c = m1
d = v1
return
}
catmull_rom :: proc(a, b, c, d: rl.Vector3, t: f32) -> rl.Vector3 {
t2 := t * t
t3 := t2 * t
return a * t3 + b * t2 + c * t + d
}
draw_world :: proc(world: ^World) {
tracy.Zone()
sim_state := physics.get_sim_state(&world.physics_scene)
car_model := assets.get_model(&g_mem.assetman, "assets/ice_cream_truck.glb")
phys_debug_state: physics.Debug_State
physics.init_debug_state(&phys_debug_state)
car_body := physics.get_body(sim_state, world.car_handle)
engine := physics.get_engine(sim_state, world.engine_handle)
{
if rl.IsKeyPressed(.F8) {
world.debug_state.show_menu = !world.debug_state.show_menu
}
if world.debug_state.show_menu {
ui_ctx := &g_mem.ui_context
//ui.push_font_size_style(ui_ctx, 20)
// defer ui.pop_style(ui_ctx)
if ui.window(ui_ctx, "Debug Menu", {x = 0, y = 0, w = 200, h = 300}) {
cnt := ui.get_current_container(ui_ctx)
cnt.rect.x = max(cnt.rect.x, 0)
cnt.rect.y = max(cnt.rect.y, 0)
cnt.rect.w = max(cnt.rect.w, 200)
cnt.rect.h = max(cnt.rect.h, 300)
if .ACTIVE in ui.header(ui_ctx, "General", {.EXPANDED}) {
ui.checkbox(
ui_ctx,
"Show Physics Scene",
&world.debug_state.draw_physics_scene,
)
}
if .ACTIVE in ui.header(ui_ctx, "Car", {.EXPANDED}) &&
car_body.alive &&
engine.alive {
gear_ratios := physics.get_gear_ratios(sim_state, engine.gear_ratios)
ui.keyval(ui_ctx, "p", car_body.x)
ui.keyval(ui_ctx, "v", car_body.v)
ui.keyval(ui_ctx, "gear", engine.gear)
ui.keyval(ui_ctx, "ratio", physics.lookup_gear_ratio(gear_ratios, engine.gear))
ui.keyval(ui_ctx, "rpm", physics.angular_velocity_to_rpm(engine.w))
ui.keyval(ui_ctx, "clutch", engine.clutch)
ui.keyval(ui_ctx, "speed", linalg.length(car_body.v) * 3.6)
}
if .ACTIVE in ui.header(ui_ctx, "Physics") {
physics.draw_debug_ui(
&phys_debug_state,
ui_ctx,
&world.physics_scene,
SOLVER_CONFIG,
)
}
} else {
log.infof("Window closed")
world.debug_state.show_menu = false
}
}
}
if world.debug_state.draw_physics_scene {
physics.draw_debug_scene(&world.physics_scene, &phys_debug_state)
}
car_matrix := rl.QuaternionToMatrix(car_body.q)
car_matrix =
(auto_cast linalg.matrix4_translate_f32(physics.body_get_shape_pos(car_body))) * car_matrix
// basic_shader := assets.get_shader(
// &g_mem.assetman,
// "assets/shaders/lit_vs.glsl",
// "assets/shaders/lit_ps.glsl",
// {.Ambient, .LightDir, .LightColor},
// )
// light_dir := linalg.normalize(rl.Vector3{1, -1, 0})
// ambient := rl.Vector3{0.1, 0.1, 0.1}
// light_color := rl.Vector3{0.816, 0.855, 0.89}
// rl.SetShaderValue(basic_shader.shader, basic_shader.locations[.LightDir], &light_dir, .VEC3)
// rl.SetShaderValue(basic_shader.shader, basic_shader.locations[.Ambient], &ambient, .VEC3)
// rl.SetShaderValue(
// basic_shader.shader,
// basic_shader.locations[.LightColor],
// &light_color,
// .VEC3,
// )
scene_draw(&world.main_scene)
render.draw_model(car_model, {}, car_matrix)
render.draw_mesh_light(
assets.get_model(&g_mem.assetman, "assets/ae86_lights.glb"),
car_matrix,
rl.Color{255, 255, 255, 100},
)
}
draw :: proc() {
tracy.Zone()
rl.BeginDrawing()
defer rl.EndDrawing()
rl.ClearBackground(rl.GRAY)
render.clear_stencil()
world := get_world()
camera := game_camera_3d()
points := &world.track.points
interpolated_points := calculate_spline_interpolated_points(points[:], context.temp_allocator)
// collision, segment_idx := raycast_spline_tube(
// interpolated_points,
// rl.GetScreenToWorldRay(rl.GetMousePosition(), camera),
// )
{
rl.BeginMode3D(camera)
defer rl.EndMode3D()
draw_world(world)
{
// Debug draw spline road
if false {
rlgl.EnableWireMode()
defer rlgl.DisableWireMode()
rlgl.Color3f(1, 0, 0)
debug_draw_spline(interpolated_points)
debug_draw_spline_mesh(interpolated_points)
}
if g_mem.editor {
es := &g_mem.es
switch es.track_edit_state {
case .Select:
case .Move:
rlgl.Begin(rlgl.LINES)
defer rlgl.End()
axes_buf: [2]rl.Vector3
colors_buf: [2]rl.Color
axes, colors := get_movement_axes(es.move_axis, &axes_buf, &colors_buf)
for v in soa_zip(axis = axes, color = colors) {
rlgl.Color4ub(v.color.r, v.color.g, v.color.b, v.color.a)
start, end :=
es.move_initial_pos -
v.axis * 100000,
es.move_initial_pos +
v.axis * 100000
rlgl.Vertex3f(start.x, start.y, start.z)
rlgl.Vertex3f(end.x, end.y, end.z)
}
}
}
}
}
{
ui.end(&g_mem.ui_context)
rl.BeginMode2D(ui_camera())
defer rl.EndMode2D()
rl.DrawFPS(0, 0)
if g_mem.editor {
rl.DrawText("Editor", 5, 5, 8, rl.ORANGE)
switch g_mem.es.track_edit_state {
case .Select:
case .Move:
rl.DrawText(
fmt.ctprintf("%v %v", g_mem.es.move_axis, g_mem.es.total_movement_world),
5,
16,
8,
rl.ORANGE,
)
}
}
ui.rl_draw(&g_mem.ui_context)
}
if g_mem.editor {
es := &g_mem.es
points_len := len(points)
if points_len > 0 {
// Add point before first
{
tangent: rl.Vector3
if points_len > 1 {
tangent = linalg.normalize0(points[1] - points[0])
} else {
tangent = rl.Vector3{-1, 0, 0}
}
new_point_pos := points[0] - tangent * 4
if (spline_handle(new_point_pos, camera, false)) {
inject_at(&world.track.points, 0, new_point_pos)
log.debugf("add point before 0")
}
}
// Add point after last
{
tangent: rl.Vector3
if points_len > 1 {
tangent = linalg.normalize0(points[points_len - 1] - points[points_len - 2])
} else {
tangent = rl.Vector3{-1, 0, 0}
}
new_point_pos := points[points_len - 1] + tangent * 4
if (spline_handle(new_point_pos, camera, false)) {
inject_at(&world.track.points, points_len - 1 + 1, new_point_pos)
log.debugf("add point before 0")
}
}
}
selected_point := false
for i in 0 ..< points_len {
if i < points_len - 1 {
t := (f32(i) + 0.5) / f32(points_len)
middle_pos := sample_spline(points[:], t)
if (spline_handle(middle_pos, camera, false)) {
inject_at(&world.track.points, i + 1, middle_pos)
log.debugf("add point after %d", i)
}
}
if spline_handle(world.track.points[i], camera, es.point_selection[i]) {
if !rl.IsKeyDown(.LEFT_CONTROL) {
clear(&g_mem.es.point_selection)
}
g_mem.es.point_selection[i] = true
selected_point = true
}
}
if rl.IsMouseButtonPressed(.LEFT) && !selected_point {
clear(&g_mem.es.point_selection)
}
}
// axis lines
if g_mem.editor {
size := f32(100)
pos := rl.Vector2{20, f32(rl.GetScreenHeight()) - 20 - size}
view_rotation := linalg.transpose(rl.GetCameraMatrix(camera))
view_rotation[3].xyz = 0
view_proj := view_rotation * rl.MatrixOrtho(-1, 1, 1, -1, -1, 1)
center := (rl.Vector4{0, 0, 0, 1} * view_proj).xy * 0.5 + 0.5
x_axis := (rl.Vector4{1, 0, 0, 1} * view_proj).xy * 0.5 + 0.5
y_axis := (rl.Vector4{0, 1, 0, 1} * view_proj).xy * 0.5 + 0.5
z_axis := (rl.Vector4{0, 0, 1, 1} * view_proj).xy * 0.5 + 0.5
old_width := rlgl.GetLineWidth()
rlgl.SetLineWidth(4)
defer rlgl.SetLineWidth(old_width)
rl.DrawLineV(pos + center * size, pos + x_axis * size, rl.RED)
rl.DrawLineV(pos + center * size, pos + y_axis * size, rl.GREEN)
rl.DrawLineV(pos + center * size, pos + z_axis * size, rl.BLUE)
}
}
spline_handle :: proc(
world_pos: rl.Vector3,
camera: rl.Camera,
selected: bool,
size := f32(20),
) -> (
clicked: bool,
) {
if linalg.dot(camera.target - camera.position, world_pos - camera.position) < 0 {
return
}
pos := rl.GetWorldToScreen(world_pos, camera)
min, max := pos - size, pos + size
mouse_pos := rl.GetMousePosition()
is_hover :=
(mouse_pos.x >= min.x &&
mouse_pos.y >= min.y &&
mouse_pos.x <= max.x &&
mouse_pos.y <= max.y)
rl.DrawCircleV(pos, size / 2, selected ? rl.BLUE : (is_hover ? rl.ORANGE : rl.WHITE))
return rl.IsMouseButtonPressed(.LEFT) && is_hover
}
@(export)
game_update :: proc() -> bool {
tracy.Zone()
defer tracy.FrameMark()
assets.assetman_tick(&g_mem.assetman)
update()
draw()
when ODIN_OS != .JS {
// Never run this proc in browser. It contains a 16 ms sleep on web!
return !rl.WindowShouldClose()
}
return true
}
@(export)
game_init_window :: proc(args: []string) {
tracy.SetThreadName("Main")
init_physfs(args)
rl.SetConfigFlags({.WINDOW_RESIZABLE, .VSYNC_HINT})
rl.InitWindow(1280, 720, "Gutter Runner")
rl.InitAudioDevice()
rl.SetExitKey(.KEY_NULL)
rl.SetWindowPosition(200, 200)
rl.SetTargetFPS(120)
}
DEV_BUILD :: #config(DEV, false)
@(export)
game_init :: proc() {
g_mem = new(Game_Memory)
g_mem^ = Game_Memory{}
name.init(&g_mem.name_container)
name.setup_global_container(&g_mem.name_container)
init_physifs_raylib_callbacks()
assets.assetman_init(&g_mem.assetman)
editor_state_init(&g_mem.es, 100)
runtime_world_init(&g_mem.runtime_world, DEV_BUILD ? 100 : 1)
g_mem.default_font = rl.GetFontDefault()
ui.init(&g_mem.ui_context)
g_mem.ui_context.default_style.font = ui.Font(&g_mem.default_font)
g_mem.ui_context.default_style.font_size = 20
g_mem.ui_context.text_size = ui.rl_measure_text_2d
log.debugf("game_init")
game_hot_reloaded(g_mem)
}
@(export)
game_shutdown :: proc() {
name.destroy()
assets.shutdown(&g_mem.assetman)
editor_state_destroy(&g_mem.es)
delete(g_mem.es.point_selection)
runtime_world_destroy(&g_mem.runtime_world)
free(g_mem)
}
@(export)
game_shutdown_window :: proc() {
rl.CloseAudioDevice()
rl.CloseWindow()
deinit_physfs()
}
@(export)
game_memory :: proc() -> rawptr {
return g_mem
}
@(export)
game_memory_size :: proc() -> int {
return size_of(Game_Memory)
}
@(export)
game_hot_reloaded :: proc(mem: rawptr) {
g_mem = (^Game_Memory)(mem)
name.setup_global_container(&g_mem.name_container)
render.init(&g_mem.assetman)
ui.rl_init()
g_mem.runtime_world.orbit_camera.distance = 6
log.debugf("hot reloaded")
}
@(export)
game_force_reload :: proc() -> bool {
return rl.IsKeyPressed(.F5)
}
@(export)
game_force_restart :: proc() -> bool {
return rl.IsKeyPressed(.F6)
}
game_parent_window_size_changed :: proc(w, h: int) {
rl.SetWindowSize(i32(w), i32(h))
}