// 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 "core:c" import "core:fmt" import "core:log" import "core:math" import "core:math/linalg" import "game:physics" import rl "vendor:raylib" import "vendor:raylib/rlgl" PIXEL_WINDOW_HEIGHT :: 360 Track :: struct { points: [dynamic]rl.Vector3, } World :: struct { track: Track, physics_scene: physics.Scene, } destroy_world :: proc(world: ^World) { delete(world.track.points) physics.destroy_physics_scene(&world.physics_scene) } Car :: struct { pos: rl.Vector3, } SOLVER_CONFIG :: physics.Solver_Config { timestep = 1.0 / 120, gravity = rl.Vector3{0, -9.8, 0}, } Game_Memory :: struct { assetman: assets.Asset_Manager, player_pos: rl.Vector3, camera_yaw_pitch: rl.Vector2, camera_speed: f32, camera: rl.Camera3D, solver_state: physics.Solver_State, car_handle: physics.Body_Handle, es: Editor_State, editor: bool, } Track_Edit_State :: enum { // Point selection Select, // Moving points Move, } Move_Axis :: enum { None, X, Y, Z, XZ, XY, YZ, } Editor_State :: struct { world: World, mouse_captured: bool, point_selection: map[int]bool, track_edit_state: Track_Edit_State, move_axis: Move_Axis, total_movement_world: rl.Vector3, initial_point_pos: rl.Vector3, } g_mem: ^Game_Memory get_world :: proc() -> ^World { return &g_mem.es.world } game_camera :: proc() -> rl.Camera2D { w := f32(rl.GetScreenWidth()) h := f32(rl.GetScreenHeight()) return {zoom = h / PIXEL_WINDOW_HEIGHT, target = g_mem.player_pos.xy, offset = {w / 2, h / 2}} } camera_rotation_matrix :: proc() -> matrix[3, 3]f32 { return linalg.matrix3_from_euler_angles_xy(g_mem.camera_yaw_pitch.x, g_mem.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 { return { position = g_mem.player_pos, up = {0, 1, 0}, fovy = 60, target = g_mem.player_pos + camera_forward_vec(), projection = .PERSPECTIVE, } } return g_mem.camera } ui_camera :: proc() -> rl.Camera2D { return {zoom = f32(rl.GetScreenHeight()) / PIXEL_WINDOW_HEIGHT} } update_free_look_camera :: proc() { es := &g_mem.es 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 } should_capture_mouse := rl.IsMouseButtonDown(.RIGHT) if es.mouse_captured != should_capture_mouse { if should_capture_mouse { rl.DisableCursor() } else { rl.EnableCursor() } } es.mouse_captured = should_capture_mouse if es.mouse_captured { g_mem.camera_yaw_pitch += rl.GetMouseDelta().yx * -1 * 0.001 } g_mem.camera_speed += rl.GetMouseWheelMove() * 0.01 g_mem.camera_speed = linalg.clamp(g_mem.camera_speed, 0.01, 10) rotation_matrix := camera_rotation_matrix() forward := -rotation_matrix[2] right := linalg.cross(rl.Vector3{0, 1, 0}, forward) input = linalg.normalize0(input) g_mem.player_pos += (input.x * right + input.y * forward) * g_mem.camera_speed } 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 := camera_rotation_matrix()[2] append(&get_world().track.points, g_mem.player_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] } update_editor :: proc() { es := &g_mem.es switch es.track_edit_state { case .Select: { if rl.IsKeyPressed(.F) { add_track_spline_point() } if is_point_selected() { if rl.IsKeyPressed(.X) { if len(es.point_selection) <= 1 { for i in es.point_selection { ordered_remove(&get_world().track.points, i) } } else { #reverse for _, i in get_world().track.points { if i in es.point_selection { ordered_remove(&get_world().track.points, i) } } } clear(&es.point_selection) } if rl.IsKeyPressed(.G) { es.track_edit_state = .Move es.move_axis = .None es.total_movement_world = {} // es.initial_point_pos = g_mem.track.points[es.selected_track_point] } } } case .Move: { if rl.IsKeyPressed(.ESCAPE) { es.track_edit_state = .Select // g_mem.track.points[es.selected_track_point] = es.initial_point_pos break } if (rl.IsMouseButtonPressed(.LEFT)) { es.track_edit_state = .Select break } if !es.mouse_captured { // Blender style movement if rl.IsKeyDown(.LEFT_SHIFT) { if rl.IsKeyPressed(.X) { es.move_axis = .YZ } if rl.IsKeyPressed(.Y) { es.move_axis = .XZ } if rl.IsKeyPressed(.Z) { es.move_axis = .XY } } else { if rl.IsKeyPressed(.X) { es.move_axis = .X } if rl.IsKeyPressed(.Y) { es.move_axis = .Y } if rl.IsKeyPressed(.Z) { es.move_axis = .Z } } // log.debugf("Move axis %v", es.move_axis) camera := game_camera_3d() mouse_delta := rl.GetMouseDelta() * 0.05 view_rotation := linalg.transpose(rl.GetCameraMatrix(camera)) view_rotation[3].xyz = 0 view_proj := view_rotation * rl.MatrixOrtho(-1, 1, 1, -1, -1, 1) axes_buf: [2]rl.Vector3 colors_buf: [2]rl.Color axes, _ := get_movement_axes(es.move_axis, &axes_buf, &colors_buf) movement_world: rl.Vector3 for axis in axes { axis_screen := (rl.Vector4{axis.x, axis.y, axis.z, 1} * view_proj).xy axis_screen = linalg.normalize0(axis_screen) movement_screen := linalg.dot(axis_screen, mouse_delta) * axis_screen movement_world += (rl.Vector4{movement_screen.x, movement_screen.y, 0, 1} * rl.MatrixInvert(view_proj)).xyz } for k in es.point_selection { get_world().track.points[k] += movement_world } es.total_movement_world += movement_world } } } } update :: proc() { if rl.IsKeyPressed(.TAB) { g_mem.editor = !g_mem.editor // if g_mem.editor { // rl.EnableCursor() // } else { // rl.DisableCursor() // } } dt := rl.GetFrameTime() if !g_mem.editor { car_model := assets.get_model(&g_mem.assetman, "assets/toyota_corolla_ae86_trueno.glb") car_bounds := rl.GetModelBoundingBox(car_model) g_mem.car_handle = physics.immediate_body( &get_world().physics_scene, &g_mem.solver_state, #hash("car", "fnv32a"), physics.Body_Config { initial_pos = {0, 1, 0}, initial_rot = linalg.QUATERNIONF32_IDENTITY, initial_ang_vel = {0, 0, 0}, mass = 100, inertia_tensor = physics.inertia_tensor_box(car_bounds.max - car_bounds.min), }, ) g_mem.camera.up = rl.Vector3{0, 1, 0} g_mem.camera.fovy = 60 g_mem.camera.projection = .PERSPECTIVE g_mem.camera.target = physics.get_body(&get_world().physics_scene, g_mem.car_handle).x if g_mem.camera.position == {} { g_mem.camera.position = g_mem.camera.target - rl.Vector3{10, 0, 10} } // 1.6 is a good value wheel_extent_x := f32(2.0) rest := f32(0.9) suspension_stiffness := f32(10000) compliance := 1.0 / suspension_stiffness physics.immediate_suspension_constraint( &get_world().physics_scene, &g_mem.solver_state, #hash("FL", "fnv32a"), { rel_pos = {-wheel_extent_x, 0, 2.5}, rel_dir = {0, -1, 0}, rest = rest, compliance = compliance, body = g_mem.car_handle, }, ) physics.immediate_suspension_constraint( &get_world().physics_scene, &g_mem.solver_state, #hash("FR", "fnv32a"), { rel_pos = {wheel_extent_x, 0, 2.5}, rel_dir = {0, -1, 0}, rest = rest, compliance = compliance, body = g_mem.car_handle, }, ) physics.immediate_suspension_constraint( &get_world().physics_scene, &g_mem.solver_state, #hash("RL", "fnv32a"), { rel_pos = {-wheel_extent_x, 0, -3}, rel_dir = {0, -1, 0}, rest = rest, compliance = compliance, body = g_mem.car_handle, }, ) physics.immediate_suspension_constraint( &get_world().physics_scene, &g_mem.solver_state, #hash("RR", "fnv32a"), { rel_pos = {wheel_extent_x, 0, -3}, rel_dir = {0, -1, 0}, rest = rest, compliance = compliance, body = g_mem.car_handle, }, ) } else { update_free_look_camera() update_editor() } physics.simulate( &g_mem.es.world.physics_scene, &g_mem.solver_state, SOLVER_CONFIG, g_mem.editor ? 0 : dt, ) } 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 :: proc() { rl.BeginDrawing() defer rl.EndDrawing() rl.ClearBackground(rl.BLACK) camera := game_camera_3d() points := &get_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), ) car_model := assets.get_model(&g_mem.assetman, "assets/toyota_corolla_ae86_trueno.glb") { rl.BeginMode3D(camera) defer rl.EndMode3D() if collision.hit { // tangent, bitangent := get_point_frame(interpolated_points, segment_idx) // rot_matrix: linalg.Matrix3f32 // rot_matrix[0] = bitangent // rot_matrix[1] = interpolated_points[segment_idx].normal // rot_matrix[2] = -tangent // angle, axis := linalg.angle_axis_from_quaternion( // linalg.quaternion_from_matrix3(rot_matrix), // ) } rl.DrawGrid(100, 1) if !g_mem.editor { car_body := physics.get_body(&get_world().physics_scene, g_mem.car_handle) car_matrix := rl.QuaternionToMatrix(car_body.q) car_model.transform = car_matrix rl.DrawModel(car_model, car_body.x, 1, rl.WHITE) } physics.draw_debug_scene(&get_world().physics_scene) // road: rl.Mesh // defer rl.UnloadMesh(road) // road_vertices: [dynamic]f32 // road_normals: [dynamic]f32 // road_uvs: [dynamic]f32 // road_indices: [dynamic]u16 // road_vertices.allocator = context.temp_allocator // road_normals.allocator = context.temp_allocator // road_uvs.allocator = context.temp_allocator // road_indices.allocator = context.temp_allocator { // Debug draw spline road { 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.initial_point_pos - v.axis * 100000, es.initial_point_pos + v.axis * 100000 rlgl.Vertex3f(start.x, start.y, start.z) rlgl.Vertex3f(end.x, end.y, end.z) } } } } if collision.hit { rl.DrawSphereWires(collision.point, 1, 8, 8, rl.RED) } } { rl.BeginMode2D(ui_camera()) defer rl.EndMode2D() if g_mem.editor { rl.DrawText("Editor", 5, 5, 8, rl.ORANGE) if collision.hit { rl.DrawText(fmt.ctprintf("Segment: %v", segment_idx), 5, 32, 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, ) } } else { car_pos := physics.get_body(&get_world().physics_scene, g_mem.car_handle).x rl.DrawText( fmt.ctprintf("Car Pos: %v. Mesh count: %v", car_pos, car_model.meshCount), 5, 32, 8, rl.ORANGE, ) } } 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, rl.GuiIconName.ICON_TARGET_POINT, )) { inject_at(&get_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, rl.GuiIconName.ICON_TARGET_POINT, )) { inject_at(&get_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, rl.GuiIconName.ICON_TARGET_POINT)) { inject_at(&get_world().track.points, i + 1, middle_pos) log.debugf("add point after %d", i) } } if spline_handle(get_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, icon := rl.GuiIconName.ICON_NONE, 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)) if icon != .ICON_NONE { rl.GuiDrawIcon( icon, c.int(pos.x) - 7, c.int(pos.y) - 7, 1, selected || is_hover ? rl.WHITE : rl.BLACK, ) } // rl.DrawRectangleV(pos, size, selected ? rl.BLUE : (is_hover ? rl.ORANGE : rl.WHITE)) return rl.IsMouseButtonPressed(.LEFT) && is_hover } @(export) game_update :: proc() -> bool { update() draw() return !rl.WindowShouldClose() } @(export) game_init_window :: proc() { rl.SetConfigFlags({.WINDOW_RESIZABLE, .VSYNC_HINT}) rl.InitWindow(1280, 720, "Odin + Raylib + Hot Reload template!") rl.SetExitKey(.KEY_NULL) rl.SetWindowPosition(200, 200) rl.SetTargetFPS(60) } @(export) game_init :: proc() { g_mem = new(Game_Memory) g_mem^ = Game_Memory{} game_hot_reloaded(g_mem) } @(export) game_shutdown :: proc() { assets.shutdown(&g_mem.assetman) destroy_world(get_world()) delete(g_mem.es.point_selection) physics.destroy_solver_state(&g_mem.solver_state) free(g_mem) } @(export) game_shutdown_window :: proc() { rl.CloseWindow() } @(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) } @(export) game_force_reload :: proc() -> bool { return rl.IsKeyPressed(.F5) } @(export) game_force_restart :: proc() -> bool { return rl.IsKeyPressed(.F6) }