Part 6¶
Part Overview¶
In this part we’re going to add a main menu and pause menu, add a respawn system for the player, and change/move the sound system so we can use it from any script.
This is the last part of the FPS tutorial, by the end of this you will have a solid base to build amazing FPS games with Godot!
Note
You are assumed to have finished Part 5 before moving on to this part of the tutorial.
The finished project from Part 4 will be the starting project for part 6
Let’s get started!
Making the Globals
singleton¶
Now, for this all to work we need to create the Globals
singleton. Make a new script in the Script
tab and call it Globals.gd
.
Add the following to Globals.gd
.
extends Node
var mouse_sensitivity = 0.08
var joypad_sensitivity = 2
func _ready():
pass
func load_new_scene(new_scene_path):
get_tree().change_scene(new_scene_path)
As you can see, it’s quite small and simple. As this part progresses we will
keeping adding complexities to Global.gd
, but for now all it is doing is holding two variables for us, and abstracting how we change scenes.
mouse_sensitivity
: The current sensitivity for our mouse, so we can load it inPlayer.gd
.joypad_sensitivity
: The current sensitivity for our joypad, so we can load it inPlayer.gd
.
Right now all we’re using Globals.gd
for is a way to carry variables across scenes. Because the sensitivity for our mouse and joypad are
stored in Globals.gd
, any changes we make in one scene (like Main_Menu
) effect the sensitivity for our player.
All we’re doing in load_new_scene
is calling SceneTree’s change_scene
function, passing in the scene path given in load_new_scene
.
That’s all of the code needed for Globals.gd
right now! Before we can test the main menu, we first need to set Globals.gd
as an autoload script.
Open up the project settings and click the AutoLoad
tab.
Then select the path to Globals.gd
in the Path
field by clicking the button beside it. Make sure the name in the Node Name
field is Globals
. If you
have everything like the picture above, then press Add
!
This will make Globals.gd
a singleton/autoload script, which will allow us to access it from anywhere in any scene.
Tip
For more information on singleton/autoload scripts, see Singletons (AutoLoad).
Now that Globals.gd
is a singleton/autoload script, you can test the main menu!
You may also want to change the main scene from Testing_Area.tscn
to Main_Menu.tscn
so when we export the game we start at the main menu. You can do this
through the project settings, under the General
tab. Then in the Application
category, click the Run
subcategory and you can change the main scene by changing
the value in Main Scene
.
Warning
You’ll have to set the paths to the correct files in Main_Menu
in the editor before testing the main menu!
Otherwise you will not be able to change scenes from the level select menu/screen.
Starting the respawn system¶
Since our player can lose all their health, it would be ideal if our players died and respawned too, so let’s add that!
First, open up Player.tscn
and expand HUD
. Notice how there’s a ColorRect called Death_Screen
.
When the player dies, we’re going to make Death_Screen
visible, and show them how long they have to wait before they’re able to respawn.
Open up Player.gd
and add the following global variables:
const RESPAWN_TIME = 4
var dead_time = 0
var is_dead = false
var globals
RESPAWN_TIME
: The amount of time (in seconds) it takes to respawn.dead_time
: A variable to track how long the player has been dead.is_dead
: A variable to track whether or not the player is currently dead.globals
: A variable to hold theGlobals.gd
singleton.
We now need to add a couple lines to _ready
, so we can use Globals.gd
. Add the following to _ready
:
globals = get_node("/root/Globals")
global_transform.origin = globals.get_respawn_position()
Now we’re getting the Globals.gd
singleton and assigning it to globals
. We also set our global position
using the origin from our global Transform to the position returned by globals.get_respawn_position
.
Note
Don’t worry, we’ll add get_respawn_position
further below!
Next we need to make a few changes to physics_process
. Change physics_processing
to the following:
func _physics_process(delta):
if !is_dead:
process_input(delta)
process_view_input(delta)
process_movement(delta)
if (grabbed_object == null):
process_changing_weapons(delta)
process_reloading(delta)
process_UI(delta)
process_respawn(delta)
Now we’re not processing input or movement input when we’re dead. We’re also now calling process_respawn
, but we haven’t written
process_respawn
yet, so let’s change that.
Let’s add process_respawn
. Add the following to Player.gd
:
func process_respawn(delta):
# If we just died
if health <= 0 and !is_dead:
$Body_CollisionShape.disabled = true
$Feet_CollisionShape.disabled = true
changing_weapon = true
changing_weapon_name = "UNARMED"
$HUD/Death_Screen.visible = true
$HUD/Panel.visible = false
$HUD/Crosshair.visible = false
dead_time = RESPAWN_TIME
is_dead = true
if grabbed_object != null:
grabbed_object.mode = RigidBody.MODE_RIGID
grabbed_object.apply_impulse(Vector3(0,0,0), -camera.global_transform.basis.z.normalized() * OBJECT_THROW_FORCE / 2)
grabbed_object.collision_layer = 1
grabbed_object.collision_mask = 1
grabbed_object = null
if is_dead:
dead_time -= delta
var dead_time_pretty = str(dead_time).left(3)
$HUD/Death_Screen/Label.text = "You died\n" + dead_time_pretty + " seconds till respawn"
if dead_time <= 0:
global_transform.origin = globals.get_respawn_position()
$Body_CollisionShape.disabled = false
$Feet_CollisionShape.disabled = false
$HUD/Death_Screen.visible = false
$HUD/Panel.visible = true
$HUD/Crosshair.visible = true
for weapon in weapons:
var weapon_node = weapons[weapon]
if weapon_node != null:
weapon_node.reset_weapon()
health = 100
grenade_amounts = {"Grenade":2, "Sticky Grenade":2}
current_grenade = "Grenade"
is_dead = false
Let’s go through what this function is doing.
First we check to see if we just died by checking to see if health
is equal or less than 0
and is_dead
is false
.
If we just died, we disable our collision shapes for the player. We do this to make sure we’re not blocking anything with our dead body.
We next set changing_weapon
to true
and set changing_weapon_name
to UNARMED
. This is so if we are using a weapon, we put it away
when we die.
We then make the Death_Screen
ColorRect visible so we get a nice grey overlay over everything. We then make the rest of the UI,
the Panel
and Crosshair
nodes, invisible.
Next we set dead_time
to RESPAWN_TIME
so we can start counting down how long we’ve been dead. We also set is_dead
to true
so we know we’ve died.
If we are holding an object when we died, we need to throw it. We first check to see if we are holding an object or not. If we are, we then throw it, using the same code as the throwing code we added in Part 5.
Then we check to see if we are dead. If we are, we then remove delta
from dead_time
.
We then make a new variable called dead_time_pretty
, where we convert dead_time
to a string, using only the first three characters starting from the left. This gives
us a nice looking string showing how much time we have left to wait before we respawn.
We then change the Label in Death_Screen
to show how much time we have left.
Next we check to see if we’ve waited long enough and can respawn. We do this by checking to see if dead_time
is 0
or less.
If we have waited long enough to respawn, we set the player’s position to a new respawn position provided by get_respawn_position
.
We then enable both of our collision shapes so the player can collide with the environment.
Next we make the Death_Screen
invisible and make the rest of the UI, the Panel
and Crosshair
nodes, visible again.
We then go through each weapon and call it’s reset_weapon
function. We’ll add reset_weapon
soon.
Then we reset health
to 100
, grenade_amounts
to it’s default values, and change current_grenade
to Grenade
.
Finally, we set is_dead
to false
.
Before we leave Player.gd
, we need to add one quick thing to _input
. Add the following at the beginning of _input
:
if is_dead:
return
Now when we’re dead we cannot look around with the mouse.
Finishing the respawn system¶
First let’s open Weapon_Pistol.gd
and add the reset_weapon
function. Add the following:
func reset_weapon():
ammo_in_weapon = 10
spare_ammo = 20
Now when we call reset_weapon
, the ammo in our weapon and the ammo in the spares will be reset to their default values.
Now let’s add reset_weapon
in Weapon_Rifle.gd
:
func reset_weapon():
ammo_in_weapon = 50
spare_ammo = 100
And add the following to Weapon_Knife.gd
:
func reset_weapon():
ammo_in_weapon = 1
spare_ammo = 1
Now our weapons will reset when we die.
Now we need to add a few things to Globals.gd
. First, add the following global variable:
var respawn_points = null
respawn_points
: A variable to hold all of the respawn points in a level
Because we’re getting a random spawn point each time, we need to randomize the number generator. Add the following to _ready
:
randomize()
randomize
will get us a new random seed so we get a (relatively) random string of numbers when we using any of the random functions.
Now let’s add get_respawn_position
to Globals.gd
:
func get_respawn_position():
if respawn_points == null:
return Vector3(0, 0, 0)
else:
var respawn_point = rand_range(0, respawn_points.size()-1)
return respawn_points[respawn_point].global_transform.origin
Let’s go over what this function does.
First we check to see if we have any respawn_points
by checking to see if respawn_points
is null
or not.
If respawn_points
is null
, we return a position of empty Vector 3 with the position (0, 0, 0)
.
If respawn_points
is not null
, we then get a random number between 0
and the number of elements we have in respawn_points
, minus 1
since
most programming languages (including GDScript
) start counting from 0
when you are accessing elements in a list.
We then return the position of the Spatial node at respawn_point
position in respawn_points
.
Before we’re done with Globals.gd
. We need to add the following to load_new_scene
:
respawn_points = null
We set respawn_points
to null
so when/if we get to a level with no respawn points, we do not respawn
at the respawn points in the level prior.
Now all we need is a way to set the respawn points. Open up Ruins_Level.tscn
and select Spawn_Points
. Add a new script called
Respawn_Point_Setter.gd
and attach it to Spawn_Points
. Add the following to Respawn_Point_Setter.gd
:
extends Spatial
func _ready():
var globals = get_node("/root/Globals")
globals.respawn_points = get_children()
Now when a node with Respawn_Point_Setter.gd
has it’s _ready
function called, all of the children
nodes of the node with Respawn_Point_Setter.gd
, Spawn_Points
in the case of Ruins_Level.tscn
, will be added
to respawn_points
in Globals.gd
.
Warning
Any node with Respawn_Point_Setter.gd
has to be above the player in the SceneTree so the respawn points are set
before the player needs them in the player’s _ready
function.
Now when you die you’ll respawn after waiting 4
seconds!
Note
No spawn points are already set up for any of the levels besides Ruins_Level.tscn
!
Adding spawn points to Space_Level.tscn
is left as an exercise for the reader.
Writing a sound system we can use anywhere¶
Finally, lets make a sound system so we can play sounds from anywhere, without having to use the player.
First, open up SimpleAudioPlayer.gd
and change it to the following:
extends Spatial
var audio_node = null
var should_loop = false
var globals = null
func _ready():
audio_node = $Audio_Stream_Player
audio_node.connect("finished", self, "sound_finished")
audio_node.stop()
globals = get_node("/root/Globals")
func play_sound(audio_stream, position=null):
if audio_stream == null:
print ("No audio stream passed, cannot play sound")
globals.created_audio.remove(globals.created_audio.find(self))
queue_free()
return
audio_node.stream = audio_stream
# If you are using a AudioPlayer3D, then uncomment these lines to set the position.
# if position != null:
# audio_node.global_transform.origin = position
audio_node.play(0.0)
func sound_finished():
if should_loop:
audio_node.play(0.0)
else:
globals.created_audio.remove(globals.created_audio.find(self))
audio_node.stop()
queue_free()
There’s several changes from the old version, first and foremost being we’re no longer storing the sound files in SimpleAudioPlayer.gd
anymore.
This is much better for performance since we’re no longer loading each audio clip when we create a sound, but instead we’re forcing a audio stream to be passed
in to play_sound
.
Another change is we have a new global variable called should_loop
. Instead of just destroying the audio player every time it’s finished, we instead want check to
see if we are set to loop or not. This allows us to have audio like looping background music without having to spawn a new audio player with the music
when the old one is finished.
Finally, instead of being instanced/spawned in Player.gd
, we’re instead going to be spawned in Globals.gd
so we can create sounds from any scene. We now need
to store the Globals.gd
singleton so when we destroy the audio player, we also remove it from a list in Globals.gd
.
Let’s go over the changes.
For the global variables
we removed all of the audio_[insert name here]
variables since we will instead have these passed in to.
We also added two new global variables, should_loop
and globals
. We’ll use should_loop
to tell whether we want to loop when the sound has
finished, and globals
will hold the Globals.gd
singleton.
The only change in _ready
is now we’re getting the Globals.gd
singleton and assigning it to globals
In play_sound
we now expect a audio stream, named audio_stream
, to be passed in, instead of sound_name
. Instead of checking the
sound name and setting the stream for the audio player, we instead check to make sure an audio stream was passed in. If a audio stream is not passed
in, we print an error message, remove the audio player from a list in the Globals.gd
singleton called created_audio
, and then free the audio player.
Finally, in sound_finished
we first check to see if we are supposed to loop or not using should_loop
. If we are supposed to loop, we play the sound
again from the start of the audio, at position 0.0
. If we are not supposed to loop, we remove the audio player from a list in the Globals.gd
singleton
called created_audio
, and then free the audio player.
Now that we’ve finished our changes to SimpleAudioPlayer.gd
, we now need to turn our attention to Globals.gd
. First, add the following global variables:
# ------------------------------------
# All of the audio files.
# You will need to provide your own sound files.
var audio_clips = {
"pistol_shot":null, #preload("res://path_to_your_audio_here!")
"rifle_shot":null, #preload("res://path_to_your_audio_here!")
"gun_cock":null, #preload("res://path_to_your_audio_here!")
}
const SIMPLE_AUDIO_PLAYER_SCENE = preload("res://Simple_Audio_Player.tscn")
var created_audio = []
# ------------------------------------
Lets go over these global variables.
audio_clips
: A dictionary holding all of the audio clips we can play.SIMPLE_AUDIO_PLAYER_SCENE
: The simple audio player scene.created_audio
: A list to hold all of the simple audio players we create
Note
If you want to add additional audio, you need to add it to audio_clips
. No audio files are provided in this tutorial,
so you will have to provide your own.
One site I’d recommend is GameSounds.xyz. I’m using the Gamemaster audio gun sound pack included in the Sonniss’ GDC Game Audio bundle for 2017. The tracks I’ve used (with some minor editing) are as follows:
- gun_revolver_pistol_shot_04,
- gun_semi_auto_rifle_cock_02,
- gun_submachine_auto_shot_00_automatic_preview_01
Now we need to add a new function called play_sound
to Globals.gd
:
func play_sound(sound_name, loop_sound=false, sound_position=null):
if audio_clips.has(sound_name):
var new_audio = SIMPLE_AUDIO_PLAYER_SCENE.instance()
new_audio.should_loop = loop_sound
add_child(new_audio)
created_audio.append(new_audio)
new_audio.play_sound(audio_clips[sound_name], sound_position)
else:
print ("ERROR: cannot play sound that does not exist in audio_clips!")
Let’s go over what this script does.
First we check to see if we have a audio clip with the name sound_name
in audio_clips
. If we do not, we print an error message.
If we do have a audio clip with the name sound_name
, we then instance/spawn a new SIMPLE_AUDIO_PLAYER_SCENE
and assign it to new_audio
.
We then set should_loop
, and add new_audio
as a child of Globals.gd
.
Note
Remember, we have to be careful adding nodes to a singleton, since these nodes will not be destroyed when changing scenes.
We then call play_sound
, passing in the audio clip associated with sound_name
, and the sound position.
Before we leave Globals.gd
, we need to add a few lines of code to load_new_scene
so when we change scenes, we destroy all of the audio.
Add the following to load_new_scene
:
for sound in created_audio:
if (sound != null):
sound.queue_free()
created_audio.clear()
Now before we change scenes we go through each simple audio player in created_sounds
and free/destroy them. Once we’ve gone through
all of the sounds in created_audio
, we clear created_audio
so it no longer holds any references to any of the previously created simple audio players.
Let’s change create_sound
in Player.gd
to use this new system. First, remove simple_audio_player
from Player.gd
’s global variables, since we will
no longer be directly instancing/spawning sounds from Player.gd
.
Now, change create_sound
to the following:
func create_sound(sound_name, position=null):
globals.play_sound(sound_name, false, position)
Now whenever create_sound
is called, we simply call play_sound
in Globals.gd
, passing in all of the arguments we’ve revived.
Now all of the sounds in our FPS can be played from anywhere. All we have to do is get the Globals.gd
singleton, and call play_sound
, passing in the name of the sound
we want to play, whether we want it to loop or not, and the position to play the sound from.
For example, if you want to play an explosion sound when the grenades explode you’d need to add a new sound to audio_clips
in Globals.gd
,
get the Globals.gd
singleton, and then you just need to add something like
globals.play_sound("explosion", false, global_transform.origin)
in the grenades
_process
function, right after the grenade damages all of the bodies within it’s blast radius.
Final notes¶
Now you have a fully working single player FPS!
At this point you have a good base to build more complicated FPS games.
Warning
If you ever get lost, be sure to read over the code again!
You can download the finished project for the entire tutorial here: Godot_FPS_Part_6.zip
Note
The finished project source files contain the same exact code, just written in a different order. This is because the finished project source files are what the tutorial is based on.
The finished project code was written in the order that features were created, not necessarily in a order that is ideal for learning.
Other than that, the source is exactly the same, just with helpful comments explaining what each part does.
Tip
The finished project source is hosted on Github as well: https://github.com/TwistedTwigleg/Godot_FPS_Tutorial
Please note that the code in Github may or may not be in sync with the tutorial on the documentation.
The code in the documentation is likely better managed and/or more up to date. If you are unsure on which to use, use the project(s) provided in the documentation as they are maintained by the Godot community.
You can download all of the .blend
files used in this tutorial here: Godot_FPS_BlenderFiles.zip
All assets provided in the started assets (unless otherwise noted) were originally created by TwistedTwigleg, with changes/additions by the Godot community.
All original assets provided for this tutorial are released under the MIT
license.
Feel free to use these assets however you want! All original assets belong to the Godot community, with the other assets belonging to those listed below:
The skybox is created by StumpyStrust and can be found at OpenGameArt.org. https://opengameart.org/content/space-skyboxes-0
. The skybox is licensed under the CC0
license.
The font used is Titillium-Regular, and is licensed under the SIL Open Font License, Version 1.1
.
The skybox was convert to a 360 equirectangular image using this tool: https://www.360toolkit.co/convert-cubemap-to-spherical-equirectangular.html
While no sounds are provided, you can find many game ready sounds at https://gamesounds.xyz/
Warning
OpenGameArt.org, 360toolkit.co, the creator(s) of Titillium-Regular, StumpyStrust, and GameSounds.xyz are in no way involved in this tutorial.