[feat]: initial commit
This commit is contained in:
commit
7b48f34e94
24 changed files with 1354 additions and 0 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
use flake
|
||||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
build/**
|
||||||
|
project.sublime-workspace
|
||||||
|
game_hot_reload.exe
|
||||||
|
game_hot_reload.bin
|
||||||
|
game_hot_reload.bin.DSYM
|
||||||
|
raylib.dll
|
||||||
|
linux/**
|
||||||
|
.direnv
|
||||||
5
Justfile
Normal file
5
Justfile
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
build:
|
||||||
|
./build_hot_reload.sh
|
||||||
|
|
||||||
|
run:
|
||||||
|
./game_hot_reload.bin
|
||||||
16
build_debug.bat
Normal file
16
build_debug.bat
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
@echo off
|
||||||
|
|
||||||
|
:: This creates a build that is similar to a release build, but it's debuggable.
|
||||||
|
:: There is no hot reloading and no separate game library.
|
||||||
|
|
||||||
|
set OUT_DIR=build\debug
|
||||||
|
|
||||||
|
if not exist %OUT_DIR% mkdir %OUT_DIR%
|
||||||
|
|
||||||
|
odin build source\main_release -out:%OUT_DIR%\game_debug.exe -strict-style -vet -debug
|
||||||
|
IF %ERRORLEVEL% NEQ 0 exit /b 1
|
||||||
|
|
||||||
|
xcopy /y /e /i assets %OUT_DIR%\assets > nul
|
||||||
|
IF %ERRORLEVEL% NEQ 0 exit /b 1
|
||||||
|
|
||||||
|
echo Debug build created in %OUT_DIR%
|
||||||
11
build_debug.sh
Executable file
11
build_debug.sh
Executable file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# This creates a build that is similar to a release build, but it is debuggable.
|
||||||
|
# There is no hot reloading and no separate game library.
|
||||||
|
|
||||||
|
OUT_DIR="build/debug"
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
odin build source/main_release -out:$OUT_DIR/game_debug.bin -strict-style -vet -debug
|
||||||
|
cp -R assets $OUT_DIR
|
||||||
|
echo "Debug build created in $OUT_DIR"
|
||||||
81
build_hot_reload.bat
Normal file
81
build_hot_reload.bat
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
@echo off
|
||||||
|
|
||||||
|
set GAME_RUNNING=false
|
||||||
|
|
||||||
|
:: OUT_DIR is for everything except the exe. The exe needs to stay in root
|
||||||
|
:: folder so it sees the assets folder, without having to copy it.
|
||||||
|
set OUT_DIR=build\hot_reload
|
||||||
|
set GAME_PDBS_DIR=%OUT_DIR%\game_pdbs
|
||||||
|
|
||||||
|
set EXE=game_hot_reload.exe
|
||||||
|
|
||||||
|
:: Check if game is running
|
||||||
|
FOR /F %%x IN ('tasklist /NH /FI "IMAGENAME eq %EXE%"') DO IF %%x == %EXE% set GAME_RUNNING=true
|
||||||
|
|
||||||
|
if not exist %OUT_DIR% mkdir %OUT_DIR%
|
||||||
|
|
||||||
|
:: If game isn't running then:
|
||||||
|
:: - delete all game_XXX.dll files
|
||||||
|
:: - delete all PDBs in pdbs subdir
|
||||||
|
:: - optionally create the pdbs subdir
|
||||||
|
:: - write 0 into pdbs\pdb_number so game.dll PDBs start counting from zero
|
||||||
|
::
|
||||||
|
:: This makes sure we start over "fresh" at PDB number 0 when starting up the
|
||||||
|
:: game and it also makes sure we don't have so many PDBs laying around.
|
||||||
|
if %GAME_RUNNING% == false (
|
||||||
|
del /q /s %OUT_DIR% >nul 2>nul
|
||||||
|
if not exist "%GAME_PDBS_DIR%" mkdir %GAME_PDBS_DIR%
|
||||||
|
echo 0 > %GAME_PDBS_DIR%\pdb_number
|
||||||
|
)
|
||||||
|
|
||||||
|
:: Load PDB number from file, increment and store back. For as long as the game
|
||||||
|
:: is running the pdb_number file won't be reset to 0, so we'll get a PDB of a
|
||||||
|
:: unique name on each hot reload.
|
||||||
|
set /p PDB_NUMBER=<%GAME_PDBS_DIR%\pdb_number
|
||||||
|
set /a PDB_NUMBER=%PDB_NUMBER%+1
|
||||||
|
echo %PDB_NUMBER% > %GAME_PDBS_DIR%\pdb_number
|
||||||
|
|
||||||
|
:: Build game dll, use pdbs\game_%PDB_NUMBER%.pdb as PDB name so each dll gets
|
||||||
|
:: its own PDB. This PDB stuff is done in order to make debugging work.
|
||||||
|
:: Debuggers tend to lock PDBs or just misbehave if you reuse the same PDB while
|
||||||
|
:: the debugger is attached. So each time we compile `game.dll` we give the
|
||||||
|
:: PDB a unique PDB.
|
||||||
|
::
|
||||||
|
:: Note that we could not just rename the PDB after creation; the DLL contains a
|
||||||
|
:: reference to where the PDB is.
|
||||||
|
::
|
||||||
|
:: Also note that we always write game.dll to the same file. game_hot_reload.exe
|
||||||
|
:: monitors this file and does the hot reload when it changes.
|
||||||
|
echo Building game.dll
|
||||||
|
odin build source -strict-style -vet -debug -define:RAYLIB_SHARED=true -build-mode:dll -out:%OUT_DIR%/game.dll -pdb-name:%GAME_PDBS_DIR%\game_%PDB_NUMBER%.pdb > nul
|
||||||
|
IF %ERRORLEVEL% NEQ 0 exit /b 1
|
||||||
|
|
||||||
|
:: If game.exe already running: Then only compile game.dll and exit cleanly
|
||||||
|
if %GAME_RUNNING% == true (
|
||||||
|
echo Hot reloading... && exit /b 0
|
||||||
|
)
|
||||||
|
|
||||||
|
:: Build game.exe, which starts the program and loads game.dll och does the logic for hot reloading.
|
||||||
|
echo Building %EXE%
|
||||||
|
odin build source\main_hot_reload -strict-style -vet -debug -out:%EXE% -pdb-name:%OUT_DIR%\main_hot_reload.pdb
|
||||||
|
IF %ERRORLEVEL% NEQ 0 exit /b 1
|
||||||
|
|
||||||
|
set ODIN_PATH=
|
||||||
|
for /f "delims=" %%i in ('odin root') do set "ODIN_PATH=%%i"
|
||||||
|
|
||||||
|
if not exist "raylib.dll" (
|
||||||
|
if exist "%ODIN_PATH%\vendor\raylib\windows\raylib.dll" (
|
||||||
|
echo raylib.dll not found in current directory. Copying from %ODIN_PATH%\vendor\raylib\windows\raylib.dll
|
||||||
|
copy "%ODIN_PATH%\vendor\raylib\windows\raylib.dll" .
|
||||||
|
IF %ERRORLEVEL% NEQ 0 exit /b 1
|
||||||
|
) else (
|
||||||
|
echo "Please copy raylib.dll from <your_odin_compiler>/vendor/raylib/windows/raylib.dll to the same directory as game.exe"
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%~1"=="run" (
|
||||||
|
echo Running %EXE%...
|
||||||
|
start %EXE%
|
||||||
|
)
|
||||||
|
|
||||||
56
build_hot_reload.sh
Executable file
56
build_hot_reload.sh
Executable file
|
|
@ -0,0 +1,56 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# OUT_DIR is for everything except the exe. The exe needs to stay in root
|
||||||
|
# folder so it sees the assets folder, without having to copy it.
|
||||||
|
OUT_DIR=build/hot_reload
|
||||||
|
EXE=game_hot_reload.bin
|
||||||
|
|
||||||
|
mkdir -p $OUT_DIR
|
||||||
|
|
||||||
|
# root is a special command of the odin compiler that tells you where the Odin
|
||||||
|
# compiler is located.
|
||||||
|
ROOT=$(odin root)
|
||||||
|
|
||||||
|
# Figure out which DLL extension to use based on platform. Also copy the Linux
|
||||||
|
# so libs.
|
||||||
|
case $(uname) in
|
||||||
|
"Darwin")
|
||||||
|
DLL_EXT=".dylib"
|
||||||
|
EXTRA_LINKER_FLAGS="-Wl,-rpath $ROOT/vendor/raylib/macos"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
DLL_EXT=".so"
|
||||||
|
EXTRA_LINKER_FLAGS="'-Wl,-rpath=\$ORIGIN/linux'"
|
||||||
|
|
||||||
|
# Copy the linux libraries into the project automatically.
|
||||||
|
if [ ! -d "$OUT_DIR/linux" ]; then
|
||||||
|
mkdir -p $OUT_DIR/linux
|
||||||
|
cp -r $ROOT/vendor/raylib/linux/libraylib*.so* $OUT_DIR/linux
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Build the game. Note that the game goes into $OUT_DIR while the exe stays in
|
||||||
|
# the root folder.
|
||||||
|
echo "Building game$DLL_EXT"
|
||||||
|
odin build source -extra-linker-flags:"$EXTRA_LINKER_FLAGS" -define:RAYLIB_SHARED=true -build-mode:dll -out:$OUT_DIR/game_tmp$DLL_EXT -strict-style -vet -debug
|
||||||
|
|
||||||
|
# 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 $OUT_DIR/game_tmp$DLL_EXT $OUT_DIR/game$DLL_EXT
|
||||||
|
|
||||||
|
# If the executable is already running, then don't try to build and start it.
|
||||||
|
# -f is there to make sure we match against full name, including .bin
|
||||||
|
if pgrep -f $EXE > /dev/null; then
|
||||||
|
echo "Hot reloading..."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building $EXE"
|
||||||
|
odin build source/main_hot_reload -out:$EXE -strict-style -vet -debug
|
||||||
|
|
||||||
|
if [ $# -ge 1 ] && [ $1 == "run" ]; then
|
||||||
|
echo "Running $EXE"
|
||||||
|
./$EXE &
|
||||||
|
fi
|
||||||
15
build_release.bat
Normal file
15
build_release.bat
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
:: This script creates an optimized release build.
|
||||||
|
|
||||||
|
@echo off
|
||||||
|
|
||||||
|
set OUT_DIR=build\release
|
||||||
|
|
||||||
|
if not exist %OUT_DIR% mkdir %OUT_DIR%
|
||||||
|
|
||||||
|
odin build source\main_release -out:%OUT_DIR%\game_release.exe -strict-style -vet -no-bounds-check -o:speed -subsystem:windows
|
||||||
|
IF %ERRORLEVEL% NEQ 0 exit /b 1
|
||||||
|
|
||||||
|
xcopy /y /e /i assets %OUT_DIR%\assets > nul
|
||||||
|
IF %ERRORLEVEL% NEQ 0 exit /b 1
|
||||||
|
|
||||||
|
echo Release build created in %OUT_DIR%
|
||||||
10
build_release.sh
Executable file
10
build_release.sh
Executable file
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# This script creates an optimized release build.
|
||||||
|
|
||||||
|
OUT_DIR="build/release"
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
odin build source/main_release -out:$OUT_DIR/game_release.bin -strict-style -vet -no-bounds-check -o:speed
|
||||||
|
cp -R assets $OUT_DIR
|
||||||
|
echo "Release build created in $OUT_DIR"
|
||||||
40
build_web.bat
Normal file
40
build_web.bat
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
@echo off
|
||||||
|
|
||||||
|
:: Point this to where you installed emscripten.
|
||||||
|
set EMSCRIPTEN_SDK_DIR=c:\SDK\emsdk
|
||||||
|
set OUT_DIR=build\web
|
||||||
|
|
||||||
|
if not exist %OUT_DIR% mkdir %OUT_DIR%
|
||||||
|
|
||||||
|
set EMSDK_QUIET=1
|
||||||
|
call %EMSCRIPTEN_SDK_DIR%\emsdk_env.bat
|
||||||
|
|
||||||
|
:: Note RAYLIB_WASM_LIB=env.o -- env.o is an internal WASM object file. You can
|
||||||
|
:: see how RAYLIB_WASM_LIB is used inside <odin>/vendor/raylib/raylib.odin.
|
||||||
|
::
|
||||||
|
:: The emcc call will be fed the actual raylib library file. That stuff will end
|
||||||
|
:: up in env.o
|
||||||
|
::
|
||||||
|
:: Note that there is a rayGUI equivalent: -define:RAYGUI_WASM_LIB=env.o
|
||||||
|
odin build source\main_web -target:js_wasm32 -build-mode:obj -define:RAYLIB_WASM_LIB=env.o -define:RAYGUI_WASM_LIB=env.o -vet -strict-style -out:%OUT_DIR%\game.wasm.o
|
||||||
|
IF %ERRORLEVEL% NEQ 0 exit /b 1
|
||||||
|
|
||||||
|
for /f "delims=" %%i in ('odin root') do set "ODIN_PATH=%%i"
|
||||||
|
|
||||||
|
copy "%ODIN_PATH%\core\sys\wasm\js\odin.js" %OUT_DIR%
|
||||||
|
|
||||||
|
set files=%OUT_DIR%\game.wasm.o "%ODIN_PATH%\vendor\raylib\wasm\libraylib.a" "%ODIN_PATH%\vendor\raylib\wasm\libraygui.a"
|
||||||
|
|
||||||
|
:: index_template.html contains the javascript code that calls the procedures in
|
||||||
|
:: source/main_web/main_web.odin
|
||||||
|
set flags=-sUSE_GLFW=3 -sWASM_BIGINT -sWARN_ON_UNDEFINED_SYMBOLS=0 -sASSERTIONS --shell-file source\main_web\index_template.html --preload-file assets
|
||||||
|
|
||||||
|
:: For debugging: Add `-g` to `emcc` (gives better error callstack in chrome)
|
||||||
|
::
|
||||||
|
:: This uses `cmd /c` to avoid emcc stealing the whole command prompt. Otherwise
|
||||||
|
:: it does not run the lines that follow it.
|
||||||
|
cmd /c emcc -o %OUT_DIR%\index.html %files% %flags%
|
||||||
|
|
||||||
|
del %OUT_DIR%\game.wasm.o
|
||||||
|
|
||||||
|
echo Web build created in %OUT_DIR%
|
||||||
38
build_web.sh
Executable file
38
build_web.sh
Executable file
|
|
@ -0,0 +1,38 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# Point this to where you installed emscripten. Optional on systems that already
|
||||||
|
# have `emcc` in the path.
|
||||||
|
EMSCRIPTEN_SDK_DIR="$HOME/repos/emsdk"
|
||||||
|
OUT_DIR="build/web"
|
||||||
|
|
||||||
|
mkdir -p $OUT_DIR
|
||||||
|
|
||||||
|
export EMSDK_QUIET=1
|
||||||
|
[[ -f "$EMSCRIPTEN_SDK_DIR/emsdk_env.sh" ]] && . "$EMSCRIPTEN_SDK_DIR/emsdk_env.sh"
|
||||||
|
|
||||||
|
# Note RAYLIB_WASM_LIB=env.o -- env.o is an internal WASM object file. You can
|
||||||
|
# see how RAYLIB_WASM_LIB is used inside <odin>/vendor/raylib/raylib.odin.
|
||||||
|
#
|
||||||
|
# The emcc call will be fed the actual raylib library file. That stuff will end
|
||||||
|
# up in env.o
|
||||||
|
#
|
||||||
|
# Note that there is a rayGUI equivalent: -define:RAYGUI_WASM_LIB=env.o
|
||||||
|
odin build source/main_web -target:js_wasm32 -build-mode:obj -define:RAYLIB_WASM_LIB=env.o -define:RAYGUI_WASM_LIB=env.o -vet -strict-style -out:$OUT_DIR/game.wasm.o
|
||||||
|
|
||||||
|
ODIN_PATH=$(odin root)
|
||||||
|
|
||||||
|
cp $ODIN_PATH/core/sys/wasm/js/odin.js $OUT_DIR
|
||||||
|
|
||||||
|
files="$OUT_DIR/game.wasm.o ${ODIN_PATH}/vendor/raylib/wasm/libraylib.a ${ODIN_PATH}/vendor/raylib/wasm/libraygui.a"
|
||||||
|
|
||||||
|
# index_template.html contains the javascript code that calls the procedures in
|
||||||
|
# source/main_web/main_web.odin
|
||||||
|
flags="-sUSE_GLFW=3 -sWASM_BIGINT -sWARN_ON_UNDEFINED_SYMBOLS=0 -sASSERTIONS --shell-file source/main_web/index_template.html --preload-file assets"
|
||||||
|
|
||||||
|
# For debugging: Add `-g` to `emcc` (gives better error callstack in chrome)
|
||||||
|
emcc -o $OUT_DIR/index.html $files $flags
|
||||||
|
|
||||||
|
rm $OUT_DIR/game.wasm.o
|
||||||
|
|
||||||
|
echo "Web build created in ${OUT_DIR}"
|
||||||
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1777578337,
|
||||||
|
"narHash": "sha256-Ad49moKWeXtKBJNy2ebiTQUEgdLyvGmTeykAQ9xM+Z4=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "15f4ee454b1dce334612fa6843b3e05cf546efab",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
31
flake.nix
Normal file
31
flake.nix
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
description = "Odin + Raylib";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs }:
|
||||||
|
let
|
||||||
|
allSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
|
||||||
|
|
||||||
|
forAllSystems = f: nixpkgs.lib.genAttrs allSystems (system: f {
|
||||||
|
pkgs = import nixpkgs { inherit system; };
|
||||||
|
});
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells = forAllSystems ({ pkgs }: {
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
packages = with pkgs; [
|
||||||
|
odin
|
||||||
|
ols
|
||||||
|
raylib
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
export LD_LIBRARY_PATH="${pkgs.raylib}/lib:$LD_LIBRARY_PATH"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
19
source/engine/main.odin
Normal file
19
source/engine/main.odin
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
package engine
|
||||||
|
|
||||||
|
init :: proc() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
update :: proc() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
draw :: proc() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
unload :: proc() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
173
source/game.odin
Normal file
173
source/game.odin
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
/*
|
||||||
|
This file is the starting point of your game.
|
||||||
|
|
||||||
|
Some important procedures are:
|
||||||
|
- game_init_window: Opens the window
|
||||||
|
- game_init: Sets up the game state
|
||||||
|
- game_update: Run once per frame
|
||||||
|
- game_should_close: For stopping your game when close button is pressed
|
||||||
|
- game_shutdown: Shuts down game and frees memory
|
||||||
|
- game_shutdown_window: Closes window
|
||||||
|
|
||||||
|
The procs above are used regardless if you compile using the `build_release`
|
||||||
|
script or the `build_hot_reload` script. However, in the hot reload case, the
|
||||||
|
contents of this file is compiled as part of `build/hot_reload/game.dll` (or
|
||||||
|
.dylib/.so on mac/linux). In the hot reload cases some other procedures are
|
||||||
|
also used in order to facilitate the hot reload functionality:
|
||||||
|
|
||||||
|
- game_memory: Run just before a hot reload. That way game_hot_reload.exe has a
|
||||||
|
pointer to the game's memory that it can hand to the new game DLL.
|
||||||
|
- game_hot_reloaded: Run after a hot reload so that the `g` global
|
||||||
|
variable can be set to whatever pointer it was in the old DLL.
|
||||||
|
|
||||||
|
NOTE: When compiled as part of `build_release`, `build_debug` or `build_web`
|
||||||
|
then this whole package is just treated as a normal Odin package. No DLL is
|
||||||
|
created.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// NOTE: `fmt.ctprintf` uses the temp allocator. The temp allocator is
|
||||||
|
// cleared at the end of the frame by the main application, meaning inside
|
||||||
|
// `main_hot_reload.odin`, `main_release.odin` or `main_web_entry.odin`.
|
||||||
|
|
||||||
|
package game
|
||||||
|
|
||||||
|
import "engine"
|
||||||
|
import rl "vendor:raylib"
|
||||||
|
|
||||||
|
GAME_WIDTH :: 640
|
||||||
|
GAME_HEIGHT :: 270
|
||||||
|
|
||||||
|
Game_Memory :: struct {
|
||||||
|
render_target: rl.RenderTexture,
|
||||||
|
render_scale: f32,
|
||||||
|
run: bool,
|
||||||
|
}
|
||||||
|
g: ^Game_Memory
|
||||||
|
|
||||||
|
update :: proc() {
|
||||||
|
if rl.IsKeyPressed(.ESCAPE) {
|
||||||
|
g.run = false
|
||||||
|
}
|
||||||
|
|
||||||
|
g.render_scale = min(
|
||||||
|
f32(rl.GetScreenWidth()) / f32(GAME_WIDTH),
|
||||||
|
f32(rl.GetScreenHeight()) / f32(GAME_HEIGHT),
|
||||||
|
)
|
||||||
|
|
||||||
|
rl.SetMouseOffset(
|
||||||
|
-i32((f32(rl.GetScreenWidth()) - (f32(GAME_WIDTH) * g.render_scale)) * 0.5),
|
||||||
|
-i32((f32(rl.GetScreenHeight()) - (f32(GAME_HEIGHT) * g.render_scale)) * 0.5),
|
||||||
|
)
|
||||||
|
rl.SetMouseScale(1 / g.render_scale, 1 / g.render_scale)
|
||||||
|
|
||||||
|
engine.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
draw :: proc() {
|
||||||
|
rl.BeginTextureMode(g.render_target)
|
||||||
|
rl.ClearBackground(rl.BLACK)
|
||||||
|
engine.draw()
|
||||||
|
rl.EndTextureMode()
|
||||||
|
|
||||||
|
rl.BeginDrawing()
|
||||||
|
rl.ClearBackground(rl.BLACK)
|
||||||
|
rl.DrawTexturePro(
|
||||||
|
g.render_target.texture,
|
||||||
|
{0, 0, f32(g.render_target.texture.width), f32(-g.render_target.texture.height)},
|
||||||
|
{
|
||||||
|
(f32(rl.GetScreenWidth()) - (f32(GAME_WIDTH) * g.render_scale)) * 0.5,
|
||||||
|
(f32(rl.GetScreenHeight()) - (f32(GAME_HEIGHT) * g.render_scale)) * 0.5,
|
||||||
|
f32(GAME_WIDTH) * g.render_scale,
|
||||||
|
f32(GAME_HEIGHT) * g.render_scale,
|
||||||
|
},
|
||||||
|
{0, 0},
|
||||||
|
0,
|
||||||
|
rl.WHITE,
|
||||||
|
)
|
||||||
|
rl.EndDrawing()
|
||||||
|
}
|
||||||
|
|
||||||
|
@(export)
|
||||||
|
game_update :: proc() {
|
||||||
|
update()
|
||||||
|
draw()
|
||||||
|
free_all(context.temp_allocator)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(export)
|
||||||
|
game_init_window :: proc() {
|
||||||
|
rl.SetConfigFlags({.WINDOW_RESIZABLE, .VSYNC_HINT})
|
||||||
|
rl.InitWindow(1280, 720, "Untitle Roguelike")
|
||||||
|
rl.SetWindowPosition(200, 200)
|
||||||
|
rl.SetTargetFPS(60)
|
||||||
|
rl.SetExitKey(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(export)
|
||||||
|
game_init :: proc() {
|
||||||
|
g = new(Game_Memory)
|
||||||
|
|
||||||
|
g^ = Game_Memory {
|
||||||
|
render_target = rl.LoadRenderTexture(GAME_WIDTH, GAME_HEIGHT),
|
||||||
|
run = true,
|
||||||
|
}
|
||||||
|
engine.init()
|
||||||
|
game_hot_reloaded(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(export)
|
||||||
|
game_should_run :: proc() -> bool {
|
||||||
|
when ODIN_OS != .JS {
|
||||||
|
if rl.WindowShouldClose() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return g.run
|
||||||
|
}
|
||||||
|
|
||||||
|
@(export)
|
||||||
|
game_shutdown :: proc() {
|
||||||
|
free(g)
|
||||||
|
engine.unload()
|
||||||
|
}
|
||||||
|
|
||||||
|
@(export)
|
||||||
|
game_shutdown_window :: proc() {
|
||||||
|
rl.CloseWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
@(export)
|
||||||
|
game_memory :: proc() -> rawptr {
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
@(export)
|
||||||
|
game_memory_size :: proc() -> int {
|
||||||
|
return size_of(Game_Memory)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(export)
|
||||||
|
game_hot_reloaded :: proc(mem: rawptr) {
|
||||||
|
g = (^Game_Memory)(mem)
|
||||||
|
|
||||||
|
// Here you can also set your own global variables. A good idea is to make
|
||||||
|
// your global variables into pointers that point to something inside `g`.
|
||||||
|
}
|
||||||
|
|
||||||
|
@(export)
|
||||||
|
game_force_reload :: proc() -> bool {
|
||||||
|
return rl.IsKeyPressed(.F5)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(export)
|
||||||
|
game_force_restart :: proc() -> bool {
|
||||||
|
return rl.IsKeyPressed(.F6)
|
||||||
|
}
|
||||||
|
|
||||||
|
// In a web build, this is called when browser changes size. Remove the
|
||||||
|
// `rl.SetWindowSize` call if you don't want a resizable game.
|
||||||
|
game_parent_window_size_changed :: proc(w, h: int) {
|
||||||
|
rl.SetWindowSize(i32(w), i32(h))
|
||||||
|
}
|
||||||
|
|
||||||
229
source/main_hot_reload/main_hot_reload.odin
Normal file
229
source/main_hot_reload/main_hot_reload.odin
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
/*
|
||||||
|
Development game exe. Loads build/hot_reload/game.dll and reloads it whenever it
|
||||||
|
changes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "core:dynlib"
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:c/libc"
|
||||||
|
import "core:os"
|
||||||
|
import "core:log"
|
||||||
|
import "core:mem"
|
||||||
|
import "core:path/filepath"
|
||||||
|
import "core:time"
|
||||||
|
|
||||||
|
when ODIN_OS == .Windows {
|
||||||
|
DLL_EXT :: ".dll"
|
||||||
|
} else when ODIN_OS == .Darwin {
|
||||||
|
DLL_EXT :: ".dylib"
|
||||||
|
} else {
|
||||||
|
DLL_EXT :: ".so"
|
||||||
|
}
|
||||||
|
|
||||||
|
GAME_DLL_DIR :: "build/hot_reload/"
|
||||||
|
GAME_DLL_PATH :: GAME_DLL_DIR + "game" + DLL_EXT
|
||||||
|
|
||||||
|
// We copy the DLL because using it directly would lock it, which would prevent
|
||||||
|
// the compiler from writing to it.
|
||||||
|
copy_dll :: proc(to: string) -> bool {
|
||||||
|
copy_err := os.copy_file(to, GAME_DLL_PATH)
|
||||||
|
|
||||||
|
if copy_err != nil {
|
||||||
|
fmt.printfln("Failed to copy " + GAME_DLL_PATH + " to {0}: %v", to, copy_err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
Game_API :: struct {
|
||||||
|
lib: dynlib.Library,
|
||||||
|
init_window: proc(),
|
||||||
|
init: proc(),
|
||||||
|
update: proc(),
|
||||||
|
should_run: 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: time.Time,
|
||||||
|
api_version: int,
|
||||||
|
}
|
||||||
|
|
||||||
|
load_game_api :: proc(api_version: int) -> (api: Game_API, ok: bool) {
|
||||||
|
mod_time, mod_time_error := os.last_write_time_by_name(GAME_DLL_PATH)
|
||||||
|
if mod_time_error != os.ERROR_NONE {
|
||||||
|
fmt.printfln(
|
||||||
|
"Failed getting last write time of " + GAME_DLL_PATH + ", error code: {1}",
|
||||||
|
mod_time_error,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
game_dll_name := fmt.tprintf(GAME_DLL_DIR + "game_{0}" + DLL_EXT, api_version)
|
||||||
|
copy_dll(game_dll_name) or_return
|
||||||
|
|
||||||
|
// This proc matches the names of the fields in Game_API to symbols in the
|
||||||
|
// game DLL. It actually looks for symbols starting with `game_`, which is
|
||||||
|
// why the argument `"game_"` is there.
|
||||||
|
_, 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: ^Game_API) {
|
||||||
|
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_DLL_DIR + "game_{0}" + DLL_EXT, api.api_version)) != nil {
|
||||||
|
fmt.printfln("Failed to remove {0}game_{1}" + DLL_EXT + " copy", GAME_DLL_DIR, api.api_version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: proc() {
|
||||||
|
// Set working dir to dir of executable.
|
||||||
|
exe_path := os.args[0]
|
||||||
|
exe_dir := filepath.dir(string(exe_path), context.temp_allocator)
|
||||||
|
os.set_working_directory(exe_dir)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
log.errorf("%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]Game_API, default_allocator)
|
||||||
|
|
||||||
|
for game_api.should_run() {
|
||||||
|
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_PATH)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
force_restart = force_restart || game_api.memory_size() != new_game_api.memory_size()
|
||||||
|
|
||||||
|
if !force_restart {
|
||||||
|
// This does the normal hot reload
|
||||||
|
|
||||||
|
// Note that we don't unload the old game APIs because that
|
||||||
|
// would unload the DLL. The DLL can contain stored info
|
||||||
|
// such as string literals. The old DLLs are only unloaded
|
||||||
|
// on a full reset or on shutdown.
|
||||||
|
append(&old_game_apis, game_api)
|
||||||
|
game_memory := game_api.memory()
|
||||||
|
game_api = new_game_api
|
||||||
|
game_api.hot_reloaded(game_memory)
|
||||||
|
} else {
|
||||||
|
// This does a full reset. That's basically like opening and
|
||||||
|
// closing the game, without having to restart the executable.
|
||||||
|
//
|
||||||
|
// You end up in here if the game requests a full reset OR
|
||||||
|
// if the size of the game memory has changed. That would
|
||||||
|
// probably lead to a crash anyways.
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
game_api_version += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tracking_allocator.bad_free_array) > 0 {
|
||||||
|
for b in tracking_allocator.bad_free_array {
|
||||||
|
log.errorf("Bad free at: %v", b.location)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This prevents the game from closing without you seeing the bad
|
||||||
|
// frees. This is mostly needed because I use Sublime Text and my game's
|
||||||
|
// console isn't hooked up into Sublime's console properly.
|
||||||
|
libc.getchar()
|
||||||
|
panic("Bad free detected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free_all(context.temp_allocator)
|
||||||
|
game_api.shutdown()
|
||||||
|
if reset_tracking_allocator(&tracking_allocator) {
|
||||||
|
// This prevents the game from closing without you seeing the memory
|
||||||
|
// leaks. This is mostly needed because I use Sublime Text and my game's
|
||||||
|
// console isn't hooked up into Sublime's console properly.
|
||||||
|
libc.getchar()
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
@(export)
|
||||||
|
NvOptimusEnablement: u32 = 1
|
||||||
|
|
||||||
|
@(export)
|
||||||
|
AmdPowerXpressRequestHighPerformance: i32 = 1
|
||||||
72
source/main_release/main_release.odin
Normal file
72
source/main_release/main_release.odin
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
For making a release exe that does not use hot reload.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main_release
|
||||||
|
|
||||||
|
import "core:log"
|
||||||
|
import "core:os"
|
||||||
|
import "core:path/filepath"
|
||||||
|
import "core:mem"
|
||||||
|
import game ".."
|
||||||
|
|
||||||
|
_ :: mem
|
||||||
|
|
||||||
|
USE_TRACKING_ALLOCATOR :: #config(USE_TRACKING_ALLOCATOR, false)
|
||||||
|
|
||||||
|
main :: proc() {
|
||||||
|
// Set working dir to dir of executable.
|
||||||
|
exe_path := os.args[0]
|
||||||
|
exe_dir := filepath.dir(string(exe_path), context.temp_allocator)
|
||||||
|
os.set_working_directory(exe_dir)
|
||||||
|
|
||||||
|
mode := os.Permissions { .Read_User, .Write_User, .Read_Group, .Read_Other }
|
||||||
|
logh, logh_err := os.open("log.txt", {.Create, .Trunc, .Read, .Write}, mode)
|
||||||
|
|
||||||
|
if logh_err == os.ERROR_NONE {
|
||||||
|
os.stdout = logh
|
||||||
|
os.stderr = logh
|
||||||
|
}
|
||||||
|
|
||||||
|
logger_alloc := context.allocator
|
||||||
|
logger := logh_err == os.ERROR_NONE ? log.create_file_logger(logh, allocator = logger_alloc) : log.create_console_logger(allocator = logger_alloc)
|
||||||
|
context.logger = logger
|
||||||
|
|
||||||
|
when USE_TRACKING_ALLOCATOR {
|
||||||
|
default_allocator := context.allocator
|
||||||
|
tracking_allocator: mem.Tracking_Allocator
|
||||||
|
mem.tracking_allocator_init(&tracking_allocator, default_allocator)
|
||||||
|
context.allocator = mem.tracking_allocator(&tracking_allocator)
|
||||||
|
}
|
||||||
|
|
||||||
|
game.game_init_window()
|
||||||
|
game.game_init()
|
||||||
|
|
||||||
|
for game.game_should_run() {
|
||||||
|
game.game_update()
|
||||||
|
}
|
||||||
|
|
||||||
|
free_all(context.temp_allocator)
|
||||||
|
game.game_shutdown()
|
||||||
|
game.game_shutdown_window()
|
||||||
|
|
||||||
|
when USE_TRACKING_ALLOCATOR {
|
||||||
|
for _, value in tracking_allocator.allocation_map {
|
||||||
|
log.errorf("%v: Leaked %v bytes\n", value.location, value.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
mem.tracking_allocator_destroy(&tracking_allocator)
|
||||||
|
}
|
||||||
|
|
||||||
|
if logh_err == os.ERROR_NONE {
|
||||||
|
log.destroy_file_logger(logger, logger_alloc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// make game use good GPU on laptops etc
|
||||||
|
|
||||||
|
@(export)
|
||||||
|
NvOptimusEnablement: u32 = 1
|
||||||
|
|
||||||
|
@(export)
|
||||||
|
AmdPowerXpressRequestHighPerformance: i32 = 1
|
||||||
126
source/main_web/emscripten_allocator.odin
Normal file
126
source/main_web/emscripten_allocator.odin
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
/*
|
||||||
|
This allocator uses the malloc, calloc, free and realloc procs that emscripten
|
||||||
|
exposes in order to allocate memory. Just like Odin's default heap allocator
|
||||||
|
this uses proper alignment, so that maps and simd works.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main_web
|
||||||
|
|
||||||
|
import "core:mem"
|
||||||
|
import "core:c"
|
||||||
|
import "base:intrinsics"
|
||||||
|
|
||||||
|
// This will create bindings to emscripten's implementation of libc
|
||||||
|
// memory allocation features.
|
||||||
|
@(default_calling_convention = "c")
|
||||||
|
foreign {
|
||||||
|
calloc :: proc(num, size: c.size_t) -> rawptr ---
|
||||||
|
free :: proc(ptr: rawptr) ---
|
||||||
|
malloc :: proc(size: c.size_t) -> rawptr ---
|
||||||
|
realloc :: proc(ptr: rawptr, size: c.size_t) -> rawptr ---
|
||||||
|
}
|
||||||
|
|
||||||
|
emscripten_allocator :: proc "contextless" () -> mem.Allocator {
|
||||||
|
return mem.Allocator{emscripten_allocator_proc, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
emscripten_allocator_proc :: proc(
|
||||||
|
allocator_data: rawptr,
|
||||||
|
mode: mem.Allocator_Mode,
|
||||||
|
size, alignment: int,
|
||||||
|
old_memory: rawptr,
|
||||||
|
old_size: int,
|
||||||
|
location := #caller_location
|
||||||
|
) -> (data: []byte, err: mem.Allocator_Error) {
|
||||||
|
// These aligned alloc procs are almost indentical those in
|
||||||
|
// `_heap_allocator_proc` in `core:os`. Without the proper alignment you
|
||||||
|
// cannot use maps and simd features.
|
||||||
|
|
||||||
|
aligned_alloc :: proc(size, alignment: int, zero_memory: bool, old_ptr: rawptr = nil) -> ([]byte, mem.Allocator_Error) {
|
||||||
|
a := max(alignment, align_of(rawptr))
|
||||||
|
space := size + a - 1
|
||||||
|
|
||||||
|
allocated_mem: rawptr
|
||||||
|
if old_ptr != nil {
|
||||||
|
original_old_ptr := mem.ptr_offset((^rawptr)(old_ptr), -1)^
|
||||||
|
allocated_mem = realloc(original_old_ptr, c.size_t(space+size_of(rawptr)))
|
||||||
|
} else if zero_memory {
|
||||||
|
// calloc automatically zeros memory, but it takes a number + size
|
||||||
|
// instead of just size.
|
||||||
|
allocated_mem = calloc(c.size_t(space+size_of(rawptr)), 1)
|
||||||
|
} else {
|
||||||
|
allocated_mem = malloc(c.size_t(space+size_of(rawptr)))
|
||||||
|
}
|
||||||
|
aligned_mem := rawptr(mem.ptr_offset((^u8)(allocated_mem), size_of(rawptr)))
|
||||||
|
|
||||||
|
ptr := uintptr(aligned_mem)
|
||||||
|
aligned_ptr := (ptr - 1 + uintptr(a)) & -uintptr(a)
|
||||||
|
diff := int(aligned_ptr - ptr)
|
||||||
|
if (size + diff) > space || allocated_mem == nil {
|
||||||
|
return nil, .Out_Of_Memory
|
||||||
|
}
|
||||||
|
|
||||||
|
aligned_mem = rawptr(aligned_ptr)
|
||||||
|
mem.ptr_offset((^rawptr)(aligned_mem), -1)^ = allocated_mem
|
||||||
|
|
||||||
|
return mem.byte_slice(aligned_mem, size), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
aligned_free :: proc(p: rawptr) {
|
||||||
|
if p != nil {
|
||||||
|
free(mem.ptr_offset((^rawptr)(p), -1)^)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aligned_resize :: proc(p: rawptr, old_size: int, new_size: int, new_alignment: int) -> ([]byte, mem.Allocator_Error) {
|
||||||
|
if p == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return aligned_alloc(new_size, new_alignment, true, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch mode {
|
||||||
|
case .Alloc:
|
||||||
|
return aligned_alloc(size, alignment, true)
|
||||||
|
|
||||||
|
case .Alloc_Non_Zeroed:
|
||||||
|
return aligned_alloc(size, alignment, false)
|
||||||
|
|
||||||
|
case .Free:
|
||||||
|
aligned_free(old_memory)
|
||||||
|
return nil, nil
|
||||||
|
|
||||||
|
case .Resize:
|
||||||
|
if old_memory == nil {
|
||||||
|
return aligned_alloc(size, alignment, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes := aligned_resize(old_memory, old_size, size, alignment) or_return
|
||||||
|
|
||||||
|
// realloc doesn't zero the new bytes, so we do it manually.
|
||||||
|
if size > old_size {
|
||||||
|
new_region := raw_data(bytes[old_size:])
|
||||||
|
intrinsics.mem_zero(new_region, size - old_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes, nil
|
||||||
|
|
||||||
|
case .Resize_Non_Zeroed:
|
||||||
|
if old_memory == nil {
|
||||||
|
return aligned_alloc(size, alignment, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return aligned_resize(old_memory, old_size, size, alignment)
|
||||||
|
|
||||||
|
case .Query_Features:
|
||||||
|
set := (^mem.Allocator_Mode_Set)(old_memory)
|
||||||
|
if set != nil {
|
||||||
|
set^ = {.Alloc, .Free, .Resize, .Query_Features}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
|
||||||
|
case .Free_All, .Query_Info:
|
||||||
|
return nil, .Mode_Not_Implemented
|
||||||
|
}
|
||||||
|
return nil, .Mode_Not_Implemented
|
||||||
|
}
|
||||||
91
source/main_web/emscripten_logger.odin
Normal file
91
source/main_web/emscripten_logger.odin
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
This logger is largely a copy of the console logger in `core:log`, but it uses
|
||||||
|
emscripten's `puts` proc to write into he console of the web browser.
|
||||||
|
|
||||||
|
This is more or less identical to the logger in Aronicu's repository:
|
||||||
|
https://github.com/Aronicu/Raylib-WASM/tree/main
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main_web
|
||||||
|
|
||||||
|
import "core:c"
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:log"
|
||||||
|
import "core:strings"
|
||||||
|
|
||||||
|
Emscripten_Logger_Opts :: log.Options{.Level, .Short_File_Path, .Line}
|
||||||
|
|
||||||
|
create_emscripten_logger :: proc (lowest := log.Level.Debug, opt := Emscripten_Logger_Opts) -> log.Logger {
|
||||||
|
return log.Logger{data = nil, procedure = logger_proc, lowest_level = lowest, options = opt}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This create's a binding to `puts` which will be linked in as part of the
|
||||||
|
// emscripten runtime.
|
||||||
|
@(default_calling_convention = "c")
|
||||||
|
foreign {
|
||||||
|
puts :: proc(buffer: cstring) -> c.int ---
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private="file")
|
||||||
|
logger_proc :: proc(
|
||||||
|
logger_data: rawptr,
|
||||||
|
level: log.Level,
|
||||||
|
text: string,
|
||||||
|
options: log.Options,
|
||||||
|
location := #caller_location
|
||||||
|
) {
|
||||||
|
b := strings.builder_make(context.temp_allocator)
|
||||||
|
strings.write_string(&b, Level_Headers[level])
|
||||||
|
do_location_header(options, &b, location)
|
||||||
|
fmt.sbprint(&b, text)
|
||||||
|
|
||||||
|
if bc, bc_err := strings.to_cstring(&b); bc_err == nil {
|
||||||
|
puts(bc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private="file")
|
||||||
|
Level_Headers := [?]string {
|
||||||
|
0 ..< 10 = "[DEBUG] --- ",
|
||||||
|
10 ..< 20 = "[INFO ] --- ",
|
||||||
|
20 ..< 30 = "[WARN ] --- ",
|
||||||
|
30 ..< 40 = "[ERROR] --- ",
|
||||||
|
40 ..< 50 = "[FATAL] --- ",
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private="file")
|
||||||
|
do_location_header :: proc(opts: log.Options, buf: ^strings.Builder, location := #caller_location) {
|
||||||
|
if log.Location_Header_Opts & opts == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.sbprint(buf, "[")
|
||||||
|
file := location.file_path
|
||||||
|
if .Short_File_Path in opts {
|
||||||
|
last := 0
|
||||||
|
for r, i in location.file_path {
|
||||||
|
if r == '/' {
|
||||||
|
last = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file = location.file_path[last:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if log.Location_File_Opts & opts != nil {
|
||||||
|
fmt.sbprint(buf, file)
|
||||||
|
}
|
||||||
|
if .Line in opts {
|
||||||
|
if log.Location_File_Opts & opts != nil {
|
||||||
|
fmt.sbprint(buf, ":")
|
||||||
|
}
|
||||||
|
fmt.sbprint(buf, location.line)
|
||||||
|
}
|
||||||
|
|
||||||
|
if .Procedure in opts {
|
||||||
|
if (log.Location_File_Opts | {.Line}) & opts != nil {
|
||||||
|
fmt.sbprint(buf, ":")
|
||||||
|
}
|
||||||
|
fmt.sbprintf(buf, "%s()", location.procedure)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.sbprint(buf, "] ")
|
||||||
|
}
|
||||||
114
source/main_web/index_template.html
Normal file
114
source/main_web/index_template.html
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en-us">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
|
|
||||||
|
<title>Odin + Raylib on the web</title>
|
||||||
|
<meta name="title" content="Odin + Raylib on the web">
|
||||||
|
<meta name="description" content="Make games using Odin + Raylib that work in the browser">
|
||||||
|
<meta name="viewport" content="width=device-width">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0px;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
canvas.game_canvas {
|
||||||
|
border: 0px none;
|
||||||
|
background-color: black;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas class="game_canvas" id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1" onmousedown="event.target.focus()" onkeydown="event.preventDefault()"></canvas>
|
||||||
|
<script type="text/javascript" src="odin.js"></script>
|
||||||
|
<script>
|
||||||
|
var odinMemoryInterface = new odin.WasmMemoryInterface();
|
||||||
|
odinMemoryInterface.setIntSize(4);
|
||||||
|
var odinImports = odin.setupDefaultImports(odinMemoryInterface);
|
||||||
|
|
||||||
|
// The Module is used as configuration for emscripten.
|
||||||
|
var Module = {
|
||||||
|
// This is called by emscripten when it starts up.
|
||||||
|
instantiateWasm: (imports, successCallback) => {
|
||||||
|
const newImports = {
|
||||||
|
...odinImports,
|
||||||
|
...imports
|
||||||
|
}
|
||||||
|
|
||||||
|
return WebAssembly.instantiateStreaming(fetch("index.wasm"), newImports).then(function(output) {
|
||||||
|
var e = output.instance.exports;
|
||||||
|
odinMemoryInterface.setExports(e);
|
||||||
|
odinMemoryInterface.setMemory(e.memory);
|
||||||
|
return successCallback(output.instance);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// This happens a bit after `instantiateWasm`, when everything is
|
||||||
|
// done setting up. At that point we can run code.
|
||||||
|
onRuntimeInitialized: () => {
|
||||||
|
var e = wasmExports;
|
||||||
|
|
||||||
|
// Calls any procedure marked with @init
|
||||||
|
e._start();
|
||||||
|
|
||||||
|
// See source/main_web/main_web.odin for main_start,
|
||||||
|
// main_update and main_end.
|
||||||
|
e.main_start();
|
||||||
|
|
||||||
|
function send_resize() {
|
||||||
|
var canvas = document.getElementById('canvas');
|
||||||
|
e.web_window_size_changed(canvas.width, canvas.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', function(event) {
|
||||||
|
send_resize();
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
// This can probably be done better: Ideally we'd feed the
|
||||||
|
// initial size to `main_start`. But there seems to be a
|
||||||
|
// race condition. `canvas` doesn't have it's correct size yet.
|
||||||
|
send_resize();
|
||||||
|
|
||||||
|
// Runs the "main loop".
|
||||||
|
function do_main_update() {
|
||||||
|
if (!e.main_update()) {
|
||||||
|
e.main_end();
|
||||||
|
|
||||||
|
// Calls procedures marked with @fini
|
||||||
|
e._end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.requestAnimationFrame(do_main_update);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.requestAnimationFrame(do_main_update);
|
||||||
|
},
|
||||||
|
print: (function() {
|
||||||
|
var element = document.getElementById("output");
|
||||||
|
if (element) element.value = ''; // clear browser cache
|
||||||
|
return function(text) {
|
||||||
|
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
|
||||||
|
console.log(text);
|
||||||
|
if (element) {
|
||||||
|
element.value += text + "\n";
|
||||||
|
element.scrollTop = element.scrollHeight; // focus on bottom
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})(),
|
||||||
|
canvas: (function() {
|
||||||
|
return document.getElementById("canvas");
|
||||||
|
})()
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Emscripten injects its javascript here -->
|
||||||
|
{{{ SCRIPT }}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
54
source/main_web/main_web.odin
Normal file
54
source/main_web/main_web.odin
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
These procs are the ones that will be called from `main_wasm.c`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main_web
|
||||||
|
|
||||||
|
import "base:runtime"
|
||||||
|
import "core:c"
|
||||||
|
import "core:mem"
|
||||||
|
import game ".."
|
||||||
|
|
||||||
|
@(private="file")
|
||||||
|
web_context: runtime.Context
|
||||||
|
|
||||||
|
@export
|
||||||
|
main_start :: proc "c" () {
|
||||||
|
context = runtime.default_context()
|
||||||
|
|
||||||
|
// The WASM allocator doesn't seem to work properly in combination with
|
||||||
|
// emscripten. There is some kind of conflict with how the manage memory.
|
||||||
|
// So this sets up an allocator that uses emscripten's malloc.
|
||||||
|
context.allocator = emscripten_allocator()
|
||||||
|
runtime.init_global_temporary_allocator(1*mem.Megabyte)
|
||||||
|
|
||||||
|
// Since we now use js_wasm32 we should be able to remove this and use
|
||||||
|
// context.logger = log.create_console_logger(). However, that one produces
|
||||||
|
// extra newlines on web. So it's a bug in that core lib.
|
||||||
|
context.logger = create_emscripten_logger()
|
||||||
|
|
||||||
|
web_context = context
|
||||||
|
|
||||||
|
game.game_init_window()
|
||||||
|
game.game_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
@export
|
||||||
|
main_update :: proc "c" () -> bool {
|
||||||
|
context = web_context
|
||||||
|
game.game_update()
|
||||||
|
return game.game_should_run()
|
||||||
|
}
|
||||||
|
|
||||||
|
@export
|
||||||
|
main_end :: proc "c" () {
|
||||||
|
context = web_context
|
||||||
|
game.game_shutdown()
|
||||||
|
game.game_shutdown_window()
|
||||||
|
}
|
||||||
|
|
||||||
|
@export
|
||||||
|
web_window_size_changed :: proc "c" (w: c.int, h: c.int) {
|
||||||
|
context = web_context
|
||||||
|
game.game_parent_window_size_changed(int(w), int(h))
|
||||||
|
}
|
||||||
12
source/utils.odin
Normal file
12
source/utils.odin
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Wraps os.read_entire_file and os.write_entire_file, but they also work with emscripten.
|
||||||
|
|
||||||
|
package game
|
||||||
|
|
||||||
|
@(require_results)
|
||||||
|
read_entire_file :: proc(name: string, allocator := context.allocator, loc := #caller_location) -> (data: []byte, success: bool) {
|
||||||
|
return _read_entire_file(name, allocator, loc)
|
||||||
|
}
|
||||||
|
|
||||||
|
write_entire_file :: proc(name: string, data: []byte, truncate := true) -> (success: bool) {
|
||||||
|
return _write_entire_file(name, data, truncate)
|
||||||
|
}
|
||||||
16
source/utils_default.odin
Normal file
16
source/utils_default.odin
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#+build !wasm32
|
||||||
|
#+build !wasm64p32
|
||||||
|
|
||||||
|
package game
|
||||||
|
|
||||||
|
import "core:os"
|
||||||
|
|
||||||
|
_read_entire_file :: proc(name: string, allocator := context.allocator, loc := #caller_location) -> (data: []byte, success: bool) {
|
||||||
|
err: os.Error
|
||||||
|
data, err = os.read_entire_file(name, allocator, loc)
|
||||||
|
return data, err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_write_entire_file :: proc(name: string, data: []byte, truncate := true) -> (err: bool) {
|
||||||
|
return os.write_entire_file(name, data, truncate = truncate) == nil
|
||||||
|
}
|
||||||
109
source/utils_web.odin
Normal file
109
source/utils_web.odin
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
// Implementations of `read_entire_file` and `write_entire_file` using the libc
|
||||||
|
// stuff emscripten exposes. You can read the files that get bundled by
|
||||||
|
// `--preload-file assets` in `build_web` script.
|
||||||
|
|
||||||
|
#+build wasm32, wasm64p32
|
||||||
|
|
||||||
|
package game
|
||||||
|
|
||||||
|
import "base:runtime"
|
||||||
|
import "core:log"
|
||||||
|
import "core:c"
|
||||||
|
import "core:strings"
|
||||||
|
|
||||||
|
// These will be linked in by emscripten.
|
||||||
|
@(default_calling_convention = "c")
|
||||||
|
foreign {
|
||||||
|
fopen :: proc(filename, mode: cstring) -> ^FILE ---
|
||||||
|
fseek :: proc(stream: ^FILE, offset: c.long, whence: Whence) -> c.int ---
|
||||||
|
ftell :: proc(stream: ^FILE) -> c.long ---
|
||||||
|
fclose :: proc(stream: ^FILE) -> c.int ---
|
||||||
|
fread :: proc(ptr: rawptr, size: c.size_t, nmemb: c.size_t, stream: ^FILE) -> c.size_t ---
|
||||||
|
fwrite :: proc(ptr: rawptr, size: c.size_t, nmemb: c.size_t, stream: ^FILE) -> c.size_t ---
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private="file")
|
||||||
|
FILE :: struct {}
|
||||||
|
|
||||||
|
Whence :: enum c.int {
|
||||||
|
SET,
|
||||||
|
CUR,
|
||||||
|
END,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Similar to raylib's LoadFileData
|
||||||
|
_read_entire_file :: proc(name: string, allocator := context.allocator, loc := #caller_location) -> (data: []byte, success: bool) {
|
||||||
|
if name == "" {
|
||||||
|
log.error("No file name provided")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file := fopen(strings.clone_to_cstring(name, context.temp_allocator), "rb")
|
||||||
|
|
||||||
|
if file == nil {
|
||||||
|
log.errorf("Failed to open file %v", name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer fclose(file)
|
||||||
|
|
||||||
|
fseek(file, 0, .END)
|
||||||
|
size := ftell(file)
|
||||||
|
fseek(file, 0, .SET)
|
||||||
|
|
||||||
|
if size <= 0 {
|
||||||
|
log.errorf("Failed to read file %v", name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data_err: runtime.Allocator_Error
|
||||||
|
data, data_err = make([]byte, size, allocator, loc)
|
||||||
|
|
||||||
|
if data_err != nil {
|
||||||
|
log.errorf("Error allocating memory: %v", data_err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
read_size := fread(raw_data(data), 1, c.size_t(size), file)
|
||||||
|
|
||||||
|
if read_size != c.size_t(size) {
|
||||||
|
log.warnf("File %v partially loaded (%i bytes out of %i)", name, read_size, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debugf("Successfully loaded %v", name)
|
||||||
|
return data, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Similar to raylib's SaveFileData.
|
||||||
|
//
|
||||||
|
// Note: This can save during the current session, but I don't think you can
|
||||||
|
// save any data between sessions. So when you close the tab your saved files
|
||||||
|
// are gone. Perhaps you could communicate back to emscripten and save a cookie.
|
||||||
|
// Or communicate with a server and tell it to save data.
|
||||||
|
_write_entire_file :: proc(name: string, data: []byte, truncate := true) -> (success: bool) {
|
||||||
|
if name == "" {
|
||||||
|
log.error("No file name provided")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file := fopen(strings.clone_to_cstring(name, context.temp_allocator), truncate ? "wb" : "ab")
|
||||||
|
defer fclose(file)
|
||||||
|
|
||||||
|
if file == nil {
|
||||||
|
log.errorf("Failed to open '%v' for writing", name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes_written := fwrite(raw_data(data), 1, len(data), file)
|
||||||
|
|
||||||
|
if bytes_written == 0 {
|
||||||
|
log.errorf("Failed to write file %v", name)
|
||||||
|
return
|
||||||
|
} else if bytes_written != len(data) {
|
||||||
|
log.errorf("File partially written, wrote %v out of %v bytes", bytes_written, len(data))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debugf("File written successfully: %v", name)
|
||||||
|
return true
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue