diff --git a/.gitignore b/.gitignore index 78dbaab..19da50f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ log.txt *.dSYM linux -build/ \ No newline at end of file +build/ +ols.json \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..110830b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,31 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "cppvsdbg", + "request": "launch", + "preLaunchTask": "Build Debug", + "name": "Debug", + "program": "${workspaceFolder}/game_debug.exe", + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "cppvsdbg", + "request": "launch", + "preLaunchTask": "Build Release", + "name": "Release", + "program": "${workspaceFolder}/game_release.exe", + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Run File", + "program": "odin", + "args": ["run", "${fileBasename}", "-file"], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..08e7ac8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "workbench.colorCustomizations": { + "activityBar.background": "#322C2D", + "titleBar.activeBackground": "#463E3F", + "titleBar.activeForeground": "#FAFAFA" + } +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..d4d47ba --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,60 @@ +{ + "version": "2.0.0", + "command": "", + "args": [], + "tasks": [ + { + "label": "Build Debug", + "type": "shell", + "windows": { + "command": "${workspaceFolder}/scripts/build_debug.bat", + }, + "linux": { + "command": "${workspaceFolder}/scripts/build_debug.sh", + }, + "osx": { + "command": "${workspaceFolder}/scripts/build_debug.sh", + }, + "group": "build" + }, + { + "label": "Build Release", + "type": "shell", + "windows": { + "command": "${workspaceFolder}/scripts/build_release.bat", + }, + "linux": { + "command": "${workspaceFolder}/scripts/build_release.sh", + }, + "osx": { + "command": "${workspaceFolder}/scripts/build_release.sh", + }, + "group": "build" + }, + { + "label": "Build Hot Reload", + "type": "shell", + "windows": { + "command": "${workspaceFolder}/scripts/build_hot_reload.bat; start game.exe", + }, + "linux": { + "command": "${workspaceFolder}/scripts/build_hot_reload.sh", + }, + "osx": { + "command": "${workspaceFolder}/scripts/build_hot_reload.sh", + }, + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": true + }, + "group": { + "kind": "build", + "isDefault": true + }, + } + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f4dd8fb --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2024 Stefan Stefanov + +This software is provided "as-is", without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely. + +You do not need to credit the authors and this notice may be removed from redistributed versions. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6dc89db --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ + +# YAAP +Yet-Another-Atlas-Packer by Stefan Stefanov + +## Description + +Simple atlas packer using `stb_rect_pack` from the `stb` family of header libraries & `raylib` for rendering/ui. + +Project template provided by Karl Zylinski on github [here](https://github.com/karl-zylinski/odin-raylib-hot-reload-game-template). diff --git a/scripts/build_debug.bat b/scripts/build_debug.bat new file mode 100644 index 0000000..ecaabef --- /dev/null +++ b/scripts/build_debug.bat @@ -0,0 +1,2 @@ +@echo off +odin build src/main_release -define:RAYLIB_SHARED=false -out:build/game_debug.exe -no-bounds-check -subsystem:windows -debug diff --git a/scripts/build_debug.sh b/scripts/build_debug.sh new file mode 100644 index 0000000..7df52c7 --- /dev/null +++ b/scripts/build_debug.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +odin build src/main_release -out:build/game_debug.bin -no-bounds-check -debug diff --git a/scripts/build_hot_reload.bat b/scripts/build_hot_reload.bat new file mode 100644 index 0000000..3890995 --- /dev/null +++ b/scripts/build_hot_reload.bat @@ -0,0 +1,21 @@ +@echo off + +rem Build game.dll +odin build src -show-timings -use-separate-modules -define:RAYLIB_SHARED=true -build-mode:dll -out:build/game.dll -strict-style -vet-unused -vet-using-stmt -vet-using-param -vet-style -vet-semicolon -debug +IF %ERRORLEVEL% NEQ 0 exit /b 1 + +rem If game.exe already running: Then only compile game.dll and exit cleanly +QPROCESS "game.exe">NUL +IF %ERRORLEVEL% EQU 0 exit /b 1 + +rem build game.exe +odin build src/main_hot_reload -use-separate-modules -out:build/game.exe -strict-style -vet-using-stmt -vet-using-param -vet-style -vet-semicolon -debug +IF %ERRORLEVEL% NEQ 0 exit /b 1 + +rem copy raylib.dll from odin folder to here +if not exist "raylib.dll" ( + echo "Please copy raylib.dll from /vendor/raylib/windows/raylib.dll to the same directory as game.exe" + exit /b 1 +) + +exit /b 0 diff --git a/scripts/build_hot_reload.sh b/scripts/build_hot_reload.sh new file mode 100644 index 0000000..99bbd1d --- /dev/null +++ b/scripts/build_hot_reload.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +VET="-strict-style -vet-unused -vet-using-stmt -vet-using-param -vet-style -vet-semicolon" + +# NOTE: this is a recent addition to the Odin compiler, if you don't have this command +# you can change this to the path to the Odin folder that contains vendor, eg: "~/Odin". +ROOT=$(odin root) +if [ ! $? -eq 0 ]; then + echo "Your Odin compiler does not have the 'odin root' command, please update or hardcode it in the script." + exit 1 +fi + +set -eu + +# Figure out the mess that is dynamic libraries. +case $(uname) in +"Darwin") + case $(uname -m) in + "arm64") LIB_PATH="macos-arm64" ;; + *) LIB_PATH="macos" ;; + esac + + DLL_EXT=".dylib" + EXTRA_LINKER_FLAGS="-Wl,-rpath $ROOT/vendor/raylib/$LIB_PATH" + ;; +*) + DLL_EXT=".so" + EXTRA_LINKER_FLAGS="'-Wl,-rpath=\$ORIGIN/linux'" + + # Copy the linux libraries into the project automatically. + if [ ! -d "linux" ]; then + mkdir linux + cp -r $ROOT/vendor/raylib/linux/libraylib*.so* linux + fi + ;; +esac + +# Build the game. +odin build . -use-separate-modules -extra-linker-flags:"$EXTRA_LINKER_FLAGS" -show-timings -define:RAYLIB_SHARED=true -build-mode:dll -out:build/game_tmp$DLL_EXT -debug $VET + +# Need to use a temp file on Linux because it first writes an empty `game.so`, which the game will load before it is actually fully written. +mv game_tmp$DLL_EXT game$DLL_EXT + +# Do not build the game.bin if it is already running. +if ! pgrep game.bin > /dev/null; then + odin build src/main_hot_reload -use-separate-modules -out:build/game.bin $VET -debug +fi diff --git a/scripts/build_release.bat b/scripts/build_release.bat new file mode 100644 index 0000000..aa3e8bd --- /dev/null +++ b/scripts/build_release.bat @@ -0,0 +1,2 @@ +@echo off +odin build src/main_release -define:RAYLIB_SHARED=false -out:build/game_release.exe -no-bounds-check -o:speed -strict-style -vet-unused -vet-using-stmt -vet-using-param -vet-style -vet-semicolon -subsystem:windows diff --git a/scripts/build_release.sh b/scripts/build_release.sh new file mode 100644 index 0000000..257abc0 --- /dev/null +++ b/scripts/build_release.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +odin build src/main_release -out:build/game_release.bin -no-bounds-check -o:speed -strict-style -vet-unused -vet-using-stmt -vet-using-param -vet-style -vet-semicolon diff --git a/src/animation.odin b/src/animation.odin new file mode 100644 index 0000000..aeeaed5 --- /dev/null +++ b/src/animation.odin @@ -0,0 +1,53 @@ +// This implements simple animations using sprite sheets. The texture in the +// `Animation` struct is assumed to contain a horizontal strip of the frames +// in the animation. Call `animation_update` to update and then call +// `animation_rect` when you wish to know the source rect to use in the texture +// With the source rect you can run rl.DrawTextureRec to draw the current frame. + +package game + +import "core:log" + +Animation :: struct { + texture: Texture, + num_frames: int, + current_frame: int, + frame_timer: f32, + frame_length: f32, +} + +animation_create :: proc(tex: Texture, num_frames: int, frame_length: f32) -> Animation { + return( + Animation { + texture = tex, + num_frames = num_frames, + frame_length = frame_length, + frame_timer = frame_length, + } \ + ) +} + +animation_update :: proc(a: ^Animation, dt: f32) { + a.frame_timer -= dt + + if a.frame_timer <= 0 { + a.frame_timer = a.frame_length + a.frame_timer + a.current_frame += 1 + + if a.current_frame >= a.num_frames { + a.current_frame = 0 + } + } +} + +animation_rect :: proc(a: Animation) -> Rect { + if a.num_frames == 0 { + log.error("Animation has zero frames") + return RectEmpty + } + + w := f32(a.texture.width) / f32(a.num_frames) + h := f32(a.texture.height) + + return {x = f32(a.current_frame) * w, y = 0, width = w, height = h} +} diff --git a/src/game.odin b/src/game.odin new file mode 100644 index 0000000..c257fa7 --- /dev/null +++ b/src/game.odin @@ -0,0 +1,58 @@ +// This file is compiled as part of the `odin.dll` file. It contains the +// procs that `game.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. + +package game + +// import "core:fmt" +// import "core:math/linalg" +import rl "vendor:raylib" + +PixelWindowHeight :: 180 + +GameMemory :: struct {} + +g_mem: ^GameMemory + +game_camera :: proc() -> rl.Camera2D { + w := f32(rl.GetScreenWidth()) + h := f32(rl.GetScreenHeight()) + + return {zoom = h / PixelWindowHeight, target = {}, offset = {w / 2, h / 2}} +} + +ui_camera :: proc() -> rl.Camera2D { + return {zoom = f32(rl.GetScreenHeight()) / PixelWindowHeight} +} + +update :: proc() { +} + +draw :: proc() { + rl.BeginDrawing() + rl.ClearBackground(rl.BLACK) + + rl.BeginMode2D(game_camera()) + // rl.DrawRectangleV(g_mem.player_pos, {4, 8}, rl.WHITE) + rl.DrawRectangleV({20, 20}, {10, 20}, rl.RED) + rl.EndMode2D() + + rl.BeginMode2D(ui_camera()) + // rl.DrawText( + // fmt.ctprintf("some_number: %v\nplayer_pos: %v", g_mem.some_number, g_mem.player_pos), + // 5, + // 5, + // 8, + // rl.WHITE, + // ) + rl.EndMode2D() + + rl.EndDrawing() +} \ No newline at end of file diff --git a/src/handle_array.odin b/src/handle_array.odin new file mode 100644 index 0000000..69d9709 --- /dev/null +++ b/src/handle_array.odin @@ -0,0 +1,141 @@ +// This handle-based array gives you a statically allocated array where you can +// use index based handles instead of pointers. The handles have a generation +// that makes sure you don't get bugs when slots are re-used. +// Read more about it here: https://floooh.github.io/2018/06/17/handles-vs-pointers.html */ + +package game + +Handle :: struct($T: typeid) { + // idx 0 means unused. Note that slot 0 is a dummy slot, it can never be used. + idx: u32, + gen: u32, +} + +HandleArrayItem :: struct($T: typeid) { + item: T, + handle: Handle(T), +} + +// TODO: Add a freelist that uses some kind of bit array... We should be able to +// check 64 item slots at a time that way, but without any dynamic array. +HandleArray :: struct($T: typeid, $N: int) { + items: #soa[N]HandleArrayItem(T), + num_items: u32, +} + +ha_add :: proc(a: ^HandleArray($T, $N), v: T) -> (Handle(T), bool) #optional_ok { + for idx in 1 ..< a.num_items { + i := &a.items[idx] + + if idx != 0 && i.handle.idx == 0 { + i.handle.idx = u32(idx) + i.item = v + return i.handle, true + } + } + + // Index 0 is dummy + if a.num_items == 0 { + a.num_items += 1 + } + + if a.num_items == len(a.items) { + return {}, false + } + + idx := a.num_items + i := &a.items[a.num_items] + a.num_items += 1 + i.handle.idx = idx + i.handle.gen = 1 + i.item = v + return i.handle, true +} + +ha_get :: proc(a: HandleArray($T, $N), h: Handle(T)) -> (T, bool) { + if h.idx == 0 { + return {}, false + } + + if int(h.idx) < len(a.items) && h.idx < a.num_items && a.items[h.idx].handle == h { + return a.items[h.idx].item, true + } + + return {}, false +} + +ha_get_ptr :: proc(a: HandleArray($T, $N), h: Handle(T)) -> ^T { + if h.idx == 0 { + return nil + } + + if int(h.idx) < len(a.items) && h.idx < a.num_items && a.items[h.idx].handle == h { + return &ha.items[h.idx].item + } + + return nil +} + +ha_remove :: proc(a: ^HandleArray($T, $N), h: Handle(T)) { + if h.idx == 0 { + return + } + + if int(h.idx) < len(a.items) && h.idx < a.num_items && a.items[h.idx].handle == h { + a.items[h.idx].handle.idx = 0 + a.items[h.idx].handle.gen += 1 + } +} + +ha_valid :: proc(a: HandleArray($T, $N), h: Handle(T)) -> bool { + if h.idx == 0 { + return false + } + + return int(h.idx) < len(a.items) && h.idx < a.num_items && a.items[h.idx].handle == h +} + +HandleArrayIter :: struct($T: typeid, $N: int) { + a: ^HandleArray(T, N), + index: int, +} + +ha_make_iter :: proc(a: ^HandleArray($T, $N)) -> HandleArrayIter(T, N) { + return HandleArrayIter(T, N){a = a} +} + +ha_iter :: proc(it: ^HandleArrayIter($T, $N)) -> (val: T, h: Handle(T), cond: bool) { + cond = it.index < int(it.a.num_items) + + for ; cond; cond = it.index < int(it.a.num_items) { + if it.a.items[it.index].handle.idx == 0 { + it.index += 1 + continue + } + + val = it.a.items[it.index].item + h = it.a.items[it.index].handle + it.index += 1 + break + } + + return +} + +ha_iter_ptr :: proc(it: ^HandleArrayIter($T, $N)) -> (val: ^T, h: Handle(T), cond: bool) { + cond = it.index < int(it.a.num_items) + + for ; cond; cond = it.index < int(it.a.num_items) { + if it.a.items[it.index].handle.idx == 0 { + it.index += 1 + continue + } + + val = &it.a.items[it.index].item + h = it.a.items[it.index].handle + it.index += 1 + break + } + + return +} diff --git a/src/helpers.odin b/src/helpers.odin new file mode 100644 index 0000000..190d044 --- /dev/null +++ b/src/helpers.odin @@ -0,0 +1,41 @@ +// generic odin helpers + +package game + +import "core:intrinsics" +import "core:reflect" +import "core:strings" + +increase_or_wrap_enum :: proc(e: $T) -> T { + ei := int(e) + 1 + + if ei >= len(T) { + ei = 0 + } + + return T(ei) +} + +union_type :: proc(a: any) -> typeid { + return reflect.union_variant_typeid(a) +} + +temp_cstring :: proc(s: string) -> cstring { + return strings.clone_to_cstring(s, context.temp_allocator) +} + +// There is a remap in core:math but it doesn't clamp in the new range, which I +// always want. +remap :: proc "contextless" ( + old_value, old_min, old_max, new_min, new_max: $T, +) -> ( + x: T, +) where intrinsics.type_is_numeric(T), + !intrinsics.type_is_array(T) { + old_range := old_max - old_min + new_range := new_max - new_min + if old_range == 0 { + return new_range / 2 + } + return clamp(((old_value - old_min) / old_range) * new_range + new_min, new_min, new_max) +} diff --git a/src/main_hot_reload/main_hot_reload.odin b/src/main_hot_reload/main_hot_reload.odin new file mode 100644 index 0000000..99f4692 --- /dev/null +++ b/src/main_hot_reload/main_hot_reload.odin @@ -0,0 +1,196 @@ +// Development game exe. Loads game.dll and reloads it whenever it changes. + +package main + +import "core:c/libc" +import "core:dynlib" +import "core:fmt" +import "core:log" +import "core:mem" +import "core:os" + +when ODIN_OS == .Windows { + DLL_EXT :: ".dll" +} else when ODIN_OS == .Darwin { + DLL_EXT :: ".dylib" +} else { + DLL_EXT :: ".so" +} + +copy_dll :: proc(to: string) -> bool { + exit: i32 + when ODIN_OS == .Windows { + exit = libc.system(fmt.ctprintf("copy game.dll {0}", to)) + } else { + exit = libc.system(fmt.ctprintf("cp game" + DLL_EXT + " {0}", to)) + } + + if exit != 0 { + fmt.printfln("Failed to copy game" + DLL_EXT + " to {0}", to) + return false + } + + return true +} + +GameAPI :: struct { + lib: dynlib.Library, + init_window: proc(), + init: proc(), + update: proc() -> bool, + shutdown: proc(), + shutdown_window: proc(), + memory: proc() -> rawptr, + memory_size: proc() -> int, + hot_reloaded: proc(mem: rawptr), + force_reload: proc() -> bool, + force_restart: proc() -> bool, + modification_time: os.File_Time, + api_version: int, +} + +load_game_api :: proc(api_version: int) -> (api: GameAPI, ok: bool) { + mod_time, mod_time_error := os.last_write_time_by_name("game" + DLL_EXT) + if mod_time_error != os.ERROR_NONE { + fmt.printfln( + "Failed getting last write time of game" + DLL_EXT + ", error code: {1}", + mod_time_error, + ) + return + } + + // NOTE: this needs to be a relative path for Linux to work. + game_dll_name := fmt.tprintf( + "{0}game_{1}" + DLL_EXT, + "./" when ODIN_OS != .Windows else "", + api_version, + ) + copy_dll(game_dll_name) or_return + + _, ok = dynlib.initialize_symbols(&api, game_dll_name, "game_", "lib") + if !ok { + fmt.printfln("Failed initializing symbols: {0}", dynlib.last_error()) + } + + api.api_version = api_version + api.modification_time = mod_time + ok = true + + return +} + +unload_game_api :: proc(api: ^GameAPI) { + if api.lib != nil { + if !dynlib.unload_library(api.lib) { + fmt.printfln("Failed unloading lib: {0}", dynlib.last_error()) + } + } + + if os.remove(fmt.tprintf("game_{0}" + DLL_EXT, api.api_version)) != 0 { + fmt.printfln("Failed to remove game_{0}" + DLL_EXT + " copy", api.api_version) + } +} + +main :: proc() { + context.logger = log.create_console_logger() + + default_allocator := context.allocator + tracking_allocator: mem.Tracking_Allocator + mem.tracking_allocator_init(&tracking_allocator, default_allocator) + context.allocator = mem.tracking_allocator(&tracking_allocator) + + reset_tracking_allocator :: proc(a: ^mem.Tracking_Allocator) -> bool { + err := false + + for _, value in a.allocation_map { + fmt.printf("%v: Leaked %v bytes\n", value.location, value.size) + err = true + } + + mem.tracking_allocator_clear(a) + return err + } + + game_api_version := 0 + game_api, game_api_ok := load_game_api(game_api_version) + + if !game_api_ok { + fmt.println("Failed to load Game API") + return + } + + game_api_version += 1 + game_api.init_window() + game_api.init() + + old_game_apis := make([dynamic]GameAPI, default_allocator) + + window_open := true + for window_open { + window_open = game_api.update() + force_reload := game_api.force_reload() + force_restart := game_api.force_restart() + reload := force_reload || force_restart + game_dll_mod, game_dll_mod_err := os.last_write_time_by_name("game" + DLL_EXT) + + if game_dll_mod_err == os.ERROR_NONE && game_api.modification_time != game_dll_mod { + reload = true + } + + if reload { + new_game_api, new_game_api_ok := load_game_api(game_api_version) + + if new_game_api_ok { + if game_api.memory_size() != new_game_api.memory_size() || force_restart { + game_api.shutdown() + reset_tracking_allocator(&tracking_allocator) + + for &g in old_game_apis { + unload_game_api(&g) + } + + clear(&old_game_apis) + unload_game_api(&game_api) + game_api = new_game_api + game_api.init() + } else { + append(&old_game_apis, game_api) + game_memory := game_api.memory() + game_api = new_game_api + game_api.hot_reloaded(game_memory) + } + + game_api_version += 1 + } + } + + for b in tracking_allocator.bad_free_array { + log.error("Bad free at: %v", b.location) + } + + clear(&tracking_allocator.bad_free_array) + free_all(context.temp_allocator) + } + + free_all(context.temp_allocator) + game_api.shutdown() + reset_tracking_allocator(&tracking_allocator) + + for &g in old_game_apis { + unload_game_api(&g) + } + + delete(old_game_apis) + + game_api.shutdown_window() + unload_game_api(&game_api) + mem.tracking_allocator_destroy(&tracking_allocator) +} + +// make game use good GPU on laptops etc + +@(export) +NvOptimusEnablement: u32 = 1 + +@(export) +AmdPowerXpressRequestHighPerformance: i32 = 1 diff --git a/src/main_release/main_release.odin b/src/main_release/main_release.odin new file mode 100644 index 0000000..1d335d1 --- /dev/null +++ b/src/main_release/main_release.odin @@ -0,0 +1,77 @@ +// For making a release exe that does not use hot reload. + +package main_release + +import "core:log" +import "core:os" + +import game ".." + +UseTrackingAllocator :: #config(UseTrackingAllocator, false) + +main :: proc() { + when UseTrackingAllocator { + default_allocator := context.allocator + tracking_allocator: Tracking_Allocator + tracking_allocator_init(&tracking_allocator, default_allocator) + context.allocator = allocator_from_tracking_allocator(&tracking_allocator) + } + + mode: int = 0 + when ODIN_OS == .Linux || ODIN_OS == .Darwin { + mode = os.S_IRUSR | os.S_IWUSR | os.S_IRGRP | os.S_IROTH + } + + logh, logh_err := os.open("log.txt", (os.O_CREATE | os.O_TRUNC | os.O_RDWR), mode) + + if logh_err == os.ERROR_NONE { + os.stdout = logh + os.stderr = logh + } + + logger := + logh_err == os.ERROR_NONE ? log.create_file_logger(logh) : log.create_console_logger() + context.logger = logger + + game.game_init_window() + game.game_init() + + window_open := true + for window_open { + window_open = game.game_update() + + when UseTrackingAllocator { + for b in tracking_allocator.bad_free_array { + log.error("Bad free at: %v", b.location) + } + + clear(&tracking_allocator.bad_free_array) + } + + free_all(context.temp_allocator) + } + + free_all(context.temp_allocator) + game.game_shutdown() + game.game_shutdown_window() + + if logh_err == os.ERROR_NONE { + log.destroy_file_logger(&logger) + } + + when UseTrackingAllocator { + for key, value in tracking_allocator.allocation_map { + log.error("%v: Leaked %v bytes\n", value.location, value.size) + } + + tracking_allocator_destroy(&tracking_allocator) + } +} + +// make game use good GPU on laptops etc + +@(export) +NvOptimusEnablement: u32 = 1 + +@(export) +AmdPowerXpressRequestHighPerformance: i32 = 1 diff --git a/src/math.odin b/src/math.odin new file mode 100644 index 0000000..4f6634c --- /dev/null +++ b/src/math.odin @@ -0,0 +1,8 @@ +package game + +Vec2i :: [2]int +Vec2 :: [2]f32 + +vec2_from_vec2i :: proc(p: Vec2i) -> Vec2 { + return {f32(p.x), f32(p.y)} +} diff --git a/src/raylib_helpers.odin b/src/raylib_helpers.odin new file mode 100644 index 0000000..d884fc4 --- /dev/null +++ b/src/raylib_helpers.odin @@ -0,0 +1,70 @@ +package game + +import "core:slice" +import rl "vendor:raylib" + +Texture :: rl.Texture +Color :: rl.Color + +texture_rect :: proc(tex: Texture, flip_x: bool) -> Rect { + return( + { + x = 0, + y = 0, + width = flip_x ? -f32(tex.width) : f32(tex.width), + height = f32(tex.height), + } \ + ) +} + +load_premultiplied_alpha_ttf_from_memory :: proc(file_data: []byte, font_size: int) -> rl.Font { + font := rl.Font { + baseSize = i32(font_size), + glyphCount = 95, + } + + font.glyphs = rl.LoadFontData( + &file_data[0], + i32(len(file_data)), + font.baseSize, + {}, + font.glyphCount, + .DEFAULT, + ) + + if font.glyphs != nil { + font.glyphPadding = 4 + + atlas := rl.GenImageFontAtlas( + font.glyphs, + &font.recs, + font.glyphCount, + font.baseSize, + font.glyphPadding, + 0, + ) + atlas_u8 := slice.from_ptr((^u8)(atlas.data), int(atlas.width * atlas.height * 2)) + + for i in 0 ..< atlas.width * atlas.height { + a := atlas_u8[i * 2 + 1] + v := atlas_u8[i * 2] + atlas_u8[i * 2] = u8(f32(v) * (f32(a) / 255)) + } + + font.texture = rl.LoadTextureFromImage(atlas) + rl.SetTextureFilter(font.texture, .BILINEAR) + + // Update glyphs[i].image to use alpha, required to be used on ImageDrawText() + for i in 0 ..< font.glyphCount { + rl.UnloadImage(font.glyphs[i].image) + font.glyphs[i].image = rl.ImageFromImage(atlas, font.recs[i]) + } + //TRACELOG(LOG_INFO, "FONT: Data loaded successfully (%i pixel size | %i glyphs)", font.baseSize, font.glyphCount); + + rl.UnloadImage(atlas) + } else { + font = rl.GetFontDefault() + } + + return font +} diff --git a/src/rect.odin b/src/rect.odin new file mode 100644 index 0000000..4291aa2 --- /dev/null +++ b/src/rect.odin @@ -0,0 +1,66 @@ +// procs for modifying and managing rects + +package game + +import rl "vendor:raylib" + +Rect :: rl.Rectangle + +RectEmpty :: Rect{} + +split_rect_top :: proc(r: Rect, y: f32, m: f32) -> (top, bottom: Rect) { + top = r + bottom = r + top.y += m + top.height = y + bottom.y += y + m + bottom.height -= y + m + return +} + +split_rect_left :: proc(r: Rect, x: f32, m: f32) -> (left, right: Rect) { + left = r + right = r + left.width = x + right.x += x + m + right.width -= x + m + return +} + +split_rect_bottom :: proc(r: rl.Rectangle, y: f32, m: f32) -> (top, bottom: rl.Rectangle) { + top = r + top.height -= y + m + bottom = r + bottom.y = top.y + top.height + m + bottom.height = y + return +} + +split_rect_right :: proc(r: Rect, x: f32, m: f32) -> (left, right: Rect) { + left = r + right = r + right.width = x + left.width -= x + m + right.x = left.x + left.width + return +} + +rect_middle :: proc(r: Rect) -> Vec2 { + return {r.x + f32(r.width) * 0.5, r.y + f32(r.height) * 0.5} +} + +inset_rect :: proc(r: Rect, x: f32, y: f32) -> Rect { + return {r.x + x, r.y + y, r.width - x * 2, r.height - y * 2} +} + +rect_add_pos :: proc(r: Rect, p: Vec2) -> Rect { + return {r.x + p.x, r.y + p.y, r.width, r.height} +} + +mouse_in_rect :: proc(r: Rect) -> bool { + return rl.CheckCollisionPointRec(rl.GetMousePosition(), r) +} + +mouse_in_world_rect :: proc(r: Rect, camera: rl.Camera2D) -> bool { + return rl.CheckCollisionPointRec(rl.GetScreenToWorld2D(rl.GetMousePosition(), camera), r) +} diff --git a/src/symbol_exports.odin b/src/symbol_exports.odin new file mode 100644 index 0000000..c82fd83 --- /dev/null +++ b/src/symbol_exports.odin @@ -0,0 +1,61 @@ +package game + +@(export) +game_update :: proc() -> bool { + update() + draw() + return !rl.WindowShouldClose() +} + +@(export) +game_init_window :: proc() { + rl.SetConfigFlags({.WINDOW_RESIZABLE}) + rl.InitWindow(1280, 720, "Odin + Raylib + Hot Reload template!") + rl.SetWindowPosition(200, 200) + rl.SetTargetFPS(500) +} + +@(export) +game_init :: proc() { + g_mem = new(GameMemory) + + g_mem^ = GameMemory { + } + + game_hot_reloaded(g_mem) +} + +@(export) +game_shutdown :: proc() { + 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(GameMemory) +} + +@(export) +game_hot_reloaded :: proc(mem: rawptr) { + g_mem = (^GameMemory)(mem) +} + +@(export) +game_force_reload :: proc() -> bool { + return rl.IsKeyPressed(.F5) +} + +@(export) +game_force_restart :: proc() -> bool { + return rl.IsKeyPressed(.F6) +} \ No newline at end of file