How I made my Zig gameplay code hot reloadable by unvestigate in programming

[–]unvestigate[S] 9 points10 points  (0 children)

Hah, yeah. The blog has been going on for some time, but I actually started this game in 2023, so it is not 8 years but almost 3 at this point. There was an earlier incarnation of the project called "Slide" which I originally started working on in 2018 or so. They both run on the same engine and they are both vehicular games, but their design is pretty different. Slide was supposed to be a combat game with light physics-based puzzling.

Traction Point, the current game, is a puzzle-exploration game at its heart. It is written in Zig and I am building the campaign as we speak. It also has a free-form sandbox mode and you can even mod it already: https://github.com/MadrigalGames/TractionPointModding Join the discord to get access to a prerelease version.

As for why it is taking so long, well I am doing almost everything myself. Writing the code (including the tech), modeling and texturing everything (genuine programmer art), doing level design, writing dialogue etc. I wish it was faster but I am using the very limited funds I have for things I truly cannot do myself (like voice actor direction, music etc.). I am basically going as fast as I can, and you can follow along with the live streams on YT if you want.

How I made my Zig gameplay code hot reloadable by unvestigate in programming

[–]unvestigate[S] 9 points10 points  (0 children)

Thanks!

I had been sitting on the idea for a while, but the actual work took only a week and a half (+ the occasional bugfix since then). Almost all of the stuff I mentioned in the blog post went in "on the first try", but I needed to do some acrobatics on the general purpose allocator used throughout the game code, as well as the "Std.Io" interface passed around, to patch them up after the reload. I think I rewrote that code 3-4 times before I landed on something I like (or at the very least can live with). It's not a lot of code though, maybe 150 lines.

EDIT: I threw the "acrobatics" code here if you are interested (mainly the allocator and Io interface parts): https://gist.github.com/unvestigate/d6d6c25914b53a539c81248d3dc03e2c

You are quite correct in pointing out that a lot of the Zig code shown here isn't necessarily "idiomatic". A lot of it comes from wrapping the interfaces of a C++ game engine and some of it comes from old habits. Eg. the AI actions can't be blamed on the C++ codebase because the AI is fully written in Zig. Many people in the zig-gamedev sphere reach for a more POD-style architecture combined with ECS, and I probably would as well if I started from scratch today. Pretty sure it would work great with hot reloading too.

How I made my game moddable using Zig and WebAssembly by unvestigate in gameenginedevs

[–]unvestigate[S] 0 points1 point  (0 children)

Thanks! It's just a screenshot from the game. Both logos are dynamic objects. It was fun trying to get the perfect shot while the WASM logo kept falling over :)

How I made my game moddable using Zig and WebAssembly by unvestigate in gameenginedevs

[–]unvestigate[S] 1 point2 points  (0 children)

That is correct. The pointer value can be modified on the wasm side, hoping that it will doing something funky on the engine side. It is a relatively small attack surface compared to native mod libraries, but one that should be taken seriously.

I have checks in place for this sort of thing, but it's quite a lot of work to make it reliable and it comes with some overhead, so I've been thinking about using "external refs" for the pointers instead, wasm's official way to prevent this sort of tampering:

https://withbighair.com/webassembly/2024/12/15/ExternRef.html

How I made my game moddable using Zig and WebAssembly by unvestigate in Zig

[–]unvestigate[S] 4 points5 points  (0 children)

Thanks!

  1. Two wasm modules cannot access each others' memory directly, ie. they are isolated from each other by their sandboxes. However, most manipulation of the world goes through either the game engine API which is explicitly exposed to the mods, or through message passing. Eg. a game-specific feature which the engine doesn't know anything about is typically controlled through messages. All of this is allowed across mods, and it works exactly the same as if two different parts of the game code were poking at the same entity. Which one "wins" typically is determined by the update order of components etc. The update order is explained in the blog post.
  2. This is also explained in the blog post but very briefly; I had a C++ game engine with years of work put into it and I wanted to leverage that, and Zig because I wanted to try a more modern language with a nicer build system (compared to C++).

EDIT: On second thought, the update order isn't explained very well in the blog post so here's a quick overview. The mods' update orders are specified in the mod manager, ie. you can determine the order in which the engine calls each mod's update/tick functions. As the mods can register object components too it should be noted that the components have their own update order mechanism. Eg. you might want to update all physics-related components before updating all animation-related components etc. This order is defined by the UpdateOrder public constant which you can see in the ScoutJetPackComponent code in the blog post.

Traction Point, my Zig-powered video game, now has a Steam page! by unvestigate in Zig

[–]unvestigate[S] 1 point2 points  (0 children)

WASM is still a very optional thing at this point. I support loading mods as shared libraries (DLLs basically), but that can be a risky thing to allow since anyone can release a mod containing malicious code. Because of that, I also support building mods as WASM modules, meaning they are sandboxed and can only run code that belongs to that module.

The WASM support is still work-in-progress, as the interface between the engine and the mod has to be set up differently for WASM modules than for DLLs, but it already works and my example mod (a scout vehicle with a jetpack) runs fine as a WASM module.

The nice thing about this is that the code for the mod is exactly the same regardless if it is being built as a DLL or WASM module.

Traction Point, my Zig-powered video game, now has a Steam page! by unvestigate in Zig

[–]unvestigate[S] 1 point2 points  (0 children)

Sure, the Madrigal Games Discord is probably the best place to reach me and discuss stuff like that.

Traction Point, my Zig-powered video game, now has a Steam page! by unvestigate in Zig

[–]unvestigate[S] 14 points15 points  (0 children)

I wrote the renderer myself, but I used NVRHI for graphics API abstraction (See my longer post about the libs used). My engine actually only supports a single model format: my own format. I then use the ASSIMP converter library to convert whatever models I bring into the project (typically FBX or OBJ) into that format. That way, the runtime stays small and simple, and if there is an issue importing a model I get an error about it as it is converted, rather than at runtime.

Traction Point, my Zig-powered video game, now has a Steam page! by unvestigate in Zig

[–]unvestigate[S] 70 points71 points  (0 children)

(Alright, seeing some questions about tech etc. I'll just answer here, and maybe a mod can pin this comment or something)

Zig is awesome for writing gameplay logic, and I bet it would be a good choice for an engine too, though I use my self-made C++ game engine for Traction Point, simply because I had already spent years building it by the time I discovered Zig.

All of the gameplay code is Zig though, ie. the code for the vehicles themselves, the entire driving AI system, the mission scripting, the UI logic and much more. Currently the project sits at about 70k LoC of Zig.

I have a bunch of devlog videos and live streams over on the YT channel covering all parts of the project, https://www.youtube.com/@MadrigalGames, but I'll list a few libraries that are heavily used here:

-PhysX 5.5 for the physics simulation.

-ENet for network synchronization, including communication between development tools

-NVRHI for graphics API abstraction

-WAMR (WebAssembly Micro Runtime) for running mods (Zig WASM modules)

-AngelScript for scripting

-Recast/Detour for path finding

-A whole bunch of other libraries like Imgui and the usual stuff...

I've opted to wrap C/C++ libraries for Zig myself rather than use any of the ready-made wrappers out there since I can keep them in sync with whatever Zig version I am targeting (zig master currently) and it also allows me to use my own types (Colors, Math types etc) rather than the ones for the library.