gutter_runner/game/game.odin
2025-01-03 00:42:38 +04:00

512 lines
11 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/linalg"
import rl "vendor:raylib"
import "vendor:raylib/rlgl"
PIXEL_WINDOW_HEIGHT :: 360
Track :: struct {
points: [dynamic]rl.Vector3,
}
Game_Memory :: struct {
assetman: assets.Asset_Manager,
player_pos: rl.Vector3,
camera_yaw_pitch: rl.Vector2,
camera_speed: f32,
track: Track,
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 {
mouse_captured: bool,
selected_track_point: int,
track_edit_state: Track_Edit_State,
move_axis: Move_Axis,
initial_point_pos: rl.Vector3,
}
g_mem: ^Game_Memory
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
}
add_track_spline_point :: proc() {
forward := camera_rotation_matrix()[2]
append(&g_mem.track.points, g_mem.player_pos + forward)
g_mem.es.selected_track_point = len(g_mem.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(.ENTER) {
add_track_spline_point()
}
if rl.IsKeyPressed(.G) && es.selected_track_point >= 0 {
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
}
g_mem.track.points[es.selected_track_point] += 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()
}
}
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 := &g_mem.track.points
points_len := len(points)
{
rlgl.Begin(rlgl.LINES)
defer rlgl.End()
rlgl.Color3f(1, 0, 0)
SPLINE_SUBDIVS :: 8
for i in 0 ..< points_len {
if i >= 1 && i < points_len - 2 {
for j in 0 ..< SPLINE_SUBDIVS {
t := f32(j) / f32(SPLINE_SUBDIVS)
t2 := f32(j + 1) / f32(SPLINE_SUBDIVS)
point := linalg.catmull_rom(
points[i - 1],
points[i],
points[i + 1],
points[i + 2],
t,
)
point2 := linalg.catmull_rom(
points[i - 1],
points[i],
points[i + 1],
points[i + 2],
t2,
)
rlgl.Vertex3f(point.x, point.y, point.z)
rlgl.Vertex3f(point2.x, point2.y, point2.z)
}
}
}
if g_mem.editor {
es := &g_mem.es
switch es.track_edit_state {
case .Select:
case .Move:
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 := &g_mem.track.points
points_len := len(points)
for i in 0 ..< points_len {
if spline_handle(g_mem.track.points[i], camera, es.selected_track_point == i) {
g_mem.es.selected_track_point = i
}
}
}
// 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{}
g_mem.es.selected_track_point = -1
game_hot_reloaded(g_mem)
}
@(export)
game_shutdown :: proc() {
assets.shutdown(&g_mem.assetman)
delete(g_mem.track.points)
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)
}