AlexStv

Unity voxel block tutorial pt. 8

Sorry if anyone was expecting regular updates with this voxel tutorial, it doesn't seem like that's going to happen but at least by this point all the bare bones are there and you don't strictly need a tutorial to continue. In this part we're going to improve the performance of our project and add caves and trees to the world generation.

Before we can get started in Unity5 Terrain is a reserved class name so we'll change it in the two places it's used to EditTerrain:

Firstly, the script Terrain will need to be renamed to EditTerrain and in the file change the class name:

 public static class EditTerrain
 {

and in Modify:

             if (Physics.Raycast(transform.position, transform.forward, out hit, 100))
             {
                 EditTerrain.SetBlock(hit, new BlockAir());
             }

Next open your WorldPos script. You may have noticed we get a warning that WorldPos overrides Equals without overriding GetHashCode. I was ignoring that but now we'll address it, add the following function to the struct:

     public override int GetHashCode()
     {
         unchecked
         {
             int hash = 47;
             hash = hash * 227 + x.GetHashCode();
             hash = hash * 227 + y.GetHashCode();
             hash = hash * 227 + z.GetHashCode();
             return hash;
         }
     }

What this does is that it generates a unique integer for any WorldPos, it's not actually completely unique though, it would be possible for two positions to get the same hash but very unlikely. To make the hash as unique as possible we start it as a prime number and multiply it by another prime number before every addition of a dimension, then those operations are all in an unchecked environment meaning that the integers are not checked for overflows because if it overflows we don't want to clamp it or do anything it just wraps back to zero and keeps going.

This will remove the warning but as this is a really fast way to convert a WorldPos into a unique hash we can also use it for our existing equality check so we'll replace what we currently have there with this:

     public override bool Equals(object obj)
     {
         if (GetHashCode() == obj.GetHashCode())
             return true;
         return false;
     }

This should be slightly faster than what we had before but since it's used so many times for so much it should speed up chunk generation times.

Next up open the LoadChunks script because we'll make some changes to the loading of chunks starting with deleting chunks. We already have a timer on the chunk deletion because it doesn't need to run super often but still when it runs it means that frame runs a deletion on top of all the loading we're already doing so instead we'll delete every tenth frame and on that frame only delete chunks and not load any. So add a return value to the DeleteChunks function that returns whether or not chunks were deleted:

     bool DeleteChunks()    //Change the void on this line to bool
     {
         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;
             return true;    //Add this line
         }
         timer++;
         return false;    //Add this line
     }

And we'll change the update loop to use this:

     void Update()
     {
         if (DeleteChunks()) //Check to see if a delete happened
             return;                 //and if so return early
         FindChunksToLoad();
         LoadAndRenderChunks();
     }

Now in the FindChunksToLoad function we'll change the way we set up the update and build list, first of all change the line:

         //If there aren't already chunks to generate
         if (buildList.Count == 0) //this line
         {

to use the updateList instead:

 //if there aren't already chunks to generate
         if (updateList.Count == 0)
         {

This is to set it so that we don't add anything to either queue unless everything is built and rendered rather than just built. The way the code works right now is that it adds one column to the build list and then when building it builds each chunk and the adjacent chunks, were going to change that so that we add all the chunks to be built at once.

replace this for loop:

                 //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));
                 }

With this loop which adds the area to the build list and the central column to the update list:

                 //load a column of chunks in this position
                 for (int y = -4; y < 4; y++)
                 {
                     for (int x = newChunkPos.x - Chunk.chunkSize; x <= newChunkPos.x + Chunk.chunkSize; x += Chunk.chunkSize)
                     {
                         for (int z = newChunkPos.z - Chunk.chunkSize; z <= newChunkPos.z + Chunk.chunkSize; z += Chunk.chunkSize)
                         {
                             buildList.Add(new WorldPos(
                                 x, y * Chunk.chunkSize, z));
                         }
                     }
                     updateList.Add(new WorldPos(
                                 newChunkPos.x, y * Chunk.chunkSize, newChunkPos.z));
                 }

Now we'll make some small changes to the LoadAndRenderChunks function, the way it's set up it will build 8 chunks per frame if there are any to build and it will update one chunk per frame once all the chunks are built:

     void LoadAndRenderChunks()
     {
         if (buildList.Count != 0)
         {
             for (int i = 0; i < buildList.Count && i < 8; i++)
             {
                 BuildChunk(buildList[0]);
                 buildList.RemoveAt(0);
             }
             //If chunks were built return early
             return;
         }
         if ( updateList.Count!=0)
         {
             Chunk chunk = world.GetChunk(updateList[0].x, updateList[0].y, updateList[0].z);
             if (chunk != null)
                 chunk.update = true;
             updateList.RemoveAt(0);
         }
     }

And finally because all of the logic of building adjacent blocks and only rendering the centre ones is done when placing chunks into lists we can simplify the BuildChunk function to this:

     void BuildChunk(WorldPos pos)
     {
         if (world.GetChunk(pos.x,pos.y,pos.z) == null)
             world.CreateChunk(pos.x,pos.y,pos.z);
     }

I'll keep adding performance optimizations as I make them but at this point it should perform well on any fairly modern graphics card.

Caves

Caves are quite easy to add so we'll start with that, open the TerrainGen script and we'll start by adding the variables to control cave size and frequency:

     float caveFrequency = 0.025f;
     int caveSize = 7;

And now we'll modify parts of the ChunkColumnGen function by adding a line to create a value for each block determining the chance of a cave appearing and then in the two if functions that decide whether or not to add dirt and stone we'll add a condition checking if the space is a cave or not:

 for (int y = chunk.pos.y; y < chunk.pos.y + Chunk.chunkSize; y++)
 {
     //Get a value to base cave generation on
     int caveChance = GetNoise(x, y, z, caveFrequency, 100); //Add this line
     if (y <= stoneHeight && caveSize < caveChance) //Add caveSize < caveChance
     {
         chunk.SetBlock(x - chunk.pos.x, y - chunk.pos.y, z - chunk.pos.z, new Block());
     }
     else if (y <= dirtHeight && caveSize < caveChance) //Add caveSize < caveChance
     {
         chunk.SetBlock(x - chunk.pos.x, y - chunk.pos.y, z - chunk.pos.z, new BlockGrass());
     }
     else
     {
         chunk.SetBlock(x - chunk.pos.x, y - chunk.pos.y, z - chunk.pos.z, new BlockAir());
     }
 }

That should put some swiss cheese style holes in the terrain, check it out!

Trees

To start off for the trees we're going to need some new blocks and they will need textures. I've added some textures to my tilesheet if you would like something to test with:

To use the transparency on the leaves you will need to change the shader used on the chunk prefab from diffuse to transparent/cutout/diffuse. In Unity5 this shader is moved to legacy/transparent/cutout/diffuse.

Next we need a wood block and a leaves block. Creating these will be a lot like creating the grass block before so I won't go into detail for each one. First is the wood block (the texture positions I've used are based on my tile sheet). create a new script called BlockWood:

 using UnityEngine;
 using System.Collections;
 using System;
 [Serializable]
 public class BlockWood : Block
 {
     public BlockWood()
         : base()
     {
     }
     public override Tile TexturePosition(Direction direction)
     {
         Tile tile = new Tile();
         switch (direction)
         {
             case Direction.up:
                 tile.x = 2;
                 tile.y = 1;
                 return tile;
             case Direction.down:
                 tile.x = 2;
                 tile.y = 1;
                 return tile;
         }
         tile.x = 1;
         tile.y = 1;
         return tile;
     }
 }

This extends the default block object with a change in textures to show one wood texture on the sides and another on the top. Next is the leaves block, create a new script called BlockLeaves:

 using UnityEngine;
 using System.Collections;
 using System;
 [Serializable]
 public class BlockLeaves : Block
 {
     public BlockLeaves()
         : base()
     {
     }
     public override Tile TexturePosition(Direction direction)
     {
         Tile tile = new Tile();
         tile.x = 0;
         tile.y = 1;
         return tile;
     }
     public override bool IsSolid(Direction direction)
     {
         return false;
     }
 }

As before this extends Block and replaces the texture but it also returns false for IsSolid so that blocks behind leaves are still rendered.

So far all the terrain we've added has been parts of the terrain and it's been possible to generate on a per column basis. Trees behave a little differently because the column that generates a tree contains the tree trunk but the leaves will extend in all directions into other columns and possible even other chunks than the one we're generating. This will require some changes to how we're handling creating blocks during terrain generation so we'll add a function for setting blocks. There are two things we need out of a function like this. First since we're no longer only going to be creating blocks sequentially we need a toggle for whether or not to replace blocks if the space is already occupied and second we need a check to make sure that the block position is actually inside the chunk. We don't want to accidentally be adding to neighbouring chunks and forcing them to update and with the check built in to the set block function we can be less careful when building the trees.

Add this function:

 public static void SetBlock(int x, int y, int z, Block block, Chunk chunk, bool replaceBlocks = false)
 {
     x -= chunk.pos.x;
     y -= chunk.pos.y;
     z -= chunk.pos.z;
     if (Chunk.InRange(x) && Chunk.InRange(y) && Chunk.InRange(z))
     {
         if (replaceBlocks || chunk.blocks[x, y, z] == null)
             chunk.SetBlock(x, y, z, block);
     }
 }

Now replace all calls to chunk.SetBlock with SetBlock. There should be three instances to replace so far, one for stone, dirt and air. You will need to change those set blocks from:

SetBlock(x - chunk.pos.x, y - chunk.pos.y, z - chunk.pos.z, new Block());

to:

SetBlock(x, y, z, new Block(), chunk);

So the new function only needs the local coordinates of the block relative to the chunk, not the absolute coordinates so we can remove the addition of the chunk position and just use x,y,z. Then also add an extra parameter to the end with the chunk to add the block to. Do this for stone, dirt and air replacing new Block() with BlockDirt and BlockAir respectively.

Next we'll create a function for building trees at a given location, the function is quite simple:

 void CreateTree(int x, int y, int z, Chunk chunk)
 {
     //create leaves
     for (int xi = -2; xi <= 2; xi++)
     {
         for (int yi = 4; yi <= 8; yi++)
         {
             for (int zi = -2; zi <= 2; zi++)
             {
                 SetBlock(x + xi, y + yi, z + zi, new BlockLeaves(), chunk, true);
             }
         }
     }
     //create trunk
     for (int yt = 0; yt < 6; yt++)
     {
         SetBlock(x, y + yt, z, new BlockWood(), chunk, true);
     }
 }

It uses for loops to create a 5x5x5 block of leaves 4 blocks above the ground with replace blocks set to true, then it creates a trunk from the block specified upwards into the leaves replacing blocks. Now add some variables to control tree placement:

     float treeFrequency = 0.2f;
     int treeDensity = 3;

We'll check for tree placement and call the CreateTree function from within ChunkColumnGen right after adding grass like this:

 ...
 else if (y <= dirtHeight && caveSize < caveChance)
 {
     SetBlock(x, y, z, new BlockGrass(), chunk);
     if (y == dirtHeight && GetNoise(x, 0, z, treeFrequency, 100) < treeDensity)    //Add this line
         CreateTree(x, y+1, z, chunk);                                              //And this line
 }
 else
 ...

So that now for each top level grass block we check if the spot satisfies the requirement for a tree and if so call the create tree function on the block above the grass block. If you run this now you'll see some trees show up but they wont extend past the borders of their own chunk. This is beause of the checks we added that don't let us place blocks in neighboring chunks. To get around this and make even the chunks that don't contain the tree trunk generate the leaves in the chunk we'll add some overlap to the generation. In the ChunkGen function where loop over the chunks x and y we will now start at -3 and end at chunk size + 3 so that we catch the start of any trees that will have leaves in the chunk. Like this:

public Chunk ChunkGen(Chunk chunk)
 {
     for (int x = chunk.pos.x-3; x < chunk.pos.x + Chunk.chunkSize + 3; x++) //Change this line
     {
         for (int z = chunk.pos.z-3; z < chunk.pos.z + Chunk.chunkSize + 3; z++)//and this line
         {
             chunk = ChunkColumnGen(chunk, x, z);
         }
     }
     return chunk;
 }

And we'll need to do the same for y in the ChunkColumnGen function but in this case because trees are generated from a point and extend 8 blocks up the overlap needs to be 8 blocks downwards like this:

     for (int y = chunk.pos.y - 8; y < chunk.pos.y + Chunk.chunkSize; y++)
     {

This should catch any trees that were being chopped off before so try and run it!

[image]../../images/trees.png[/image]

By the way, at this point with all the block we have I recommend organizing your scripts into folders but I'll leave that up to you.

Tutorial part 7Download scripts so far

4th Apr, 2015
Alex