gutter_runner/game/game.odin

573 lines
12 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 "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)
}