// 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)) }