3
\$\begingroup\$

In Voxel Islands, each individual voxel is represented by their own unique class. Each of those unique classes is a subclass of a parent, Voxel class:

using UnityEngine;
using System.Collections;
using VoxelIslands.Engine.Utilities;

namespace VoxelIslands.Engine
{
    /// <summary>
    /// This class represents a voxel in the world, and stores the specific data related
    /// to said voxel, like type, UV coordinates, mesh behaviours, etc.
    /// </summary>
    public class Voxel
    {
        public enum VoxelFaceDirection
        {
            North = 0,
            South = 1,
            East = 2,
            West = 3,
            Up = 4,
            Down = 5
        }

        public enum VoxelType : byte
        {
            Invalid = 255,
            Empty = 254,

            Stone = 0,
            Dirt = 1,
            Grass = 2,
            Wood = 3,
            Leaves = 4,
            Sand = 5,
            Slab = 6,
            Plank = 7,
            Pole = 8,
            Glass = 9,
            Timber = 10,

            Explosive = 251,
            Water = 252,
            Cloud = 253
        }

        public bool OverrideFaceGenerationRules { get; set; }
        public bool IsCollideable { get; set; }
        public Vector3 Offset { get; set; }
        public Vector3 HalvedOffset { get; set; }
        public VoxelType Type { get; set; }

        /// <summary>
        /// Constructor for the Voxel class.
        /// </summary>
        public Voxel(bool overrideFaceGenerationRules, bool isCollideable, Vector3 offset, Vector3 halvedOffset, VoxelType type)
        {
            this.OverrideFaceGenerationRules = overrideFaceGenerationRules;
            this.IsCollideable = isCollideable;
            this.Offset = offset;
            this.HalvedOffset = halvedOffset;
            this.Type = type;
        }

        /// <summary>
        /// Check to see whether a specific voxel face is solid or not.
        /// </summary>
        /// <param name="faceDirection">The direction the face is facing.</param>
        /// <returns>Whether or not the face is solid.</returns>
        public virtual bool IsFaceSolid(VoxelFaceDirection faceDirection)
        {
            return true;
        }

        /// <summary>
        /// This function is responsible for generating the different face data associated with the
        /// voxel. This function can be overriden to allow for a variety of different face combonations.
        /// </summary>
        /// <param name="parentlevel">The voxel's parent level.</param>
        /// <param name="currentMeshData">The level's current mesh data.</param>
        /// <param name="x">The x coordinate of the voxel.</param>
        /// <param name="y">The y coordinate of the voxel.</param>
        /// <param name="z">The z coordinate of the voxel.</param>
        /// <returns>An updated level mesh.</returns>
        public virtual DynamicMesh CreateMeshFaces(Level parentlevel, DynamicMesh currentMeshData, int x, int y, int z)
        {
            // Generate the northern face of the voxel.
            if(!parentlevel.FetchVoxel(x, y, z + 1).IsFaceSolid(VoxelFaceDirection.South) || this.OverrideFaceGenerationRules)
            {
                currentMeshData = this.CreateNorthFace(currentMeshData, x, y, z);
            }

            // Generate the southern face of the voxel.
            if(!parentlevel.FetchVoxel(x, y, z - 1).IsFaceSolid(VoxelFaceDirection.North) || this.OverrideFaceGenerationRules)
            {
                currentMeshData = this.CreateSouthFace(currentMeshData, x, y, z);
            }

            // Generate the eastern face of the voxel.
            if(!parentlevel.FetchVoxel(x + 1, y, z).IsFaceSolid(VoxelFaceDirection.West) || this.OverrideFaceGenerationRules)
            {
                currentMeshData = this.CreateEastFace(currentMeshData, x, y, z);
            }

            // Generate the western face of the voxel.
            if(!parentlevel.FetchVoxel(x - 1, y, z).IsFaceSolid(VoxelFaceDirection.East) || this.OverrideFaceGenerationRules)
            {
                currentMeshData = this.CreateWestFace(currentMeshData, x, y, z);
            }

            // Generate the upwards face of the voxel.
            if(!parentlevel.FetchVoxel(x, y + 1, z).IsFaceSolid(VoxelFaceDirection.Down) || this.OverrideFaceGenerationRules)
            {
                currentMeshData = this.CreateUpwardsFace(currentMeshData, x, y, z);
            }

            // Generate the downwards face of the voxel.
            if(!parentlevel.FetchVoxel(x, y - 1, z).IsFaceSolid(VoxelFaceDirection.Up) || this.OverrideFaceGenerationRules)
            {
                currentMeshData = this.CreateDownwardsFace(currentMeshData, x, y, z);
            }

            return currentMeshData;
        }

        /// <summary>
        /// Generate UV coordinate data for a specific face on the voxel.
        /// </summary>
        /// <param name="faceDirection">The direction that the face is facing.</param>
        protected virtual Vector2[] GenerateUVs(VoxelFaceDirection faceDirection)
        {
            Vector2[] UVs = new Vector2[4];
            UVs[0] = new UVTile(3, 0).UVTileToUVCoordinate(UVTile.UVTileCorner.BottomLeft);
            UVs[1] = new UVTile(3, 1).UVTileToUVCoordinate(UVTile.UVTileCorner.TopLeft);
            UVs[2] = new UVTile(4, 1).UVTileToUVCoordinate(UVTile.UVTileCorner.TopRight);
            UVs[3] = new UVTile(4, 0).UVTileToUVCoordinate(UVTile.UVTileCorner.BottomRight);
            return UVs;
        }

        protected virtual DynamicMesh CreateNorthFace(DynamicMesh currentMeshData, int x, int y, int z)
        {
            currentMeshData.AddVertex(new Vector3(x + this.HalvedOffset.x, y - this.HalvedOffset.y, z + this.HalvedOffset.z), this.Offset, this.IsCollideable);
            currentMeshData.AddVertex(new Vector3(x + this.HalvedOffset.x, y + this.HalvedOffset.y, z + this.HalvedOffset.z), this.Offset, this.IsCollideable);
            currentMeshData.AddVertex(new Vector3(x - this.HalvedOffset.x, y + this.HalvedOffset.y, z + this.HalvedOffset.z), this.Offset, this.IsCollideable);
            currentMeshData.AddVertex(new Vector3(x - this.HalvedOffset.x, y - this.HalvedOffset.y, z + this.HalvedOffset.z), this.Offset, this.IsCollideable);
            currentMeshData.AddQuad(this.IsCollideable);
            currentMeshData.AddUVs(this.GenerateUVs(VoxelFaceDirection.North));
            return currentMeshData;
        }

        protected virtual DynamicMesh CreateSouthFace(DynamicMesh currentMeshData, int x, int y, int z)
        {
            currentMeshData.AddVertex(new Vector3(x - this.HalvedOffset.x, y - this.HalvedOffset.y, z - this.HalvedOffset.z), this.Offset, this.IsCollideable);
            currentMeshData.AddVertex(new Vector3(x - this.HalvedOffset.x, y + this.HalvedOffset.y, z - this.HalvedOffset.z), this.Offset, this.IsCollideable);
            currentMeshData.AddVertex(new Vector3(x + this.HalvedOffset.x, y + this.HalvedOffset.y, z - this.HalvedOffset.z), this.Offset, this.IsCollideable);
            currentMeshData.AddVertex(new Vector3(x + this.HalvedOffset.x, y - this.HalvedOffset.y, z - this.HalvedOffset.z), this.Offset, this.IsCollideable);
            currentMeshData.AddQuad(this.IsCollideable);
            currentMeshData.AddUVs(this.GenerateUVs(VoxelFaceDirection.South));
            return currentMeshData;
        }

        protected virtual DynamicMesh CreateEastFace(DynamicMesh currentMeshData, int x, int y, int z)
        {
            currentMeshData.AddVertex(new Vector3(x + this.HalvedOffset.x, y - this.HalvedOffset.y, z - this.HalvedOffset.z), this.Offset, this.IsCollideable);
            currentMeshData.AddVertex(new Vector3(x + this.HalvedOffset.x, y + this.HalvedOffset.y, z - this.HalvedOffset.z), this.Offset, this.IsCollideable);
            currentMeshData.AddVertex(new Vector3(x + this.HalvedOffset.x, y + this.HalvedOffset.y, z + this.HalvedOffset.z), this.Offset, this.IsCollideable);
            currentMeshData.AddVertex(new Vector3(x + this.HalvedOffset.x, y - this.HalvedOffset.y, z + this.HalvedOffset.z), this.Offset, this.IsCollideable);
            currentMeshData.AddQuad(this.IsCollideable);
            currentMeshData.AddUVs(this.GenerateUVs(VoxelFaceDirection.East));
            return currentMeshData;
        }

        protected virtual DynamicMesh CreateWestFace(DynamicMesh currentMeshData, int x, int y, int z)
        {
            currentMeshData.AddVertex(new Vector3(x - this.HalvedOffset.x, y - this.HalvedOffset.y, z + this.HalvedOffset.z), this.Offset, this.IsCollideable);
            currentMeshData.AddVertex(new Vector3(x - this.HalvedOffset.x, y + this.HalvedOffset.y, z + this.HalvedOffset.z), this.Offset, this.IsCollideable);
            currentMeshData.AddVertex(new Vector3(x - this.HalvedOffset.x, y + this.HalvedOffset.y, z - this.HalvedOffset.z), this.Offset, this.IsCollideable);
            currentMeshData.AddVertex(new Vector3(x - this.HalvedOffset.x, y - this.HalvedOffset.y, z - this.HalvedOffset.z), this.Offset, this.IsCollideable);
            currentMeshData.AddQuad(this.IsCollideable);
            currentMeshData.AddUVs(this.GenerateUVs(VoxelFaceDirection.West));
            return currentMeshData;
        }

        protected virtual DynamicMesh CreateUpwardsFace(DynamicMesh currentMeshData, int x, int y, int z)
        {
            currentMeshData.AddVertex(new Vector3(x - this.HalvedOffset.x, y + this.HalvedOffset.y, z + this.HalvedOffset.z), this.Offset, this.IsCollideable);
            currentMeshData.AddVertex(new Vector3(x + this.HalvedOffset.x, y + this.HalvedOffset.y, z + this.HalvedOffset.z), this.Offset, this.IsCollideable);
            currentMeshData.AddVertex(new Vector3(x + this.HalvedOffset.x, y + this.HalvedOffset.y, z - this.HalvedOffset.z), this.Offset, this.IsCollideable);
            currentMeshData.AddVertex(new Vector3(x - this.HalvedOffset.x, y + this.HalvedOffset.y, z - this.HalvedOffset.z), this.Offset, this.IsCollideable);
            currentMeshData.AddQuad(this.IsCollideable);
            currentMeshData.AddUVs(this.GenerateUVs(VoxelFaceDirection.Up));
            return currentMeshData;
        }

        protected virtual DynamicMesh CreateDownwardsFace(DynamicMesh currentMeshData, int x, int y, int z)
        {
            currentMeshData.AddVertex(new Vector3(x - this.HalvedOffset.x, y - this.HalvedOffset.y, z - this.HalvedOffset.z), this.Offset, this.IsCollideable);
            currentMeshData.AddVertex(new Vector3(x + this.HalvedOffset.x, y - this.HalvedOffset.y, z - this.HalvedOffset.z), this.Offset, this.IsCollideable);
            currentMeshData.AddVertex(new Vector3(x + this.HalvedOffset.x, y - this.HalvedOffset.y, z + this.HalvedOffset.z), this.Offset, this.IsCollideable);
            currentMeshData.AddVertex(new Vector3(x - this.HalvedOffset.x, y - this.HalvedOffset.y, z + this.HalvedOffset.z), this.Offset, this.IsCollideable);
            currentMeshData.AddQuad(this.IsCollideable);
            currentMeshData.AddUVs(this.GenerateUVs(VoxelFaceDirection.Down));
            return currentMeshData;
        }
    }
}

The next class, Level, is responsible for a variety of things. It will generate the initial level terrain (comprised of floating islands), which is done in the following steps:

  1. Generate three perlin noise values, and perform linear interpolation between the first two using the third as the "time" value to produce a height value.
  2. Iterate upwards for a certain distance based on the resulting height value and a height multiplier, and fill each coordinate with a solid voxel.
  3. Iterate downwards for a certain distance based on the resulting height value and a different height multiplier and fill each coordinate with a solid voxel.
  4. Determine whether certain features like rocks, trees and caves should be generated.

Here are some examples of what the terrain generator produces:

enter image description here

enter image description here

The level class is also responsible for updating the terrain based on player actions, like setting/destroying voxels, and a few other minor things.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using VoxelIslands.Engine.Utilities;
using VoxelIslands.Engine.Voxels;
using VoxelIslands.Utilities;

namespace VoxelIslands.Engine
{
    /// <summary>
    /// This class is intended to represent a "level" or island in the world. This class
    /// is responsible for generating, updating, and managing terrain in a "level".
    /// </summary>
    [RequireComponent(typeof(MeshFilter))]
    [RequireComponent(typeof(MeshRenderer))]
    [RequireComponent(typeof(MeshCollider))]
    public class Level : MonoBehaviour
    {
        public bool UseUniqueSeed = true;

        public int Width = 128;
        public int Height = 256;
        public int Depth = 128;

        public int ChunkWidth = 16;
        public int ChunkHeight = 16;
        public int ChunkDepth = 16;
        public bool UseLevelSizeStorage = true;

        public bool GenerateCaves = true;
        public float CaveGenerationChance = 20.0f;
        public int MinimumCaveRadius = 1;
        public int MaximumCaveRadius = 3;
        public int MinimumCaveLength = 10;
        public int MaximumCaveLength = 25;

        public bool GenerateClouds = false;
        public float CloudFrequency = 50.0f;
        public float CloudSolidityThreshold = 0.5f;
        public int BaseCloudHeight = 64;
        public int CloudThickness = 2;

        public float TerrainFrequencyOne = 20.0f;
        public float TerrainFrequencyTwo = 40.0f;
        public float TerrainFrequencyThree = 60.0f;
        public float FalloffCurveAMultiplier = 3;
        public float FalloffCurveBMultiplier = 2.25f;
        public float TopHeightMultiplier = 3.0f;
        public float BottomHeightMultiplier = 6.0f;
        public float DirtGenerationChance = 35.0f;
        public float DirtThreshold = 1.2f;
        public float RandomDirtThreshold = 0.15f;
        public float SolidityThreshold = 0.45f;

        public float TreeGenerationChance = 5.0f;
        public int MinimumTrunkHeight = 7;
        public int MaximumTrunkHeight = 10;
        public int MinimumLeavesRadius = 1;
        public int MaximumLeavesRadius = 3;
        public float BoulderGenerationChance = 1.0f;

        public int BaseIslandHeight = 64;
        public int BaseColumnHeightMultiplier = 10;

        public bool UsesPlayer = true;
        public GameObject EmptyChunkPrefab;

        private int WidthInChunks;
        private int HeightInChunks;
        private int DepthInChunks;

        public GameObject VoxelDestructionParticleSystem;

        public Voxel[,,] Voxels;
        public GameObject[,,] Chunks;
        public Vector3 PlayerSpawnPosition;

        private int _Seed;

        /// <summary>
        /// This function will update the chunk that resides at the chunk indexes of
        /// x, y and z. This function is intended for use in things like voxel placement,
        /// where multiple chunk updates may need to occur. If the supplied coordinates
        /// are out of bounds, this function will simply do nothing.
        /// </summary>
        /// <param name="x">The x coordinate of the chunk.</param>
        /// <param name="y">The y coordinate of the chunk.</param>
        /// <param name="z">The z coordinate of the chunk.</param>
        public void UpdateChunk(int x, int y, int z)
        {
            if(
                x >= 0 && x < this.WidthInChunks &&
                y >= 0 && y < this.HeightInChunks &&
                z >= 0 && z < this.DepthInChunks
            )
            {
                // Grab the chunk object, it's mesh filter and collider, and create an empty
                // DynamicMesh which represents the chunk's updated mesh.
                GameObject chunkObject = this.Chunks[x, y, z];
                MeshFilter chunkMeshFilter = chunkObject.GetComponent<MeshFilter>();
                MeshCollider chunkMeshCollider = chunkObject.GetComponent<MeshCollider>();
                DynamicMesh newChunkMesh = new DynamicMesh(
                    new Vector3(x * this.ChunkWidth, y * this.ChunkHeight, z * this.ChunkDepth)
                );

                // Re-generate the chunk based on the new voxel data.
                for(int currentX = x * this.ChunkWidth; currentX < (x * this.ChunkWidth) + this.ChunkWidth; currentX++)
                {
                    for(int currentY = y * this.ChunkHeight; currentY < (y * this.ChunkHeight) + this.ChunkHeight; currentY++)
                    {
                        for(int currentZ = z * this.ChunkDepth; currentZ < (z * this.ChunkDepth) + this.ChunkDepth; currentZ++)
                        {
                            newChunkMesh = this.Voxels[currentX, currentY, currentZ].CreateMeshFaces(
                                this,
                                newChunkMesh,
                                currentX,
                                currentY,
                                currentZ
                            );
                        }
                    }
                }

                // Update the mesh and mesh collider of the chunk.
                chunkMeshFilter.mesh = newChunkMesh.CreateRenderingMesh();
                chunkMeshCollider.sharedMesh = null;
                chunkMeshCollider.sharedMesh = newChunkMesh.CreateColliderMesh();
            }
        }

        /// <summary>
        /// This function will perform a simple check to determine whether or not a
        /// set of provided coordinates are valid or not.
        /// </summary>
        /// <param name="x">The x coordinate.</param>
        /// <param name="y">The y coordinate.</param>
        /// <param name="z">The z coordinate.</param>
        /// <returns>Whether or not the coordinate is valid.</returns>
        public bool IsValidCoordinate(int x, int y, int z)
        {
            return (
                x >= 0 && x < this.Width &&
                y >= 0 && y < this.Height &&
                z >= 0 && z < this.Depth
            );
        }

        /// <summary>
        /// This function will "generate" an explosion with a certain radius and
        /// will visual display terrain destruction using particles.
        /// </summary>
        /// <param name="x">The x origin of the explosion.</param>
        /// <param name="y">The y origin of the explosion.</param>
        /// <param name="z">The z origin of the explosion.</param>
        /// <param name="explosionRadius">The radius of the explosion.</param>
        /// <param name="updateRadius">The radius of which chunks to update.</param>
        public void GenerateExplosion(int x, int y, int z, int explosionRadius, int updateRadius)
        {
            List<Vector3> chunksToUpdate = new List<Vector3>() { };

            for(int currentX = x - explosionRadius; currentX <= x + explosionRadius; currentX++)
            {
                for(int currentY = y - explosionRadius; currentY <= y + explosionRadius; currentY++)
                {
                    for(int currentZ = z - explosionRadius; currentZ <= z + explosionRadius; currentZ++)
                    {
                        Vector3 edgeDistance = new Vector3(x + explosionRadius, y + explosionRadius, z + explosionRadius);
                        Vector3 origin = new Vector3(x, y, z);
                        Vector3 currentPosition = new Vector3(currentX, currentY, currentZ);

                        if(Random.Range(0, Vector3.Distance(origin, edgeDistance) / 2.0f) >= Vector3.Distance(origin, currentPosition))
                        {
                            if(this.IsValidCoordinate(currentX, currentY, currentZ))
                            {
                                if(
                                    Random.Range(0.0f, 100.0f) <= 20.0f && 
                                    this.Voxels[currentX, currentY, currentZ].Type != Voxel.VoxelType.Empty
                                )
                                {
                                    GameObject destructionParticles = (GameObject)Instantiate(
                                        this.VoxelDestructionParticleSystem,
                                        new Vector3(currentX, currentY, currentZ),
                                        Quaternion.identity
                                    );

                                    if((int)this.Voxels[currentX, currentY, currentZ].Type < ResourceManager.StaticVoxelSprites.Length)
                                    {
                                        destructionParticles.GetComponent<Renderer>().material.mainTexture = ResourceManager.StaticVoxelSprites[
                                            (int)this.Voxels[currentX, currentY, currentZ].Type
                                        ].texture;
                                    }
                                }

                                this.Voxels[currentX, currentY, currentZ] = new Empty();

                                int chunkX = (int)(System.Math.Floor((float)x / (float)this.ChunkWidth) * (float)this.ChunkWidth);
                                int chunkY = (int)(System.Math.Floor((float)y / (float)this.ChunkHeight) * (float)this.ChunkHeight);
                                int chunkZ = (int)(System.Math.Floor((float)z / (float)this.ChunkDepth) * this.ChunkDepth);

                                if(
                                    !chunksToUpdate.Contains(
                                        new Vector3(
                                            chunkX / this.ChunkWidth, 
                                            chunkY / this.ChunkHeight,
                                            chunkZ / this.ChunkDepth
                                        )
                                    )
                                )
                                {
                                    chunksToUpdate.Add(
                                        new Vector3(
                                            chunkX / this.ChunkWidth, 
                                            chunkY / this.ChunkHeight,
                                            chunkZ / this.ChunkDepth
                                        )
                                    );
                                }
                            }
                        }
                    }
                }
            }

            for(int i = 0; i < chunksToUpdate.Count; i++)
            {
                int chunkX = (int)chunksToUpdate[i].x;
                int chunkY = (int)chunksToUpdate[i].y;
                int chunkZ = (int)chunksToUpdate[i].z;

                this.UpdateChunk(chunkX, chunkY, chunkZ);

                for(int newChunkX = -updateRadius; newChunkX < updateRadius; newChunkX++)
                {
                    for(int newChunkY = -updateRadius; newChunkY < updateRadius; newChunkY++)
                    {
                        for(int newChunkZ = -updateRadius; newChunkZ < updateRadius; newChunkZ++)
                        {
                            if(
                                (newChunkX != -updateRadius || newChunkX != updateRadius) &&
                                (newChunkY != -updateRadius || newChunkY != updateRadius) &&
                                (newChunkZ != -updateRadius || newChunkZ != updateRadius)
                            )
                            {
                                this.UpdateChunk(chunkX + newChunkX, chunkY + newChunkY, chunkZ + newChunkZ);
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// This function will recursively generate a cave until it has finished,
        /// given the that the required conditions have been fufilled.
        /// </summary>
        /// <param name="x">The current x coordinate of the cave.</param>
        /// <param name="y">The current y coordinate of the cave.</param>
        /// <param name="z">The current z coordinate of the cave.</param>
        /// <param name="caveLength">The current length of the cave.</param>
        public void GenerateCave(int x, int y, int z, float caveLength)
        {
            if(caveLength > 0)
            {
                caveLength -= 1;

                int caveAreaRadius = Random.Range(this.MinimumCaveRadius, this.MaximumCaveRadius);

                // Fill the specified cave area with empty voxels, or the provided fill voxel.
                // which will create the cave section.
                for(int emptyX = x - caveAreaRadius; emptyX < x + caveAreaRadius + 1; emptyX++)
                {
                    for(int emptyY = y - caveAreaRadius; emptyY < y + caveAreaRadius + 1; emptyY++)
                    {
                        for(int emptyZ = z - caveAreaRadius; emptyZ < z + caveAreaRadius + 1; emptyZ++)
                        {
                            if(this.IsValidCoordinate(emptyX, emptyY, emptyZ))
                            {
                                this.Voxels[emptyX, emptyY, emptyZ] = new Empty();
                            }
                        }
                    }
                }

                // Generate a new random direction which determines the direction that the cave will
                // "move" in to create new areas.
                Vector3 newRandomDirection = new Vector3(
                    Random.Range(-2, 2),
                    Random.Range(-2, 2),
                    Random.Range(-2, 2)
                );

                this.GenerateCave(
                    x + (int)newRandomDirection.x, 
                    y + (int)newRandomDirection.y, 
                    z + (int)newRandomDirection.z, 
                    caveLength
                );
            }
            else
            {
                return;
            }
        }

        /// <summary>
        /// This function will set a voxel based on the provided positions, and then
        /// recalculate the mesh of the chunk that the voxel resides in once the voxel
        /// data has been updated. This function will simply do nothing if the provided
        /// coordinates reside outside of the voxel array.
        /// </summary>
        /// <param name="voxelType">The type of voxel to set.</param>
        /// <param name="x">The x coordinate of the voxel.</param>
        /// <param name="y">The y coordinate of the voxel.</param>
        /// <param name="z">The z coordinate of the voxel.</param>
        /// <param name="setMode">The voxel setting mode. True is normal, and false is destruction.</param>
        public void SetVoxel(Voxel voxelType, int x, int y, int z, bool setMode = true)
        {
            if(this.IsValidCoordinate(x, y, z))
            {
                // Round the inputted coordinates to match the position of the chunk we
                // want to update.
                int chunkX = (int)(System.Math.Floor((float)x / (float)this.ChunkWidth) * (float)this.ChunkWidth);
                int chunkY = (int)(System.Math.Floor((float)y / (float)this.ChunkHeight) * (float)this.ChunkHeight);
                int chunkZ = (int)(System.Math.Floor((float)z / (float)this.ChunkDepth) * (float)this.ChunkDepth);

                // Set the provided coordinates to the provided voxel, if the setMode is
                // true, otherwise, set the provided voxel, and spawn a particle system
                // similar to that of the previous voxel.
                if(setMode)
                {
                    this.Voxels[x, y, z] = voxelType;
                }
                else
                {
                    // Create a new destruction-based particle system that will visual
                    // indicate the destruction of the voxel.
                    GameObject newDestructionParticleSystem = (GameObject)Instantiate(
                        this.VoxelDestructionParticleSystem,
                        new Vector3(x, y, z),
                        Quaternion.identity
                    );

                    // Set the texture of the newly created particle system to the
                    // corresponding texture. If a texture cannot be found, then
                    // the default particles are used.
                    if((int)this.Voxels[x, y, z].Type < ResourceManager.StaticVoxelSprites.Length)
                    {
                        newDestructionParticleSystem.GetComponent<Renderer>().material.mainTexture = ResourceManager.StaticVoxelSprites[
                            (int)this.Voxels[x, y, z].Type
                        ].texture;
                    }

                    this.Voxels[x, y, z] = new Empty();
                }

                this.UpdateChunk(chunkX / this.ChunkWidth, chunkY / this.ChunkHeight, chunkZ / this.ChunkDepth);

                // If the voxel's position is along a chunk x border, then update the
                // mesh of the neighboring chunk to prevent strange mesh issues.
                if(x == chunkX)
                {
                    this.UpdateChunk((chunkX / this.ChunkWidth) - 1, chunkY / this.ChunkHeight, chunkZ / this.ChunkDepth);
                }
                else if(x == chunkX + this.ChunkWidth - 1)
                {
                    this.UpdateChunk((chunkX / this.ChunkWidth) + 1, chunkY / this.ChunkHeight, chunkZ / this.ChunkDepth);
                }

                // If the voxel's position is along a chunk y border, then update the
                // mesh of the below chunk to prevent strange mesh issues.
                if(y == chunkY)
                {
                    this.UpdateChunk(chunkX / this.ChunkWidth, (chunkY / this.ChunkHeight) - 1, chunkZ / this.ChunkDepth);
                }
                else if(y == chunkY + this.ChunkHeight - 1)
                {
                    this.UpdateChunk(chunkX / this.ChunkWidth, (chunkY / this.ChunkHeight) + 1, chunkZ / this.ChunkDepth);
                }

                // If the voxel's position is along a chunk z border, then update the
                // mesh of the neighboring chunk to prevent strange mesh issues.
                if(z == chunkZ)
                {
                    this.UpdateChunk(chunkX / this.ChunkWidth, chunkY / this.ChunkHeight, (chunkZ / this.ChunkDepth) - 1);
                }
                else if(z == chunkZ + this.ChunkDepth - 1)
                {
                    this.UpdateChunk(chunkX / this.ChunkWidth, chunkY / this.ChunkHeight, (chunkZ / this.ChunkDepth) + 1);
                }
            }
        }

        /// <summary>
        /// Fetch a voxel based on the provided coordinates. If the provided coordinates are out of range,
        /// then an empty voxel is returned. This will force edge voxels to render their faces.
        /// </summary>
        /// <param name="x">The x coordinate of the voxel.</param>
        /// <param name="y">The y coordinate of the voxel.</param>
        /// <param name="z">The z coordinate of the voxel.</param>
        /// <returns></returns>
        public Voxel FetchVoxel(int x, int y, int z)
        {
            if(this.IsValidCoordinate(x, y, z))
            {
                return this.Voxels[x, y, z];
            }
            else
            {
                return new Empty();
            }
        }

        /// <summary>
        /// Fill the chunk data with newly generated GameObject representations of chunks, each
        /// with their own custom mesh based on where they're positioned.
        /// </summary>
        public void GenerateChunkData()
        {
            for(int x = 0; x < this.WidthInChunks; x++)
            {
                for(int y = 0; y < this.HeightInChunks; y++)
                {
                    for(int z = 0; z < this.DepthInChunks; z++)
                    {
                        // Create a new, empty chunk object prefab which will represent the chunk itself
                        // in the world.
                        GameObject chunkObject = (GameObject)Instantiate(
                            this.EmptyChunkPrefab,
                            new Vector3(x * this.ChunkWidth, y * this.ChunkHeight, z * this.ChunkDepth),
                            Quaternion.identity
                        );

                        // Set the chunk's parent and name. This will help keep the heirarchy from getting
                        // cluttered with "Chunk Prefab (Clone)"s and will allow us to minimize the chunks.
                        chunkObject.transform.parent = this.transform;
                        chunkObject.name = string.Format("chunk_{0}_{1}_{2}", x, y, z);

                        // Grab the chunk object's mesh filter and collider, and create a new DynamicMesh
                        // which will represent the mesh of the chunk.
                        MeshFilter chunkObjectMeshFilter = chunkObject.GetComponent<MeshFilter>();
                        MeshCollider chunkObjectMeshCollider = chunkObject.GetComponent<MeshCollider>();
                        DynamicMesh chunkObjectMesh = new DynamicMesh(
                            new Vector3(x * this.ChunkWidth, y * this.ChunkHeight, z * this.ChunkDepth)
                        );

                        // Set the chunk's starting position.
                        int voxelXStart = x * this.ChunkWidth;
                        int voxelYStart = y * this.ChunkHeight;
                        int voxelZStart = z * this.ChunkDepth;

                        // Iterate over a range of values which lie inside the chunk's bound,
                        // and generate the chunk's mesh accordingly.
                        for(int voxelX = voxelXStart; voxelX < voxelXStart + this.ChunkWidth; voxelX++)
                        {
                            for(int voxelY = voxelYStart; voxelY < voxelYStart + this.ChunkHeight; voxelY++)
                            {
                                for(int voxelZ = voxelZStart; voxelZ < voxelZStart + this.ChunkDepth; voxelZ++)
                                {
                                    chunkObjectMesh = this.Voxels[voxelX, voxelY, voxelZ].CreateMeshFaces(
                                        this,
                                        chunkObjectMesh,
                                        voxelX,
                                        voxelY,
                                        voxelZ
                                    );
                                }
                            }
                        }

                        // Set the mesh and mesh collider of the chunk object to the newly
                        // generated mesh and mesh colliders.
                        chunkObjectMeshFilter.mesh = chunkObjectMesh.CreateRenderingMesh();
                        chunkObjectMeshCollider.sharedMesh = null;
                        chunkObjectMeshCollider.sharedMesh = chunkObjectMesh.CreateColliderMesh();

                        // Add the chunk to the chunk array.
                        this.Chunks[x, y, z] = chunkObject;
                    }
                }
            }
        }

        /// <summary>
        /// Fill the voxel array with procedurally generated voxel data based on what method of generation
        /// is used and the generation parameters provided. This method will do an "initialization fill"
        /// which fills the voxel array with empty voxels.
        /// </summary>
        public void GenerateVoxelData()
        {
            bool playerSpawnCreated = false;

            // Perform an initial "sweep" of the data and fill it with empty voxels.
            // This prevents NREs if we were to try and access an index with nothing in it.
            for(int x = 0; x < this.Width; x++)
            {
                for(int y = 0; y < this.Height; y++)
                {
                    for(int z = 0; z < this.Depth; z++)
                    {
                        this.Voxels[x, y, z] = new Empty();
                    }
                }
            }

            // Iterate across the level's x and z width ranges and begin to generate the
            // actual terrain.
            for(int x = 0; x < this.Width; x++)
            {
                for(int z = 0; z < this.Depth; z++)
                {
                    // Generate the first perlin noise result. This value is used as the first
                    // value in the noise lerp.
                    float perlinNoiseResultOne = Mathf.PerlinNoise(
                        (x + this._Seed) / this.TerrainFrequencyOne,
                        (z + this._Seed) / this.TerrainFrequencyOne
                    );

                    // Generate the second perlin noise result. This value is used as the second
                    // value in the noise lerp.
                    float perlinNoiseResultTwo = Mathf.PerlinNoise(
                        (x + this._Seed) / this.TerrainFrequencyTwo,
                        (z + this._Seed) / this.TerrainFrequencyTwo
                    );

                    // Generate the third perlin noise result. This value is used as the "time" 
                    // value in the noise lerp.
                    float perlinNoiseResultThree = Mathf.PerlinNoise(
                        (x + this._Seed) / this.TerrainFrequencyThree,
                        (z + this._Seed) / this.TerrainFrequencyThree
                    );

                    // Generate the final noise result, which is a lerp between the first and second
                    // noise results based on the third noise result.
                    float finalNoiseResult = Mathf.Lerp(
                        perlinNoiseResultOne,
                        perlinNoiseResultTwo,
                        perlinNoiseResultThree
                    ) - UtilityFunctions.GenerateFalloffValue(this.Width, x, z, this.FalloffCurveAMultiplier, this.FalloffCurveBMultiplier);

                    // Generate a terrain column if the perlin noise result meets the requirements
                    // for the solidity threshold.
                    if(finalNoiseResult >= this.SolidityThreshold)
                    {
                        bool treeGenerated = false;

                        int baseColumnHeight = (int)(
                            ((finalNoiseResult - this.SolidityThreshold) * this.BaseColumnHeightMultiplier) +
                            this.BaseIslandHeight
                        );
                        int topColumnHeight = (int)((finalNoiseResult - this.SolidityThreshold) * this.TopHeightMultiplier);
                        int bottomColumnHeight = (int)((finalNoiseResult - this.SolidityThreshold) * this.BottomHeightMultiplier);

                        // Generate a grass voxel at the base island height if the top column height
                        // is equal to zero. If the top column height is greater than zero, then a
                        // dirt voxel is generated.
                        if(topColumnHeight <= 0)
                        {
                            this.Voxels[x, baseColumnHeight, z] = new Sand();
                        }
                        else
                        {
                            this.Voxels[x, baseColumnHeight, z] = new Dirt();
                        }

                        // Generate the top column based on the generated height. This will generate
                        // dirt until it hits the very last value in the top column.
                        for(int y = baseColumnHeight + 1; y < (baseColumnHeight + 1) + topColumnHeight; y++)
                        {
                            if(y >= 0 && y < this.Height)
                            {
                                // Generate a grass voxel, if the current y value is at the last value
                                // in the top column. If not, then dirt is generated.
                                if(((baseColumnHeight + 1) + topColumnHeight) - y == 1)
                                {
                                    this.Voxels[x, y, z] = new Grass();
                                }
                                else
                                {
                                    this.Voxels[x, y, z] = new Dirt();
                                }
                            }
                        }

                        // Generate the bottom column based on the generated height. This will generate
                        // a mix of dirt and stone, with the occurence of stone becoming more likely the
                        // farther down the column is.
                        for(int y = baseColumnHeight - 1; y > (baseColumnHeight - 1) - bottomColumnHeight; y--)
                        {
                            if(y >= 0 && y < this.Height)
                            {
                                // If the current y value is greater than the dirt threshold then a dirt
                                // voxel is generated. If not, then a stone voxel is generated.
                                if(y >= (baseColumnHeight - 1) - (bottomColumnHeight / (topColumnHeight + 1)))
                                {
                                    this.Voxels[x, y, z] = new Dirt();
                                }
                                else
                                {
                                    if(Random.Range(0.0f, 100.0f) >= baseColumnHeight - (bottomColumnHeight * 5.95f))
                                    {
                                        this.Voxels[x, y, z] = new Stone();
                                    }
                                    else
                                    {
                                        this.Voxels[x, y, z] = new Dirt();
                                    }
                                }
                            }
                        }

                        // Generate a tree, if a randomly generated number is less than or equal
                        // to the tree generation chance.
                        if(
                            Random.Range(0.0f, 100.0f) <= this.TreeGenerationChance && 
                            finalNoiseResult <= this.SolidityThreshold + 0.25f
                        )
                        {
                            treeGenerated = true;
                            int trunkHeight = Random.Range(this.MinimumTrunkHeight, this.MaximumTrunkHeight);
                            int trunkTop = baseColumnHeight + topColumnHeight + trunkHeight;
                            int leavesRadius = Random.Range(this.MinimumLeavesRadius, this.MaximumLeavesRadius);

                            // Generate the trunk of the tree, with a height that is similar to that of
                            // the generated tree height.
                            for(
                                int trunkY = baseColumnHeight + topColumnHeight + 1;
                                trunkY < trunkTop;
                                trunkY++
                            )
                            {
                                if (this.IsValidCoordinate(x, trunkY, z))
                                {
                                    this.Voxels[x, trunkY, z] = new Wood();
                                }
                            }

                            // Generate the lower portion of the tree, with a larger width
                            // and depth than the top portions.
                            for(int leavesX = x - leavesRadius; leavesX <= x + leavesRadius; leavesX++)
                            {
                                for(int leavesY = trunkTop; leavesY <= trunkTop + leavesRadius; leavesY++)
                                {
                                    for(int leavesZ = z - leavesRadius; leavesZ <= z + leavesRadius; leavesZ++)
                                    {
                                        if(this.IsValidCoordinate(leavesX, leavesY, leavesZ))
                                        {
                                            if(
                                                this.Voxels[leavesX, leavesY, leavesZ] is Empty &&
                                                (leavesX != x - leavesRadius || leavesZ != z - leavesRadius) &&
                                                (leavesX != x + leavesRadius || leavesZ != z + leavesRadius) &&
                                                (leavesX != x + leavesRadius || leavesZ != z - leavesRadius) &&
                                                (leavesX != x - leavesRadius || leavesZ != z + leavesRadius)
                                            )
                                            {
                                                this.Voxels[leavesX, leavesY, leavesZ] = new Leaves();
                                            }
                                        }
                                    }
                                }
                            }

                            // Generate the upper portion of the tree, with a slightly smaller
                            // width and depth than the bottom portions.
                            for(int leavesX = x - (leavesRadius - 1); leavesX <= x + (leavesRadius - 1); leavesX++)
                            {
                                for(int leavesZ = z - (leavesRadius - 1); leavesZ <= z + (leavesRadius - 1); leavesZ++)
                                {
                                    if(this.IsValidCoordinate(leavesX, trunkTop + leavesRadius + 1, leavesZ))
                                    {
                                        if(this.Voxels[leavesX, trunkTop + leavesRadius + 1, leavesZ] is Empty)
                                        {
                                            this.Voxels[leavesX, trunkTop + leavesRadius + 1, leavesZ] = new Leaves();
                                        }
                                    }
                                }
                            }
                        }

                        // If a tree hasn't been generated, and the random condition succeeds, then generate
                        // a boulder.
                        if(Random.Range(0.0f, 100.0f) <= this.BoulderGenerationChance && !treeGenerated)
                        {
                            if(this.IsValidCoordinate(x, baseColumnHeight + topColumnHeight, z))
                            {
                                this.Voxels[x, baseColumnHeight + topColumnHeight, z] = new Stone();
                            }

                            if(Random.Range(0.0f, 1.0f) >= 0.45f)
                            {
                                if(this.IsValidCoordinate(x + 1, baseColumnHeight + topColumnHeight, z))
                                {
                                    this.Voxels[x + 1, baseColumnHeight + topColumnHeight, z] = new Stone();
                                }
                            }
                            if(Random.Range(0.0f, 1.0f) >= 0.45f)
                            {
                                if(this.IsValidCoordinate(x - 1, baseColumnHeight + topColumnHeight, z))
                                {
                                    this.Voxels[x - 1, baseColumnHeight + topColumnHeight, z] = new Stone();
                                }
                            }
                            if(Random.Range(0.0f, 1.0f) >= 0.45f)
                            {
                                if(this.IsValidCoordinate(x, baseColumnHeight + topColumnHeight, z + 1))
                                {
                                    this.Voxels[x, baseColumnHeight + topColumnHeight, z + 1] = new Stone();
                                }
                            }
                            if(Random.Range(0.0f, 1.0f) >= 0.45f)
                            {
                                if(this.IsValidCoordinate(x, baseColumnHeight + topColumnHeight, z - 1))
                                {
                                    this.Voxels[x, baseColumnHeight + topColumnHeight, z - 1] = new Stone();
                                }
                            }
                            if(Random.Range(0.0f, 1.0f) >= 0.65f)
                            {
                                if(this.IsValidCoordinate(x, baseColumnHeight + topColumnHeight + 1, z))
                                {
                                    this.Voxels[x, baseColumnHeight + topColumnHeight + 1, z] = new Stone();
                                }
                            }
                        }

                        // Generate a cave, if cave generation has been specified. Otherwise, do nothing.
                        if(this.GenerateCaves)
                        {
                            bool generateCave = Random.Range(0.0f, 100.0f) <= this.CaveGenerationChance;

                            // Start a cave flood fill if a random number is less than or equal to
                            // the cave generation chance.
                            if(generateCave)
                            {
                                int caveY = (baseColumnHeight + topColumnHeight) - Random.Range(0, 35);
                                int caveLength = Random.Range(this.MinimumCaveLength, this.MaximumCaveLength);

                                this.GenerateCave(x, caveY, z, caveLength);
                            }
                        }

                        // Set the player spawn position if the terrain generator's current position
                        // is greater than half the width & depth, if a spawn hasn't already been created,
                        // and if a tree hasn't been generated.
                        if(
                            x > (float)this.Width / 1.5f && 
                            z > (float)this.Depth / 1.5f && 
                            !playerSpawnCreated && 
                            !treeGenerated &&
                            this.UsesPlayer
                        )
                        {
                            this.PlayerSpawnPosition = new Vector3(x, baseColumnHeight + topColumnHeight + 2, z);
                            playerSpawnCreated = true;
                        }
                    }
                }
            }

            if(this.GenerateClouds)
            {
                // Perform one final iteration across the level's x and z axes, and add clouds. The
                // _Seed for the cloud noise operations is determined using the same world _Seed.
                for(int cloudX = 0; cloudX < this.Width; cloudX++)
                {
                    for(int cloudZ = 0; cloudZ < this.Depth; cloudZ++)
                    {
                        // Generate the first perlin noise result. This value is used as the first
                        // value in the noise lerp.
                        float perlinNoiseResultOne = Mathf.PerlinNoise(
                            (cloudX + this._Seed) / this.CloudFrequency,
                            (cloudZ + this._Seed) / this.CloudFrequency
                        );

                        // Generate the second perlin noise result. This value is used as the second
                        // value in the noise lerp.
                        float perlinNoiseResultTwo = Mathf.PerlinNoise(
                            (cloudX + this._Seed) / (this.CloudFrequency * 2),
                            (cloudZ + this._Seed) / (this.CloudFrequency * 2)
                        );

                        // Generate the third perlin noise result. This value is used as the "time" 
                        // value in the noise lerp.
                        float perlinNoiseResultThree = Mathf.PerlinNoise(
                            (cloudX + this._Seed) / (this.CloudFrequency * 3),
                            (cloudZ + this._Seed) / (this.CloudFrequency * 3)
                        );

                        // Generate the final noise result, which is a lerp between the first and second
                        // noise results based on the third noise result.
                        float finalPerlinNoiseResult = Mathf.Lerp(
                            perlinNoiseResultOne,
                            perlinNoiseResultTwo,
                            perlinNoiseResultThree
                        ) - UtilityFunctions.GenerateFalloffValue(
                            this.Width, 
                            cloudX, 
                            cloudZ, 
                            this.FalloffCurveAMultiplier, 
                            this.FalloffCurveBMultiplier
                        );

                        // Generate a cloud column if the perlin noise result meets the requirements
                        // for the solidity threshold.
                        if(finalPerlinNoiseResult >= this.CloudSolidityThreshold)
                        {
                            for(int y = this.BaseCloudHeight - this.CloudThickness; y < this.BaseCloudHeight + this.CloudThickness; y++)
                            {
                                if(this.Voxels[cloudX, y, cloudZ] is Empty)
                                {
                                    this.Voxels[cloudX, y, cloudZ] = new Cloud();
                                }
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Update is called once per frame.
        /// </summary>
        public void Update()
        {
            if(Input.GetKey(KeyCode.Alpha1))
            {
                this.SetVoxel(new Stone(), 16, 16, 16);
            }
        }

        /// <summary>
        /// This function is responsible for creating a set of walls which surround the level
        /// and prevent the player from leaving the bounds of the level.
        /// </summary>
        public void GenerateLevelWalls()
        {
            GameObject wallsParent = new GameObject();
            wallsParent.name = "Walls";
            wallsParent.transform.position = Vector3.zero;

            GameObject northWall = GameObject.CreatePrimitive(PrimitiveType.Cube);
            northWall.name = "North Wall";
            northWall.transform.parent = wallsParent.transform;
            northWall.transform.position = new Vector3(this.Width / 2.0f, this.Height / 2.0f, this.Depth);
            northWall.transform.localScale = new Vector3(this.Width, this.Height + 3, 1.0f);
            northWall.GetComponent<MeshRenderer>().enabled = false;
            northWall.layer = LayerMask.NameToLayer("Ignore Raycast");

            GameObject southWall = GameObject.CreatePrimitive(PrimitiveType.Cube);
            southWall.name = "South Wall";
            southWall.transform.parent = wallsParent.transform;
            southWall.transform.position = new Vector3(this.Width / 2.0f, this.Height / 2.0f, 0);
            southWall.transform.localScale = new Vector3(this.Width, this.Height + 3, 1.0f);
            southWall.GetComponent<MeshRenderer>().enabled = false;
            southWall.layer = LayerMask.NameToLayer("Ignore Raycast");

            GameObject eastWall = GameObject.CreatePrimitive(PrimitiveType.Cube);
            eastWall.name = "East Wall";
            eastWall.transform.parent = wallsParent.transform;
            eastWall.transform.position = new Vector3(this.Width, this.Height / 2.0f, this.Depth / 2.0f);
            eastWall.transform.localScale = new Vector3(1.0f, this.Height + 3, this.Depth);
            eastWall.GetComponent<MeshRenderer>().enabled = false;
            eastWall.layer = LayerMask.NameToLayer("Ignore Raycast");

            GameObject westWall = GameObject.CreatePrimitive(PrimitiveType.Cube);
            westWall.name = "West Wall";
            westWall.transform.parent = wallsParent.transform;
            westWall.transform.position = new Vector3(0, this.Height / 2.0f, this.Depth / 2.0f);
            westWall.transform.localScale = new Vector3(1.0f, this.Height + 3, this.Depth);
            westWall.GetComponent<MeshRenderer>().enabled = false;
            westWall.layer = LayerMask.NameToLayer("Ignore Raycast");

            GameObject topWall = GameObject.CreatePrimitive(PrimitiveType.Cube);
            topWall.name = "Top Wall";
            topWall.transform.parent = wallsParent.transform;
            topWall.transform.position = new Vector3(this.Width / 2.0f, this.Height + 3, this.Depth / 2.0f);
            topWall.transform.localScale = new Vector3(this.Width, 1.0f, this.Depth);
            topWall.GetComponent<MeshRenderer>().enabled = false;
            topWall.layer = LayerMask.NameToLayer("Ignore Raycast");
        }

        /// <summary>
        /// Start is called once.
        /// </summary>
        public void Start()
        {
            // Set the width, height, and depth of the level to the settings
            // provided in the cross-scene object containing the level size
            // settings.
            if(this.UseLevelSizeStorage)
            {
                LevelSizeStorage levelSizeStorage = GameObject.Find("Level Size Storage").GetComponent<LevelSizeStorage>();
                this.Width = levelSizeStorage.Width;
                this.Height = levelSizeStorage.Height;
                this.Depth = levelSizeStorage.Depth;
            }

            // Initialize the voxel and chunk arrays to their specified sizes.
            this.Voxels = new Voxel[this.Width, this.Height, this.Depth];
            this.Chunks = new GameObject[this.Width / this.ChunkWidth, this.Height / this.ChunkHeight, this.Depth / this.ChunkDepth];

            // Set the values for the level's width, height, and depth in chunks.
            this.WidthInChunks = this.Width / this.ChunkWidth;
            this.HeightInChunks = this.Height / this.ChunkHeight;
            this.DepthInChunks = this.Depth / this.ChunkDepth;

            // Create a random _Seed for the RNG and perlin noise generators.
            if(this.UseUniqueSeed)
            {
                this._Seed = Random.Range(-1000000, 1000000);
                Random.seed = this._Seed;
            }
            else
            {
                unchecked
                {
                    this._Seed = (int)(((this.Width / 2.0f) + (this.Depth / 2.0f)) / 2.0f);
                }
            }

            // Generate the voxel, chunk and wall data.
            this.GenerateVoxelData();
            this.GenerateChunkData();
            this.GenerateLevelWalls();

            // Set the position of the player equal to the generated spawn position.
            if(this.UsesPlayer)
            {
                GameObject.Find("Player Object").transform.position = this.PlayerSpawnPosition;
                GameObject.Find("Player Object").GetComponent<PlayerController>().StartingPosition = this.PlayerSpawnPosition;
            }
        }
    }
}

This post is part two of a series based off this GitHub repository. Part one is here.

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

General

To me your code contains waaaay to many thises. I'd do without them. Sometimes you even mix the conventions and use this as well as an underscored fields like: this._Seed.

Level class

It contains a lot of public fields that look like they were some constants but neither of them is marked as const or readonly.

There are lines that use magic numbers like:

Random.Range(0.0f, 100.0f) <= 20.0f

or

if(Random.Range(0.0f, 1.0f) >= 0.45f)

I assume they were simply overseen because you have already defined quite a few of them.


You are not using helper variables consequently. In the same if you define one

bool generateCave = Random.Range(0.0f, 100.0f) <= this.CaveGenerationChance;

// Start a cave flood fill if a random number is less than or equal to
// the cave generation chance.
if(generateCave)
{
    int caveY = (baseColumnHeight + topColumnHeight) - Random.Range(0, 35);
    int caveLength = Random.Range(this.MinimumCaveLength, this.MaximumCaveLength);

    this.GenerateCave(x, caveY, z, caveLength);
}

and a few lines later you chose a huge if

// Set the player spawn position if the terrain generator's current position
// is greater than half the width & depth, if a spawn hasn't already been created,
// and if a tree hasn't been generated.
if(
    x > (float)this.Width / 1.5f && 
    z > (float)this.Depth / 1.5f && 
    !playerSpawnCreated && 
    !treeGenerated &&
    this.UsesPlayer
)
{
    this.PlayerSpawnPosition = new Vector3(x, baseColumnHeight + topColumnHeight + 2, z);
    playerSpawnCreated = true;
}  

The GenerateVoxelData method is a good candidate for refactoring. It's 400 lines long. Almost each if inside the for-loop could be a separate method for generating something and the last if behind it for generating clouds definitely.

They not only should have their own methods but also go into another class like Object/ModelGenerator or something like that followed by other generate routines like GenerateLevelWalls that luckily already has its own function ;-)


Voxel class

You pass a DynamicMesh data to various methods of the voxel class but you don't store it anywhere.

I think it would be a better idea to move dynamic mesh related properties and methods to the DynamicMesh and override its methods in different dynamic meshes if necessary rather then the Voxel's:

public class DynamicMesh
{       
    public Vector3 HalvedOffset { get; set; }

    // ... other properties and methods

    public virtual DynamicMesh CreateNorthFace(int x, int y, int z)
    {
        AddVertex(new Vector3(x + HalvedOffset.x, y - HalvedOffset.y, z + HalvedOffset.z), Offset, IsCollideable);
        AddVertex(new Vector3(x + HalvedOffset.x, y + HalvedOffset.y, z + HalvedOffset.z), Offset, IsCollideable);
        AddVertex(new Vector3(x - HalvedOffset.x, y + HalvedOffset.y, z + HalvedOffset.z), Offset, IsCollideable);
        AddVertex(new Vector3(x - HalvedOffset.x, y - HalvedOffset.y, z + HalvedOffset.z), Offset, IsCollideable);
        AddQuad(IsCollideable);
        AddUVs(GenerateUVs(VoxelFaceDirection.North));
        return this;
    }
}

The voxel could recieve a dynamic mesh via its constructor.

Enums

Both enums should be moved to their own files because they are public already. It would be fine to place them inside a class if they were private. Now if you wanted to use them somewhere else you need to write Voxel.VoxelFaceDirection which is an unnecessary repetition. You could remove the voxel prefix but then another problem arises that you cannot have a property with the same name inside the voxel class. That's why I prefer to put them in separate files.

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.