AlexStv

Unity voxel block tutorial pt. 7

In this part we're going to add some functions to load chunks around the player and unload chunks when they're too far away. This makes it possible to let the player roam however they would like and keep the number of chunks in memory and being rendered to a minimum.

We'll start with some preliminary changes, open the Chunk script and set the initial value of the update boolean to false:

//Chunk.cs
     public bool update = false;

And check the chunk prefab in the editor to make sure that update is false there too.

Now in the World script we're going to remove the existing methods for triggering chunk creation so remove the newChunk and genChunk variables and remove the Update and Delete functions:

// Remove these lines from World.cs
//    public int newChunkX;
//    public int newChunkY;
//    public int newChunkZ;
//    public bool genChunk;
//    // Use this for initialization
//    void Start()
//    {
//        for (int x = -4; x < 4; x++)
//        {
//            for (int y = -1; y < 3; y++)
//            {
//                for (int z = -4; z < 4; z++)
//                {
//                    CreateChunk(x * 16, y * 16, z * 16);
//                }
//            }
//        }
//    }
//    // Update is called once per frame
//    void Update()
//    {
//        if (genChunk)
//        {
//            genChunk = false;
//            WorldPos chunkPos = new WorldPos(newChunkX, newChunkY, newChunkZ);
//            Chunk chunk = null;
//            if (chunks.TryGetValue(chunkPos, out chunk))
//            {
//                DestroyChunk(chunkPos.x, chunkPos.y, chunkPos.z);
//            }
//            else
//            {
//                CreateChunk(chunkPos.x, chunkPos.y, chunkPos.z);
//            }
//        }
//    }

Now let's create the terrain loading script, create a new script called LoadChunks and start it like this:

//LoadChunks.cs
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class LoadChunks : MonoBehaviour {
     public World world;
     // Update is called once per frame
     void Update () {
     }
}

LoadChunks is going to be a script that goes on the player or on the camera and loads the chunks around it but discards the chunks too far away from it. We'll start with creating chunks around the player and we'll tackle it in three parts, identifying the positions that need chunks generated, creating the chunk objects and then rendering the mesh for the chunk. This is one of the most resource intensive parts of voxel terrain because in order to maintain a feeling of infinite terrain you need to be able to create chunks in front of the player faster than they can move.

Once we find chunks we'll add them to a list of chunks to generate and once the chunks are generated we'll add them to a list of chunks to render. We don't want to generate and load them at the same time because it gives us the freedom to load chunks but not show them immediately but we'll get to that later. For now add two lists to LoadChunks:

//LoadChunks.cs
     List<WorldPos> updateList = new List<WorldPos>();
     List<WorldPos> buildList = new List<WorldPos>();

And to be able to load a chunk and not render it we need to add a boolean the chunk script to show that it has or hasn't been rendered yet. Open the Chunk script and add this boolean:

//Chunk.cs
     public bool rendered;

And set it to true in the UpdateChunk function:

//Chunk.cs
     // Updates the chunk based on its contents
     public bool UpdateChunk()
     {
         rendered = true;
         ...

Switch to the LoadChunks script where we'll start searching for chunks near the player to create. We need to create blocks nearest the player first so I've created a queue of chunk positions radiating from 0,0,0 sorted by closeness to the centre. Add this variable to your script:

//LoadChunks.cs
    static  WorldPos[] chunkPositions= {   new WorldPos( 0, 0,  0), new WorldPos(-1, 0,  0), new WorldPos( 0, 0, -1), new WorldPos( 0, 0,  1), new WorldPos( 1, 0,  0),
                             new WorldPos(-1, 0, -1), new WorldPos(-1, 0,  1), new WorldPos( 1, 0, -1), new WorldPos( 1, 0,  1), new WorldPos(-2, 0,  0),
                             new WorldPos( 0, 0, -2), new WorldPos( 0, 0,  2), new WorldPos( 2, 0,  0), new WorldPos(-2, 0, -1), new WorldPos(-2, 0,  1),
                             new WorldPos(-1, 0, -2), new WorldPos(-1, 0,  2), new WorldPos( 1, 0, -2), new WorldPos( 1, 0,  2), new WorldPos( 2, 0, -1),
                             new WorldPos( 2, 0,  1), new WorldPos(-2, 0, -2), new WorldPos(-2, 0,  2), new WorldPos( 2, 0, -2), new WorldPos( 2, 0,  2),
                             new WorldPos(-3, 0,  0), new WorldPos( 0, 0, -3), new WorldPos( 0, 0,  3), new WorldPos( 3, 0,  0), new WorldPos(-3, 0, -1),
                             new WorldPos(-3, 0,  1), new WorldPos(-1, 0, -3), new WorldPos(-1, 0,  3), new WorldPos( 1, 0, -3), new WorldPos( 1, 0,  3),
                             new WorldPos( 3, 0, -1), new WorldPos( 3, 0,  1), new WorldPos(-3, 0, -2), new WorldPos(-3, 0,  2), new WorldPos(-2, 0, -3),
                             new WorldPos(-2, 0,  3), new WorldPos( 2, 0, -3), new WorldPos( 2, 0,  3), new WorldPos( 3, 0, -2), new WorldPos( 3, 0,  2),
                             new WorldPos(-4, 0,  0), new WorldPos( 0, 0, -4), new WorldPos( 0, 0,  4), new WorldPos( 4, 0,  0), new WorldPos(-4, 0, -1),
                             new WorldPos(-4, 0,  1), new WorldPos(-1, 0, -4), new WorldPos(-1, 0,  4), new WorldPos( 1, 0, -4), new WorldPos( 1, 0,  4),
                             new WorldPos( 4, 0, -1), new WorldPos( 4, 0,  1), new WorldPos(-3, 0, -3), new WorldPos(-3, 0,  3), new WorldPos( 3, 0, -3),
                             new WorldPos( 3, 0,  3), new WorldPos(-4, 0, -2), new WorldPos(-4, 0,  2), new WorldPos(-2, 0, -4), new WorldPos(-2, 0,  4),
                             new WorldPos( 2, 0, -4), new WorldPos( 2, 0,  4), new WorldPos( 4, 0, -2), new WorldPos( 4, 0,  2), new WorldPos(-5, 0,  0),
                             new WorldPos(-4, 0, -3), new WorldPos(-4, 0,  3), new WorldPos(-3, 0, -4), new WorldPos(-3, 0,  4), new WorldPos( 0, 0, -5),
                             new WorldPos( 0, 0,  5), new WorldPos( 3, 0, -4), new WorldPos( 3, 0,  4), new WorldPos( 4, 0, -3), new WorldPos( 4, 0,  3),
                             new WorldPos( 5, 0,  0), new WorldPos(-5, 0, -1), new WorldPos(-5, 0,  1), new WorldPos(-1, 0, -5), new WorldPos(-1, 0,  5),
                             new WorldPos( 1, 0, -5), new WorldPos( 1, 0,  5), new WorldPos( 5, 0, -1), new WorldPos( 5, 0,  1), new WorldPos(-5, 0, -2),
                             new WorldPos(-5, 0,  2), new WorldPos(-2, 0, -5), new WorldPos(-2, 0,  5), new WorldPos( 2, 0, -5), new WorldPos( 2, 0,  5),
                             new WorldPos( 5, 0, -2), new WorldPos( 5, 0,  2), new WorldPos(-4, 0, -4), new WorldPos(-4, 0,  4), new WorldPos( 4, 0, -4),
                             new WorldPos( 4, 0,  4), new WorldPos(-5, 0, -3), new WorldPos(-5, 0,  3), new WorldPos(-3, 0, -5), new WorldPos(-3, 0,  5),
                             new WorldPos( 3, 0, -5), new WorldPos( 3, 0,  5), new WorldPos( 5, 0, -3), new WorldPos( 5, 0,  3), new WorldPos(-6, 0,  0),
                             new WorldPos( 0, 0, -6), new WorldPos( 0, 0,  6), new WorldPos( 6, 0,  0), new WorldPos(-6, 0, -1), new WorldPos(-6, 0,  1),
                             new WorldPos(-1, 0, -6), new WorldPos(-1, 0,  6), new WorldPos( 1, 0, -6), new WorldPos( 1, 0,  6), new WorldPos( 6, 0, -1),
                             new WorldPos( 6, 0,  1), new WorldPos(-6, 0, -2), new WorldPos(-6, 0,  2), new WorldPos(-2, 0, -6), new WorldPos(-2, 0,  6),
                             new WorldPos( 2, 0, -6), new WorldPos( 2, 0,  6), new WorldPos( 6, 0, -2), new WorldPos( 6, 0,  2), new WorldPos(-5, 0, -4),
                             new WorldPos(-5, 0,  4), new WorldPos(-4, 0, -5), new WorldPos(-4, 0,  5), new WorldPos( 4, 0, -5), new WorldPos( 4, 0,  5),
                             new WorldPos( 5, 0, -4), new WorldPos( 5, 0,  4), new WorldPos(-6, 0, -3), new WorldPos(-6, 0,  3), new WorldPos(-3, 0, -6),
                             new WorldPos(-3, 0,  6), new WorldPos( 3, 0, -6), new WorldPos( 3, 0,  6), new WorldPos( 6, 0, -3), new WorldPos( 6, 0,  3),
                             new WorldPos(-7, 0,  0), new WorldPos( 0, 0, -7), new WorldPos( 0, 0,  7), new WorldPos( 7, 0,  0), new WorldPos(-7, 0, -1),
                             new WorldPos(-7, 0,  1), new WorldPos(-5, 0, -5), new WorldPos(-5, 0,  5), new WorldPos(-1, 0, -7), new WorldPos(-1, 0,  7),
                             new WorldPos( 1, 0, -7), new WorldPos( 1, 0,  7), new WorldPos( 5, 0, -5), new WorldPos( 5, 0,  5), new WorldPos( 7, 0, -1),
                             new WorldPos( 7, 0,  1), new WorldPos(-6, 0, -4), new WorldPos(-6, 0,  4), new WorldPos(-4, 0, -6), new WorldPos(-4, 0,  6),
                             new WorldPos( 4, 0, -6), new WorldPos( 4, 0,  6), new WorldPos( 6, 0, -4), new WorldPos( 6, 0,  4), new WorldPos(-7, 0, -2),
                             new WorldPos(-7, 0,  2), new WorldPos(-2, 0, -7), new WorldPos(-2, 0,  7), new WorldPos( 2, 0, -7), new WorldPos( 2, 0,  7),
                             new WorldPos( 7, 0, -2), new WorldPos( 7, 0,  2), new WorldPos(-7, 0, -3), new WorldPos(-7, 0,  3), new WorldPos(-3, 0, -7),
                             new WorldPos(-3, 0,  7), new WorldPos( 3, 0, -7), new WorldPos( 3, 0,  7), new WorldPos( 7, 0, -3), new WorldPos( 7, 0,  3),
                             new WorldPos(-6, 0, -5), new WorldPos(-6, 0,  5), new WorldPos(-5, 0, -6), new WorldPos(-5, 0,  6), new WorldPos( 5, 0, -6),
                             new WorldPos( 5, 0,  6), new WorldPos( 6, 0, -5), new WorldPos( 6, 0,  5) };

It's a big array but it never changes so there's no point calculating it on the fly. Now we can go on to creating the function:

//LoadChunks.cs
     void FindChunksToLoad()
     {
         //Get the position of this gameobject to generate around
         WorldPos playerPos = new WorldPos(
             Mathf.FloorToInt(transform.position.x / Chunk.chunkSize) * Chunk.chunkSize,
             Mathf.FloorToInt(transform.position.y / Chunk.chunkSize) * Chunk.chunkSize,
             Mathf.FloorToInt(transform.position.z / Chunk.chunkSize) * Chunk.chunkSize
             );
         //If there aren't already chunks to generate
         if (buildList.Count == 0)
         {
             //Cycle through the array of positions
             for (int i = 0; i < chunkPositions.Length; i++)
             {
                 //translate the player position and array position into chunk position
                 WorldPos newChunkPos = new WorldPos(
                     chunkPositions[i].x * Chunk.chunkSize + playerPos.x,
                     0,
                     chunkPositions[i].z * Chunk.chunkSize + playerPos.z
                     );
                 //Get the chunk in the defined position
                 Chunk newChunk = world.GetChunk(
                     newChunkPos.x, newChunkPos.y, newChunkPos.z);
                 //If the chunk already exists and it's already
                 //rendered or in queue to be rendered continue
                 if (newChunk != null
                     && (newChunk.rendered || updateList.Contains(newChunkPos)))
                     continue;
                 //load a column of chunks in this position
                 for (int y = -4; y < 4; y++)
                 {
                     buildList.Add(new WorldPos(
                         newChunkPos.x, y * Chunk.chunkSize, newChunkPos.z));
                 }
                 return;
             }
         }
     }

To summarize this takes the player position's x and z, then for each entry in the chunkPositions array offsets the player position by the chunk position. Then in that position it checks for a chunk, if there is no chunk or the chunk there isn't yet rendered yet add a column of chunks in that position to the build list. Once it finds a chunk column to add it returns so that only one column is added to the list.

Now that we have the build list being populated we'll need a function to build those chunks. From what I've found when rendering a chunk you need to have all the neighbouring chunks loaded so that the blocks on the edge of the chunk being rendered can get block information on the blocks next to them in the neighbouring chunk. If you don't load the neighbouring chunks the blocks have to assume the neighbour is solid or not, solid will make a lot of errors where the rendering assumes the face can't be seen and assuming not solid will render a lot of block faces that are actually hidden especially underground. Having the neighbours loaded but not rendered solves this and often we'll be loading those chunks next anyway.

So now when we build a chunk we'll also build all the chunks near except chunks too high or low it and only render the one we were given:

//LoadChunks.cs
     void BuildChunk(WorldPos pos)
     {
         for (int y = pos.y - Chunk.chunkSize; y <= pos.y + Chunk.chunkSize; y += Chunk.chunkSize)
         {
             if (y > 64 || y < -64)
                 continue;
             for (int x = pos.x - Chunk.chunkSize; x <= pos.x + Chunk.chunkSize; x += Chunk.chunkSize)
             {
                 for (int z = pos.z - Chunk.chunkSize; z <= pos.z + Chunk.chunkSize; z += Chunk.chunkSize)
                 {
                     if (world.GetChunk(x, y, z) == null)
                         world.CreateChunk(x, y, z);
                 }
             }
         }
         updateList.Add(pos);
     }

That handles building a chunk and populates the update list so next we'll create a function to call it and to handle the chunks in the update list:

//LoadChunks.cs
     void LoadAndRenderChunks()
     {
         for (int i = 0; i < 4; i++)
         {
             if (buildList.Count != 0)
             {
                 BuildChunk(buildList[0]);
                 buildList.RemoveAt(0);
             }
         }
         for (int i = 0; i < updateList.Count; i++)
         {
                 Chunk chunk = world.GetChunk(updateList[0].x, updateList[0].y, updateList[0].z);
                 if (chunk != null)
                     chunk.update = true;
                 updateList.RemoveAt(0);
         }
     }

The first partloops through up to 4 chunks to be built and builds them. You can limit how intensive this is per frame by changing the for loop around the build list and the call to BuildChunk to iterate more or fewer times per frame. The second part loops through all the chunks to update and updates them, there's no reason to limit how many of these to do per frame because there will only ever be one to update per chunk built so we can limit the chunks built instead.

Next we can add these functions to the update function and test it:

//LoadChunks.cs
// Update is called once per frame
void Update () {
FindChunksToLoad();
LoadAndRenderChunks();
}

With our changes so far that should find, load and render chunks around the object using this script. Go ahead and try it out but it will grind to a halt quickly because so many chunks will be loaded.

So next lets set up deleting of chunks to fix that. Because it isn't time sensitive like loading chunks we can delete less often, for that we'll add a variable called timer to only delete chunks every 10 frames:

//LoadChunk.cs
     int timer = 0;

And now the function:

//LoadChunks.cs
     void DeleteChunks()
     {
         if (timer == 10)
         {
             var chunksToDelete = new List<WorldPos>();
             foreach (var chunk in world.chunks)
             {
                 float distance = Vector3.Distance(
                     new Vector3(chunk.Value.pos.x, 0, chunk.Value.pos.z),
                     new Vector3(transform.position.x, 0, transform.position.z));
                 if (distance > 256)
                     chunksToDelete.Add(chunk.Key);
             }
             foreach (var chunk in chunksToDelete)
                 world.DestroyChunk(chunk.x, chunk.y, chunk.z);
             timer = 0;
         }
         timer++;
     }

The timer makes every tenth frame run the delete code, then for each chunk loaded we check the distance to the gameobject and if it's over 256 add it to a list of chunks to delete. Then we call DestroyChunk on all those chunks in the list and reset the timer.

Add a call to DeleteChunks in Update:

//LoadChunks.cs
    void Update () {
         DeleteChunks();
         FindChunksToLoad();
         LoadAndRenderChunks();
    }

This should now smoothly load terrain and delete it behind you! Try it out!

If you haven't added lights to the scene yet I recommend you add a directional light so you can see a little better and hard shadows enabled. Then set the ambient light in render settings to a light grey for nice shadows. Anyway it should look a little like this but with the chunks loading further away:











In this example I'm using unity's first person controller with the LoadChunks script on it.

Tutorial part 6Download scripts so farTutorial part 8

8th Feb, 2015
Alex