Postponum of writing a "voxel" engine

Ever since Minecraft's success voxel games have become a rather common occurrence. Years ago, I attempted developing a contribution to this pool. As with so many spare-time projects, the effort didn't survive the demand other parts of life (like school and social life and marriage and other necessities like such) put on my spare time.

The actual game I wanted to implement here is something I'd like to retry to make, so I'm keeping the goal to myself. But the code is available on GitHub and GitLab, and the takeaways of its development is something I'm going to analyze in this post.

Quick remarks

This project is already a bit older. Some things we'll notice pretty immediately is that I didn't know how to use an actual makefile, and pushed CodeBlocks project files instead🙈. Git submodules were also a function I hadn't experienced yet, so the repository size is much larger than it should be thanks to including the glm and Bullet source code.

It's also before the time I had met Rust yet, which I would now choose over C++ any day (and I started re-implementing this project in Rust later, but that's a story for another day). I used learnopengl.com as guide for any graphical code, as this was my first sizable project with a graphical API. So that will also explain the rather simple included shaders. Bullet was used for collision detection and physics, glm for any graphical rendering, glut for OpenGL 3.3 extension bindings.

To make the project more generally usable I've pushed a CMake build script.

Program structure

The approach was to write the actual game logic and data as independent from any IO logic as possible. This of course explains the choice for SDL2, but it was also the intention to write abstractions of top of the IO library by using the Game object and some other controller object. But as to not spend to much early time of development on non business-logic code this is only partly implemented, and something I'll have to revisit another time.

World structure

As for the actual world data structure, I wanted something lightweight that runs well on many kinds of hardware. That's why the world is a 2D grid, with upper and lower bounds for the terrain. It does introduce the limitation of not allowing stacked terrain (e.g. caves), but that's not a great sacrifice to make for the game I was aiming to make. Some form of byte packing could work around this limitation though, since for the majority of use-cases the first seven of even six bits would suffice. Another advantage of storing the heights this way is that you're essentially storing a height-map, and are thus able to leverage decades of image-compression science for data compression. Although, that used to be the case in an older revision. In the latest I've made a concession in performance for this functionality by using the following storage structure:

std::vector<std::vector<uint16_t> > worldHeightDataLower;
std::vector<std::vector<uint16_t> > worldHeightDataUpper;

This causes extra indirection and heap allocations for the trade-off of having the extra functionality. Working with a compile-time known sized data structure would increase memory overhead, but would also increase performance. But that would be a premature optimization at this point. Better to design the code to allow for re-factor-less alternative implementations.

For enabling a "streaming" world I opted for the very original solution of splitting the world up in chunks. The code has a hard-coded amount of chunk-objects loaded, which get updated with new data and coordinates as the player object moves around. This works because I was aiming for an orthographic projection. The amount of chunks visible would always depend on the view space, which you can control.

While back then the novelty of an endless voxel world was wearing of quickely, today those kind of games are one in a dozen. Distinctions between games have to come from game-play, which I gather is a good thing.

Rendering

Here it really becomes apparent that we're dealing with an unfinished product. Most geometry is separated from the render code, but most other things are hard-coded. Hard coding things for purposefully designed software without a moving target is fine. But most of this code looks structured as if it's been based off a tutorial. Which it is. Including four hard-coded moving point-lights.

And about the geometry generation: it could also be made much DRY'er. Each voxel gets checked for upper and adjacent neighbors. Having the following code six times in five blocks isn't necessarily very welcoming to maintain.

vertices.push_back((GLfloat)x);
vertices.push_back((GLfloat)y);
vertices.push_back((GLfloat)z);
vertices.push_back(0.0f);
vertices.push_back(1.0f);
vertices.push_back(0.0f);
btVector3 vector1{x, y, z};

When it comes to specifically the rendering of this voxel world, to largest complexity is making the terrain mesh dynamic in an efficient way. That's something I hadn't started with yet. While the terrain didn't have to be modified for my purposes, (parts of) the mesh did. Using smaller chunks may make this possible, but will also increase the amount of draw calls. That's what GL_DYNAMIC_DRAW is for, but actually altering the mesh is another story.

Aiming high, crashing quick

As is generally the trouble with spare time projects, keeping momentum is almost impossible. After initial development early 2015, I spent part of a vacation in 2016 on the project. Of course this isn't wasted effort, there's a bunch of experience I take with me to the re-try. So, what would I take-away?

For starters, finish features before moving on to the next. This will make the code-base better maintainable. Once the technical dept starts piling up, the effort for organizing goes up and the motivation down. It results in tutorial-code staying in this long.

Another is to avoid early feature-creep. I could've focused on some game-play related code, instead of, for instance, a split-screen feature. But there's a flip-side: thinking about such stuff so soon may leave room for implementation, even if you're not doing so immediately. Writing the code for cameras and view-ports modular from the get-go will decrease friction later quite a bit.

Some others I already stated: I'd use Rust the next time. The fact majority of bugs I encountered during development were segmentation faults. Writing safe Rust code would've avoided that. I'd probably look at using Vulkan or gfx-rs instead of OpenGL, as there aren't many platforms with decent OpenGL driver and less-decent Vulkan drivers. But maybe the complexity of Vulkan may make me reconsider. Or maybe the complexity really isn't that bad. Who knows.

Some things I definitely have to think about includes how to alter geometry efficiently. It would be a pity if just minor alterations to geometry would increase processing overhead considerable. Another key technicality to deliberate is how to do terrain collision detection, as feeding gigantic meshes to a physics engine is probably not the most efficient approach. Especially not if that mesh changes. But this should be a solved problem already, a bit of research may yield the necessary answers.

Closing thoughts

But, maybe the most important consideration for spare time projects: you have to keep it enjoyable, mix the dull parts with fun parts. Like playing with the terrain generator. Or applying visual tricks to the shaders, even if you're not keeping them. Play is an important component for exploration, and video games have lots of opportunities to explore.

The code is available on GitHub and GitLab for your pleasure. Feel free to play with, fork or laugh at the code.