Exploring the game development world: Drawing a voxel terrain
In the last post we explored the theory about terrain generation and decided how we are going to draw our voxels in our terrain. Now we are ready to write code to actually draw something in our game.
As we are going to create a mesh at runtime we start off by creating an empty GameObject and add it a script, lets say “ProceduralTerrain”.
To draw a custom mesh it’s really simple in Unity, just modify the MeshFilter
on the GameObject we want.
Mesh mesh = new Mesh();
GetComponent<MeshFilter>().mesh = mesh;
mesh.vertices = myVertices;
mesh.triangles = myTriangles;
GetComponent<MeshCollider>().sharedMesh = mesh; // To add collision
And that’s it, we link out mesh to the object and then we can add our vertices there as we like, and the triangles are just indexes of the vertices
array to define their order. That’s the easy part, now we must define how we’re going to add those vertices.
Starting as a graph
We will decide what faces to draw using a graph model, so we could start drawing from one block (a root), and then jump over every neighbor and, at each one, do the same over and over.
A graph has nodes and edges, nodes ideally stores a value, for us this will be data that each block will store, for now it’s enough to store type of block it is:
public enum BlockType {
Air,
Dirt,
Stone
}
These are the most basic types we could find in any exploration game, Air
is merely a value for our enum
to denote emptiness, that is our null
, for now we will only use Dirt
to denote existence because we are not defining types yet, we just want a terrain mesh.
Now, I want each block to be aware of where it is and who are their neighbors, so I added them the location in the voxel grid:
Vector3i
is just a copy of the Unity’s vector but using only integer values, just to simplify the code.
As we can see, there is an indexer where we can get the block’s neighbor positions given a CubeFace
direction, which is nothing more than an enum with the values:
public enum CubeFace {
Up, Down, North, South, East, West
}
Because I just don’t want to go adding simple math operations wherever I need to check neighbors.
Now, to traverse the graph and draw each block’s face based on their neighbors I wrote this piece of code
This code is away from perfect, as we will get some duplicated vertices because as we are avoiding inner faces, we are still repeating vertices at the corners in the blocks but it’s something anyway.
The Draw
method is a breadth-first search for an algorithm.
By filling blockMap
with arbitrary data we could create this chunk. Now let’s make it bigger and add some mountains.
Adding Perlin Noise to the mix
We can start adding some mountains and hills using the Perlin Noise algorithm to create a heightmap where we can fill blockMap
accordingly.
Now, you might be wondering “how are we going to use the Perlin Noise without knowing how it works?”, well, it’s because Unity already has an implementation for us! It’s Mathf.PerlinNoise
.
This function accept two floats (x, y), and returns another float between 0 and 1. But keep always in mind that the input values should be between 0 and 1 as well, as the implementation will apply a modulus-1 operation that will get rid of the integer part, so if we run the function with (0.5, 0.5), (1.5, 1.5) and (2.5, 2.5) we will get the same output.
So it’s easy to set the inputs for the function to be each coordinate (x, z) divided by the terrain’s total width and depth respectively so we always get different results.
Basically, we are traversing each (x, z) pair of our block terrain, convert each coordinate to [0, 1], then use them to run PerlinNoise and linearly interpolate the result between [-maxY, maxY], then we just need to set the blockMap to accordingly to our height map. Without calling the flatCenter()
and using a small terrain we got something like this:
Isn’t it cute? Our first piece of Minecraft terrain. Now, using a big-size space and using flatCenter()
to keep a place of quiet we got this:
Ok, we go mountains, but they are too much pronounced, we need to leverage things to get a more flattened world and, if possible, flat at the center. The thing here is that our noise is too drastic, passing from 0 to 1 in so little distance. So the answer here is to scale up the noise space by scaling down our inputs for the PerlinNoise, so we modify the scale
variable to 0.07f
and we won’t need anymore the flatCenter()
function. Giving us something like this:
No we are getting something pretty, I almost feel like in a voxel world already. Now let’s make a much, much bigger terrain!
Oops! Looks like we have a limit of vertices that a mesh can have… makes sense, because I don’t want to imagine the power needed to keep track of collisions with so many vertices and faces for example. So that’s why voxel games tends to separate the terrain in chunks, we will have to do the same here and separate our terrain in consistently sized chunks.
Now we have a next goal with our terrain, separate it in chunks to handle it better. Next, we could look for ways to make the world less blocky, or explore other gameplay concept, I don’t know, now is time to write my stories while I code and solve more problems, I don’t know how much time will it take to post new stories but I hope it doesn’t take long. Thanks for reading so far!