These weeks in PlanetKit #7: changing the world, one block at a time


PlanetKit is a project aimed at creating a toolkit for making interactive virtual worlds. I’m writing it in the Rust programming language, which I’m finding delightful to work with, and a great fit for the task.

This week I’m going to allow the player to remove blocks from my voxel world.

Yes, I realise as I write this that it’s getting a bit tedious to look at that same scene. I’m planning to freshen it up a bit before my next post, with things like a much larger world, and a new random seed each time. But the way I’m taking videos right now is really painful, so I’m not prepared to re-make them right now for this post.

Movement follow-up

Before I built the bits that let you modify the world, I figured I’d better follow up on the half-baked movement system from last time. You may have noticed if you read it that I kind of glossed over how movement interacts with the terrain. And that would be because it… doesn’t. You could just walk into the ground, and wander inside the planet.

So before I started making it so you can modify the world, I quickly made amends.

This involved implementing the following rules:

  • Reject attempt if there is terrain where you’re trying to move.
  • Reject attempt if there is no ground beneath you, to stop you from skipping over gaps by moving quickly across them when you should fall into them.
  • If there’s ground where you’re trying to move to, then look one block above it, and try again. This lets you step up hills by at most one block.

And one final piece: gravity. I’ve implemented gravity simply as another System that affects all CellDwellers, rather than creating a new component type for things affected by gravity. I can see an argument for making each component handle as little as possible, but for now I’m going to err on the side of treating gravity as a common enough behaviour that it doesn’t hurt to assume all CellDwellers are at least potentially affected by it.

Dynamic chunk creation

First step was to implement dynamic chunk creation. Here’s what the demo app looks like now when you start it.

Note that this is only slow to update because I’ve deliberately imposed a limit on how many chunk meshes I will generate per second. My purpose here is to prevent chunk mesh generation from having a noticeable impact on gameplay, no matter how many chunks need to be regenerated. This is in anticipation of having very frequent updates to terrain in, say, a multi-player arena shooter.

In future I’ll ameliorate the overly-conservative slow loading by adding a priority system whereby chunks closer to the player have their meshes generated first, and probably some kind of time budget per frame to be spent on generating chunk meshes, rather than limiting it strictly to at most one chunk per frame as it is now.

Now for some implementation details. I’m trying to embrace the Entity–component–system paradigm, so I made another System responsible for creating a mesh for a chunk when there’s either no mesh made yet, or when there is a mesh but it is out-of-date with the current chunk data.

I fairly quickly ran into a problem: I can’t actually make the VBOs and other video card resources for the mesh in my new System or in the render System, because:

The consequence is that all video card resource creation must happen on the device thread — in my case, the main thread.

My solution:

Split out a “mesh repository”, containing both video card resource handles and also proto-meshes that contain all the information required to build those video card resources. This is then shared via an Arc<Mutex<MeshRepository>> by all the systems that need it.

Then any system that wants can create new proto-meshes, and in each frame on the main thread I reify any proto-meshes into video card VBOs and friends. This is plenty quick enough as long as I’m throttling the creation of proto-meshes elsewhere.

Actually mining for realsies!

My initial pass on this is a very simple system that’s been able to re-use much of the work I already did to handle movement. It lets you remove dirt blocks in front of you. Neato! This was trivial to get up and running with a couple of minutes of copy, paste, hack.

But this alone doesn’t let you see the changes you’ve made. So I needed to start regenerating chunk meshes when the chunk contents changes. That’s pretty easy in theory, except that this now also requires me to copy updates to chunks into their neighbouring chunks whenever the first chunk is the source of truth for the cells along the edge with said neighbor. (Phew–that’s a mouthful!) Otherwise we’ll just update one of the chunk meshes when we remove a block on an edge between chunks and it’ll only look like half of it goes away. (I go into some detail about sharing of cells along chunk boundaries in a previous post.)

So I made all chunks build a list of their neighbor chunk locations when created, and used that to update dirty chunks.

And that’s when I ran into borrow checker woes. Which surprised me, because things have actually been pretty cruisy between me and the compiler up until now. Usually whenever we fight, I pretty quickly concede that Rust is just pointing out to me that what I’m trying to achieve is actually wrong, and I need to amend my wicked ways.

But I think in this case what I was trying to achieve is pretty reasonable. Part of Globe looks like this:

pub struct Globe {
    // ...
    chunks: Vec<Chunk>,
    // ...
}

And part of Chunk looks like this…

pub struct Chunk {
    // ...
    pub origin: CellPos,
    pub cells: Vec<Cell>,
    pub chunk_resolution: [IntCoord; 3],
    pub authoritative_neighbors: Vec<Neighbor>,
    // ...
}

And for each chunk, I want to use its authoritative_neighbors to find all the other relevant chunks in the same collection that we need to copy cells from. And that means that I’m trying to borrow

  • multiple elements of the Vec<Chunk>, one mutably and N immutably
  • parts of the mutably borrowed Chunk twice:
    • once to iterate mutably over its authoritative_neighbors (mutably so I can mark Neighbors as being up-to-date)
    • once so I can call a function on it – thereby defeating the compiler’s knowledge of disjoint struct fields.

The next problem comes from Globe needing to be a specs Component, which implies Send. So I can’t fall back to using RefCells to get interior mutability checked at runtime.

Here’s what I ended up doing. Look away if you’re squeamish. First I temporarily remove the target chunk from the globe, so that we can simultaneously write to it and read from a bunch of other chunks via the chunks vector, and then I put it back into that vector at the end…

let mut target_chunk = self.chunks.swap_remove(target_chunk_index);

// ...

self.chunks.push(target_chunk);

And then between those, because I want to be able to mutate the chunk through its own functions while also iterating over a list of its neighbours, temporarily remove the list of neighbours from the Chunk and put it back at the end:

let mut neighbors = Vec::<super::chunk::Neighbor>::new();
use std::mem::swap;
swap(&mut neighbors, &mut target_chunk.authoritative_neighbors);

// ...

swap(&mut neighbors, &mut target_chunk.authoritative_neighbors);

Et voilà, it works!

Long story short, I’m doing a bunch of moving values in and out of structs, because I couldn’t figure out a better way to prove to the borrow checker that my intended access pattern is actually safe. Yuck!

This works, and so I’m going to move on for now. But what I really want is something like multi_mut provides, once the dust settles on all that undefined behaviour.

What’s next?

I’m going to take a brief detour from building shiny new fun stuff to clean up the code organisation. I wrote up a super-brief proto-roadmap in response to a GitHub issue. (Check out the rest of the thread to see some cool stuff that tedsta is doing!) Doing so reminded me that I really need to get around to turning this into a proper library, so that it’s easier to build new components and systems on top of PlanetKit to build a game, rather than needing to fork it for even the most basic applications.

And, as promised at the start of this post, I’ll try to get some more interesting terrain up and running.

As always, the source for everything I’m talking about here is up in the planetkit repository on GitHub.