Godot Voxel Terrain Tutorial Part 2
Pre-tutorial note
This tutorial uses Markdeep! Markdeep renders to HTML locally on your web browser and this may take a second, so please be patient! 🙂
**Godot Voxel Terrain Tutorial** # Part overviewIn this part, we'll add the a way to add and remove voxels to our voxel terrain system, along with a way to spawn spheres that will bounce around the terrain. On top of that, we'll make a FPS camera system for moving around the scene. Let's jump right in! # Making the Player Camera First, let's start by making a way to move the camera around the scene. Open up Main_Scene.tscn
(res://
->Main_Scene.tscn
) if it is not already open. You'll find there is a node calledPlayer_Camera
. Go ahead and expand it to show its children, and then select it. If you want, go ahead and make theUI
node visible. Let's go over it's node structure:* Player_Camera
: A Spatial node to hold all of the nodes needed for the player camera. *View_Camera
: The Camera node that is the player's actual view. This is ultimately what renders the view the player sees. *UI
: A Control node to hold all of the nodes needed for the UI. *Crosshair
: A TextureRect node that holds the texture that will help show the center of the screen. *Voxel_Inventory
: A HBoxContainer node to hold all of the buttons that will change the voxel the player can place. *Voxel_Button
-Voxel_Button4
: Buttons that will change the voxel the player can place. *Select_Texture
: A TexturedRect that helps show which of the buttons is the currently selected button. *Sprite
: A Sprite node that shows a image of the voxel that the player will be using when the button is pressed. *FPS_Label
: A label to show the FPS (frames per second) Godot is running at. Now that we looked at how everything for the player camera is setup, let's start writing the code that will allow the player camera to work. ## Making The Main Script Let's start with making the main script. This will control the camera, and will add/remove voxels in the world. Select thePlayer_Camera
node and make a new script calledPlayer_Camera.gd
. Add the following toPlayer_Camera.gd
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
extends Spatial const NORMAL_SPEED = 8; const SHIFT_SPEED = 12; var keyboard_control_down = false; var view_cam; const MOUSE_SENSITIVITY = 0.05; const MIN_MAX_ANGLE = 85; var do_raycast = false; var mode_remove_voxel = false; export (NodePath) var path_to_voxel_world; var voxel_world; var current_voxel = "Cobble"; export (PackedScene) var physics_object_scene; func _ready(): view_cam = get_node("View_Camera"); voxel_world = get_node(path_to_voxel_world); func _process(delta): if (Input.is_mouse_button_pressed(BUTTON_RIGHT) == true): if (Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED): Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED); else: if (Input.get_mouse_mode() != Input.MOUSE_MODE_VISIBLE): Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE); if (Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED): var speed = NORMAL_SPEED; if (Input.is_key_pressed(KEY_SHIFT)): speed = SHIFT_SPEED; var dir = Vector3(0,0,0); if Input.is_key_pressed(KEY_W) or Input.is_key_pressed(KEY_UP): dir.z = -1; if Input.is_key_pressed(KEY_S) or Input.is_key_pressed(KEY_DOWN): dir.z = 1; if Input.is_key_pressed(KEY_A) or Input.is_key_pressed(KEY_LEFT): dir.x = -1; if Input.is_key_pressed(KEY_D) or Input.is_key_pressed(KEY_RIGHT): dir.x = 1; if (Input.is_key_pressed(KEY_SPACE)): dir.y = 1; dir = dir.normalized(); global_translate(view_cam.global_transform.basis.z * dir.z * delta * speed); global_translate(view_cam.global_transform.basis.x * dir.x * delta * speed); global_translate(Vector3(0, 1, 0) * dir.y * delta * speed); if (Input.is_key_pressed(KEY_CONTROL)): if (keyboard_control_down == false): keyboard_control_down = true; var new_obj = physics_object_scene.instance(); new_obj.global_transform = view_cam.global_transform; get_parent().add_child(new_obj); else: keyboard_control_down = false; func _physics_process(delta): if (do_raycast == true): do_raycast = false; var space_state = get_world().direct_space_state; var from = view_cam.project_ray_origin(get_viewport().get_mouse_position()); var to = from + view_cam.project_ray_normal(get_viewport().get_mouse_position()) * 100; var result = space_state.intersect_ray(from, to, [self]); if (result): if (mode_remove_voxel == false): voxel_world.set_world_voxel(result.position + (result.normal/2), voxel_world.get_voxel_int_from_string(current_voxel)); else: voxel_world.set_world_voxel(result.position - (result.normal/2), null); func _unhandled_input(event): if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED: if event is InputEventMouseMotion: view_cam.rotate_x(deg2rad(event.relative.y * MOUSE_SENSITIVITY * -1)) self.rotate_y(deg2rad(event.relative.x * MOUSE_SENSITIVITY * -1)) var camera_rot = view_cam.rotation_degrees camera_rot.x = clamp(camera_rot.x, -MIN_MAX_ANGLE, MIN_MAX_ANGLE) view_cam.rotation_degrees = camera_rot else: if (event is InputEventMouseButton): if (event.pressed == true): if (event.button_index == BUTTON_LEFT): do_raycast = true; mode_remove_voxel = false; if (event.button_index == BUTTON_MIDDLE): do_raycast = true; mode_remove_voxel = true; |
Let's go over how this script works, starting with its class variables: *NORMAL_SPEED
: The speed the player moves at normally when moving. *SHIFT_SPEED
: The speed the player moves at when the shift key is held down. *keyboard_control_down
: A variable for tracking whether the Control key is pressed/down. *view_cam
: A variable for holding the Camera node. *MOUSE_SENSITIVITY
: A variable for defining how sensitive the mouse is. You may need to adjust this value based on the sensitivity of your mouse! *MIN_MAX_ANGLE
: The minimum and maximum angles the player can move the camera, on thex
axis. *do_raycast
: A variable for tracking whether we need to send out a raycast to add/remove a voxel. *mode_remove_voxel
: A variable for determining whether we need to remove a voxel, or add a voxel. If this variable istrue
, we assume that we need to remove a voxel. *path_to_voxel_world
: A exported NodePath to the voxel world. We make this exported so we can set it toVoxel_World
from the editor. *voxel_world
: A variable to hold the node that hasVoxel_World.gd
. *current_voxel
: The name of the currently selected voxel. *physics_object_scene
: A exported PackedScene to hold the physics object we will spawn when the control button is pressed. This is quite a few class variables. Hopefully I explained what each one will do in a way that makes sense, but if you have any questions, feel free to ask in the comments section! Now that we have looked at the class variables, let's take a look at the functions! ### Going Through_ready
All we are doing here is getting theView_Camera
node and assigned it toview_cam
, and getting the nodepath_to_voxel_world
points to and assigning it tovoxel_world
. ### Going Through_process
First, we check to see if the right mouse button is pressed/down. If it is, we then check to see if the current mouse mode is *not*MOUSE_MODE_CAPTURED
. If it is not, then we set the current mouse mode toMOUSE_MODE_CAPTURED
. This is so the player can move around the scene FPS style while holding the right mouse button. If the right mouse button is not pressed/down, then we check to see if the current mouse mode is *not*MOUSE_MODE_VISIBLE
. If it is not, then we set the current mouse mode toMOUSE_MODE_VISIBLE
. ________ Next we check to see if the current mouse mode isMOUSE_MODE_CAPTURED
. We do this because we only want to process the camera movement code if the right mouse button is pressed/held down. If it is, we then make a new variable calledspeed
and set its value toNORMAL_SPEED
. Then we check to see if the shift key is pressed/down, and if it is we setspeed
toSHIFT_SPEED
. Next we make another new variable calleddir
and assign it to a empty Vector3.dir
will store the direction the player wants to move towards. Then, based on which keys are pressed, we change the values indir
on the axis the key that is pressed should move towards. For example, if theW
key orup arrow
key is pressed, then we setdir.z
to-1
, so the player will move in the same direction the camera is facing. !!! TIP: Tip In Godot, the Camera nodes faces the negative Z axis! Once we have checked each of the keys and changeddir
as needed, we then normalizedir
by calling thenormalize
function. Finally, we move the camera according to the direction(s) we need to go. First, we move the player along the Z axis stored in the basis of the global transform of the camera. We multiply this byspeed
anddelta
so the movement will only be as fast asspeed
and will move at the same rate regardless of the frame rate. This will move the camera forward/backwards according to the direction the camera is facing. Next, we move the player along the X axis stored in the basis of the global transform of the camera. We multiply this byspeed
anddelta
so the movement will only be as fast asspeed
and will move at the same rate regardless of the frame rate. This will move the camera left/right according to the direction the camera is facing. Finally, we move the player along the global Y axis, and unlike the others, we always move straight up. We multiply this byspeed
anddelta
so the movement will only be as fast asspeed
and will move at the same rate regardless of the frame rate. This will move the camera up regardless of which direction the camera is facing. ________ The last thing we do is check to see if the control key is pressed. If it is, we then check to see ifkeyboard_control_down
is false. If it is, then that means the control key has just been pressed. If the control key has just been pressed, we setkeyboard_control_down
totrue
, since it has now been pressed. We then make a new instance of whatever scenephysics_object_scene
points to. We then set the newly spawned scene's transform to the transform of the camera, and then we add it as a child of the parent ofPlayer_Camera
. If the control key is not pressed, then we setkeyboard_control_down
tofalse
, so we can detect when the control key is first pressed. ### Going Through_physics_process
First we check to see ifdo_raycast
is equal totrue
. If it is, we then setdo_raycast
tofalse
, so we only send out a single raycast at a time. Then we get the space state and assign it to a variable calledspace_state
, the origin of the ray and assign it to a variable calledfrom
, and the end point of the ray and assign it to a variable calledto
. !!! NOTE: Note Check out the [Raycasting section of the Godot documentation](https://docs.godotengine.org/en/3.0/tutorials/physics/ray-casting.html) for more information on raycasting from code in Godot. Next we get a result from a raycast using theto
andfrom
positions and assign the results of the raycast to a new variable calledresult
. We add the camera as a collision exception so it does not get in the way. We then check to see if the raycast hit anything by checking to see ifraycast
is notnull
. We do this withif (raycast)
, which will returntrue
so long asraycast
is notnull
. If the raycast hit something andresult
is notnull
, then we check to see if we are adding a voxel, or removing one. We do this by checking to see ifmode_remove_voxel
isfalse
. If it is, then we are adding a voxel, not removing one. If we are adding a voxel,mode_remove_voxel
isfalse
, then we callset_world_voxel
invoxel_world
. We pass in the position where the raycast hit,position
, and we add half of the normal so the position passed is sticking slightly out of the voxel that the raycast collided with. For the voxel we are placing, we convertcurrent_voxel
into a string usingget_voxel_int_from_string
invoxel_world
. If we are removing a voxel,mode_remove_voxel
istrue
, then we callset_world_voxel
invoxel_world
. We pass in the position where the raycast hit,position
, and then we subtract half of the normal so the position passed is sticking into the voxel that the raycast collided with. For the voxel we are placing, we passnull
so the voxel at the raycast collision position is removed. ### Going Through_unhandled_input
!!! NOTE: Note_unhandled_input
is only called when a input event is NOT captured or processed by other nodes. For example, this means that when you click on a UI/GUI node,_unhandled_input
will not receive the event, while_input
always receives input events regardless of whether they have already been processed. First we check to see if the mouse mode isMOUSE_MODE_CAPTURED
. If it is, we then check to see if the event is a InputEventMouseMotion event. We do this because for a FPS view, we need to rotate the camera as the mouse moves. If the event is a InputEventMouseMotion event, we rotateview_cam
on thex
axis relative to the vertical,y
, motion of the mouse. We also rotate thePlayer_Camera
spatial node on they
axis relative to the horizontal,x
, motion of the mouse. !!! TIP: Tip We multiply both of these rotations byMOUSE_SENSITIVITY
and-1
.MOUSE_SENSITIVITY
changes how much rotation the mouse motion will apply to the camera, while the-1
inverts the axis. You may need to change one, or both, of these values according to both your mouse and your personal preferences. Finally, we clamp the rotation on thex
axis inview_cam
so the camera cannot rotate upside down. To do this, we get therotation_degrees
vector fromview_cam
, and then use theclamp
function to clamp the rotation on thex
axis from-MIN_MAX_ANGLE
toMIN_MAX_ANGLE
. We then reapply the, now clamped, rotation toview_cam
. _______ If the mouse is not captured, the mouse mode is notMOUSE_MODE_CAPTURED
, we check to see if the input event is a InputEventMouseButton event. A InputEventMouseButton event is called whenever the mouse has a button pressed or released. If the event is a InputEventMouseButton event, then we check to see if thepressed
variable in the event istrue
.pressed
will only betrue
if a mouse button was just pressed.pressed
becomesfalse
when the button is held or released. If the mouse button was just pressed, we then check to see if the left mouse button was pressed,BUTTON_LEFT
, or the middle mouse button,BUTTON_MIDDLE
. If the left mouse button is pressed, we setdo_raycast
totrue
, and setmode_remove_voxel
tofalse
so a new voxel will be placed at the raycast position. If the middle mouse button is pressed, we setdo_raycast
totrue
, and setmode_remove_voxel
totrue
so the voxel at the raycast position will be removed. ## Final touches Now that we have finished the player camera script, there is a couple more things we need to do before we can move on to the UI. In the scene inspector, select another node and then selectPlayer_Camera
again. You should find there are now two exposedScript Variables
fields. In thePath To Voxel World
field, assign it toVoxel_World
, and in thePhysics Object Scene
field, assign it to theRigid_Sphere.tscn
(res://
->Rigid_Sphere.tscn
) scene. !!! NOTE: Note Feel free to take a look at howRigid_Sphere.tscn
is setup if you want! It has a simple embedded script that will push the sphere forward on the negativez
axis, and after a certain amount of time,6
seconds by default, it will destroy itself.With that done, go ahead and run the scene! You should find that you can add voxels by pressing the left mouse button, remove voxels with the middle mouse button, and if you hold down the right mouse button, you can move around the scene in a first person style. If you press the control key on your keyboard, you'll find you create a red sphere that bounces around the enviroment for awhile before destroying itself. # Making a FPS label Next, let's add a simple label that will show how many FPS (Frames Per Second) the game is running at. This is helpful for seeing how fast the game is running, and it is really simple and easy to add. Select the FPS_Label
node, which is a child of theUI
node inPlayer_Camera
and make a new script. Personally, I have just embedded the script into the scene, but feel free to save it to a file if you want! If you want to embed a script into a scene file, just enable theBuild-in Script
toggle, like in the picture below.Regardless of how you save the script, once the script editor is open, add the following code:
1 2 3 4 |
extends Label func _process(delta): text = "FPS:" + str(Engine.get_frames_per_second()); |
All this script is doing is getting the current FPS from theEngine
class, converting it into a string, and then making the Label node'stext
display the FPS. Now when you run the game, in the top left corner of the screen you will be able to see the FPS the game is currently running at! This is handy for quickly seeing if the game is running slow without having to have theDebugger
tab in the Godot editor open. # Adding The Voxel Inventory The last thing we're going to do for this tutorial part is add a way to change the voxels that is placed when the game is running. We will be making a 'inventory' of sorts. It will not be a true inventory, as we cannot add/remove to it from run time, but it will give us a way to change voxels. ## Making The Main Inventory First, let's work on the main script that will run the inventory. Select theVoxel_Inventory
node, which is a child of theUI
node inPlayer_Camera
. Make a new script calledVoxel_Inventory.gd
, and save it in theUI_Assets
folder if you want. OnceVoxel_Inventory.gd
is open, add the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
extends HBoxContainer export (NodePath) var path_to_player; var player; var last_pressed_button = null; func _ready(): if (path_to_player == null): return; player = get_node(path_to_player); func change_player_voxel(new_voxel_name, button): if (path_to_player == null): return player.current_voxel = new_voxel_name; if (last_pressed_button != null): last_pressed_button.deselect(); last_pressed_button = button; last_pressed_button.select(); |
Let's go over how this script works, starting with its class variables: *path_to_player
: A exported NodePath to the player camera. This is so we can change the voxel thePlayer_Camera
node will place. *player
: A variable to hold a reference to thePlayer_Camera
node. *last_pressed_button
: A variable to hold a reference to the last pressed button in the inventory. We need this so we can deselect it when a new button is pressed. Next, let's go through the functions, starting with_ready
. ### Going Through_ready
First we check to see ifpath_to_player
isnull
. If it is, then we simply justreturn
. Ifpath_to_player
is notnull
, then we get the node it points to and assign it to theplayer
variable for later use. ### Going Throughchange_player_voxel
This function will change the player's voxel to the passed in voxel,new_voxel_name
. This function also expects the button that was pressed to send itself as an argument,button
, so we can deselect it when another button is pressed. First, we check to see if thepath_to_player
variable isnull
. If it is, then we simplyreturn
. Next, we set the player'scurrent_voxel
variable to the passed in voxel name,new_voxel_name
. Then we check to see iflast_pressed_button
is not equal tonull
. If it is not, then we call thedeselect
function inlast_button_pressed
so the button can do whatever it needs to do when it is not selected. Finally, we assignlast_pressed_button
to the passed in button,button
, and then we call it'sselect
function so it can do whatever it needs to do when the button is selected. ## Making The Inventory Buttons Now all that is left is making the buttons that will callchange_player_voxel
when they are pressed. Select one of theVoxel_Button
nodes and make a new script calledVoxel_Inventory_Button.gd
. Save it in theUI_Assets
folder if you want, and then add the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
extends TextureButton export (String) var voxel_name; export (bool) var start_selected = false; func _ready(): connect("pressed", self, "on_press"); if (start_selected == true): get_parent().last_pressed_button = self; select(); else: deselect(); func on_press(): get_parent().change_player_voxel(voxel_name, self); func select(): get_node("Select_Texture").visible = true; func deselect(): get_node("Select_Texture").visible = false; |
Let's go through this script, starting with the class variables: *voxel_name
: A exported string to hold the name that this button will change the voxel to when it is pressed. *start_selected
: A exported boolean for setting whether this button should be selected already when the game is started. Next, let's look at the functions, starting with_ready
. ### Going Through_ready
First we connect thepressed
signal from the Button node to theon_press
function. This is the same process as if you connect signals in the Godot editor, but we're just doing in through code instead of through the Godot Editor. Next, we check to see ifstart_selected
istrue
. If it is, then we tellVoxel_Inventory
that this button is/was selected. We then call theselect
function so the button can do whatever it needs to do when it is selected. !!! NOTE: Note We are making a assumption thatVoxel_Inventory
will always be the parent node of the voxels buttons. For this tutorial, this is not a huge deal, but for bigger projects it may be advisable not to make assumptions on how a scene will be setup as thing can change as a game develops. Ifstart_selected
isfalse
, then we just calldeselect
, so the button can do whatever it needs to do when deselected. ### Going Throughon_press
All we are doing here is calling thechange_player_voxel
function inVoxel_Inventory
and passing invoxel_name
as the name of the voxel we want to change to, andself
as the button. ### Going Throughselect
All we are doing here is making theSelect_Texture
node visible, so there is a green boarder around the button. ### Going Throughdeselect
Like withselect
, all we are doing here is making theSelect_Texture
node invisible, so there is no longer a green boarder around the button. # Final touches Now there is only a couple things we need to do, and then the inventory is complete. First, select theVoxel_Inventory
node and assign thePath To Player
script variable so that it points toPlayer_Camera
. Then select each of theVoxel_Button
nodes, and set theVoxel Name
andStart Selected
properties to the proper values for each button. For the included starter asset project, I will be using the following values: *Voxel_Button
:Voxel Name
->Stone
,Start Selected
->False
*Voxel_Button2
:Voxel Name
->Cobble
,Start Selected
->True
*Voxel_Button3
:Voxel Name
->Dirt
,Start Selected
->False
*Voxel_Button4
:Voxel Name
->Grass
,Start Selected
->False
# Final NotesGo ahead and a try running the project, and now when you click the inventory buttons on the bottom, you should find that it will change the voxels you can place when you click with the left mouse button! Now you have a working voxel terrain system that you can add and remove voxels from. Everything for the voxel terrain is created using the SurfaceTool. Hopefully this helps show how powerful the SurfaceTool can be, and now you have a cool Godot voxel terrain system you can use in your projects. There are still several things we could add to this system. If you are looking for ideas, here are some things you could try: * Make it where you can save/load the voxel world to a file. This would allow for sharing terrain across computers! * Using the vertex colors, you could add some simple ambient occlusion to the corners of voxels. * Add a navigation mesh and a navigation agent! In theory you would just need to use the collision mesh to generate a navigation mesh. * Convert this code to C++ and use GDNative for improved performance. Because we are using GDScript, which is fast enough for most cases but struggles a bit here, and the SurfaceTool, which is really intended to only be used on meshes that do not change very often, performance is not quite as good as it could be. By using GDNative and C++, you could probably speed things up considerably. * Add a first person character akin to the main character in MineCraft. * Add a 'real' inventory that can hold varying amounts and types of voxels. !!! TIP: 07/19/2020 Update The tileset texture used for this tutorial may cause gaps between voxels. This is because of the rounding used for calculating the UV coordinates. **To fix this issue, use a tileset texture that is a power of two** (examples: 64x64, 128x128, 256x256, 512x512, etc). Huge thanks to Ian Perez for discovering and sharing this solution! !!! WARNING: Warning If you ever get lost, be sure to read over the code again! If you have any questions, feel free to ask in the comments below! You can access the [finished project here](https://drive.google.com/open?id=1VHVtF6iFoPa24iHUK4t6yNiRfDh13vZ8)! You can also find the downloads for this tutorial series on the [Downloads page](https://randommomentania.com/freebie-downloads/). Please read LICENSE.html
orLICENSE.pdf
for details on attribution. **You must include attributions to use some/all of the assets provided**. See theLICENSE
files for details. !!! TIP: ThanksThanks for going through this tutorial! If you like what we do, **please consider buying one of [our paid games](https://randommomentania.com/paid-games/) or [products](https://randommomentania.com/other-products/) to help support RandomMomentania!** Have a suggestion for a future tutorial? Let us know either in the comments section below, or by following the instructions on our [Contact and FAQ](https://randommomentania.com/contact/) page! © RandomMomentania 2019