Building a High-Performance Voxel Engine in Unity: A Step-by-Step Guide Part 1: Voxels & Chunks

CodeComplex
6 min readNov 16, 2023

--

Welcome to this exciting journey where we’ll be diving into the intricate process of building a high-performance voxel engine using Unity. This guide is crafted for both enthusiasts and professionals who are passionate about game development and are keen to explore the world of voxel-based environments.

In this series, we’ll start from the ground up, laying a strong foundation with a basic voxel structure, and progressively delve into more complex aspects such as chunk systems, world generation, and efficient rendering techniques. Our focus will be on leveraging Unity’s powerful features, including the Job System, to ensure our engine is not only functional but also optimized for performance.

Whether you’re looking to create vast, explorable landscapes, intricate building mechanics, or simply understand the inner workings of a voxel engine, this guide is tailored for you. We’ll break down each component into manageable steps, ensuring that you can follow along comfortably, regardless of your expertise level.

Join me in this comprehensive and hands-on approach to building a voxel engine in Unity, where each article in the series will represent a new milestone in our development journey. Let’s embark on this adventure together, transforming our ideas into a dynamic and immersive voxel world!

Create a Voxel.cs script add this code…

using UnityEngine;

// Define a simple Voxel struct
public struct Voxel
{
public Vector3 position;
public Color color;
public bool isActive;
public Voxel(Vector3 position, Color color, bool isActive = true)
{
this.position = position;
this.color = color;
this.isActive = isActive;
}
}

This struct includes three main properties:

position: A Vector3 representing the voxel’s position in 3D space.
color: A Color to define the voxel’s color.
isActive: A bool to determine if the voxel is active or not.

You can use this struct as a foundation and expand it according to your specific needs. This represents one voxel.

The Chunk System

Chunks will allow us to manage and render voxels efficiently in manageable segments. You are a Shepard, voxels are your sheep.

Voxel Sheep

Create a file Chunk.cs and add this code…

using System.Collections.Generic;
using UnityEngine;

public class Chunk : MonoBehaviour
{
private Voxel[,,] voxels;
private int chunkSize = 16;

void Start()
{
voxels = new Voxel[chunkSize, chunkSize, chunkSize];
InitializeVoxels();

}

private void InitializeVoxels()
{
for (int x = 0; x < chunkSize; x++)
{
for (int y = 0; y < chunkSize; y++)
{
for (int z = 0; z < chunkSize; z++)
{
voxels[x, y, z] = new Voxel(transform.position + new Vector3(x, y, z), Color.white);
}
}
}
}
}

The Chunk class holds a position property, a Vector3, indicating its location in the world. This property is crucial for determining where the chunk is rendered in the game space. It contains a 3D array of Voxel structs (voxels), representing the actual voxel data within the chunk. The size of this array is determined by chunkSize, which defines the dimensions of the chunk (e.g., 16x16x16).

To visualize this array of voxels just add a new function in the chunk script…

void OnDrawGizmos()
{
if (voxels != null)
{
for (int x = 0; x < chunkSize; x++)
{
for (int y = 0; y < chunkSize; y++)
{
for (int z = 0; z < chunkSize; z++)
{
if (voxels[x, y, z].isActive)
{
Gizmos.color = voxels[x, y, z].color;
Gizmos.DrawCube(transform.position + new Vector3(x, y, z), Vector3.one);
}
}
}
}
}
}

Save the file and add a new GameObject to your scene, call it Chunk and add the Chunk script to it. Press play and you will see a structured cube representing the voxels.

We are using gizmos, in-edtior tools to help us see the output of the voxel array. We have yet to generate mesh.

Now we need a way to initialize chunks from our World class which we will create shortly. Add this method to the Chunk script…

public void Initialize(int size)
{
this.chunkSize = size;
voxels = new Voxel[size, size, size];
InitializeVoxels();
}

From this method we can set the chunk size and add voxels to it.

Now start a new script call it World.cs and add the following code…

using System.Collections.Generic;
using UnityEngine;

public class World : MonoBehaviour
{
public int worldSize = 5; // Size of the world in number of chunks
private int chunkSize = 16; // Assuming chunk size is 16x16x16

private Dictionary<Vector3, Chunk> chunks;

void Start()
{
chunks = new Dictionary<Vector3, Chunk>();

GenerateWorld();
}

private void GenerateWorld()
{
for (int x = 0; x < worldSize; x++)
{
for (int y = 0; y < worldSize; y++)
{
for (int z = 0; z < worldSize; z++)
{
Vector3 chunkPosition = new Vector3(x * chunkSize, y * chunkSize, z * chunkSize);
GameObject newChunkObject = new GameObject($"Chunk_{x}_{y}_{z}");
newChunkObject.transform.position = chunkPosition;
newChunkObject.transform.parent = this.transform;

Chunk newChunk = newChunkObject.AddComponent<Chunk>();
newChunk.Initialize(chunkSize);
chunks.Add(chunkPosition, newChunk);
}
}
}
}

// Additional methods for managing chunks, like loading and unloading, can be added here
}

The World class will be responsible for managing chunks in the voxel world. Create a new GameObject in the editor call it World and add the World script to it.

For clarity, at the Start() method we set the chunks dictionary and call GenerateWorld(), which in turn loops through the x y & z axis by the worldSize number of times, we position our chunks, creating a new GameObject and calling it chunk, prefixed with it’s position. We also add a chunk class to the chunk then initialize it, so thereby adding the voxels to it then add it to the chunks dictionary.

Voxels fill the entire space of a chunk along the x, y and the z… however a voxel can also be air, we will not render air voxels.

So we can visualize the chunks add a new var in your Chunk class…

private Color gizmoColor;

and in the initlize function add this line at the end…

// Assign a random color for this chunk's gizmos
gizmoColor = new Color(Random.value, Random.value, Random.value, 0.4f); // Semi-transparent

Then add a new OnDrawGizmos() function, remove the old voxel OnDrawGizmos() aswell…

void OnDrawGizmos()
{
if (voxels != null)
{
Gizmos.color = gizmoColor;
Gizmos.DrawCube(transform.position + new Vector3(chunkSize / 2, chunkSize / 2, chunkSize / 2), new Vector3(chunkSize, chunkSize, chunkSize));
}
}

Press Play…

These are a visual representation of our chunks, each chunk houses 16x16x16 (or whatever size you need) worth of voxels. Right now they are just data and they do not have any mesh attached to them or materials so we can not see them. The OnDrawGizmo() function, for now, allows us a quick look at the concept.

In the next part we will delve into mesh generation.

--

--

CodeComplex

Exploring the digital depths of coding and tech. Innovator at heart, sharing insights on voxel engine development and Unity. #CodeComplex 🌐💻🚀