Unity Voxel Tutorial Part 7: Modifying


Hello everyone following the voxel tutorial, it's been a long time since an update. In this time I've written a new updated tutorial on voxel terrain that supports infinite terrain and saving/loading of chunks. Try it out on my new site: AlexStv.com



I went ahead and added block breaking and block placing to make our chunks interactive.


This tutorial is about editing the terrain similar to part 4 but in 3d. First of all we're going to change the scripts we've already written a little bit. If I had been smarter I would have written them this way in the first place but I didn't think ahead.

In Chunk.cs the change is simple, make the GenerateMesh function public so we can call it whenever we make changes to a mesh.
public void GenerateMesh(){ //Made this public

In World.cs our changes are bigger, we're changing the chunks array from a GameObject array to a Chunk array because we don't actually need access to the gameobjects we need access to the chunk scripts. And we're offsetting the positions the chunks are created at because if you remember in the 2d prototype whenever we checked the position of a block we had to offset the point of collision before we could round to get the block position. This time we're doing it right and just making sure the center of every block is its x,y,z position.

We'll start with the position change, in the instatiate line for the chunks we'll add offsets to each axis:
chunks[x,y,z]=Instantiate(chunk,
 new Vector3(x*chunkSize-0.5f,y*chunkSize+0.5f,z*chunkSize-0.5f),
 new Quaternion(0,0,0,0)) as GameObject;
See how after x*chunkSize I have -0.5f? That's the offset, -0.5 to x, +0.5 to y and -0.5 to z.

Now we'll change that chunks array, change it in the variable definition first:
public Chunk[,,] chunks;  //Changed from public GameObject[,,] chunks;

Next change the line where we define the size of the array:
  chunks=new Chunk[Mathf.FloorToInt(worldX/chunkSize),
 Mathf.FloorToInt(worldY/chunkSize),Mathf.FloorToInt(worldZ/chunkSize)];
Just change the chunks= new GameObject... to chunks=new Chunk...

Now all the stuff in those for loops below is wrong so we need to switch some stuff around:
//Create a temporary Gameobject for the new chunk instead of using chunks[x,y,z]
GameObject newChunk= Instantiate(chunk,new Vector3(x*chunkSize-0.5f,
 y*chunkSize+0.5f,z*chunkSize-0.5f),new Quaternion(0,0,0,0)) as GameObject;
     
//Now instead of using a temporary variable for the script assign it
//to chunks[x,y,z] and use it instead of the old \"newChunkScript\" 
chunks[x,y,z]= newChunk.GetComponent(\"Chunk\") as Chunk;
chunks[x,y,z].worldGO=gameObject;
chunks[x,y,z].chunkSize=chunkSize;
chunks[x,y,z].chunkX=x*chunkSize;
chunks[x,y,z].chunkY=y*chunkSize;
chunks[x,y,z].chunkZ=z*chunkSize;

Ok great! Sorry about that but now it's done. Now we can get started, create a new script called "ModifyTerrain.cs" and open it. This script is going to have a collection of functions for adding and removing blocks. We'll set it up by creating all the functions first and then writing what they do after.
public void ReplaceBlockCenter(float range, byte block){
 //Replaces the block directly in front of the player
}

public void AddBlockCenter(float range, byte block){
 //Adds the block specified directly in front of the player
}

public void ReplaceBlockCursor(byte block){
 //Replaces the block specified where the mouse cursor is pointing
}

public void AddBlockCursor( byte block){
 //Adds the block specified where the mouse cursor is pointing
}

public void ReplaceBlockAt(RaycastHit hit, byte block) {
 //removes a block at these impact coordinates, you can raycast against the terrain and call this with the hit.point
}

public void AddBlockAt(RaycastHit hit, byte block) {
 //adds the specified block at these impact coordinates, you can raycast against the terrain and call this with the hit.point
}

public void SetBlockAt(Vector3 position, byte block) {
 //sets the specified block at these coordinates
}

public void SetBlockAt(int x, int y, int z, byte block) {
 //adds the specified block at these coordinates
}

public void UpdateChunkAt(int x, int y, int z){
 //Updates the chunk containing this block
}


That's a lot of functions but the way this is going to work is that if the player calls one of the top functions like ReplaceBlockCursor(block) it runs and then calls ReplaceBlockAt(RaycastHit, block) that runs and calls SetBlockAt (vector3, block) which calls SetBlockAt(int,int,int, block) which sets the block and calls UpdateChunkAt. So if you have a reason to you can set a block by its coordinates or you can just send collision data to the script or you can just call a function to remove the block in front of the player.

Now we also need access to some other things so add the following variables:
World world;
GameObject cameraGO;

And we'll assign those in the start function. We can get the world script  with gameObject.getComponent because we'll place both scripts on the same game object and we'll get the camera by its tag.

void Start () {
 
 world=gameObject.GetComponent("World") as World;
 cameraGO=GameObject.FindGameObjectWithTag("MainCamera");
  
}

Now we can start with the functions, let's start with SetBlockAt(int,int,int,block), here we just change the value in the data array in World.cs and run UpdateChunk:
public void SetBlockAt(int x, int y, int z, byte block) {
 //adds the specified block at these coordinates
 
 print("Adding: " + x + ", " + y + ", " + z);
 
 
 world.data[x,y,z]=block;
 UpdateChunkAt(x,y,z);
 
}

Let's get UpdateChunkAt while we're at it, we need to derive which chunk the block is in from its coordinates and run an update on that block. For now we'll just update the block immediately to get it working but this is an inefficient method because often the player will be editing multiple blocks in the same chunk in a single frame and this way we generate the mesh again for each change. Later we'll switch to setting a flag in the chunk and then updating the chunk if the flag is set at the end of the frame.
//To do: add a way to just flag the chunk for update then it update it in lateupdate
public void UpdateChunkAt(int x, int y, int z){ 
 //Updates the chunk containing this block
 
 int updateX= Mathf.FloorToInt( x/world.chunkSize);
 int updateY= Mathf.FloorToInt( y/world.chunkSize);
 int updateZ= Mathf.FloorToInt( z/world.chunkSize);
 
 print(\"Updating: \" + updateX + \", \" + updateY + \", \" + updateZ);
 
 world.chunks[updateX,updateY, updateZ].GenerateMesh();
 
}

So what we do is take each axis, divide the value by the chunk size and this gives us a value that used to be between 0 and (world width) as a value between 0 and (number of chunks on this axis) so when we round to the nearest whole number we round to the closed chunk index.

Continuing to work our way up move to SetBlockAt(Vector3, block) This takes a vector3 and finds the nearest block by rounding the float components of the vector3 to ints:
public void SetBlockAt(Vector3 position, byte block) {
 //sets the specified block at these coordinates
 
 int x= Mathf.RoundToInt( position.x );
 int y= Mathf.RoundToInt( position.y );
 int z= Mathf.RoundToInt( position.z );
 
 SetBlockAt(x,y,z,block);
}

Now the RaycastHit functions, these will take a collision and find either the block collided with or the one next to the one collided with for placing blocks where you're looking. Lets start first with ReplaceBlockAt(RaycastHit, block):
public void ReplaceBlockAt(RaycastHit hit, byte block) {
 //removes a block at these impact coordinates, you can raycast against the terrain and call this with the hit.point
 Vector3 position = hit.point;
 position+=(hit.normal*-0.5f);
 
 SetBlockAt(position, block);
}

This takes the impact coordinates of a raycast and finds the block it hit by moving the point inwards into the block so that when we round it it's within the cube and is rounded to its coordinates. hit.normal is the outwards direction of the surface it hit so the reverse of that in the direction into the cube. Therefore we take the hit position and add to it the half the reverse normal (Half so that it doesn't come out the other end of the block). This places the point within the bounds of the cube so we send it off to SetBlockAt(Vector3, block).

The other RaycastHit function, AddBlockAt(RaycastHit,block) is very similar only that it doesn't invert the normal because it places a block at the block next to the block hit so we move the impact position outwards from the surface hit and run SetBlockAt(Vector3, block):
public void AddBlockAt(RaycastHit hit, byte block) {
 //adds the specified block at these impact coordinates, you can raycast against the terrain and call this with the hit.point
 Vector3 position = hit.point;
 position+=(hit.normal*0.5f);
  
 SetBlockAt(position,block);
  
}

Now that we have ways to handle raycast information we should carry out some raycasts, start with the cursor functions. These are only different in what function they call after they're done and they're pretty standard raycast from mouse position functions:
 public void ReplaceBlockCursor(byte block){
  //Replaces the block specified where the mouse cursor is pointing
  
  Ray ray = Camera.main.ScreenPointToRay (Input.mousePosition);
  RaycastHit hit;
 
  if (Physics.Raycast (ray, out hit)) {
   
   ReplaceBlockAt(hit, block);
   Debug.DrawLine(ray.origin,ray.origin+( ray.direction*hit.distance),
    Color.green,2);
   
  }
  
 }
 
 public void AddBlockCursor( byte block){
  //Adds the block specified where the mouse cursor is pointing
  
  Ray ray = Camera.main.ScreenPointToRay (Input.mousePosition);
  RaycastHit hit;
 
  if (Physics.Raycast (ray, out hit)) {
   
   AddBlockAt(hit, block);
   Debug.DrawLine(ray.origin,ray.origin+( ray.direction*hit.distance),
    Color.green,2);
  }
  
 }

We define a ray using the mouse position, then use that to raycast and send the information with the RaycastHit functions.

The last two functions, the center functions are very similar to the last two just that the ray is derived from the camera position and rotation not taking into account the position of the mouse and these functions take a range parameter which stops the function if the terrain is beyond range:
 public void ReplaceBlockCenter(float range, byte block){
  //Replaces the block directly in front of the player
  
  Ray ray = new Ray(cameraGO.transform.position, cameraGO.transform.forward);
  RaycastHit hit;
 
  if (Physics.Raycast (ray, out hit)) {
   
   if(hit.distance<range){
    ReplaceBlockAt(hit, block);
   }
  }
  
 }

 public void AddBlockCenter(float range, byte block){
  //Adds the block specified directly in front of the player
  
  Ray ray = new Ray(cameraGO.transform.position, cameraGO.transform.forward);
  RaycastHit hit;
 
  if (Physics.Raycast (ray, out hit)) {
   
   if(hit.distance<range){
    AddBlockAt(hit,block);
   }
   Debug.DrawLine(ray.origin,ray.origin+( ray.direction*hit.distance),Color.green,2);
  }
  
 }



You can test all this by adding this to the update loop:
if(Input.GetMouseButtonDown(0)){
 ReplaceBlockCursor(0);
}

if(Input.GetMouseButtonDown(1)){
 AddBlockCursor(1);
}

You should be able to place and remove blocks but you might notice that sometimes it seems to glitch and you can see through the terrain after a block is removed. What's happening here is that after you remove a block and update the chunk it updates fine but if the block is on the border with another chunk then that chunk still won't update meaning the side of the now exposed block in the neighbor chunk won't get drawn.

To fix this we need to get back to the chunk update script which we were going to change a bit anyway to make more efficient. Lets start with the update method, we'll make some changes in Chunk.cs, first add a bool and call it update. Then create a new function called LateUpdate, this is a unity function called after all the other update functions, here we'll update the chunk if update is true:
public bool update;

 void LateUpdate () {
  if(update){
   GenerateMesh();
   update=false;
  }
 }


Now instead of calling the GenerateTerrain function in UpdateChunkAt in the ModifyTerrain.cs script just set update to true:
world.chunks[updateX,updateY, updateZ].update=true;

Now on to making neighbor blocks update when necessary, this is only needed when the block changed is on the edge of its chunk so only if x, y or z is 0 or 15 relative to its chunk. Based on the coordinates of the block we can find if its on the edge and also which edge like this:
if(x-(world.chunkSize*updateX)==0 && updateX!=0){
 world.chunks[updateX-1,updateY, updateZ].update=true;
}

if(x-(world.chunkSize*updateX)==15 && updateX!=world.chunks.GetLength(0)-1){
 world.chunks[updateX+1,updateY, updateZ].update=true;
}

if(y-(world.chunkSize*updateY)==0 && updateY!=0){
 world.chunks[updateX,updateY-1, updateZ].update=true;
}

if(y-(world.chunkSize*updateY)==15 && updateY!=world.chunks.GetLength(1)-1){
 world.chunks[updateX,updateY+1, updateZ].update=true;
}
  
if(z-(world.chunkSize*updateZ)==0 && updateZ!=0){
 world.chunks[updateX,updateY, updateZ-1].update=true;
}
  
if(z-(world.chunkSize*updateZ)==15 && updateZ!=world.chunks.GetLength(2)-1){
 world.chunks[updateX,updateY, updateZ+1].update=true;
}

This should keep all the neighbors updated if they need to be, it finds the x, y or z of the block relative to the chunk by subtracting the chunk's coordinates (world.chunkSize*updateX where updateX is how many chunks along on the x axis this chunk is) and then if the relative coordinate is 0 it updates the block further down on that axis, if it's 15 it updates the one further up. It also checks to make sure that there is a chunk in that direction in case it's the edge of the level.

It should now work as intended to left click and right click to remove and place blocks. Also at this point it's probably a good idea to add a directional light to the scene. I hope you guys come up with some cool uses for this!

Student Game Dev. Unfortunately I am art impaired.
Edit:
With fog enabled in render settings and shadows enabled on your directional lights it can look pretty cool.



Feel free to follow me on twitter or g+ and as always of you have a problem please let me know and I'll do my best to fix it.

Part 8: Loading Chunks

24 comments:

  1. Hi again Alexandros,

    Just two things:

    1. " Now instead of calling the GenerateTerrain function just set update to true: " this paragraph should say we are about to make changes to "UpdateChunkAt" (I guess) and also we don't need to call "GenerateMesh".

    2. I get index errors when placing a block and "Failed setting triangles. Some indices are referencing out of bounds vertices." when removing. Did I miss something?

    That's all, it's been a entertaining tutorial!

    ReplyDelete
    Replies
    1. Ok I solved 2.

      It had 2 errors:

      1. I changed some lines, that was causing the out of bounds errors (index errors as I said).

      2. Chunk.GenerateMesh needs to reset faceCount back to zero at the end so new calls don't make more than 2 triangles per face. So just "faceCount = 0;" at the bottom of GenerateMesh (Chunk.cs) and voilá!

      Delete
    2. Ok, I made it clear where I meant to make changes to the chunk updating and added the faceCount=0; to the previous part. Thanks for figuring these out and letting me know, I sure everyone else who does the tutorial will really appreciate this!

      Delete
  2. Can you send me a link for your ModifyTerrain.cs , World.cs nd Chunk.cs scripts, I seem to be having some errors coming up saying NullReferenceException : Object reference not set to an instance of an objet.

    ReplyDelete
    Replies
    1. Hello friend,
      I had the same problem and solved it by applying the "ModifyTerrain" script to the World GameObject, because in the start () function, it tries to acces the world component which is only present on the World GameObject

      Delete
  3. I am also getting an error at the line: world.data[x, y, z] = block;
    That is within SetBlockAt () And the error specifically says this:
    NullReferenceException: Object reference not set to an instance of an object
    ModifyTerrain.SetBlockAt(Int32 x, Int32 y, Int32 z, Byte block)

    ReplyDelete
  4. where does this go ?

    if(x-(world.chunkSize*updateX)==0 && updateX!=0){
    world.chunks[updateX-1,updateY, updateZ].update=true;
    }

    if(x-(world.chunkSize*updateX)==15 && updateX!=world.chunks.GetLength(0)-1){
    world.chunks[updateX+1,updateY, updateZ].update=true;
    }

    if(y-(world.chunkSize*updateY)==0 && updateY!=0){
    world.chunks[updateX,updateY-1, updateZ].update=true;
    }

    if(y-(world.chunkSize*updateY)==15 && updateY!=world.chunks.GetLength(1)-1){
    world.chunks[updateX,updateY+1, updateZ].update=true;
    }

    if(z-(world.chunkSize*updateZ)==0 && updateZ!=0){
    world.chunks[updateX,updateY, updateZ-1].update=true;
    }

    if(z-(world.chunkSize*updateZ)==15 && updateZ!=world.chunks.GetLength(2)-1){
    world.chunks[updateX,updateY, updateZ+1].update=true;
    }

    ReplyDelete
    Replies
    1. At the end of the function UpdateChunkAt() right after the line world.chunks[updateX,updateY, updateZ].update=true;

      Delete
  5. Updating the mesh on a single chunk seems to be pretty laggy

    ReplyDelete
  6. Hi :)
    Thank you for such great tutorials.

    When I try to modify blocks, the ray cast is returning negative values on the y axis:
    "Adding: 18, -4, 16"

    Which in turn generates an error:
    IndexOutOfRangeException: Array index is out of range.
    (wrapper managed-to-managed) object:ElementAddr (object,int,int,int)
    ModifyTerrain.SetBlockAt (Int32 x, Int32 y, Int32 z, Byte block) ...

    Have I made a simple mistake somewhere that would cause this?

    ReplyDelete
    Replies
    1. Make sure your gameobject with world.cs isn't too low, I think it's supposed to be placed at 0,0,0. The script uses the world coordinates of the ray cast hit to decide what block was there but if the terrain isn't where it expects it to be it returns the wrong position.

      Delete
    2. Thank you so much for replying, and so quickly.
      I had changed something in World.cs where the temporary Chunk object is instantiated. For the life of me, I cannot think why I made the change. Once I fixed it, things started working as expected.

      Delete
    3. This comment has been removed by the author.

      Delete
  7. Hello Alexandros.

    I just wanted to add this comment outside of the reply I accidentally posted above. Thanks a lot for your hard work by the way!

    For anyone who pursues this further than the tutorials and begins working on their own project, I wanted to share a common problem I ran into.

    If you are using a First-Person controller, and attempt to change a block beneath you, the AddBlockCenter function will actually raycast into any graphics you use for your first-person camera controller!
    I did a little digging around, and you can actually change the flags for the controller to ignore raycasting. Just a helpful tip!

    ReplyDelete
  8. Hey Alexandros.

    First off all. Great tutorial. Really like it. I run into a little problem I did not looked further into it quite yet. Will do it after I posted here and if I come up with a soultion I will post it. Would be great though if you have an answer aswell incase I should not find it.

    Ok following problem: When rendering the chunks it creates copy of the Original GameObject which is intended. However the original GameObject stays intact but is dispositioned

    http://puu.sh/8GPER.jpg

    as you can see there are blocks in other blocks just a bit off to the side. So after I move the original gameobject that gets copied a bit away it looks as intented

    http://puu.sh/8GPMx.jpg


    ReplyDelete
    Replies
    1. Sorry for the name "Unknown" never commented on Blogspot before ;)

      Delete
    2. Ok. Found an dirty work arround. I moved the chunk back to 0,0,10. Now it gets rendered in the distance. No fix but a dirty workarround http://puu.sh/8GQ1G.png

      Delete
  9. I know that this tutorial is a bit old but im still following it. Im able to add and remove voxels, but only in a diagonal line from the origin. The grid is also not inline for me and I cant find where to put:
    chunks[x,y,z]=Instantiate(chunk,
    new Vector3(x*chunkSize-0.5f,y*chunkSize+0.5f,z*chunkSize-0.5f),
    new Quaternion(0,0,0,0)) as Chunk;

    Help is needed :X
    Thanks!

    ReplyDelete
  10. Hello,

    i am running into a problem during this part.

    When i try to place or remove blocks. i get an error. i have already added the modifyterrain script to the world object, so thats no fix for my problem.

    error:

    NullReferenceException: Object reference not set to an instance of an object
    ModifyTerrain.SetBlockAt (Int32 x, Int32 y, Int32 z, Byte block) (at Assets/Scripts/ModifyTerrain.cs:125)
    ModifyTerrain.SetBlockAt (Vector3 position, Byte block) (at Assets/Scripts/ModifyTerrain.cs:115)
    ModifyTerrain.ReplaceBlockAt (RaycastHit hit, Byte block) (at Assets/Scripts/ModifyTerrain.cs:94)
    ModifyTerrain.ReplaceBlockCursor (Byte block) (at Assets/Scripts/ModifyTerrain.cs:68)
    ModifyTerrain.Update () (at Assets/Scripts/ModifyTerrain.cs:21)

    code chunk from error:

    public void SetBlockAt(int x, int y, int z, byte block) {
    //adds the specified block at these coordinates

    print("Adding: " + x + ", " + y + ", " + z);


    --> world.data[x,y,z]=block;
    UpdateChunkAt(x,y,z);

    }

    the line marked with the arrow should be faulty according to the log.

    hope you can help me with this.

    ReplyDelete
    Replies
    1. In your Start() function, did you actually set the world variable to anything? It should be "world = gameObject.GetComponent();"

      Delete
  11. This comment has been removed by the author.

    ReplyDelete
  12. does this also work with the marching cubes algorithm?

    ReplyDelete
  13. I wonder if it is possible to use this with custom meshes, not just blocks. Such as halfblocks, stairs, ect. or even more complex meshes.

    ReplyDelete
  14. This comment has been removed by the author.

    ReplyDelete