Minecraft style Chunkloader

Role:
Sole developer

Engine/Tools:
Unity Engine
Visual Studio C#

Team Size:
1


Project Git Repository


Produced a very basic Minecraft style chunk loader using the unity engine as part of a hobby/experimental project.

Blocks are created dynamically as a custom mesh cube with UV mappings able to be targeted to specific positions in a texture sheet.
All cubes are technically the same holding a data object containing specific data for specific block types allowing a high level of customization for different blocks and properties.

Block Creation


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum direction { NORTH, EAST, SOUTH, WEST, UP, DOWN }

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer), typeof(MeshCollider))]
[RequireComponent(typeof(MeshBuilder))]
public class Block : MonoBehaviour
{
    #region Variables

    #region Mesh Variables
    private Mesh mesh = null;
    private MeshCollider col = null;
    private List<Vector3> vertices;
    private List<int> triangles;
    [SerializeField] private List<Vector2> Uvs;
    #endregion

    #region Block Data
    [SerializeField] private BlockData blockData = null;
    [SerializeField] private float Scale = 1f;
    [SerializeField] private Vector3 Position = Vector3.zero;
    [SerializeField] private Vector3 blockIndex = Vector3.zero;
    #endregion

    [SerializeField] private Chunk ParentChunk;

    private Vector3 blockPositions = Vector3.zero;
    private float adjScale;

    [Header("DEBUG")]
    [SerializeField] private GameObject[] Neighbours;

    #endregion

    private void Awake()
    {
        Neighbours = new GameObject[6];
        mesh = GetComponent<MeshFilter>().mesh;
        col = GetComponent<MeshCollider>();
        adjScale = Scale * 0.5f;
        blockPositions = new Vector3((float)Position.x * Scale, (float)Position.y * Scale, (float)Position.z * Scale);
        MakeCube();
    }

    private void Start()
    {
        col.sharedMesh = null;
        col.sharedMesh = mesh;
        col.enabled = blockData.getCollidable;
    }

    private void MakeCube()
    {
        vertices = new List<Vector3>();
        triangles = new List<int>();
        Uvs = new List<Vector2>();
    }

    public void MakeFace(int faceDir, float faceScale, Vector3 facePos)
    {
        vertices.AddRange(MeshBuilder.faceVertices(faceDir, faceScale, facePos));

        int VertCount = vertices.Count;

        triangles.Add(VertCount - 4);
        triangles.Add(VertCount - 3);
        triangles.Add(VertCount - 2);

        triangles.Add(VertCount - 4);
        triangles.Add(VertCount - 2);
        triangles.Add(VertCount - 1);

        Uvs.AddRange(blockData.getUVbyDir((direction)faceDir));
    }

    private void InitializeMesh()
    {
        mesh.Clear();
        vertices.Clear();
        triangles.Clear();
        Uvs.Clear();


        gameObject.name = blockData.getblockID;
        gameObject.GetComponent<MeshCollider>().enabled = blockData.getCollidable;

        if(ParentChunk.RenderChunk)
        {
            for (int i = 0; i < 6; i++)
            {
                if (!blockData.getVisibility) { }
                else if (ParentChunk.getNeighbour((int)blockIndex.x, (int)blockIndex.y, (int)blockIndex.z, (direction)i) == null || !ParentChunk.getNeighbour((int)blockIndex.x, (int)blockIndex.y, (int)blockIndex.z, (direction)i).GetComponent<Block>().blockData.getVisibility)
                { MakeFace(i, adjScale, blockPositions); }
                Neighbours[i] = (ParentChunk.getNeighbour((int)blockIndex.x, (int)blockIndex.y, (int)blockIndex.z, (direction)i));
            }
        }

        mesh.vertices = vertices.ToArray();
        //Debug.Log("Vertieces : " + mesh.vertices.Length);
        mesh.triangles = triangles.ToArray();
        mesh.RecalculateNormals();
        mesh.uv = Uvs.ToArray();
        col.sharedMesh = mesh;

    }

    #region Public Functions

    public void updateMesh() { InitializeMesh(); }

    public void setParentChunk(Chunk chunk) { ParentChunk = chunk; }

    public void setBlockData(BlockData DataPack)
    {
        blockData = DataPack;

        updateMesh();

        if(!blockData.getVisibility)
        {
            foreach (GameObject Neighbour in Neighbours)
            {
                if(Neighbour)
                { Neighbour.GetComponent<Block>().updateMesh(); }
            }
        }

    }

    #region Getters
    public float getBlockScale { get { return adjScale; } }
    public Vector3 getBlockPositions { get { return blockPositions; } }
    public float getScale { get { return adjScale; } }

    public BlockData getBlockData {  get { return blockData; } }

    //public Vector3 getBlockIndex { get { return blockIndex; } }
    //public Block getBlockData { get { return blockData; } }
    #endregion

    #region Setters
    public void setBlockIndex(Vector3 index) { blockIndex = index; }
    #endregion

    #endregion
}

Block Mesh Builder

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MeshBuilder : MonoBehaviour
{
    private static Vector3[] vertecies =
    {
        new Vector3(1, 1, 1),
        new Vector3(-1, 1, 1),
        new Vector3(-1, -1, 1),

        new Vector3(1, -1, 1),
        new Vector3(-1, 1, -1),
        new Vector3(1, 1, -1),

        new Vector3(1, -1, -1),
        new Vector3(-1, -1, -1),
    };

    private static int[][] faceTriangles =
    {
        new int[] { 0, 1, 2, 3 },
        new int[] { 5, 0, 3, 6 },
        new int[] { 4, 5, 6, 7 },
        new int[] { 1, 4, 7, 2 },
        new int[] { 5, 4, 1, 0 },
        new int[] { 3, 2, 7, 6 }
    };

    public static Vector3[] faceVertices(int dir, float faceScale, Vector3 facePos)
    {
        Vector3[] fv = new Vector3[4];
        for (int i = 0; i < fv.Length; i++)
        {
            fv[i] = (vertecies[faceTriangles[dir][i]] * faceScale) + facePos;
        }
        return fv;
    }

    public static Vector3[] faceVertices(direction dir, float faceScale, Vector3 facePos)
    {
        return faceVertices((int)dir, faceScale, facePos);
    }

}

Cubes are then generated into 16x16x16 “chunks” with faces blocked by other blocks disabled, thus only ever rendering faces that would be possibly seen by a player/camera.

Chunks are then stacked on top of each other contrary to how the classic Minecraft algorithm that produces 16x256x16 columns does it. This was done in an attempt to optimize control of what is rendered which in turn allows for more control of overall optimization while still allowing rendering of necessary “chunks”.

Chunk Creator


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public struct DataCoordinates
{
    internal int x;
    internal int y;
    internal int z;

    public DataCoordinates(int x, int y, int z)
    {
        this.x = x;
        this.y = y;
        this.z = z;
    }
}

[RequireComponent(typeof(ChunkInhabitor))]
public class Chunk : MonoBehaviour
{
    #region Variables

    private bool SpawnChunk = false;
    private bool ChunkRender = false;

    [SerializeField] private Vector3 ChunkSize = Vector3.zero;
   
    #pragma warning disable 0649
    [SerializeField] private GameObject BlockObject;
    #pragma warning restore 0649

    [SerializeField] private float BlockSize = 1.0f;

    [SerializeField] private Vector3 ChunkIndex = Vector3.zero;
    private GameObject[,,] BlockList;

    private DataCoordinates[] offsets =
{
        new DataCoordinates(0,0,1),
        new DataCoordinates(1,0,0),
        new DataCoordinates(0,0,-1),
        new DataCoordinates(-1,0,0),
        new DataCoordinates(0,1,0),
        new DataCoordinates(0,-1,0)
    };

    private World WorldRef = null;

    #endregion

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.U))
        {
            Debug.Log("Updating Chunks");
            updateChunk();
        }
    }

    public IEnumerator loadChunk()
    {
        BlockList = new GameObject[(int)ChunkSize.x, (int)ChunkSize.y, (int)ChunkSize.z];
        BlockSize = BlockObject.GetComponent().getScale;

        GenerateChunk();

        yield return null;

        transform.GetComponent().InhabitChunk();
        updateChunk();
        if (SpawnChunk)
        {
            foreach (GameObject block in BlockList)
            {
                if (block.name == "Grass")
                {
                    getWorld.getPlayer.transform.position = new Vector3(block.transform.position.x, block.transform.position.y + 2, block.transform.position.z);
                    break;
                }
            }
        }
    }

    private void GenerateChunk()
    {
        for (int y = 0; y < ChunkSize.y; y++)
        {
            for (int x = 0; x < ChunkSize.x; x++)
            {
                for (int z = 0; z < ChunkSize.z; z++)
                {
                    GameObject tempBlock = Instantiate(BlockObject, new Vector3(transform.position.x + BlockSize + x, transform.position.y + BlockSize + y, transform.position.z + BlockSize + z), Quaternion.identity);
                    Block BLogic = tempBlock.GetComponent();
                    BLogic.setParentChunk(this.gameObject.GetComponent());
                    tempBlock.transform.parent = gameObject.transform;

                    BlockList[x, y, z] = tempBlock;
                    BLogic.setBlockIndex(new Vector3(x, y, z));
                }
            }
        }
    }

    public GameObject getNeighbour(int x, int y, int z, direction dir)
    {
        Vector3 pos = new Vector3(x, y, z);
        DataCoordinates offsetToCheck = offsets[(int)dir];
        DataCoordinates neighbourCoord = new DataCoordinates((int)pos.x + offsetToCheck.x, (int)pos.y + offsetToCheck.y, (int)pos.z + offsetToCheck.z);

        if (neighbourCoord.x < 0 || neighbourCoord.x >= Width || neighbourCoord.y < 0 || neighbourCoord.y >= Height || neighbourCoord.z < 0 || neighbourCoord.z >= Depth) { return checkChunkNeighbours(x, y, z, dir); }
        else
        {
            //if(y == chunkSize.y) { Debug.Log("Returning Block at: X: " + neighbourCoord.x + " Y: " + neighbourCoord.y + " Z: " + neighbourCoord.z); }
            return GetBlock(neighbourCoord.x, neighbourCoord.y, neighbourCoord.z);
        }
    }

    private GameObject checkChunkNeighbours(int x, int y, int z, direction dir)
    {
        //Debug.Log("Chunk Direction given: " + dir);
        GameObject temp = transform.parent.GetComponent().GetChunkNeighbour((int)ChunkIndex.x, (int)ChunkIndex.y, (int)ChunkIndex.z, dir);
        if (temp != null)
        {
            GameObject blockRef = null;
            switch (dir)
            {
case direction.NORTH: 
if (temp.GetComponent().GetBlock(x, y, 0) != null) { blockRef = temp.GetComponent().GetBlock(x, y, 0); /*Debug.Log("Called blockRef for NORTH Chunk, block ref: " + blockRef);*/ } break;
case direction.SOUTH: 
if (temp.GetComponent().GetBlock(x, y, (int)ChunkSize.z -1 ) != null) { blockRef = temp.GetComponent().GetBlock(x, y, (int)ChunkSize.z - 1); /*Debug.Log("Called blockRef for SOUTH Chunk, block ref: " + blockRef);*/ } break;
case direction.EAST: 
if (temp.GetComponent().GetBlock(0, y, z) != null) { blockRef = temp.GetComponent().GetBlock(0, y, z); /*Debug.Log("Called blockRef for EAST Chunk, block ref: " + blockRef);*/ } break;
case direction.WEST: 
if (temp.GetComponent().GetBlock((int)ChunkSize.x - 1, y, z) != null) { blockRef = temp.GetComponent().GetBlock((int)ChunkSize.x - 1, y, z); /*Debug.Log("Called blockRef for WEST Chunk, block ref: " + blockRef);*/ } break;
case direction.UP: 
if (temp.GetComponent().GetBlock(x, 0, z) != null) { blockRef = temp.GetComponent().GetBlock(x, 0, z); /*Debug.Log("Called blockRef for UP Chunk, block ref: " + blockRef);*/ } break; 
case direction.DOWN: 
if (temp.GetComponent().GetBlock(x, (int)ChunkSize.y - 1, z) != null) { blockRef = temp.GetComponent().GetBlock(x, (int)ChunkSize.y - 1, z); /*Debug.Log("Called blockRef for DOWN Chunk, block ref: " + blockRef);*/ } break;
            }
            return blockRef;
        }
        else { return null; }
    }

    public GameObject GetBlock(int x, int y, int z) { return BlockList[x, y, z]; }

    public void setChunkIndex(int x, int y, int z) { ChunkIndex = new Vector3(x, y, z); }

    public void setWorldRef(World Reference) { WorldRef = Reference; }

    #region Getters
    public int Width { get { return BlockList.GetLength(0); } }

    public int Height { get { return BlockList.GetLength(1); } }

    public int Depth { get { return BlockList.GetLength(2); } }

    public Vector3 getChunkSize { get { return ChunkSize; } }

    public Vector3 getChunkIndex {  get { return ChunkIndex; } }

    public GameObject[,,] getBlockList {  get { return BlockList; } }

    public World getWorld {  get { return WorldRef; } }

    public bool IsSpawnChunk
    { set { SpawnChunk = value; } }

    public bool RenderChunk
    {
        get { return ChunkRender; }
        set { ChunkRender = value; }
    }

    #endregion

    public void updateChunk()
    {
        foreach (GameObject block in BlockList)
        {
            block.GetComponent().updateMesh();
        }
    }
}
Chunk Column Creator


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ChunkColumn : MonoBehaviour
{
    #region Variables

    #region ChunkData
    [SerializeField] GameObject ChunkObject = null;
    private Vector3 ChunkSize = Vector3.zero;
    #endregion

    #region ColumnData
    [SerializeField] private Vector3 chunkColumnIndex = Vector3.zero;
    [SerializeField] private int ColumnSize = 0;
    #endregion

    [SerializeField] private GameObject[] ChunkList;

    private IEnumerator routine = null;
    private int numSpawnedChunks = 0;
    private DataCoordinates[] offsets =
    {
        new DataCoordinates(0,0,1),
        new DataCoordinates(1,0,0),
        new DataCoordinates(0,0,-1),
        new DataCoordinates(-1,0,0),
        new DataCoordinates(0,1,0),
        new DataCoordinates(0,-1,0)
    };

    private World WorldRef;

    #endregion

    public void SpawnChunks()
    {
        ChunkList = new GameObject[ColumnSize];
        ChunkSize = ChunkObject.GetComponent().getChunkSize;

        for (int i = 0; i < ColumnSize; i++)
        {
            GameObject tempObj = Instantiate(ChunkObject, new Vector3(transform.position.x, transform.position.y + (ChunkSize.y * numSpawnedChunks + 1), transform.position.z), Quaternion.identity);
            tempObj.GetComponent().setWorldRef(WorldRef);
            setChunkData(tempObj);
            routine = tempObj.GetComponent().loadChunk();
            tempObj.GetComponent().StartCoroutine(routine);
            ChunkList[i] = tempObj;
            numSpawnedChunks++;
        }
    }

    private void setChunkData(GameObject McChunkster)
    {
        McChunkster.transform.parent = this.gameObject.transform;
        McChunkster.name = "Chunk(" + chunkColumnIndex.x + "," +numSpawnedChunks + "," + chunkColumnIndex.z + ")";
        McChunkster.GetComponent().setChunkIndex((int)chunkColumnIndex.x, numSpawnedChunks, (int)chunkColumnIndex.z);
    }

    public GameObject GetChunkNeighbour(int x, int y, int z, direction dir)
    {
        Vector3 pos = new Vector3(x, y, z);
        DataCoordinates offsetToCheck = offsets[(int)dir];
        DataCoordinates neighbourCoord = new DataCoordinates((int)pos.x + offsetToCheck.x, (int)pos.y + offsetToCheck.y, (int)pos.z + offsetToCheck.z);

        if (neighbourCoord.x != chunkColumnIndex.x || neighbourCoord.z != chunkColumnIndex.z)
        {
            return WorldRef.GetChunkNeighbour(neighbourCoord.x, neighbourCoord.y, neighbourCoord.z);
        }
        else { return GetChunk(neighbourCoord.y); }
    }

    public GameObject GetChunk(int y)
    {
        if (y >= 0 && y < ColumnSize)
        {
            if (ChunkList[y] != null)
            { return ChunkList[y]; }
            else {/* Debug.LogWarning("Attempted to access array element that does not exists, returning null");*/ return null; }
        }
        else { /*Debug.LogWarning("Attempted to access array element that does not exists, returning null");*/ return null; }
    }

    public void setColumnIndext(int x, int z) { chunkColumnIndex = new Vector3(x, 0, z); }
    public void setWorld(World world) { WorldRef = world; }

    public void updateChunks()
    {
        foreach(GameObject chunk in ChunkList)
        {
            chunk.GetComponent().updateChunk();
        }
    }

    public void inhabitChunks()
    {
        foreach(GameObject chunk in ChunkList)
        {
            chunk.GetComponent().InhabitChunk();
        }
    }

    #region Getters

    public int Width
    { get { return (int)WorldRef.Width; } }

    public int Height
    { get { return (int)ChunkSize.y; } }

    public int Depth
    { get { return (int)WorldRef.Depth; } }

    public int ChunkNumber
    { get { return ColumnSize; } }

    public Vector3 getColumnIndex
    { get { return chunkColumnIndex; } }

    #endregion
}

Stacks are then also given a heightmap based on a two dimensional (2D) Perlin noise algorithm. The heightmap offsets each cubical column within the stack to create a consistent height mapping of 1x Grass block at the top, 3x dirt blocks and then filling out the rest with stone all the way down to the final block which will always be a “bedrock” type block.

Stacks are loaded in a gridlike structure next to each other to produce a currently limited and static world size, but with the opportunity to later make the world procedurally generated.

Once the heightmap is produced a three dimensional (3D) Perlin noise is used in conjunction with a spherical hitbox search to produce caverns of alternating size and shape within the stack producing fairly realistic cavern structures.
Similar to the “Perlin worm” structure used by the original Minecraft game.