AlexStv

Unity voxel block tutorial pt. 4

In this part we're going to look at real time modification of the terrain by a player. This will usually be done by raycasting at the terrain to get a block position, then setting that block to another type or setting the empty space in front of it to another block. In this part we'll set up two classes, one with generic functions you can access from anywhere and another class as an example implementation of block modification.

Well start with a static class for the generic functions, create a new script and call it Terrain and set it up like this:

using UnityEngine;
using System.Collections;
public static class Terrain
{
}

We won't be inheriting from MonoBehaviour and this class is static because it's just going to be a helper class. Next add our first function: GetBlockPos to round vector3 positions to their nearest block position.

     public static WorldPos GetBlockPos(Vector3 pos)
     {
         WorldPos blockPos = new WorldPos(
             Mathf.RoundToInt(pos.x),
             Mathf.RoundToInt(pos.y),
             Mathf.RoundToInt(pos.z)
             );
 
         return blockPos;
     }

All we're doing is creating a WorldPos variable from the rounded components of the vector3 but it will be handy to have this function available anywhere.

Next let's get a position from a raycast collision with these two new functions:

     public static WorldPos GetBlockPos(RaycastHit hit, bool adjacent = false)
     {
         Vector3 pos = new Vector3(
             MoveWithinBlock(hit.point.x, hit.normal.x, adjacent),
             MoveWithinBlock(hit.point.y, hit.normal.y, adjacent),
             MoveWithinBlock(hit.point.z, hit.normal.z, adjacent)
             );
 
         return GetBlockPos(pos);
     }
     static float MoveWithinBlock(float pos, float norm, bool adjacent = false)
     {
         if (pos - (int)pos == 0.5f || pos - (int)pos == -0.5f)
         {
             if (adjacent)
             {
                 pos += (norm / 2);
             }
             else
             {
                 pos -= (norm / 2);
             }
         }
 
         return (float)pos;
     }

The first function is an overload of the last function we added, it has the same name but now takes a RaycastHit which you will get as the result of a raycast. It also optionally takes a boolean called adjacent which decides if you should get the block you hit or the block adjacent to the face you hit.

Overloading and optional parameters are a good way to make your life easier as a developer. Instead of having a function for each input type with a different name you can create an overloaded version for each input type. Optional parameters are useful to give you the option of sending parameter to a function but otherwise use a default if the parameter is not included.

In this function we create a new vector3 by calling MoveWithinBlock on each axis of the position. However, when we raycast onto a cube block the axis of the face the raycast hits will be 0.5, exactly half way between two blocks. To solve that we use MoveWithinBlock.

MoveWithinBlock gets called with x, y or z and the haycastHit's normal's x y or z. If the block is halfway between two blocks it will have a decimal of 0.5 so rounding it and subtracting the the rounded value from the original value which leaves us with the decimals. Then if we find that it's 0.5 we can use the normal to move it (if you're unfamiliar with normals they are vectors point in the way a triangle the facing, the normal included with the raycastHit is the normal of the triangle hit by the raycast). We can use the normal to move the point outwards or inwards, if we're getting the adjacent block add half the normal to the position pushing it outwards. Only add half because the whole thing could equal up to 1 which would push the position too far back. If we're not looking for the adjacent block subtract the same amount moving it further into the block we're pointing at.

Once we call MoveWithinBlock on every component of the position with the corresponding normal value we know that the position is within the block we want so we can call GetBlockPos with the vector3 and it will round it to a block position that we can return.

Now from anywhere in our code we can call Terrain.GetBlockPos(vector3) to round to a block position, Terrain.GetBlockPos(RaycastHit) to get the block hit by a raycast or Terrain.GetBlockPos(raycastHit, true) to get the block opposite the face hit by the raycast.

Now we'll add a globally accessible function to set blocks:

     public static bool SetBlock(RaycastHit hit, Block block, bool adjacent = false)
     {
         Chunk chunk = hit.collider.GetComponent<Chunk>();
         if (chunk == null)
             return false;
 
         WorldPos pos = GetBlockPos(hit, adjacent);
 
         chunk.world.SetBlock(pos.x, pos.y, pos.z, block);
 
         return true;
     }

This function takes a raycastHit and gets the chunk hit. If there is no chunk component we have to return false because the collider is not a chunk. Otherwise we get the position of the block and call setBlock on the chunk's world component with the parameters supplied and return true. You'll be able to call this from anywhere and know if it was successful from the return value.

We'll add the same for getting blocks:

     public static Block GetBlock(RaycastHit hit, bool adjacent = false)
     {
         Chunk chunk = hit.collider.GetComponent<Chunk>();
         if (chunk == null)
             return null;
 
         WorldPos pos = GetBlockPos(hit, adjacent);
 
         Block block = chunk.world.GetBlock(pos.x, pos.y, pos.z);
 
         return block;
     }

Same concept but here we return the block hit.

Now let's create an example script to use these functions. Create a new script called Modify and put this in it:

 using UnityEngine;
 using System.Collections;
 
 public class Modify : MonoBehaviour
 {
 
     Vector2 rot;
 
     void Update()
     {
         if (Input.GetKeyDown(KeyCode.Space))
         {
             RaycastHit hit;
             if (Physics.Raycast(transform.position, transform.forward,out hit, 100 ))
             {
                 Terrain.SetBlock(hit, new BlockAir());
             }
         }
 
         rot= new Vector2(
             rot.x + Input.GetAxis("Mouse X") * 3,
             rot.y + Input.GetAxis("Mouse Y") * 3);
 
         transform.localRotation = Quaternion.AngleAxis(rot.x, Vector3.up);
         transform.localRotation *= Quaternion.AngleAxis(rot.y, Vector3.left);
 
         transform.position += transform.forward * 3 * Input.GetAxis("Vertical");
         transform.position += transform.right * 3 * Input.GetAxis("Horizontal");
     }
 }

Now when you push space you'll raycast from the camera straight forwards and set the block hit to air. Also this script has some basic camera movement added so use the mouse to look around and W, A, S and D to move around. You should be able to destroy blocks in the chunk generated and do something like this:

[image]../../images/modify.png[/image]

Before we continue we'll need to make a change to the World script so open World.cs and scroll to the set block function. The problem we have is that we update the chunk containing the block we set but when it's on the edge of the chunk we really need to update the neighbour as well for any blocks that face the block set. So fist add this function to world:

     void UpdateIfEqual(int value1, int value2, WorldPos pos)
     {
         if (value1 == value2)
         {
             Chunk chunk = GetChunk(pos.x, pos.y, pos.z);
             if (chunk != null)
                 chunk.update = true;
         }
     }

and then under the chunk.update = true line in SetBlock add calls to the new function:

     public void SetBlock(int x, int y, int z, Block block)
     {
         Chunk chunk = GetChunk(x, y, z);
 
         if (chunk != null)
         {
             chunk.SetBlock(x - chunk.pos.x, y - chunk.pos.y, z - chunk.pos.z, block);
             chunk.update = true;
 
             //Add these lines line
           UpdateIfEqual(x - chunk.pos.x, 0, new WorldPos(x - 1, y, z));
UpdateIfEqual(x - chunk.pos.x, Chunk.chunkSize - 1, new WorldPos(x + 1, y, z));
UpdateIfEqual(y - chunk.pos.y, 0, new WorldPos(x, y - 1, z));
UpdateIfEqual(y - chunk.pos.y, Chunk.chunkSize - 1, new WorldPos(x, y + 1, z));
UpdateIfEqual(z - chunk.pos.z, 0, new WorldPos(x, y, z - 1));
UpdateIfEqual(z - chunk.pos.z, Chunk.chunkSize - 1, new WorldPos(x, y, z + 1));
         }
     }

What this does is check if value1 and value 2 are equal and if so update the chunk containing the position. We call it for x, y and z for the upper and lower bounds of the chunk. We subtract the block position from the value to get the block's local position. This should ensure that bordering chunks are always updated.

Tutorial part 3Download scripts so far Tutorial part 5

6th Jan, 2015
Alex