Game engines for indie developers have become incredibly popular over the past decade, especially with decent licenses from Unity and Epic. As an indie developer working on Nexus Rift (a web3 auto-battler with mechs, because of course it is), I've bounced between more game engines than I care to admit, chasing that mythical "perfect fit." Look, I'm not asking for the moon here. I just want: - **Web/WASM that actually works** because nobody downloads games anymore - 3D support for modern visuals - No garbage collection for consistent performance - Scripting support in a language that's actually nice to use - Easy integration with existing libraries - Web3 support! - Manageable for a solo developer (RIP Ninja) Building a modern game requires navigating through countless engine options, each with their own trade-offs. The conclusion might surprise you. A relevant post on [Reddit](https://www.reddit.com/r/gamedev/comments/1e4w6ja/an_honest_question_about_games_without_engines/#:~:text=It%20can%20be%20simpler%20and,and%20less%20to%20do%20wrong.): > I disagree, for a simple reason: Games are applications, yet games are the only kind of application, where it's preferable to open a content management tool (editor) do your stuff and hit "make me an exe" button. This is how you normally edit photos, not write software. > > We don't do websites this way. We don't do native apps this way. We don't do basically anything else this way. Only games. And it's not because it's better, necessarily, but because games are uniquely artistic, so many non-programmers want to make the game (and the fact that it's an app isn't important to them at all). ## December 2023: Ebitengine - Starting with Go [Ebitengine](https://ebitengine.org/) was appealing because I already knew Go well. The single binary deployment and solid WASM support seemed perfect, and Go's straightforward syntax made prototyping quick. Since Ebitengine is primarily a 2D engine, I had to figure out some creative 2D approaches to what I originally envisioned in 3D. ![Pixel art render of a mech from top down angle](telegram-cloud-photo-size-5-6287342332319742342-m.jpg) My first experiments involved creating a top-down perspective that could still convey the mechanical complexity I wanted. I purchased some mech assets and began testing how they'd work within Ebitengine's constraints. ![Mech asset I purchased for testing, side angle, animated arms that aim towards the cursor](telegram-cloud-document-5-6289285795975008025.mp4) The engine's API was refreshingly simple, and implementing basic gameplay mechanics felt natural. Getting weapons systems working was straightforward thanks to my Go experience. ![The top down mech but turns to face the cursor and shoots lasers](telegram-cloud-document-5-6300745554699750865.mp4) I even implemented some interesting features like raycasting for line-of-sight mechanics, which worked well within the 2D framework. ![Raycasting for top down vision](telegram-cloud-document-5-6111779337411760210.mp4) To bridge the gap between 3D assets and 2D gameplay, I developed a pipeline for rendering animated mechs to pixel art that could be used in-game. ![rendering animated mech to 2d pixel art so we can put it in game](telegram-cloud-document-5-6174758027715942690.mp4) However, the problems became clear pretty fast. No 3D support was ultimately a showstopper for what I wanted to build. The poor interop with C libraries also meant getting locked into a limited ecosystem. Ebitengine works great for 2D games, but it couldn't handle the battle arena revival we all crave. ## Bevy: The Rust Experiment Why not rewrite everything in Rust? [Bevy looked impressive](https://bevy.org/) with its Entity Component System architecture and Rust's performance promises. The active community and rapid development were encouraging signs. I dove in and built a proof of concept for networked gameplay. ![a proof of concept showing networking replication of cube movements between client and an authoritative server](telegram-cloud-document-5-6269300540052606712.mp4) But Rust turned out to be more friction than I wanted to deal with. The borrow checker slowed down iteration, and the documentation felt overwhelming. There was just too much ceremony between having an idea and seeing it work. For rapid prototyping and experimentation, Rust's strictness became a hindrance rather than a help. I've spoken on this topic a few times, so at least I can say I gave it a go. ## Godot: The Community Favorite [Godot felt nice and comfortable](https://godotengine.org/), with its simplicity and leanness. The node system made scene composition intuitive, GDScript was easy to pick up, and iteration was genuinely fast. I got one of the RMOMC mechs retopo-ed to get it ready for web support. ![Low poly recreation of the original mech](telegram-cloud-photo-size-5-6217593274613479740-x.jpg) The visual quality I could achieve in Godot was impressive, especially with its built in rendering and lighting system that made things look a lot more professional. ![GI render of low poly mech](telegram-cloud-photo-size-5-6276027116424379510-x.jpg) Godot's scripting capabilities really shined when I implemented procedural level generation, as the majority of the "backend" C++ stuff can be controlled via gdscript. ![Automatically generated level using gdscript](telegram-cloud-photo-size-5-6269071129125698548-x.jpg) The shader system was powerful enough to create compelling visual effects like this holographic shield effect for the mechs. ![mech with cool hologram bubble shield shader](telegram-cloud-document-5-6255728769814761577.mp4) The downsides were manageable but annoying. The node system sometimes felt limiting for complex game logic. WASM export worked but had occasional quirks that required workarounds. The LSP would break regularly, which disrupted the development flow more than it should have. And honestly, I just hate clicking around in an editor. ## graphics.gd: The Hybrid Attempt The promise of combining [Go with Godot](https://graphics.gd/) seemed perfect - familiar language with a proven engine. The concept was exactly what I was looking for. I managed to implement some interesting features like terrain generation using Perlin noise. ![Terrain generation using perlin noise](telegram-cloud-document-5-6116266130832102471.mp4) I even got character animation working, successfully applying Mixamo walking animations to my mech models. ![Mixamo walking animation applied to a mech](telegram-cloud-document-5-6168091048766608317.mp4) Reality was much more painful than the promise. Getting Go's API to work cleanly with Godot's C++ inheritance patterns was incredibly difficult. The project felt too early-stage, and I kept hitting walls on basic functionality that should have been straightforward. The alpha nature of the project made it unsuitable for serious development. ## Zig/Raylib: The Performance Play [Zig](https://ziglang.org/) offered compelling performance characteristics and excellent C interop. Raylib provided a clean, minimal API that felt refreshing after more complex engines. The WASM story was solid. There was a decent [up-and-coming engine](https://interrupt.github.io/delve-framework-web-examples/) that had solid WASM support. But Zig's learning curve was steeper than expected. The build system felt arcane, and the language itself was unpleasant to work with for extended periods. The mental overhead of dealing with Zig's quirks outweighed the performance benefits for this project. Also, the Zig game developer community care very little about WASM support, which I needed. I got nothing to show you from this attempt - that should tell you everything about how it went. ## Odin/Raylib/Janet/Trenchbroom: The Current Solution This combination finally hit the sweet spot. [Odin](https://odin-lang.org/) feels familiar coming from Go - clean syntax without the complexity overhead. [Janet](https://janet-lang.org/) provides terse, expressive scripting that's actually enjoyable to write. [Raylib](https://www.raylib.com/) is simple but not too high level. [Trenchbroom](https://trenchbroom.github.io/) is quick to block things out, and has a well documented format. The interop story is excellent, WASM builds are clean and predictable, and the API surface is simple but capable. Here's a demo of the vision-based influence mapping system I built, showing what a mech can see on the battlefield. ![vision based influence map demo showing what a mech can see](telegram-cloud-document-5-6305087796700780785.mp4) I've also implemented the original spectator abilities from the battle arena. So nuke, airstrikes and repairs. ![demo of spectator abiltiies (nuke, airstrike, repair)](telegram-cloud-document-5-6305087796700780789.mp4) The trade-off is being more low-level than Godot. There is no engine - just a collection of libraries that I have to stitch together myself. There's more infrastructure to build from scratch, but this also means better understanding and control over the entire system, which has proven valuable as the project has grown in complexity. Here's a screenshot of it running in web. ![[Pasted image 20250711154935.png]] I managed to slam out some integrations with existing C libraries as well. - [nii236/janet-odin](https://github.com/nii236/janet-odin) - [nii236/quakemap-odin: A Quake .map file parser and geometry builder written in Odin, ported from Zig](https://github.com/nii236/quakemap-odin) - [nii236/umka-odin: Umka bindings for Odin](https://github.com/nii236/umka-odin) One of the most interesting benefits of going no-Engine is how friendly it is to get LLM assistance with developing the game. Its all just code! AI behavior with Odin's unions make state machines easy. Not a blueprint in sight. ![[Screen Recording 2025-07-11 at 3.52.51 pm.mov]] Since I have full control of the code, multiplayer (replication via authoritative server) is a lot simpler to develop. Multiplayer development is one of those weird things that determine your entire game's architecture. I will probably need to write more on it later. ## In Conclusion The journey through traditional engines (Ebitengine, Bevy, Godot, graphics.gd, Zig/Raylib) revealed a pattern: each added layers of abstraction that either limited capabilities or increased complexity without proportional benefits. **Odin/Raylib/Janet/Trenchbroom** emerged as the solution because it provides: - **Odin**: Go-like syntax with systems programming control - **Raylib**: Minimal, clean graphics API without engine overhead - **Janet**: Expressive scripting without the weight of a full engine - **Trenchbroom**: Easy level building, simple format This combination enables true "no-engine" development - direct control over game systems without fighting against engine assumptions or limitations. You get exactly what you need without the baggage of what you don't. Some good additional reading: - https://zylinski.se/posts/a-programming-language-for-me/ - https://zylinski.se/posts/no-engine-gamedev-using-odin-and-raylib/ - https://akselmo.dev/posts/moving-from-c-to-odin/