Simple Chunk

By Plump Helmet Studios on

Simple Chunk

This simple chunk comprises of five classes: Chunk, Block, BlockSolid, BlockEmpty, and MeshData. The MeshData class holds the vertex/index data, which is instantiated in the Chunk class and passed to each Block class on render.

Chunk

This class holds a three-dimensional array of Block-derived classes, which it populates in the Awake() method. When Update() runs, it checks to see if the public boolean update is set to true, and if so, it updates & renders the blocks.

It creates a new MeshData class to hold render data for later on, which it passes to each block it renders.

using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Chunk : MonoBehaviour
{
    public static int chunkSize = 16;
    public bool update = true;

    Block[,,] blocks;
    MeshFilter meshFilter;

    void Awake()
    {
        // initialise a simple 50/50 solid/empty block array
        blocks = new Block[chunkSize, chunkSize, chunkSize];
        for (int x = 0; x < chunkSize; x++)
        {
            for (int y = 0; y < chunkSize; y++)
            {
                for (int z = 0; z < chunkSize; z++)
                {
                    Block block = (y < chunkSize / 2)
                        ? (Block)new BlockSolid()
                        : (Block)new BlockEmpty();
                    SetBlock(x, y, z, block);
                }
            }
        }
    }

    void Start()
    {
        meshFilter = GetComponent<MeshFilter>();
    }

    void Update()
    {
        if (update)
        {
            update = false;
            UpdateChunk();
        }
    }

    void UpdateChunk()
    {
        MeshData meshData = new MeshData();
        for (int x = 0; x < chunkSize; x++)
        {
            for (int y = 0; y < chunkSize; y++)
            {
                for (int z = 0; z < chunkSize; z++)
                {
                    blocks[x, y, z].Render(this, meshData, x, y, z);
                }
            }
        }
        RenderMesh(meshData);
    }

    void RenderMesh(MeshData meshData)
    {
        meshFilter.mesh.Clear();
        meshFilter.mesh.vertices = meshData.vertices.ToArray();
        meshFilter.mesh.triangles = meshData.triangles.ToArray();
        meshFilter.mesh.RecalculateNormals();
    }

    public Block GetBlock(int x, int y, int z)
    {
        if (x >= 0 && x < chunkSize &&
            y >= 0 && y < chunkSize &&
            z >= 0 && z < chunkSize)
        {
            return blocks[x, y, z];
        }
        else
        {
            return null;
        }
    }

    public void SetBlock(int x, int y, int z, Block block)
    {
        blocks[x, y, z] = block;
    }
}

Block

This class holds base functionality for a block, including the directional checks to ensure that only visible faces are rendered.

using System.Collections.Generic;
using UnityEngine;

public class Block
{
    public enum Direction { North, East, South, West, Up, Down };
    public Dictionary<Direction, Vector3> directionVectors;

    public Block()
    {
        directionVectors = new Dictionary<Direction, Vector3>() {
            { Direction.Up, new Vector3(0, 1, 0) },
            { Direction.Down, new Vector3(0, -1, 0) },
            { Direction.North, new Vector3(0, 0, 1) },
            { Direction.South, new Vector3(0, 0, -1) },
            { Direction.East, new Vector3(1, 0, 0) },
            { Direction.West, new Vector3(-1, 0, 0) }
        };
    }

    public virtual bool IsSolid(Direction direction)
    {
        return true;
    }

    public virtual void Render(Chunk chunk, MeshData meshData, int x, int y, int z)
    {
        foreach (var directionVector in directionVectors)
        {
            Vector3 vector = directionVector.Value;
            Block block = chunk.GetBlock(x + (int)vector.x, y + (int)vector.y, z + (int)vector.z);

            if (block == null || !block.IsSolid(directionVector.Key))
            {
                RenderFace(directionVector.Key, chunk, meshData, x, y, z);
            }
        }
    }

    protected virtual void RenderFace(Direction direction, Chunk chunk, MeshData meshData, int x, int y, int z)
    {
        switch (direction)
        {
            case Direction.Up:
                meshData.AddVertex(new Vector3(x, y + 1, z + 1));
                meshData.AddVertex(new Vector3(x + 1, y + 1, z + 1));
                meshData.AddVertex(new Vector3(x + 1, y + 1, z));
                meshData.AddVertex(new Vector3(x, y + 1, z));
                break;
            case Direction.Down:
                meshData.AddVertex(new Vector3(x, y, z));
                meshData.AddVertex(new Vector3(x + 1, y, z));
                meshData.AddVertex(new Vector3(x + 1, y, z + 1));
                meshData.AddVertex(new Vector3(x, y, z + 1));
                break;
            case Direction.North:
                meshData.AddVertex(new Vector3(x + 1, y, z + 1));
                meshData.AddVertex(new Vector3(x + 1, y + 1, z + 1));
                meshData.AddVertex(new Vector3(x, y + 1, z + 1));
                meshData.AddVertex(new Vector3(x, y, z + 1));
                break;
            case Direction.South:
                meshData.AddVertex(new Vector3(x, y, z));
                meshData.AddVertex(new Vector3(x, y + 1, z));
                meshData.AddVertex(new Vector3(x + 1, y + 1, z));
                meshData.AddVertex(new Vector3(x + 1, y, z));
                break;
            case Direction.East:
                meshData.AddVertex(new Vector3(x + 1, y, z));
                meshData.AddVertex(new Vector3(x + 1, y + 1, z));
                meshData.AddVertex(new Vector3(x + 1, y + 1, z + 1));
                meshData.AddVertex(new Vector3(x + 1, y, z + 1));
                break;
            case Direction.West:
                meshData.AddVertex(new Vector3(x, y, z + 1));
                meshData.AddVertex(new Vector3(x, y + 1, z + 1));
                meshData.AddVertex(new Vector3(x, y + 1, z));
                meshData.AddVertex(new Vector3(x, y, z));
                break;
        }
        meshData.AddQuadTriangles();
    }
}

BlockSolid

This class wants to be seen.

public class BlockSolid : Block
{
    public BlockSolid() : base() { }

    public override void Render(Chunk chunk, MeshData meshData, int x, int y, int z)
    {
        base.Render(chunk, meshData, x, y, z);
    }

    public override bool IsSolid(Block.Direction direction)
    {
        return true;
    }
}

BlockEmpty

This class does not wish to be seen.

public class BlockEmpty : Block
{
    public BlockEmpty() : base() { }

    public override void Render(Chunk chunk, MeshData meshData, int x, int y, int z)
    {
        // render nothing
    }

    public override bool IsSolid(Block.Direction direction)
    {
        return false;
    }
}

MeshData

This class holds triangle and vertex data, and offers a small helper method too.

using System.Collections.Generic;
using UnityEngine;

public class MeshData
{
    public List<Vector3> vertices = new List<Vector3>();
    public List<int> triangles = new List<int>();

    public MeshData() { }

    public void AddVertex(Vector3 vertex)
    {
        vertices.Add(vertex);
    }

    public void AddTriangle(int triangle)
    {
        triangles.Add(triangle);
    }

    public void AddQuadTriangles()
    {
        triangles.Add(vertices.Count - 4);
        triangles.Add(vertices.Count - 3);
        triangles.Add(vertices.Count - 2);
        triangles.Add(vertices.Count - 4);
        triangles.Add(vertices.Count - 2);
        triangles.Add(vertices.Count - 1);
    }
}

Summary

This basic prototype shows an easy way to render a large number of voxels with a single mesh, reducing the load on the GPU. This could be expanded with a further parent class which could allow chunk/block transversal for multiple chunks.