Let's place and remove blocks in our world!
But first, you have to walk on your world!
- We need a player controller :
Go to Asset > Characters and install the asset. You can disable ThirdPersonCharacter and RollerBall because we won't use them.
If you don't have Unity's Standard Assets, you can't get them here. Select your version and in the downloads list you can find Standard Assets.
- In your project folder, go to :
Standard Assets > Characters > FirstPersonCharacter > Prefab, and add "FPS Controller" to your game.
Make sure it is placed at 0,200,0. So it will spawn above the Grass layer.
You can also delete the Main Camera object or it will cause errors.
But if you test your game, you'll see that the character falls through the world. We didn't set a collision to the terrain!
- Go to your Chunk script, in the GeneratePhysicalChunk method and add a new component to the chunk object :
Now the collider will have the shape of the terrain. We will give the terrain mesh to the MeshCollider :
- There are two things to fix before we start :
1 - The camera clipping :
Go to the camera object in your FPSController object.
In the Camera component, there are two values called "Clipping planes".
Set the "Near" value to 0.01. It will bring the camera closer to the player and won't start in the wall.
2 - The player size :
You may have noticed that the player is too big or too tall to walk between blocks. We want our player to be 1 block large and 2 blocks tall.
To do that, go to your "FPSController" object and in the "Character Controller" component change Radius to 0.3 and Height to 1.5.
Also, don't forget that these values in play mode won't be saved when you exit your test.
If you want more realistic jumps, closer to Minecraft, go to the "First Person Controller" component and set the Gravity Multiplier to 3.
Now we can change our world!
- To see what we are doing, let's add a Crosshair.
Go to the Hierarchy window and right click.
Then UI > Image and in the "Source image", add your crosshair sprite.
You can change it's size in the "Scale" variables.
- Now create a new script called "World_Interaction". It will be given to the player, the FPSController object.
This is where we will add and remove blocks. To do that, we will cast a ray in front of the player and check if it hits a block.
- Let's add a variable :
We will need an object called "Selector". It will appear on the block we are looking at.
- Then, we will use a Raycast in front of the player :
Let met explain :
if (Physics.Raycast(Camera.main.transform.position, Camera.main.transform.forward, out hit, 10f))
If we have a collision, when we cast a ray from the camera, forward to 10 blocks max and store the informations int the "hit" variable.
Vector3 hitBlock;
This is the final position of the block we hit.
hitBlock = hit.point - hit.normal / 2.0f;
We take the center of the position, the center of the block we hit.
hitBlock.x = (int)(Mathf.Round(hitBlock.x));
hitBlock.y = (int)(Mathf.Round(hitBlock.y));
hitBlock.z = (int)(Mathf.Round(hitBlock.z));
We round the position to have an exact block position with ints for each of the axis.
selector.transform.position = hitBlock;
Finally, we place the selector at the block position.
else
{
selector.transform.position = new Vector3(0,-5000,0);
}
If we don't hit anything, we hide the selector far under the world
- Now go back to Unity and let's create the Selector :
First, we will need a material.
Project folder > Create > Material
I created a really basic one called "Selector". It is transparent (I selected "transparent" in the rendering mode and lowered the alpha in the color).
- Then, in the Hierarchy, create a new Cube called "Selector" and drag and drop the new material.
Set it's scale to 1.01, 1.01, 1.01 to make it just a bit bigger than cubes. Otherwise, it will glitch inside the cubes.
Also remove it's Cube Collider.
- The last thing to do is ignore raycast on the player. Because the camera will cast a ray that will collide with the player object.
Go to your Player object and set it's layer to "Ignore Raycast".
- Give your Selector to the Interaction script in the player object :
- Now you can test your game :
The selector should move to the block in front of the player.
- Before we add the Place/Remove system, i created a new Script called "Debug". It is attached to a UI Text object and gives the seed of the map. It can be used to display informations you need and test your game :
- And make your seed variable from the Noise script Public :
- Go back to the World_Interaction script :
We also need the block placing position, the block going out of the block we are looking at :
We now have the "placeBlock" position, it will be where we can place a block.
- Now let's create a new method called "Interaction", it will take care of what happens when we click :
To let it know where to place or remove blocks, we will give it the hit block position, the placing position and the raycast hit informations.
- We call it in the raycast, if something is found. Meaning that when we hit the world, we will start the method that manage the clicks and what to do with it :
- Go back to this method. We will manage the Block removing system :
If there is an input, and it's the button 0 of the mouse.
There are 3 states for the mouse.
GetMouseButton : The button is pressed and we are holding it.
GetMouseButtonDown : The button is pressed but as soon as it is pressed. Once only.
GetMouseButtonUp : When we release the mouse button.
There are also 3 buttons :
0 : Left Click
1 : Right Click
2 : Wheel
- So, what will happen if we use the Left mouse button?
We will get two values :
- First, the hit block position - the chunk we hit with the raycast gives the position of the block locally in the chunk.
- Also, the chunk at the position we hit with the raycast.
- Then, we will have to set the block at this position to "Air" in the chunk map. Then draw the chunk again. But before doing it, we will clear the chunk.
Let's create a new method called "ClearChunk" :
It will take the chunk object and remove the MeshFilter, MeshRenderer and MeshCollider because we add a new one each time we generate the physical chunk.
- We also create another method called "ClearChunk" inside the Chunk script and call it :
It clears every list and we will add new blocks values. Here every one we had but the one we remove.
- Go back to the World_Interaction script :
Let's clear the chunk before generating the new one
- The position we are looking at will be replaced by air in the chunk map :
- Finally, we draw the chunk again with the new values :
- Now you can test your game :
You'll notice that you can remove blocks but neighbours won't be updated when we do that.
- Let's update our neighbours. Go to your World script, we will add a new method called "GetNeighbour" that will give us the neighbour of a chunk :
I tried to make it easy.
When we call the method, we give the chunk we are in, and the neighbour we want.
This is how it works :
- First, we take the position of the chunk we are currently in. We store it in a variable.
-Then, for the side we want, we calculate the neighbour position and give back the neighbour stored at this position.
- Now, in the World_Interaction script, we call a new method called "UpdateClosestNeighbours" :
We give the position of our block and the chunk we are in.
It will check, for each axis, if we are at the start of the chunk or at the end and if it is the case, it will call another method called "RegenerateNeigbour" to Update the neighbour in this axis.
It will be done for each axis because for the same block, we can have multiple neighbours to update.
This is the RegenerateNeigbour method :
If we have a neighbour where we looked at, it we do exactly what we have done before. It will clear the chunk and recreate it.
- Now you can test your game again and it should work perfectly fine between chunks.
- Let's place blocks now.
We will do the same but with Mouse button 1 (Right click). But when we place a block, it is possible that it get's placed in another chunk. Remember, it comes out of the block we are looking at.
If it is in a neighbour, we can't take the actual chunk and place a block, we have to do it in the neighbour. So, in order to make it work, we will create a really usefull method called "GetBlockInWorld", in the World script :
This method will find the chunk at the position we want to check, and then convert the block world position to a local position inside of it. Then it returns the block at this position in the chunk.
We will also create another method called "GetBlockMapPosition" which is almost the same but will return a Vector3.
We can't get a block and replace it directly, we have to set a value in a chunk map and this method will give back the chunk map position :
- Go back to your World_Interaction script :
Actually, the first method, "GetBlockInWorld" won't be used, it is used to compare a block but all we need right now is the position in a chunk map. We have it and it can be used to create doors, checking the block above or bellow. Same for cactus and more.
Here, this is what we are doing :
- We find the chunk where we are placing a block.
- We clear this chunk to work with is.
- Now we find the block position in the chunk map. (Remember that air blocks are still real blocks)
- We replace it by Stone, still in the map.
- We regenerate the chunk and the neighbours.
- Test your game once again :
As you can see, we can place blocks in our world.
- There is one last thing i want to show you :
You've probably noticed that we can place blocks on the player and get stuck. We can fix this and it will be usefull.
What we will do is compare the player position in the world and we can use it to know if he is underwater, on top of a block, manage step sounds.
- Let's add two variables :
Remember that the player is two blocks high, the "body" part and the "head" part.
- In the Update method, just before the raycast, we will calculate their position :
We place these positions in the world like they were blocks.
- Now go back to the Right Click manager :
We make sure the block placing position is not the body or the head of the player.
- Test your game, you won't be able to place blocks at the player position.