|
|
@@ -4,6 +4,9 @@ import "base:runtime"
|
|
|
import "core:fmt"
|
|
|
import "core:math"
|
|
|
import "core:math/linalg"
|
|
|
+import "core:math/rand"
|
|
|
+import "core:mem"
|
|
|
+import "core:mem/virtual"
|
|
|
|
|
|
// Vendor libs
|
|
|
import mu "third-party:microui"
|
|
|
@@ -13,38 +16,73 @@ import sgl "third-party:sokol/gl"
|
|
|
import sglue "third-party:sokol/glue"
|
|
|
import slog "third-party:sokol/log"
|
|
|
|
|
|
-// --- Data ---
|
|
|
+// --- Constants ---
|
|
|
+MAX_ENTITIES :: 100_000
|
|
|
|
|
|
-Camera :: struct {
|
|
|
+// --- Data Types ---
|
|
|
+
|
|
|
+Vertex :: struct {
|
|
|
+ pos: [3]f32,
|
|
|
+ color: [3]f32,
|
|
|
+}
|
|
|
+
|
|
|
+Mesh :: struct {
|
|
|
+ vertices: #soa[dynamic]Vertex,
|
|
|
+ indices: [dynamic]u16,
|
|
|
+}
|
|
|
+
|
|
|
+// Entity Kind (Tag)
|
|
|
+Entity_Kind :: enum u8 {
|
|
|
+ Player,
|
|
|
+ Enemy,
|
|
|
+}
|
|
|
+
|
|
|
+// The Big Data Struct
|
|
|
+// Благодаря #soa, это превращается в структуру массивов:
|
|
|
+// struct { pos: [N]vec3, vel: [N]vec3, ... }
|
|
|
+Entity :: struct {
|
|
|
pos: linalg.Vector3f32,
|
|
|
+ vel: linalg.Vector3f32,
|
|
|
+ color: linalg.Vector3f32,
|
|
|
+ kind: Entity_Kind,
|
|
|
+ scale: f32,
|
|
|
+}
|
|
|
+
|
|
|
+Camera :: struct {
|
|
|
pitch: f32,
|
|
|
yaw: f32,
|
|
|
+ dist: f32, // Расстояние от игрока (вид от 3-го лица)
|
|
|
}
|
|
|
|
|
|
Input_State :: struct {
|
|
|
keys: map[sapp.Keycode]bool,
|
|
|
mouse_dx: f32,
|
|
|
mouse_dy: f32,
|
|
|
- locked: bool, // Курсор захвачен окном?
|
|
|
+ locked: bool,
|
|
|
}
|
|
|
|
|
|
App_State :: struct {
|
|
|
- // Sokol GFX resources
|
|
|
- pass_action: sg.Pass_Action,
|
|
|
+ // Memory
|
|
|
+ arena: virtual.Arena,
|
|
|
+ allocator: mem.Allocator,
|
|
|
|
|
|
- // MicroUI context
|
|
|
+ // Resources
|
|
|
+ pass_action: sg.Pass_Action,
|
|
|
mu_ctx: mu.Context,
|
|
|
-
|
|
|
- // UI Resources
|
|
|
atlas_img: sg.Image,
|
|
|
atlas_view: sg.View,
|
|
|
atlas_smp: sg.Sampler,
|
|
|
ui_pip: sgl.Pipeline,
|
|
|
+ scene_pip: sgl.Pipeline,
|
|
|
+
|
|
|
+ // Assets
|
|
|
+ mesh_cube: Mesh,
|
|
|
|
|
|
- // 3D Resources
|
|
|
- cube_pip: sgl.Pipeline,
|
|
|
+ // ECS (Entity Component System - Lite)
|
|
|
+ // Храним всех в одном массиве. Игрок всегда index 0.
|
|
|
+ entities: #soa[dynamic]Entity,
|
|
|
|
|
|
- // Runtime data
|
|
|
+ // Runtime
|
|
|
dpi_scale: f32,
|
|
|
camera: Camera,
|
|
|
input: Input_State,
|
|
|
@@ -52,189 +90,289 @@ App_State :: struct {
|
|
|
|
|
|
state: App_State
|
|
|
|
|
|
-// --- Logic / Input ---
|
|
|
+// --- Systems ---
|
|
|
|
|
|
-map_mouse_button :: proc(btn: sapp.Mousebutton) -> (mu.Mouse, bool) {
|
|
|
- switch btn {
|
|
|
- case .LEFT:
|
|
|
- return .LEFT, true
|
|
|
- case .RIGHT:
|
|
|
- return .RIGHT, true
|
|
|
- case .MIDDLE:
|
|
|
- return .MIDDLE, true
|
|
|
- case .INVALID:
|
|
|
- return nil, false
|
|
|
+// 1. Input System: Управляет скоростью игрока (Entity 0)
|
|
|
+system_input_player :: proc(dt: f32) {
|
|
|
+ if len(state.entities) == 0 do return
|
|
|
+
|
|
|
+ // Получаем указатель на игрока (SoA access)
|
|
|
+ // В Odin доступ к элементу #soa массива возвращает структуру-прокси,
|
|
|
+ // которая ведет себя как ссылка на данные в соответствующих массивах.
|
|
|
+ player := &state.entities[0]
|
|
|
+
|
|
|
+ SPEED :: 10.0
|
|
|
+
|
|
|
+ // Вращение камеры
|
|
|
+ SENSITIVITY :: 0.15 * 0.02
|
|
|
+ if state.input.locked {
|
|
|
+ state.camera.yaw -= state.input.mouse_dx * SENSITIVITY
|
|
|
+ state.camera.pitch += state.input.mouse_dy * SENSITIVITY
|
|
|
+ state.camera.pitch = math.clamp(state.camera.pitch, -1.5, 1.5)
|
|
|
}
|
|
|
- return nil, false
|
|
|
-}
|
|
|
|
|
|
-map_key :: proc(key: sapp.Keycode) -> (mu.Key, bool) {
|
|
|
- #partial switch key {
|
|
|
- case .LEFT_SHIFT, .RIGHT_SHIFT:
|
|
|
- return .SHIFT, true
|
|
|
- case .LEFT_CONTROL, .RIGHT_CONTROL:
|
|
|
- return .CTRL, true
|
|
|
- case .LEFT_ALT, .RIGHT_ALT:
|
|
|
- return .ALT, true
|
|
|
- case .ENTER:
|
|
|
- return .RETURN, true
|
|
|
- case .BACKSPACE:
|
|
|
- return .BACKSPACE, true
|
|
|
+ // Расчет направления движения относительно камеры
|
|
|
+ sin_yaw, cos_yaw := math.sincos(state.camera.yaw)
|
|
|
+ forward := linalg.Vector3f32{sin_yaw, 0, cos_yaw}
|
|
|
+ right := linalg.Vector3f32{cos_yaw, 0, -sin_yaw}
|
|
|
+
|
|
|
+ move_dir := linalg.Vector3f32{0, 0, 0}
|
|
|
+ if state.input.keys[.W] do move_dir -= forward
|
|
|
+ if state.input.keys[.S] do move_dir += forward
|
|
|
+ if state.input.keys[.A] do move_dir -= right
|
|
|
+ if state.input.keys[.D] do move_dir += right
|
|
|
+
|
|
|
+ // Применяем скорость к игроку (только XZ, Y управляется гравитацией)
|
|
|
+ target_vel_x: f32 = 0.0
|
|
|
+ target_vel_z: f32 = 0.0
|
|
|
+
|
|
|
+ if linalg.length2(move_dir) > 0.01 {
|
|
|
+ move_dir = linalg.normalize(move_dir)
|
|
|
+ target_vel_x = move_dir.x * SPEED
|
|
|
+ target_vel_z = move_dir.z * SPEED
|
|
|
+ }
|
|
|
+
|
|
|
+ // Мгновенная реакция (можно добавить инерцию, если lerp-ить)
|
|
|
+ player.vel.x = target_vel_x
|
|
|
+ player.vel.z = target_vel_z
|
|
|
+
|
|
|
+ // Прыжок
|
|
|
+ if state.input.keys[.SPACE] && player.pos.y <= 0.51 {
|
|
|
+ player.vel.y = 10.0
|
|
|
}
|
|
|
- return nil, false
|
|
|
}
|
|
|
|
|
|
-// Чистая функция обновления камеры. Принимает состояние, ввод и время.
|
|
|
-// Возвращает матрицы View и Projection.
|
|
|
-update_camera :: proc(
|
|
|
- cam: ^Camera,
|
|
|
- input: ^Input_State,
|
|
|
- dt: f32,
|
|
|
-) -> (
|
|
|
- view, proj: linalg.Matrix4f32,
|
|
|
-) {
|
|
|
- // Константы настройки
|
|
|
- SENSITIVITY :: 0.15 * 0.02 // mouse scale * dt factor
|
|
|
- SPEED :: 6.0
|
|
|
-
|
|
|
- // 1. Вращение (только если курсор захвачен)
|
|
|
- if input.locked {
|
|
|
- cam.yaw -= input.mouse_dx * SENSITIVITY
|
|
|
- cam.pitch += input.mouse_dy * SENSITIVITY
|
|
|
-
|
|
|
- // Кламп питча, чтобы не перевернуться
|
|
|
- cam.pitch = math.clamp(cam.pitch, -1.5, 1.5)
|
|
|
+// 2. Physics System: Обрабатывает ВСЕ сущности линейно
|
|
|
+system_physics :: proc(dt: f32) {
|
|
|
+ GRAVITY :: 30.0
|
|
|
+ FLOOR_Y :: 0.5
|
|
|
+
|
|
|
+ // Итерация по SoA массиву очень быстрая (кэш-френдли).
|
|
|
+ // Компилятор может векторизовать этот цикл.
|
|
|
+ for i in 0 ..< len(state.entities) {
|
|
|
+ e := &state.entities[i]
|
|
|
+
|
|
|
+ // Гравитация
|
|
|
+ e.vel.y -= GRAVITY * dt
|
|
|
+
|
|
|
+ // Интеграция позиции
|
|
|
+ e.pos += e.vel * dt
|
|
|
+
|
|
|
+ // Коллизия с полом (простейшая)
|
|
|
+ if e.pos.y < FLOOR_Y {
|
|
|
+ e.pos.y = FLOOR_Y
|
|
|
+
|
|
|
+ // Отскок
|
|
|
+ if e.kind == .Player {
|
|
|
+ e.vel.y = 0 // Игрок не прыгает как мячик
|
|
|
+ } else {
|
|
|
+ e.vel.y *= -0.8 // Враги прыгают
|
|
|
+
|
|
|
+ // Трение об пол
|
|
|
+ e.vel.x *= 0.95
|
|
|
+ e.vel.z *= 0.95
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Ограничитель мира (чтобы враги не улетали в бесконечность)
|
|
|
+ if e.pos.x > 50 do e.vel.x = -abs(e.vel.x)
|
|
|
+ if e.pos.x < -50 do e.vel.x = abs(e.vel.x)
|
|
|
+ if e.pos.z > 50 do e.vel.z = -abs(e.vel.z)
|
|
|
+ if e.pos.z < -50 do e.vel.z = abs(e.vel.z)
|
|
|
}
|
|
|
+}
|
|
|
+
|
|
|
+// 3. Render System
|
|
|
+system_render :: proc(view, proj: linalg.Matrix4f32) {
|
|
|
+ view_copy := view
|
|
|
+ proj_copy := proj
|
|
|
+
|
|
|
+ sgl.defaults()
|
|
|
+ sgl.push_pipeline()
|
|
|
+ sgl.load_pipeline(state.scene_pip)
|
|
|
|
|
|
- // 2. Векторы направления
|
|
|
- // Вычисляем forward вектор из углов Эйлера
|
|
|
- forward := linalg.Vector3f32 {
|
|
|
- math.cos(cam.pitch) * math.sin(cam.yaw),
|
|
|
- math.sin(cam.pitch),
|
|
|
- math.cos(cam.pitch) * math.cos(cam.yaw),
|
|
|
+ sgl.matrix_mode_projection()
|
|
|
+ sgl.load_matrix(cast(^f32)&proj_copy)
|
|
|
+ sgl.matrix_mode_modelview()
|
|
|
+ sgl.load_matrix(cast(^f32)&view_copy)
|
|
|
+
|
|
|
+ draw_grid(100, 1.0)
|
|
|
+
|
|
|
+ // Рисуем всех одной пачкой
|
|
|
+ // В реальном движке здесь был бы Instancing, но для SGL просто цикл.
|
|
|
+ for i in 0 ..< len(state.entities) {
|
|
|
+ e := state.entities[i] // Копия для чтения (быстро)
|
|
|
+
|
|
|
+ // Цвет меняем через uniform (в sgl это c3f)
|
|
|
+ // Геометрию берем одну и ту же
|
|
|
+ draw_mesh_instance(&state.mesh_cube, e.pos, e.scale, e.color)
|
|
|
}
|
|
|
|
|
|
- // Вычисляем right вектор (cross product с мировым UP)
|
|
|
- world_up := linalg.Vector3f32{0, 1, 0}
|
|
|
- right := linalg.normalize(linalg.cross(forward, world_up))
|
|
|
+ sgl.pop_pipeline()
|
|
|
+}
|
|
|
|
|
|
- // 3. Движение
|
|
|
- move_dir := linalg.Vector3f32{0, 0, 0}
|
|
|
- if input.keys[.W] do move_dir -= forward
|
|
|
- if input.keys[.S] do move_dir += forward
|
|
|
- if input.keys[.A] do move_dir += right
|
|
|
- if input.keys[.D] do move_dir -= right
|
|
|
- if input.keys[.SPACE] do move_dir.y += 1.0
|
|
|
- if input.keys[.LEFT_CONTROL] do move_dir.y -= 1.0
|
|
|
-
|
|
|
- // Нормализуем, чтобы стрейф не был быстрее
|
|
|
- if linalg.length2(move_dir) > 0.01 {
|
|
|
- cam.pos += linalg.normalize(move_dir) * SPEED * dt
|
|
|
+// --- Helpers ---
|
|
|
+
|
|
|
+get_camera_matrices :: proc() -> (view, proj: linalg.Matrix4f32) {
|
|
|
+ if len(state.entities) == 0 {
|
|
|
+ return linalg.MATRIX4F32_IDENTITY, linalg.MATRIX4F32_IDENTITY
|
|
|
}
|
|
|
|
|
|
- // 4. Матрицы
|
|
|
- // LookAt
|
|
|
- center := cam.pos - forward // -forward потому что в OpenGL камера смотрит в -Z
|
|
|
- view = linalg.matrix4_look_at_f32(cam.pos, center, world_up)
|
|
|
+ player_pos := state.entities[0].pos
|
|
|
+
|
|
|
+ // Камера смотрит на игрока
|
|
|
+ // Вычисляем позицию камеры на сфере вокруг игрока
|
|
|
+ cam_offset :=
|
|
|
+ linalg.Vector3f32 {
|
|
|
+ math.cos(state.camera.pitch) * math.sin(state.camera.yaw),
|
|
|
+ math.sin(state.camera.pitch),
|
|
|
+ math.cos(state.camera.pitch) * math.cos(state.camera.yaw),
|
|
|
+ } *
|
|
|
+ state.camera.dist
|
|
|
+
|
|
|
+ cam_pos := player_pos + cam_offset
|
|
|
+
|
|
|
+ view = linalg.matrix4_look_at_f32(cam_pos, player_pos, {0, 1, 0})
|
|
|
|
|
|
- // Perspective
|
|
|
aspect := sapp.widthf() / sapp.heightf()
|
|
|
- proj = linalg.matrix4_perspective_f32(60.0 * (math.PI / 180.0), aspect, 0.1, 100.0)
|
|
|
+ proj = linalg.matrix4_perspective_f32(60.0 * (math.PI / 180.0), aspect, 0.1, 200.0)
|
|
|
|
|
|
return view, proj
|
|
|
}
|
|
|
|
|
|
-// --- Callbacks ---
|
|
|
-
|
|
|
-event :: proc "c" (ev: ^sapp.Event) {
|
|
|
- context = runtime.default_context()
|
|
|
+spawn_entity :: proc(pos: linalg.Vector3f32, kind: Entity_Kind) {
|
|
|
+ e: Entity
|
|
|
+ e.pos = pos
|
|
|
+ e.kind = kind
|
|
|
+ e.scale = 1.0
|
|
|
+
|
|
|
+ switch kind {
|
|
|
+ case .Player:
|
|
|
+ e.color = {0.2, 0.8, 0.2} // Green
|
|
|
+ e.scale = 1.0
|
|
|
+ case .Enemy:
|
|
|
+ e.color = {0.8, 0.3, 0.3} // Red
|
|
|
+ e.scale = rand.float32_range(0.5, 1.5)
|
|
|
+ // Случайная начальная скорость для врагов
|
|
|
+ e.vel.x = rand.float32_range(-5, 5)
|
|
|
+ e.vel.z = rand.float32_range(-5, 5)
|
|
|
+ e.vel.y = rand.float32_range(5, 15)
|
|
|
+ }
|
|
|
|
|
|
- dpi := sapp.dpi_scale()
|
|
|
+ append_soa(&state.entities, e)
|
|
|
+}
|
|
|
|
|
|
- // Обновляем состояние ввода для игры
|
|
|
- #partial switch ev.type {
|
|
|
- case .KEY_DOWN:
|
|
|
- state.input.keys[ev.key_code] = true
|
|
|
- case .KEY_UP:
|
|
|
- state.input.keys[ev.key_code] = false
|
|
|
- case .MOUSE_MOVE:
|
|
|
- if state.input.locked {
|
|
|
- state.input.mouse_dx = ev.mouse_dx
|
|
|
- state.input.mouse_dy = ev.mouse_dy
|
|
|
- }
|
|
|
- case .MOUSE_DOWN:
|
|
|
+// --- Mesh Gen ---
|
|
|
+
|
|
|
+make_cube_mesh :: proc(allocator: mem.Allocator) -> Mesh {
|
|
|
+ m: Mesh
|
|
|
+ // 24 вершины (6 граней * 4 вершины)
|
|
|
+ m.vertices = make(#soa[dynamic]Vertex, 0, 24, allocator)
|
|
|
+ // 36 индексов (6 граней * 2 треугольника * 3 индекса)
|
|
|
+ m.indices = make([dynamic]u16, 0, 36, allocator)
|
|
|
+
|
|
|
+ // Цвета для наглядности сторон
|
|
|
+ c_front := [3]f32{1.0, 0.5, 0.2} // Orange
|
|
|
+ c_back := [3]f32{0.8, 0.4, 0.1}
|
|
|
+ c_left := [3]f32{0.9, 0.45, 0.15}
|
|
|
+ c_right := [3]f32{0.9, 0.45, 0.15}
|
|
|
+ c_top := [3]f32{1.0, 1.0, 1.0} // Lighter
|
|
|
+ c_bottom := [3]f32{1.0, 1.0, 1.0} // Darker
|
|
|
+
|
|
|
+ // Helper для добавления грани (4 вершины + 6 индексов)
|
|
|
+ add_face :: proc(m: ^Mesh, p1, p2, p3, p4: [3]f32, color: [3]f32) {
|
|
|
+ start_idx := u16(len(m.vertices))
|
|
|
+
|
|
|
+ append_soa(&m.vertices, Vertex{p1, color})
|
|
|
+ append_soa(&m.vertices, Vertex{p2, color})
|
|
|
+ append_soa(&m.vertices, Vertex{p3, color})
|
|
|
+ append_soa(&m.vertices, Vertex{p4, color})
|
|
|
+
|
|
|
+ // CCW порядок (Против часовой стрелки)
|
|
|
+ // 1---2
|
|
|
+ // | / |
|
|
|
+ // 0---3
|
|
|
+ append(&m.indices, start_idx + 0, start_idx + 1, start_idx + 2)
|
|
|
+ append(&m.indices, start_idx + 0, start_idx + 2, start_idx + 3)
|
|
|
}
|
|
|
|
|
|
- // Выход из захвата по ESC
|
|
|
- if ev.type == .KEY_DOWN && ev.key_code == .ESCAPE {
|
|
|
- state.input.locked = !state.input.locked
|
|
|
- sapp.lock_mouse(state.input.locked)
|
|
|
- }
|
|
|
+ // Координаты
|
|
|
+ // Z+ (Front), Z- (Back)
|
|
|
+ // Y+ (Top), Y- (Bottom)
|
|
|
+ // X+ (Right), X- (Left)
|
|
|
+
|
|
|
+ // Front (Z+)
|
|
|
+ add_face(&m, {-0.5, -0.5, 0.5}, {-0.5, 0.5, 0.5}, {0.5, 0.5, 0.5}, {0.5, -0.5, 0.5}, c_front)
|
|
|
+
|
|
|
+ // Back (Z-) - смотрим сзади, порядок точек зеркальный для CCW
|
|
|
+ add_face(
|
|
|
+ &m,
|
|
|
+ {0.5, -0.5, -0.5},
|
|
|
+ {0.5, 0.5, -0.5},
|
|
|
+ {-0.5, 0.5, -0.5},
|
|
|
+ {-0.5, -0.5, -0.5},
|
|
|
+ c_back,
|
|
|
+ )
|
|
|
|
|
|
- // Логика UI (только если мышь НЕ захвачена игрой)
|
|
|
- if !state.input.locked {
|
|
|
- // Нормализация координат для UI
|
|
|
- mouse_x := i32(ev.mouse_x / dpi)
|
|
|
- mouse_y := i32(ev.mouse_y / dpi)
|
|
|
+ // Top (Y+)
|
|
|
+ add_face(&m, {-0.5, 0.5, 0.5}, {-0.5, 0.5, -0.5}, {0.5, 0.5, -0.5}, {0.5, 0.5, 0.5}, c_top)
|
|
|
+
|
|
|
+ // Bottom (Y-)
|
|
|
+ add_face(
|
|
|
+ &m,
|
|
|
+ {-0.5, -0.5, -0.5},
|
|
|
+ {-0.5, -0.5, 0.5},
|
|
|
+ {0.5, -0.5, 0.5},
|
|
|
+ {0.5, -0.5, -0.5},
|
|
|
+ c_bottom,
|
|
|
+ )
|
|
|
|
|
|
- #partial switch ev.type {
|
|
|
- case .MOUSE_DOWN:
|
|
|
- if btn, ok := map_mouse_button(ev.mouse_button); ok {
|
|
|
- mu.input_mouse_down(&state.mu_ctx, mouse_x, mouse_y, btn)
|
|
|
- }
|
|
|
- case .MOUSE_UP:
|
|
|
- if btn, ok := map_mouse_button(ev.mouse_button); ok {
|
|
|
- mu.input_mouse_up(&state.mu_ctx, mouse_x, mouse_y, btn)
|
|
|
- }
|
|
|
- case .MOUSE_MOVE:
|
|
|
- mu.input_mouse_move(&state.mu_ctx, mouse_x, mouse_y)
|
|
|
- case .MOUSE_SCROLL:
|
|
|
- mu.input_scroll(&state.mu_ctx, 0, i32(ev.scroll_y))
|
|
|
- case .KEY_DOWN:
|
|
|
- if k, ok := map_key(ev.key_code); ok {
|
|
|
- mu.input_key_down(&state.mu_ctx, k)
|
|
|
- }
|
|
|
- case .KEY_UP:
|
|
|
- if k, ok := map_key(ev.key_code); ok {
|
|
|
- mu.input_key_up(&state.mu_ctx, k)
|
|
|
- }
|
|
|
- case .CHAR:
|
|
|
- if ev.char_code != 127 && ev.char_code >= 32 {
|
|
|
- text := fmt.tprintf("%r", rune(ev.char_code))
|
|
|
- mu.input_text(&state.mu_ctx, text)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ // Right (X+)
|
|
|
+ add_face(&m, {0.5, -0.5, 0.5}, {0.5, 0.5, 0.5}, {0.5, 0.5, -0.5}, {0.5, -0.5, -0.5}, c_right)
|
|
|
+
|
|
|
+ // Left (X-)
|
|
|
+ add_face(
|
|
|
+ &m,
|
|
|
+ {-0.5, -0.5, -0.5},
|
|
|
+ {-0.5, 0.5, -0.5},
|
|
|
+ {-0.5, 0.5, 0.5},
|
|
|
+ {-0.5, -0.5, 0.5},
|
|
|
+ c_left,
|
|
|
+ )
|
|
|
+
|
|
|
+ return m
|
|
|
}
|
|
|
|
|
|
+// --- App Lifecycle ---
|
|
|
+
|
|
|
init :: proc "c" () {
|
|
|
context = runtime.default_context()
|
|
|
|
|
|
+ // 1. Memory
|
|
|
+ _ = virtual.arena_init_growing(&state.arena, 64 * mem.Megabyte)
|
|
|
+ state.allocator = virtual.arena_allocator(&state.arena)
|
|
|
+
|
|
|
+ // 2. Sokol
|
|
|
sg.setup({environment = sglue.environment(), logger = {func = slog.func}})
|
|
|
sgl.setup({logger = {func = slog.func}})
|
|
|
|
|
|
- // Init UI
|
|
|
+ // 3. UI
|
|
|
mu.init(&state.mu_ctx)
|
|
|
state.mu_ctx.text_width = mu.default_atlas_text_width
|
|
|
state.mu_ctx.text_height = mu.default_atlas_text_height
|
|
|
- state.dpi_scale = 1.0
|
|
|
|
|
|
- // Init Camera
|
|
|
- state.camera.pos = {0, 1.5, 5} // Чуть выше и назад
|
|
|
-
|
|
|
- // --- Resources ---
|
|
|
-
|
|
|
- // 1. UI Atlas Texture
|
|
|
- width := mu.DEFAULT_ATLAS_WIDTH
|
|
|
- height := mu.DEFAULT_ATLAS_HEIGHT
|
|
|
- pixels := make([]u32, width * height, context.temp_allocator)
|
|
|
+ // 4. Resources
|
|
|
+ // UI Atlas
|
|
|
+ w := mu.DEFAULT_ATLAS_WIDTH
|
|
|
+ h := mu.DEFAULT_ATLAS_HEIGHT
|
|
|
+ pixels := make([]u32, w * h, context.temp_allocator)
|
|
|
for alpha, i in mu.default_atlas_alpha {
|
|
|
pixels[i] = 0x00FFFFFF | (u32(alpha) << 24)
|
|
|
}
|
|
|
state.atlas_img = sg.make_image(
|
|
|
{
|
|
|
- width = i32(width),
|
|
|
- height = i32(height),
|
|
|
+ width = i32(w),
|
|
|
+ height = i32(h),
|
|
|
pixel_format = .RGBA8,
|
|
|
data = {mip_levels = {0 = {ptr = raw_data(pixels), size = len(pixels) * 4}}},
|
|
|
},
|
|
|
@@ -242,126 +380,112 @@ init :: proc "c" () {
|
|
|
state.atlas_view = sg.make_view({texture = {image = state.atlas_img}})
|
|
|
state.atlas_smp = sg.make_sampler({min_filter = .NEAREST, mag_filter = .NEAREST})
|
|
|
|
|
|
- // 2. UI Pipeline
|
|
|
- pip_desc: sg.Pipeline_Desc
|
|
|
- pip_desc.colors[0].blend.enabled = true
|
|
|
- pip_desc.colors[0].blend.src_factor_rgb = .SRC_ALPHA
|
|
|
- pip_desc.colors[0].blend.dst_factor_rgb = .ONE_MINUS_SRC_ALPHA
|
|
|
- state.ui_pip = sgl.make_pipeline(pip_desc)
|
|
|
+ // Pipelines
|
|
|
+ ui_pip_desc: sg.Pipeline_Desc
|
|
|
+ ui_pip_desc.colors[0].blend.enabled = true
|
|
|
+ ui_pip_desc.colors[0].blend.src_factor_rgb = .SRC_ALPHA
|
|
|
+ ui_pip_desc.colors[0].blend.dst_factor_rgb = .ONE_MINUS_SRC_ALPHA
|
|
|
+ state.ui_pip = sgl.make_pipeline(ui_pip_desc)
|
|
|
|
|
|
- // 3. 3D Pipeline (Depth enabled)
|
|
|
- cube_pip_desc: sg.Pipeline_Desc
|
|
|
- cube_pip_desc.depth.write_enabled = true
|
|
|
- cube_pip_desc.depth.compare = .LESS_EQUAL
|
|
|
- state.cube_pip = sgl.make_pipeline(cube_pip_desc)
|
|
|
+ // 3D Pipeline
|
|
|
+ scene_pip_desc: sg.Pipeline_Desc
|
|
|
+ scene_pip_desc.depth.write_enabled = true
|
|
|
+ scene_pip_desc.depth.compare = .LESS_EQUAL
|
|
|
+ scene_pip_desc.cull_mode = .FRONT
|
|
|
+
|
|
|
+ state.scene_pip = sgl.make_pipeline(scene_pip_desc)
|
|
|
|
|
|
state.pass_action = {
|
|
|
colors = {0 = {load_action = .CLEAR, clear_value = {0.1, 0.1, 0.15, 1}}},
|
|
|
}
|
|
|
+
|
|
|
+ // 5. Game Data
|
|
|
+ state.mesh_cube = make_cube_mesh(state.allocator)
|
|
|
+
|
|
|
+ // Init Entities (SoA array)
|
|
|
+ // Reserve memory upfront to avoid reallocations
|
|
|
+ state.entities = make(#soa[dynamic]Entity, 0, MAX_ENTITIES, state.allocator)
|
|
|
+
|
|
|
+ // Spawn Player (Index 0)
|
|
|
+ spawn_entity({0, 5, 0}, .Player)
|
|
|
+ state.camera.dist = 10.0
|
|
|
+ state.camera.pitch = 0.5
|
|
|
+
|
|
|
+ // Spawn Enemies
|
|
|
+ for i in 0 ..< 1000 {
|
|
|
+ x := rand.float32_range(-40, 40)
|
|
|
+ z := rand.float32_range(-40, 40)
|
|
|
+ y := rand.float32_range(10, 50)
|
|
|
+ spawn_entity({x, y, z}, .Enemy)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
frame :: proc "c" () {
|
|
|
context = runtime.default_context()
|
|
|
+ defer free_all(context.temp_allocator)
|
|
|
+
|
|
|
state.dpi_scale = sapp.dpi_scale()
|
|
|
dt := f32(sapp.frame_duration())
|
|
|
|
|
|
- // 1. Update Logic
|
|
|
- view, proj := update_camera(&state.camera, &state.input, dt)
|
|
|
+ // --- Systems Update ---
|
|
|
+ system_input_player(dt)
|
|
|
+ system_physics(dt)
|
|
|
|
|
|
- // Сброс дельты мыши после обработки кадра
|
|
|
+ // Reset input deltas
|
|
|
state.input.mouse_dx = 0
|
|
|
state.input.mouse_dy = 0
|
|
|
|
|
|
- // 2. UI Logic
|
|
|
+ // --- Render ---
|
|
|
+
|
|
|
+ // UI
|
|
|
mu.begin(&state.mu_ctx)
|
|
|
- if mu.begin_window(&state.mu_ctx, "Controls", {10, 10, 200, 150}) {
|
|
|
- mu.label(&state.mu_ctx, "WASD + Mouse to move")
|
|
|
- mu.label(&state.mu_ctx, "Click to lock mouse")
|
|
|
- mu.label(&state.mu_ctx, "ESC to unlock")
|
|
|
-
|
|
|
- if .SUBMIT in mu.button(&state.mu_ctx, "Reset Camera") {
|
|
|
- state.camera.pos = {0, 1.5, 5}
|
|
|
- state.camera.yaw = 0
|
|
|
- state.camera.pitch = 0
|
|
|
- }
|
|
|
+ if mu.begin_window(&state.mu_ctx, "Stats", {10, 10, 200, 120}) {
|
|
|
+ mu.label(&state.mu_ctx, fmt.tprintf("FPS: %.0f", 1.0 / dt))
|
|
|
+ mu.label(&state.mu_ctx, fmt.tprintf("Entities: %d", len(state.entities)))
|
|
|
+ mu.label(&state.mu_ctx, "WASD to Move")
|
|
|
+ mu.label(&state.mu_ctx, "Space to Jump")
|
|
|
mu.end_window(&state.mu_ctx)
|
|
|
}
|
|
|
mu.end(&state.mu_ctx)
|
|
|
|
|
|
- // 3. Rendering
|
|
|
-
|
|
|
- // 3.1 Draw 3D Scene
|
|
|
- sgl.defaults()
|
|
|
- sgl.push_pipeline()
|
|
|
- sgl.load_pipeline(state.cube_pip) // Pipeline with depth test
|
|
|
-
|
|
|
- sgl.matrix_mode_projection()
|
|
|
- sgl.load_matrix(cast(^f32)&proj)
|
|
|
-
|
|
|
- sgl.matrix_mode_modelview()
|
|
|
- sgl.load_matrix(cast(^f32)&view)
|
|
|
-
|
|
|
- draw_cube({0, 0, 0}, 1.0, {1, 0.5, 0.2}) // Orange cube
|
|
|
- draw_grid(20, 1.0) // Floor grid
|
|
|
-
|
|
|
- sgl.pop_pipeline()
|
|
|
-
|
|
|
- // 3.2 Draw UI
|
|
|
+ // Scene
|
|
|
+ view, proj := get_camera_matrices()
|
|
|
+ system_render(view, proj)
|
|
|
render_ui()
|
|
|
|
|
|
- // 3.3 Commit
|
|
|
sg.begin_pass({action = state.pass_action, swapchain = sglue.swapchain()})
|
|
|
sgl.draw()
|
|
|
sg.end_pass()
|
|
|
sg.commit()
|
|
|
}
|
|
|
|
|
|
-// --- Render Helpers ---
|
|
|
+// --- Render Impl ---
|
|
|
|
|
|
-draw_cube :: proc(pos: linalg.Vector3f32, size: f32, color: linalg.Vector3f32) {
|
|
|
+draw_mesh_instance :: proc(m: ^Mesh, pos: linalg.Vector3f32, scale: f32, tint: linalg.Vector3f32) {
|
|
|
sgl.push_matrix()
|
|
|
sgl.translate(pos.x, pos.y, pos.z)
|
|
|
- sgl.scale(size, size, size)
|
|
|
-
|
|
|
- sgl.begin_quads()
|
|
|
- sgl.c3f(color.x, color.y, color.z)
|
|
|
-
|
|
|
- // Front
|
|
|
- sgl.v3f(-0.5, -0.5, 0.5); sgl.v3f(0.5, -0.5, 0.5)
|
|
|
- sgl.v3f(0.5, 0.5, 0.5); sgl.v3f(-0.5, 0.5, 0.5)
|
|
|
- // Back
|
|
|
- sgl.v3f(0.5, -0.5, -0.5); sgl.v3f(-0.5, -0.5, -0.5)
|
|
|
- sgl.v3f(-0.5, 0.5, -0.5); sgl.v3f(0.5, 0.5, -0.5)
|
|
|
- // Left
|
|
|
- sgl.v3f(-0.5, -0.5, -0.5); sgl.v3f(-0.5, -0.5, 0.5)
|
|
|
- sgl.v3f(-0.5, 0.5, 0.5); sgl.v3f(-0.5, 0.5, -0.5)
|
|
|
- // Right
|
|
|
- sgl.v3f(0.5, -0.5, 0.5); sgl.v3f(0.5, -0.5, -0.5)
|
|
|
- sgl.v3f(0.5, 0.5, -0.5); sgl.v3f(0.5, 0.5, 0.5)
|
|
|
- // Top
|
|
|
- sgl.c3f(color.x * 1.2, color.y * 1.2, color.z * 1.2) // Lighter top
|
|
|
- sgl.v3f(-0.5, 0.5, 0.5); sgl.v3f(0.5, 0.5, 0.5)
|
|
|
- sgl.v3f(0.5, 0.5, -0.5); sgl.v3f(-0.5, 0.5, -0.5)
|
|
|
- // Bottom
|
|
|
- sgl.c3f(color.x * 0.8, color.y * 0.8, color.z * 0.8) // Darker bottom
|
|
|
- sgl.v3f(-0.5, -0.5, -0.5); sgl.v3f(0.5, -0.5, -0.5)
|
|
|
- sgl.v3f(0.5, -0.5, 0.5); sgl.v3f(-0.5, -0.5, 0.5)
|
|
|
-
|
|
|
+ sgl.scale(scale, scale, scale)
|
|
|
+
|
|
|
+ sgl.begin_triangles()
|
|
|
+ for i := 0; i < len(m.indices); i += 1 {
|
|
|
+ idx := m.indices[i]
|
|
|
+ v := m.vertices[idx]
|
|
|
+ // Умножаем цвет вершины на цвет сущности (Tint)
|
|
|
+ sgl.c3f(v.color.x * tint.x, v.color.y * tint.y, v.color.z * tint.z)
|
|
|
+ sgl.v3f(v.pos.x, v.pos.y, v.pos.z)
|
|
|
+ }
|
|
|
sgl.end()
|
|
|
sgl.pop_matrix()
|
|
|
}
|
|
|
|
|
|
draw_grid :: proc(slices: int, spacing: f32) {
|
|
|
sgl.begin_lines()
|
|
|
- sgl.c3f(0.5, 0.5, 0.5)
|
|
|
-
|
|
|
- half_size := f32(slices) * spacing * 0.5
|
|
|
+ sgl.c3f(0.3, 0.3, 0.3)
|
|
|
+ half := f32(slices) * spacing * 0.5
|
|
|
for i in 0 ..= slices {
|
|
|
- pos := -half_size + f32(i) * spacing
|
|
|
- // X lines
|
|
|
- sgl.v3f(-half_size, 0, pos); sgl.v3f(half_size, 0, pos)
|
|
|
- // Z lines
|
|
|
- sgl.v3f(pos, 0, -half_size); sgl.v3f(pos, 0, half_size)
|
|
|
+ p := -half + f32(i) * spacing
|
|
|
+ sgl.v3f(-half, 0, p); sgl.v3f(half, 0, p)
|
|
|
+ sgl.v3f(p, 0, -half); sgl.v3f(p, 0, half)
|
|
|
}
|
|
|
sgl.end()
|
|
|
}
|
|
|
@@ -372,25 +496,22 @@ render_ui :: proc() {
|
|
|
sgl.enable_texture()
|
|
|
sgl.texture(state.atlas_view, state.atlas_smp)
|
|
|
|
|
|
- // 1. Сбрасываем Проекцию
|
|
|
sgl.matrix_mode_projection()
|
|
|
sgl.push_matrix()
|
|
|
- sgl.load_identity() // <--- ВАЖНО: Сбрасываем перспективу от 3D сцены
|
|
|
+ sgl.load_identity()
|
|
|
|
|
|
- // Настраиваем Ortho для UI
|
|
|
- logical_w := sapp.widthf() / state.dpi_scale
|
|
|
- logical_h := sapp.heightf() / state.dpi_scale
|
|
|
- sgl.ortho(0.0, logical_w, logical_h, 0.0, -1.0, +1.0)
|
|
|
+ w := sapp.widthf() / state.dpi_scale
|
|
|
+ h := sapp.heightf() / state.dpi_scale
|
|
|
+ sgl.ortho(0.0, w, h, 0.0, -1.0, +1.0)
|
|
|
|
|
|
- // 2. Сбрасываем ModelView (чтобы UI не вращался вместе с камерой)
|
|
|
sgl.matrix_mode_modelview()
|
|
|
sgl.push_matrix()
|
|
|
- sgl.load_identity() // <--- ВАЖНО: Сбрасываем поворот камеры
|
|
|
+ sgl.load_identity()
|
|
|
|
|
|
sgl.begin_quads()
|
|
|
cmd: ^mu.Command
|
|
|
- for variant in mu.next_command_iterator(&state.mu_ctx, &cmd) {
|
|
|
- #partial switch c in variant {
|
|
|
+ for v in mu.next_command_iterator(&state.mu_ctx, &cmd) {
|
|
|
+ #partial switch c in v {
|
|
|
case ^mu.Command_Rect:
|
|
|
draw_rect(c.rect, c.color)
|
|
|
case ^mu.Command_Text:
|
|
|
@@ -411,62 +532,139 @@ render_ui :: proc() {
|
|
|
}
|
|
|
sgl.end()
|
|
|
|
|
|
- // Восстанавливаем матрицы (pop в обратном порядке)
|
|
|
- sgl.matrix_mode_modelview()
|
|
|
sgl.pop_matrix()
|
|
|
-
|
|
|
sgl.matrix_mode_projection()
|
|
|
sgl.pop_matrix()
|
|
|
-
|
|
|
sgl.pop_pipeline()
|
|
|
}
|
|
|
|
|
|
-// --- UI Primitives ---
|
|
|
-
|
|
|
-draw_rect :: proc(rect: mu.Rect, color: mu.Color) {
|
|
|
- push_quad(rect, mu.default_atlas[mu.DEFAULT_ATLAS_WHITE], color)
|
|
|
+draw_rect :: proc(r: mu.Rect, c: mu.Color) {
|
|
|
+ push_quad(r, mu.default_atlas[mu.DEFAULT_ATLAS_WHITE], c)
|
|
|
}
|
|
|
|
|
|
-draw_text :: proc(str: string, pos: mu.Vec2, color: mu.Color) {
|
|
|
+draw_text :: proc(str: string, pos: mu.Vec2, c: mu.Color) {
|
|
|
p := pos
|
|
|
- for char in str {
|
|
|
- idx := int(char)
|
|
|
+ for ch in str {
|
|
|
+ idx := int(ch)
|
|
|
if idx > 127 do idx = 0
|
|
|
src := mu.default_atlas[mu.DEFAULT_ATLAS_FONT + idx]
|
|
|
- dst := mu.Rect{p.x, p.y, src.w, src.h}
|
|
|
- push_quad(dst, src, color)
|
|
|
+ push_quad({p.x, p.y, src.w, src.h}, src, c)
|
|
|
p.x += src.w
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-draw_icon :: proc(id: mu.Icon, rect: mu.Rect, color: mu.Color) {
|
|
|
+draw_icon :: proc(id: mu.Icon, r: mu.Rect, c: mu.Color) {
|
|
|
src := mu.default_atlas[int(id)]
|
|
|
- x := rect.x + (rect.w - src.w) / 2
|
|
|
- y := rect.y + (rect.h - src.h) / 2
|
|
|
- push_quad({x, y, src.w, src.h}, src, color)
|
|
|
+ x := r.x + (r.w - src.w) / 2
|
|
|
+ y := r.y + (r.h - src.h) / 2
|
|
|
+ push_quad({x, y, src.w, src.h}, src, c)
|
|
|
}
|
|
|
|
|
|
-push_quad :: proc(dst: mu.Rect, src: mu.Rect, color: mu.Color) {
|
|
|
- w_inv := 1.0 / f32(mu.DEFAULT_ATLAS_WIDTH)
|
|
|
- h_inv := 1.0 / f32(mu.DEFAULT_ATLAS_HEIGHT)
|
|
|
-
|
|
|
- u0 := f32(src.x) * w_inv
|
|
|
- v0 := f32(src.y) * h_inv
|
|
|
- u1 := f32(src.x + src.w) * w_inv
|
|
|
- v1 := f32(src.y + src.h) * h_inv
|
|
|
-
|
|
|
+push_quad :: proc(dst, src: mu.Rect, c: mu.Color) {
|
|
|
+ iw := 1.0 / f32(mu.DEFAULT_ATLAS_WIDTH)
|
|
|
+ ih := 1.0 / f32(mu.DEFAULT_ATLAS_HEIGHT)
|
|
|
+ u0, v0 := f32(src.x) * iw, f32(src.y) * ih
|
|
|
+ u1, v1 := f32(src.x + src.w) * iw, f32(src.y + src.h) * ih
|
|
|
x0, y0 := f32(dst.x), f32(dst.y)
|
|
|
x1, y1 := f32(dst.x + dst.w), f32(dst.y + dst.h)
|
|
|
|
|
|
- sgl.c4b(color.r, color.g, color.b, color.a)
|
|
|
+ sgl.c4b(c.r, c.g, c.b, c.a)
|
|
|
sgl.v2f_t2f(x0, y0, u0, v0)
|
|
|
sgl.v2f_t2f(x1, y0, u1, v0)
|
|
|
sgl.v2f_t2f(x1, y1, u1, v1)
|
|
|
sgl.v2f_t2f(x0, y1, u0, v1)
|
|
|
}
|
|
|
|
|
|
+// --- Input Glue ---
|
|
|
+
|
|
|
+map_mouse_button :: proc(btn: sapp.Mousebutton) -> (mu.Mouse, bool) {
|
|
|
+ switch btn {
|
|
|
+ case .LEFT:
|
|
|
+ return .LEFT, true
|
|
|
+ case .RIGHT:
|
|
|
+ return .RIGHT, true
|
|
|
+ case .MIDDLE:
|
|
|
+ return .MIDDLE, true
|
|
|
+ case .INVALID:
|
|
|
+ return nil, false
|
|
|
+ }
|
|
|
+ return nil, false
|
|
|
+}
|
|
|
+
|
|
|
+map_key :: proc(key: sapp.Keycode) -> (mu.Key, bool) {
|
|
|
+ #partial switch key {
|
|
|
+ case .LEFT_SHIFT, .RIGHT_SHIFT:
|
|
|
+ return .SHIFT, true
|
|
|
+ case .LEFT_CONTROL, .RIGHT_CONTROL:
|
|
|
+ return .CTRL, true
|
|
|
+ case .LEFT_ALT, .RIGHT_ALT:
|
|
|
+ return .ALT, true
|
|
|
+ case .ENTER:
|
|
|
+ return .RETURN, true
|
|
|
+ case .BACKSPACE:
|
|
|
+ return .BACKSPACE, true
|
|
|
+ }
|
|
|
+ return nil, false
|
|
|
+}
|
|
|
+
|
|
|
+event :: proc "c" (ev: ^sapp.Event) {
|
|
|
+ context = runtime.default_context()
|
|
|
+ dpi := sapp.dpi_scale()
|
|
|
+
|
|
|
+ #partial switch ev.type {
|
|
|
+ case .KEY_DOWN:
|
|
|
+ state.input.keys[ev.key_code] = true
|
|
|
+ case .KEY_UP:
|
|
|
+ state.input.keys[ev.key_code] = false
|
|
|
+ case .MOUSE_MOVE:
|
|
|
+ if state.input.locked {
|
|
|
+ state.input.mouse_dx = ev.mouse_dx
|
|
|
+ state.input.mouse_dy = ev.mouse_dy
|
|
|
+ }
|
|
|
+ case .MOUSE_DOWN:
|
|
|
+ if ev.mouse_button == .LEFT && !state.input.locked {
|
|
|
+ sapp.lock_mouse(true)
|
|
|
+ state.input.locked = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if ev.type == .KEY_DOWN && ev.key_code == .ESCAPE {
|
|
|
+ state.input.locked = false
|
|
|
+ sapp.lock_mouse(false)
|
|
|
+ }
|
|
|
+
|
|
|
+ if !state.input.locked {
|
|
|
+ mx := i32(ev.mouse_x / dpi)
|
|
|
+ my := i32(ev.mouse_y / dpi)
|
|
|
+
|
|
|
+ #partial switch ev.type {
|
|
|
+ case .MOUSE_DOWN:
|
|
|
+ if btn, ok := map_mouse_button(ev.mouse_button); ok {
|
|
|
+ mu.input_mouse_down(&state.mu_ctx, mx, my, btn)
|
|
|
+ }
|
|
|
+ case .MOUSE_UP:
|
|
|
+ if btn, ok := map_mouse_button(ev.mouse_button); ok {
|
|
|
+ mu.input_mouse_up(&state.mu_ctx, mx, my, btn)
|
|
|
+ }
|
|
|
+ case .MOUSE_MOVE:
|
|
|
+ mu.input_mouse_move(&state.mu_ctx, mx, my)
|
|
|
+ case .MOUSE_SCROLL:
|
|
|
+ mu.input_scroll(&state.mu_ctx, 0, i32(ev.scroll_y))
|
|
|
+ case .KEY_DOWN:
|
|
|
+ if k, ok := map_key(ev.key_code); ok do mu.input_key_down(&state.mu_ctx, k)
|
|
|
+ case .KEY_UP:
|
|
|
+ if k, ok := map_key(ev.key_code); ok do mu.input_key_up(&state.mu_ctx, k)
|
|
|
+ case .CHAR:
|
|
|
+ if ev.char_code >= 32 && ev.char_code != 127 {
|
|
|
+ mu.input_text(&state.mu_ctx, fmt.tprintf("%r", rune(ev.char_code)))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
cleanup :: proc "c" () {
|
|
|
context = runtime.default_context()
|
|
|
+ virtual.arena_destroy(&state.arena)
|
|
|
sgl.shutdown()
|
|
|
sg.shutdown()
|
|
|
}
|
|
|
@@ -480,10 +678,11 @@ main :: proc() {
|
|
|
event_cb = event,
|
|
|
width = 1280,
|
|
|
height = 720,
|
|
|
- window_title = "Sokol 3D + UI",
|
|
|
+ window_title = "Sokol DOD",
|
|
|
icon = {sokol_default = true},
|
|
|
logger = {func = slog.func},
|
|
|
high_dpi = true,
|
|
|
+ swap_interval = 0, // Unlock FPS
|
|
|
},
|
|
|
)
|
|
|
}
|