This commit is contained in:
Stefan Stefanov 2024-04-19 20:41:22 +03:00
parent 10f2cebd46
commit 15e5ad3454
22 changed files with 965 additions and 1 deletions

3
.gitignore vendored
View file

@ -12,4 +12,5 @@ log.txt
*.dSYM
linux
build/
build/
ols.json

31
.vscode/launch.json vendored Normal file
View file

@ -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}"
}
]
}

7
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,7 @@
{
"workbench.colorCustomizations": {
"activityBar.background": "#322C2D",
"titleBar.activeBackground": "#463E3F",
"titleBar.activeForeground": "#FAFAFA"
}
}

60
.vscode/tasks.json vendored Normal file
View file

@ -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
},
}
]
}

7
LICENSE Normal file
View file

@ -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.

9
README.md Normal file
View file

@ -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).

2
scripts/build_debug.bat Normal file
View file

@ -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

3
scripts/build_debug.sh Normal file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
odin build src/main_release -out:build/game_debug.bin -no-bounds-check -debug

View file

@ -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 <your_odin_compiler>/vendor/raylib/windows/raylib.dll to the same directory as game.exe"
exit /b 1
)
exit /b 0

View file

@ -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

View file

@ -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

3
scripts/build_release.sh Normal file
View file

@ -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

53
src/animation.odin Normal file
View file

@ -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}
}

58
src/game.odin Normal file
View file

@ -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()
}

141
src/handle_array.odin Normal file
View file

@ -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
}

41
src/helpers.odin Normal file
View file

@ -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)
}

View file

@ -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

View file

@ -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

8
src/math.odin Normal file
View file

@ -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)}
}

70
src/raylib_helpers.odin Normal file
View file

@ -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
}

66
src/rect.odin Normal file
View file

@ -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)
}

61
src/symbol_exports.odin Normal file
View file

@ -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)
}