// 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:math" import "core:math/linalg" import rl "vendor:raylib" import "vendor:raylib/rlgl" PIXEL_WINDOW_HEIGHT :: 360 Track :: struct { points: [dynamic]rl.Vector3, } World :: struct { track: Track, } Game_Memory :: struct { assetman: assets.Asset_Manager, player_pos: rl.Vector3, camera_yaw_pitch: rl.Vector2, camera_speed: f32, 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, 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 { return { position = g_mem.player_pos, up = {0, 1, 0}, fovy = 60, target = g_mem.player_pos + camera_forward_vec(), projection = .PERSPECTIVE, } } 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) { #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.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 } } } } } update :: proc() { if rl.IsKeyPressed(.TAB) { g_mem.editor = !g_mem.editor if g_mem.editor { rl.EnableCursor() } else { rl.DisableCursor() } } if g_mem.editor { update_free_look_camera() update_editor() } } 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() { rl.BeginMode3D(camera) defer rl.EndMode3D() rl.DrawModel( assets.get_model(&g_mem.assetman, "assets/toyota_corolla_ae86_trueno.glb"), rl.Vector3{0, 0, 0}, 1, rl.WHITE, ) points := &get_world().track.points // 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) interpolated_points := calculate_spline_interpolated_points( points[:], context.temp_allocator, ) 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) } } } } } { rl.BeginMode2D(ui_camera()) defer rl.EndMode2D() if g_mem.editor { rl.DrawText("Editor", 5, 5, 8, rl.ORANGE) } } if g_mem.editor { es := &g_mem.es points := &get_world().track.points points_len := len(points) selected_point := false for i in 0 ..< points_len { 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, ) -> ( clicked: bool, ) { if linalg.dot(camera.target - camera.position, world_pos - camera.position) < 0 { return } pos := rl.GetWorldToScreen(world_pos, camera) size := rl.Vector2{10, 10} 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.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(500) } @(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) delete(get_world().track.points) delete(g_mem.es.point_selection) 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) }