Muninn is a minimal boilerplate for making games with Odin and Raylib, targeting both native and WebAssembly. It focuses on fast iteration, simple builds, and frictionless distribution, making it perfect for teaching gamedev or rapid prototyping.
- Using Odin itself as our build system (because Odin is fucking awesome);
- Frictionless hot-reload so we can just save the code and see the changes immediately;
- Basic game example;
- Independent post-processing effects;
- A multi-pass shader manager to facilitate starting projects with multi-pass shaders;
- A shader-chunk system so you can conveniently inject/re-use shader chunks with
#include; - WebAssembly builds so we can run the games in browsers too;
- Single-File WebAssembly build philosophy (more on that below);
- Cool-looking WASM-compatible Debug UI using MicroUI;
- Gamepad support (soon™️);
- Mobile touch controls and virtual joysticks (soon™️);
- Sound system with procedurally generated, parameterizable sound effects (soon™️).
Instead of having to manually run the hot-reload build scripts (or triggering build commands through key bindings) after saving changes to your code, I've chosen to implement a minimal logic that monitors the source files and triggers the hot-reload build automatically once any changes are detected to any of the source files.
For the WebAssembly single-file builds, I've chosen to allow for some overhead in terms of bundle size to obtain the advantage of generating a single self-contained HTML file with everything that is necessary to run the game (including the compiled WASM file and all the assets as base64 strings in the script tag, which is inlined to the single HTML file).
That, IMHO, offers the convenience of running the game just by opening the HTML file locally in a web browser (just by double-clicking the file on your file manager), without the need to serve the files through a web server to comply with browser policies that would prevent loading the needed files directly from the disk.
That also offers a great distribution value, as sharing your game with friends only involves sending them a single HTML file, and they'll be able to play your game.
First, compile the build system itself:
odin build build.odin -fileThis creates a build.exe executable that contains all build functionality.
./build.exe hot-reloadBuilds the game with hot-reload support and automatically watches source files for changes. When you save any source file, the game will rebuild automatically and reload the updated code without restarting.
Flags:
--watch- Watch files and rebuild on changes (default behavior)--run- Run the game after building (default behavior)--build-only- Build once and exit without watching or running
./build.exe debugCreates a debug build with debug symbols at build/debug/game_debug.exe. Perfect for development and debugging with full symbol information.
./build.exe releaseCreates an optimized release build at build/release/game_release.exe with maximum performance optimizations and no bounds checking.
./build.exe web [--debug]Builds a WebAssembly version of your game at build/web/index.html. Automatically manages EMSDK installation and setup. The resulting build can be served with any web server.
Flags:
--debug- Enable debug UI and logging in the WebAssembly build
To test the web build locally:
cd build/web
python -m http.server 8000
# Then open http://localhost:8000 in your browser./build.exe web-single [--debug]Creates a single self-contained HTML file at build/web_single/index.html that includes everything needed to run the game (WebAssembly, assets, and runtime) embedded as base64. This file can be opened directly in a browser without a web server, making it perfect for easy distribution.
Flags:
--debug- Enable debug UI and logging in the single-file WebAssembly build
Note: Requires an existing web build first (run ./build.exe web before using this command).
./build.exe helpShows detailed usage information and all available commands.
If for any weird reason you do not want to bootstrap the build system into the
build.exebinary, you may also run it directly, with the following syntax:odin run build.odin -file -- hot-reload [--watch] [--run] [--build-only]odin run build.odin -file -- debugodin run build.odin -file -- releaseodin run build.odin -file -- web [--debug] [--webgl2]odin run build.odin -file -- web-single [--debug] [--webgl2]odin run build.odin -file -- help
- Start Development:
./build.exe hot-reload- Begin coding with automatic rebuilds - Test WebAssembly Version:
./build.exe web --debug- Create web build with debug UI for browser testing - Create Distribution:
./build.exe release- Build the optimized executable - WebAssembly Distribution:
./build.exe web-single- Create the single-file WebAssembly release
- WebGL2 KINDA works, but with some quirks, so I'm currently using the
USE_WEBGL2feature flag tofalsefor my WASM build to useWebGL1. That kinda sucks because all of the limitations of#version 100shaders (no layouts, old and ugly varyings, no full bitwise ops, no unsigned ints, no texelFetch, no textureGrad, etc... it's kind of a nightmare). When the WASM build targetsWebGL2everything works, but we end up withWebGL: INVALID_VALUE: vertexAttribPointer: index out of rangeandWebGL: INVALID_VALUE: enableVertexAttribArray: index out of rangewarnings all over the place. This MAY be related to Raylib's closed issue #4330, but I couldn't find the time to investigate this yet. It works, but it's a no-no to me. I won't sleep well with warnings on my console. We're flawed, but not savages.
- Raylib (or at least its implementation vendored by Odin) does not expose a
LoadRenderTexturemethod that allows to pick a specificPixelFormat. Because of that, I wrote my own implementation ofLoadRenderTextureWithFormatandLoadRT_WithFallback. The former just tries to create theRenderTexture2Dwith the chosenPixelFormatand the latter uses the former with a fall-through logic in case the wantedPixelFormatis not available (which is quite common in the WASM build). Weirdly, the WASM build works fine withUNCOMPRESSED_R32G32B32A32(FloatType) when the build targetsWebGL1and#version 100shaders, but it falls through intoUNCOMPRESSED_R8G8B8A8when the build targetsWebGL2and#version 300 esshaders. I couldn't find the time to start investigating that yet.
- No, we can't. It's not as simple as finding
round(float x)and replacing it withfloor(x + 0.5)or equally simple stuff. Even the capacity to use non-constant loop bounds/conditions/steps in simplefor/while/do-whileloops, changes everything about the way you write shaders. The fastest and simplest solution to this issue will be to offer a PR to solve the two items above upstream, as soon as I can find the time to.
This project was inspired by one of Karl Zylinski's projects, which you may find here. My version fits my personal development workflow and choices better, but you should definitely check out his work.
Karl is a prolific author and game developer who specializes in Odin and Raylib. Reading his book (which you can find here) was invaluable for my incredibly easy transition from C++ to Odin to develop my Raylib projects. If you're interested in Odin, please consider supporting Karl by buying his book or checking out his game built with Odin + Raylib on Steam.
The inspiration to create the build system with the same language that aims to be self-sufficient (soon™️) came from the recreational programming streamer Tsoding and his project nob.h
Special thanks to Ginger Bill (Creator of the Odin Programming Language, and an awesome guy), and Ray (Creator of Raylib). If you like their tech, please support their work in any way you can.