Making a Minecraft Clone

Mar 26, 2020 • Alec Shedelbower

Minecraft has remained one of my favorite games since its official release in 2011. It is fair to say that it popularized the use of voxels and inifinite procedural worlds in video games.

It has since inspired a whole slew of similar games and prototypes. After seeing some of these, I thought it would be fun to recreate my own Minecraft clone in Unity. This clone would include fully modifiable terrain, block updates and infinite world generation.

In this post I'll review some of my implementation details. Keep in mind I didn't research any techniques beforehand so my solutions are almost certainly suboptimal, but I think that's part of the fun :-)

Blocks

Minecraft represents its world with voxel data (i.e. blocks). There are a few advantages of using voxels over standard meshes. In short, meshes are memory efficient and fast to render, but difficult to modify. Whereas voxels are trivial to modify, but harder to render.

To get the best of both worlds, I use voxel data when making modifications and then convert it to a mesh for rendering.

Block Types

I store blocks as integers. Each integer corresponds to a specific block type. For instance, a block of grass may be represented by the number 2, while gravel uses the number 7. This helps to reduce memory usage as I can store the properties of blocks in a single location instead of with each individual block. A side effect of this implementation is that every block of the same type is identical (i.e. each grass block is indistinguishable from every other grass block).

In Minecraft, there is an additional concept of block data which allows individual blocks to have different states (such as crops in different stages of growth).

For my block types, I created a class that derives from Unity’s ScriptableObject. This allows me to create and edit block types like any other asset, making it easy to add new block types without having to change any code.

All block type assets used in my clone.

These block types store information such as:

Textures

Sounds Transparency

Render Method Blast Resistance

Affected by Gravity

Chunks

In order to create an infinite world on a device with not-so-infinite memory, I have to divide the world into finite chunks of blocks. At any given time, I only load chunks that are within a certain range of the player. As the player moves, chunks that enter this range are loaded while those that exit are unloaded.

Player moving with a render distance of three chunks.

Each chunk is represented as a 3D integer array, the dimensions of which are fully configurable in the Unity editor.

Minecraft uses chunks of size 16x16x256, however this gets divided into more manageable 16x16x16 sections when rendering. After playing around with different sizes, I settled on 16x16x24 as a good size for my world type.

Unlike Minecraft, my world doesn't have any arbitrary height limits. Therefore, you can go in any direction indefinitely, not just horizontally.

Rendering a Chunk

So at this point I have a chunk represented by voxel data (3D integer array). However, I need to convert it to Unity’s triangle mesh in order to render it. The process of converting voxel data to meshes is known more generally as isosurface extraction. Some algorithms like Marching Cubes and Dual Contouring can even create smooth meshes from “blocky” voxel data. However, I actually want the blocky look that gives Minecraft its unique aesthetic.

I played around with a few different meshing methods, starting with the most basic.

One Cube Per Block

The naive way to render a chunk is to simply place a cube mesh at each block in the chunk. This is straightforward and works great! …at least for a handful of blocks. If you try to render thousands of blocks you will soon be met with disgraceful frame rates as your computer struggles to render each individual cube.

Left: Filled chunk with a cube mesh for each block.

Right: Wireframe version of chunk.

Cube Culling

A simple optimization is to only render blocks that could potentially be seen by the player. This means culling any blocks surrounded on all 6 sides by opaque blocks. It's important to consider transparent blocks (like water and glass) to avoid incorrectly culling visible blocks.

Left: Chunk with culled blocks highlighted in red.

Right: Chunk with culled blocks removed.

Quad Culling

You may notice that many of the remaining cube faces (quads) are also not visible to the player. Therefore, we can further reduce the mesh to just the visible quads.

Left: Chunk with culled quads highlighted in red.

Right: Chunk with culled quads removed.

By this point my chunks were rendering nicely, so I decided to move on to texturing.

I later found an algorithm called Greedy Meshing, which futher reduces the mesh size by combining adjacent quads into larger rectangles. However, my meshing algorithm was already performant enough for my use case.

Reducing Draw Calls

While the size of the chunk mesh is efficient, it still requires hundreds of draw calls. This has a major impact on performance.

I can reduce the chunk to just one draw call by combining all the quads into a single mesh rendered with a single material. In order to allow each block to keep its unique texture, I use an atlas containing all block textures. Each quad then uses its UVs to index into this atlas.

Example of atlas texture being mapped to quads.

Billboard Blocks

So far I've only discussed rendering blocks as cubes. However, many block types like flowers are rendered using two perpendicular quads (billboards) that intersect at their center.

I consider these billboard blocks to be transparent, and will always render both of its quads if the block is visible. Otherwise, the meshing algorithm works the same as it does for cube blocks.

Examples of cube-rendered blocks (left) and billboard-rendered blocks (right).

Infinite Chunks

Performing opertaions within a single chunk is simple, as it only involves iterating over a 3D array. However, the introduction of infinite chunks makes this more difficult. It is no longer sufficient to iterate over a single chunk, as I also have to check the boundary blocks of adjacent chunks.

Data Structure

I have a WorldChunk class that acts as a wrapper around the 3D array of block types. I then use a Dictionary to store the loaded world chunks. The key for a chunk is the coordinate of its minimum corner.

Because the chunk size is consistent per dimension (x/y/z), it is easy to find the minimum corner given an arbitrary position within a chunk. In addition, since each minimum corner is separated by the same interval, I can quickly find adjacent chunks by adding/subtracting the chunk size from a chunk’s key.

Example of finding adjacent chunks (red) near the player's current chunk (blue).

Memory

I opted to not implement world saving for my Minecraft clone, as it didn’t seem worth the extra effort. Therefore the world chunks are only stored in RAM while the game is running. For efficiency, the only chunks that are loaded at any given time are those that are either in the player’s range or those that have been modified by the player. All other chunks are unloaded to free up resources.

So technically there’s no cap on how many chunks you can explore (ignoring numerical precision), though the amount of chunks you can modify is limited by your system's RAM.

Block Updates

Infinite exploration is great, but it gets old fast if the world is static. Therefore, I wanted players to be able to modify the world and see it react to their changes.

Modifying Blocks

As I mentioned previously, modifying voxels is as easy as changing a value in a 3D array. The difficulty comes in propagating changes and rebuilding chunk meshes.

Since I render each chunk as a singular mesh, I have to rebuild a chunk's mesh every time it's modified. For example, if the player removes a block then that entire chunk must be rebuilt. In addition, if that block was on a chunk border then the neighboring chunk may need to be rebuilt as well. Both of these rebuilds must occur near instantly to avoid lag spikes.

Going further, all blocks adjacent to the removed blocked must be checked for things like gravity and water flow.

Block Update Cycle

Some blocks need to perform an update when a neighboring block has changed (sand, water, sugar cane, etc.). These kind of blocks introduce a unique performance challenge.

When one of these blocks is updated, it often requires the surrounding blocks to be updated as well. I can't update all of these blocks at once as that could lead to an infinite chain of updates. Therefore, I use an update cycle.

Whenever a block is modified, that block and its neighbors are marked to be updated. Then every 0.25 seconds I scan over all the marked blocks and perform their updates in a single batch. Afterwards, I rebuild the updated chunk meshes.

Block Gravity

Blocks like sand and gravel are affected by gravity, which means they fall smoothly whenever no block is beneath them. This effect provides some realism and dynamics to the game, which is why I thought it'd be fun to include it in my clone.

Example of gravel blocks being affected by gravity.

However, this effect is tricky to implement as it breaks two previous assumptions:

Blocks can only exists at discrete positions. Blocks are rendered per-chunk rather than individually.

Like Minecraft, my solution was to temporarily convert the falling block to an entity. The trick is to seamlessly swap the block for an entity when it starts falling, and then vice versa when it lands.

Example of sand blocks being converted to entities when falling. Entities are colored red to make the transition obvious.

Flowing Water

Representing fluid with voxels poses a tradeoff between realism and performance. The difficulty comes in that fluid is inherently continuous and dynamic, while voxels are discrete and expensive to change.

The continuity problem can be solved by discretizing the possible water levels into a manageable amount of quantities. I tried implementing 16 different quantities and used shaders to offset the resulting water block heights. I also experimented with finite water (ex. a lake can be drained by flowing into a ravine).

I was able to achieve some neat results with this, but unfortunately I had to scrap it as it could easily cause massive performance issues. So instead I opted for a single quantity of water that will create copies in adjacent blocks.

Minecraft tackles the continuous problem by discretizing water blocks into about 8 different quantities of water. Blocks with a higher quantity will create lower-quantity water blocks adjacent to it.

As for the dynamic issue, that is resolved with the block update cycle I mentioned eariler.

Example of water flowing through a channel.

Conclusion

If you made it this far, I hope you learned something useful. I had a few other topics I wanted to mention, but they'll have to wait for another time.

If you have any comments, feel free to post them to the Reddit thread here.

Further Reading