Cubyz: a voxel game written in Zig

I think after 2.5 years of development (6 years if you include the java version) it’s about time I share it with you:

On the surface Cubyz is just like any other minecraft clone. You got blocks that you can break and place.
But if you look further (and deeper) you will see that Cubyz has more to offer:

  • With LOD you can see the world in its true scale. In fact the render distance is so big, that I have to use in-memory compression.
  • There are no height limits, allowing to venture into the deep unknowns. There are many unexpected biomes waiting to be discovered. And if you think you’ve seen it all, think again.
  • The procedural crafting system, based on a 5×5 grid, allows you to craft any tool you can imagine, whether its useful or not.
  • For building there are many cool options, like the ability to chisel away corners of a blocks.
  • Using UDP hole punching you can invite people directly into your game, without doing a bunch of stupid stuff, like configuring port forwarding.
  • With some simple ZON files, you can easily add your own biomes to the game. I try to make it as configurable as possible, so that you could truly change the game with this.

Now, since this is a Zig forum, I’d like to use the opportunity to also share some Zig-related details:
I think the most interesting things are my allocation strategy, and how I’m breaking the zig zen:

My allocation strategy is basically an extension of the global allocator strategy you see in most other programming languages. I use mainly two allocators:

  • I use a stack-like threadlocal allocator for local allocations that get freed at the end of the function. I use a slight extension of the normal stack that allows for out-of-order frees, so this allocator can even handle lists and stuff. To make sure that I don’t run out of memory it is backed by the global allocator.
  • I use a single, global GPA for everything else (minus some arenas and memory pools for specific things). It’s kind of bad at the moment, but I’m banking on that faster allocator implementation in the future.

A while ago I discovered that my error handling was terrible. I was rarely handling errors, and most of the time I was just being lazy and bubbled them into the main, never to be used again. I discovered that the root cause of this was all the noise that are allocator errors. Every time I allocate something I have to deal with this once-in-a-lifetime error, so of course my default is to just bubble it up the chain and crash.

This got me thinking, and I noticed that most of my allocators were relying or fall back on a single global GPA anyways. But the thing is: Even if I run out of memory the GPA won’t even fail reliably (at least not on linux because of memory overcommitting).

So to recap: Everywhere in my code I carried around an error, that basically never happens, but is indistinguishable from other errors making it difficult to implement proper error handling.
That’s why I decided to just get rid of it, by shipping my own ArrayList and my own allocator interface (which is just a shallow wrapper around the std one so I can still use it in std functionality that I don’t want to implement myself). Nowadays I am handling all of my errors locally, either in the same function or just a few functions up in the call stack.

If you want to see more you can check out the source code on github.
To run it you just need to follow the steps in the readme. There is a script that can download the correct zig version for you, so this is fairly straight-forward.

If you want to play or just chat with me or other players, you can join the discord server where I regularly (usually once a week) host a multiplayer world.

If you want to see more of the game and its history, I also have a youtube channel where I make some devlogs every couple of months.

And if you got any questions you can also ask them below of course.

45 Likes

Very cool! Looks very pretty.

1 Like

Very cool project, even just based on the README and screenshots, this is the type of project that inspires me.

I haven’t had the opportunity to give it an in-depth try yet, but I wanted to extend my thanks even just for sharing this with us.

2 Likes

It’s an impressive project, congratulation, you are amazing :astonished:

3 Likes

Wow that’s awesome! I just have two questions out of curiosity:

  • Regarding development speed and enjoyment, how did it compare to Java?
  • How nice is it to work on this kind of project (graphics / game dev) with Zig? Is there a lot of friction or not? If you used bindings, are they good? What did you find complicated to do with the language (if any)?

Thanks :slight_smile:

3 Likes

Regarding development speed and enjoyment, how did it compare to Java?

As for development speed, it’s hard to say. It doesn’t feel like I got much faster. The game did however get a lot safer/less fragile. I think this is partly because of better builtin tooling (e.g. crashes on integer overflow, the thread sanitizer, and also the fact that you have to explicitly define the rounding behavior for signed integer division. This got me so many errors in java) and partly because Zig tries to guide you towards becoming a better programmer with the strategic use of friction.

Enjoyment has definitely increased. In java I was often fighting the language. Java’s design decision to make everything a pointer is really nice, until you need to optimize. Then you are forced to give up all the nice abstraction. Want to group up your x, y, z coordinates into a vector when passing them to a function? Nope, that’s too expensive.
Now to be fair, they are slowly working on improving things in that regard, but that’s just one of many problems.

Now with Zig there are still a few pain points, but most of the time, it’s either not that important to me, or I can be hopeful that it will be fixed soon enough. Like the debug build compile time (about twice as long as in java) will hopefully be fixed soon.

How nice is it to work on this kind of project (graphics / game dev) with Zig? Is there a lot of friction or not? If you used bindings, are they good? What did you find complicated to do with the language (if any)?

In general it is quite easy to get going. Interacting with C/C++ libraries is quite pleasant. I can just @cInclude most C header files without any problems.
The main friction for me was building the C libraries, like GLFW. Now of course I could just tell the player to sudo apt install all my dependencies, but that has other issues. Instead I prefer to statically link them where possible. This means that I need to compile it from source with the Zig C/C++ compiler (aka clang), which required me translate a few CMake :face_vomiting: scripts into the Zig build system. Nowadays this probably isn’t much of an issue, since others already did it for you.

As for bindings, I have used them in the past, but I’m really not a big fan. Bindings either end up as shallow wrappers around the library, in which case they bring almost no benefit over a @cInclude, or they Ziggify so many things that it’s hard to follow the tutorial.
Furthermore they add another point of failure into your dependency chain which is why I have opted to avoid them.

12 Likes

I love looking at all the code! Between this codebase and Veloren built with Rust, I’m getting a good sense of what is painful between Zig and Rust for gamedev programmers. I am also really lazy with bubbling up errors in Zig, and the ‘try’ and ‘!’ symbols littered everywhere have lost a lot of meaning, maybe I could try out some of your types in my own code at some point instead!

One thing I’m curious about is why your code doesn’t use the default formatter? Is there something it’s doing that you don’t like, or at this point maybe formatting the whole codebase would mess with the git history too much?

3 Likes

Very nice project! I was looking a bit through the code and was wondering about the ThreadPool. There you have a run function that is passed to the thread as the worker function that contains while(true) without anything breaking from it. When calling deinit you run thread.join. won’t that wait forever because of that while loop?

1 Like

I’m still using the default formatter in the background, I’m just doing some modifications to the input before passing it on, to reduce the amount of comptime code duplication. I also made an article about this here: This overengineered log function reduced my compile-time by 15%

I do have a break in the first line:

const task = self.loadList.extractMax() catch break;

This function returns an error once the loadList got deinited.

1 Like

Ah sorry haha I read over it. Thnx!

Cool project! Thanks!

My project last weekend was packaging ghostty for guix, so this weekend I took a stab at cubyz:

It builds and runs but doesn’t work, but that’s my fault :wink:

I built an older commit before you updated to 0.14.0-dev so it’s trying to create the savedir in the read-only store. So, it can’t create and start a world. But, it was still a fun little project. :slight_smile:

1 Like