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 theUInode 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_Cameranode 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 thexaxis. *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_Worldfrom 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_readyAll we are doing here is getting theView_Cameranode and assigned it toview_cam, and getting the nodepath_to_voxel_worldpoints to and assigning it tovoxel_world. ### Going Through_processFirst, 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 calledspeedand set its value toNORMAL_SPEED. Then we check to see if the shift key is pressed/down, and if it is we setspeedtoSHIFT_SPEED. Next we make another new variable calleddirand assign it to a empty Vector3.dirwill store the direction the player wants to move towards. Then, based on which keys are pressed, we change the values indiron the axis the key that is pressed should move towards. For example, if theWkey orup arrowkey is pressed, then we setdir.zto-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 changeddiras needed, we then normalizedirby calling thenormalizefunction. 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 byspeedanddeltaso the movement will only be as fast asspeedand 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 byspeedanddeltaso the movement will only be as fast asspeedand 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 byspeedanddeltaso the movement will only be as fast asspeedand 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_downis 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_downtotrue, since it has now been pressed. We then make a new instance of whatever scenephysics_object_scenepoints 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_downtofalse, so we can detect when the control key is first pressed. ### Going Through_physics_processFirst we check to see ifdo_raycastis equal totrue. If it is, we then setdo_raycasttofalse, 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 thetoandfrompositions 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 ifraycastis notnull. We do this withif (raycast), which will returntrueso long asraycastis notnull. If the raycast hit something andresultis notnull, then we check to see if we are adding a voxel, or removing one. We do this by checking to see ifmode_remove_voxelisfalse. If it is, then we are adding a voxel, not removing one. If we are adding a voxel,mode_remove_voxelisfalse, then we callset_world_voxelinvoxel_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_voxelinto a string usingget_voxel_int_from_stringinvoxel_world. If we are removing a voxel,mode_remove_voxelistrue, then we callset_world_voxelinvoxel_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 passnullso the voxel at the raycast collision position is removed. ### Going Through_unhandled_input!!! NOTE: Note_unhandled_inputis 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_inputwill not receive the event, while_inputalways 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_camon thexaxis relative to the vertical,y, motion of the mouse. We also rotate thePlayer_Cameraspatial node on theyaxis relative to the horizontal,x, motion of the mouse. !!! TIP: Tip We multiply both of these rotations byMOUSE_SENSITIVITYand-1.MOUSE_SENSITIVITYchanges how much rotation the mouse motion will apply to the camera, while the-1inverts 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 thexaxis inview_camso the camera cannot rotate upside down. To do this, we get therotation_degreesvector fromview_cam, and then use theclampfunction to clamp the rotation on thexaxis from-MIN_MAX_ANGLEtoMIN_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 thepressedvariable in the event istrue.pressedwill only betrueif a mouse button was just pressed.pressedbecomesfalsewhen 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_raycasttotrue, and setmode_remove_voxeltofalseso a new voxel will be placed at the raycast position. If the middle mouse button is pressed, we setdo_raycasttotrue, and setmode_remove_voxeltotrueso 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_Cameraagain. You should find there are now two exposedScript Variablesfields. In thePath To Voxel Worldfield, assign it toVoxel_World, and in thePhysics Object Scenefield, assign it to theRigid_Sphere.tscn(res://->Rigid_Sphere.tscn) scene. !!! NOTE: Note Feel free to take a look at howRigid_Sphere.tscnis setup if you want! It has a simple embedded script that will push the sphere forward on the negativezaxis, and after a certain amount of time,6seconds 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_Labelnode, which is a child of theUInode inPlayer_Cameraand 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 Scripttoggle, 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 theEngineclass, converting it into a string, and then making the Label node'stextdisplay 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 theDebuggertab 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_Inventorynode, which is a child of theUInode inPlayer_Camera. Make a new script calledVoxel_Inventory.gd, and save it in theUI_Assetsfolder if you want. OnceVoxel_Inventory.gdis 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_Cameranode will place. *player: A variable to hold a reference to thePlayer_Cameranode. *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_readyFirst we check to see ifpath_to_playerisnull. If it is, then we simply justreturn. Ifpath_to_playeris notnull, then we get the node it points to and assign it to theplayervariable for later use. ### Going Throughchange_player_voxelThis 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_playervariable isnull. If it is, then we simplyreturn. Next, we set the player'scurrent_voxelvariable to the passed in voxel name,new_voxel_name. Then we check to see iflast_pressed_buttonis not equal tonull. If it is not, then we call thedeselectfunction inlast_button_pressedso the button can do whatever it needs to do when it is not selected. Finally, we assignlast_pressed_buttonto the passed in button,button, and then we call it'sselectfunction 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_voxelwhen they are pressed. Select one of theVoxel_Buttonnodes and make a new script calledVoxel_Inventory_Button.gd. Save it in theUI_Assetsfolder 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_readyFirst we connect thepressedsignal from the Button node to theon_pressfunction. 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_selectedistrue. If it is, then we tellVoxel_Inventorythat this button is/was selected. We then call theselectfunction so the button can do whatever it needs to do when it is selected. !!! NOTE: Note We are making a assumption thatVoxel_Inventorywill 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_selectedisfalse, then we just calldeselect, so the button can do whatever it needs to do when deselected. ### Going Throughon_pressAll we are doing here is calling thechange_player_voxelfunction inVoxel_Inventoryand passing invoxel_nameas the name of the voxel we want to change to, andselfas the button. ### Going ThroughselectAll we are doing here is making theSelect_Texturenode visible, so there is a green boarder around the button. ### Going ThroughdeselectLike withselect, all we are doing here is making theSelect_Texturenode 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_Inventorynode and assign thePath To Playerscript variable so that it points toPlayer_Camera. Then select each of theVoxel_Buttonnodes, and set theVoxel NameandStart Selectedproperties 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.htmlorLICENSE.pdffor details on attribution. **You must include attributions to use some/all of the assets provided**. See theLICENSEfiles 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 
