Частина 6

Part overview





You are assumed to have finished Частина 5 before moving on to this part of the tutorial. The finished project from Частина 5 will be the starting project for part 6

Let's get started!

Adding the main menu


The main menu is broken up into three different panels, each representing a different 'screen' of our main menu.


The Background_Animation node is just so the background of the menu is a bit more interesting than a solid color. It's a camera looking around the skybox, nothing fancy.

Feel free to expand all the nodes and see how they're set up. Remember to keep only Start_Menu visible when you're done, as that's the screen we want to show first when we enter the main menu.

Select Main_Menu (the root node) and create a new script called Main_Menu.gd. Add the following:

extends Control

var start_menu
var level_select_menu
var options_menu

export (String, FILE) var testing_area_scene
export (String, FILE) var space_level_scene
export (String, FILE) var ruins_level_scene

func _ready():
    start_menu = $Start_Menu
    level_select_menu = $Level_Select_Menu
    options_menu = $Options_Menu





    var globals = get_node("/root/Globals")
    $Options_Menu/HSlider_Mouse_Sensitivity.value = globals.mouse_sensitivity
    $Options_Menu/HSlider_Joypad_Sensitivity.value = globals.joypad_sensitivity

func start_menu_button_pressed(button_name):
    if button_name == "start":
        level_select_menu.visible = true
        start_menu.visible = false
    elif button_name == "open_godot":
    elif button_name == "options":
        options_menu.visible = true
        start_menu.visible = false
    elif button_name == "quit":

func level_select_menu_button_pressed(button_name):
    if button_name == "back":
        start_menu.visible = true
        level_select_menu.visible = false
    elif button_name == "testing_scene":
    elif button_name == "space_level":
    elif button_name == "ruins_level":

func options_menu_button_pressed(button_name):
    if button_name == "back":
        start_menu.visible = true
        options_menu.visible = false
    elif button_name == "fullscreen":
        OS.window_fullscreen = !OS.window_fullscreen
    elif button_name == "vsync":
        OS.vsync_enabled = $Options_Menu/Check_Button_VSync.pressed
    elif button_name == "debug":

func set_mouse_and_joypad_sensitivity():
    var globals = get_node("/root/Globals")
    globals.mouse_sensitivity = $Options_Menu/HSlider_Mouse_Sensitivity.value
    globals.joypad_sensitivity = $Options_Menu/HSlider_Joypad_Sensitivity.value

Most of the code here relates to making UIs, which is outside of the purpose of this tutorial series. We're only going to look at the UI related code briefly.


See Дизайн титульного екрана and the tutorials following for better ways to make GUIs and UIs!

Let's look at the class variables first.

  • start_menu: A variable to hold the Start_Menu Panel.

  • level_select_menu: A variable to hold the Level_Select_Menu Panel.

  • options_menu: A variable to hold the Options_Menu Panel.






Now let's go over _ready






We have not made the Globals singleton yet, so don't worry! We're going to make it soon!

In start_menu_button_pressed, we check to see which button is pressed.


In level_select_menu_button_pressed, we check to see which button is pressed.


If one of the scene changing buttons is pressed, we fist call set_mouse_and_joypad_sensitivity so the singleton (Globals.gd) has the values from the HSlider nodes. Then, we tell the singleton to change nodes using its load_new_scene function, passing in the file path of the scene the player has selected.


Don't worry about the singleton, we'll get there soon!

In options_menu_button_pressed, we check to see which button is pressed.


If the fullscreen button is pressed, we toggle the OS's full screen mode by setting it to the flipped version of its current value.

If the vsync button is pressed, we set the OS's Vsync based on the state of the Vsync check button.

Finally, lets take a look at set_mouse_and_joypad_sensitivity.



Making the Globals singleton




Add the following to Globals.gd.

extends Node

var mouse_sensitivity = 0.08
var joypad_sensitivity = 2

func _ready():

func load_new_scene(new_scene_path):

As you can see, it's quite small and simple. As this part progresses, we will keep adding more complex logic to Globals.gd, but for now, all it is doing is holding two class variables, and abstract defining how we change scenes.

  • mouse_sensitivity: The current sensitivity for our mouse, so we can load it in Player.gd.

  • joypad_sensitivity: The current sensitivity for our joypad, so we can load it in Player.gd.

Right now, all we will be using Globals.gd for is a way to carry variables across scenes. Because the sensitivities of our mouse and joypad are stored in Globals.gd, any changes we make in one scene (like in Options_Menu) will affect the sensitivity for the 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 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.



This will make Globals.gd a singleton/autoload script, which will allow us to access it from any script, in any scene.


For more information on singleton/autoload scripts, see Синглтони (Автозавантаження).

Now that Globals.gd is a singleton/autoload script, you can test the main menu!




Adding the debug menu



Let's add the code needed to fill these Labels. Select Debug_Display and create a new script called Debug_Display.gd. Add the following:

extends Control

func _ready():
    $OS_Label.text = "OS: " + OS.get_name()

func _process(delta):

Let's go over what this script does.




Now let's jump back to Main_Menu.gd and change the following in options_menu_button_pressed:

elif button_name == "debug":

to this instead:

elif button_name == "debug":

This will call a new function called set_debug_display in our singleton, so let's add that next!

Open up Globals.gd and add the following class variables:

# All the GUI/UI-related variables

var canvas_layer = null

var debug_display = null

  • canvas_layer: A canvas layer so the GUI/UI created in Globals.gd is always drawn on top.

  • DEBUG_DISPLAY: The debug display scene we worked on earlier.


Now that we have the class variables defined, we need to add a few lines to _ready so Globals.gd will have a canvas layer to use (which we will store in canvas_layer). Change _ready to the following:

func _ready():
    canvas_layer = CanvasLayer.new()

Now in _ready, we create a new canvas layer, assign it to canvas_layer and add it as a child. Because Globals.gd is an autoload/singleton, Godot will make a Node when the game is launched, and it will have Globals.gd attached to it. Since Godot makes a Node, we can treat Globals.gd like any other node with regard to adding/removing children nodes.

The reason we're adding a CanvasLayer is so all our GUI and UI nodes we instance/spawn in Globals.gd are always drawn on top of everything else.


Now we need to add set_debug_display to Globals.gd:

func set_debug_display(display_on):
    if display_on == false:
        if debug_display != null:
            debug_display = null
        if debug_display == null:
            debug_display = DEBUG_DISPLAY_SCENE.instance()

Let's go over what's happening.

First we check to see if Globals.gd is trying to turn on the debug display, or turn it off.

If Globals.gd is turning off the display, we then check to see if debug_display is not equal to null. If debug_display is not equal to null, then Globals.gd must have a debug display currently active. If Globals.gd has a debug display active, we free it using queue_free and then assign debug_display to null.

If Globals.gd is turning on the display, we then check to make sure Globals.gd do not already have a debug display active. We do this by making sure debug_display is equal to null. If debug_display is null, we instance a new DEBUG_DISPLAY_SCENE, and add it as a child of canvas_layer.



Adding a pause menu


Open up Pause_Popup.tscn.



Now that we've looked at how Pause_Popup.tscn is set up, let's write the code to make it work. Normally, we'd attach a script to the root node of the scene, Pause_Popup in this case, but since we'll need to receive a couple of signals in Globals.gd, we'll write all the code for the popup there.

Open up Globals.gd and add the following class variables:

const MAIN_MENU_PATH = "res://Main_Menu.tscn"
var popup = null
  • MAIN_MENU_PATH: The path to the main menu scene.

  • POPUP_SCENE: The pop up scene we looked at earlier.

  • popup: A variable to hold the pop up scene.

Now we need to add _process to Globals.gd so it can respond when the ui_cancel action is pressed. Add the following to _process:

func _process(delta):
    if Input.is_action_just_pressed("ui_cancel"):
        if popup == null:
            popup = POPUP_SCENE.instance()

            popup.connect("popup_hide", self, "popup_closed")



            get_tree().paused = true

Let's go over what's happening here.

Firstly, we check to see if the ui_cancel action is pressed. Then, we check to make sure Globals.gd does not already have a popup open by checking to see if popup is equal to null.

If Globals.gd do not have a pop-up open, we instance POPUP_SCENE and assign it to popup.



Then, we add popup as a child of canvas_layer so it's drawn on top. We then tell popup to pop up at the center of the screen using popup_centered.


Finally, we pause the entire SceneTree.


For more information on pausing in Godot, see Pausing games

Now, we need to add the functions to which we've connected the signals. Let's add popup_closed first.

Add the following to Globals.gd:

func popup_closed():
    get_tree().paused = false

    if popup != null:
        popup = null


popup_quit is similar, but we're also making sure the mouse is visible and changing scenes to the title screen.

Add the following to Globals.gd:

func popup_quit():
    get_tree().paused = false


    if popup != null:
        popup = null



Before we're ready to test the pop-up, we should change one thing in Player.gd.

Open up Player.gd and in process_input, change the code for capturing/freeing the cursor to the following:

Instead of:

# Capturing/Freeing cursor
if Input.is_action_just_pressed("ui_cancel"):
    if Input.get_mouse_mode() == Input.MOUSE_MODE_VISIBLE:

You will leave only:

# Capturing/Freeing cursor
if Input.get_mouse_mode() == Input.MOUSE_MODE_VISIBLE:




Запускаємо систему повернення гравця

Since the player can lose all their health, it would be ideal if the player died and respawned too, so let's add that next!

Firstly, open up Player.tscn and expand HUD. Notice how there is 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 the player is able to respawn.

Open up Player.gd and add the following class variables:

const RESPAWN_TIME = 4
var dead_time = 0
var is_dead = false

var globals



  • globals: A variable to hold the Globals.gd singleton.

We now need to add a couple lines to _ready, so we can use Globals.gd in Player.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 the player's global position by setting the origin in the player's global Transform to the position returned by globals.get_respawn_position.


Don't worry, we will be adding get_respawn_position further below!


func _physics_process(delta):

    if !is_dead:

    if (grabbed_object == null):





We have not made 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've 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.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)

        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:

            health = 100
            current_grenade = "Grenade"

            is_dead = false

Let's go through what this function is doing.













If the player has 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 the player's collision shapes so the player can collide again with the environment.




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:


Finishing the respawn system

Firstly, 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 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 all the weapons will reset when the player dies.

Now we need to add a few things to Globals.gd. Firstly, add the following class variable:

var respawn_points = null




Now let's add get_respawn_position to Globals.gd:

func get_respawn_position():
    if respawn_points == null:
        return Vector3(0, 0, 0)
        return respawn_points[respawn_point].global_transform.origin

Let's go over what this function does.

Firstly, we check if Globals.gd has any respawn_points by checking whether respawn_points is null or not.




Before we are done with Globals.gd, we need to add the following to load_new_scene:

respawn_points = null


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 its _ready function called, all 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.


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.




Writing a sound system we can use anywhere

Finally, let's make a sound system so we can play sounds from anywhere, without having to use the player.

Firstly, 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")

    globals = get_node("/root/Globals")

func play_sound(audio_stream, position=null):
    if audio_stream == null:

    audio_node.stream = audio_stream

    #if audio_node is AudioStreamPlayer3D:
    #    if position != null:
    #        audio_node.global_transform.origin = position


func sound_finished():
    if should_loop:

There are several changes from the old version, first and foremost being we are 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 are forcing an audio stream to be passed in to play_sound.

Another change is we have a new class variable called should_loop. Instead of just destroying the audio player every time it's finished, we instead want to check and see if the audio player is 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, the audio player is instead going to be spawned in Globals.gd so we can create sounds from any scene. Now the audio player stores Globals.gd singleton so when the audio player is destroyed, we can also remove it from a list in Globals.gd.

Let's go over the changes.


We also added two new class variables, should_loop and globals. We'll use should_loop to tell whether the audio player should loop when the sound has finished, and globals will hold the Globals.gd singleton.

The only change in _ready is now audio player is getting the Globals.gd singleton and assigning it to globals.



Now that we've finished our changes to SimpleAudioPlayer.gd, we now need to turn our attention to Globals.gd. First, add the following class variables:

# All the audio files.

# You will need to provide your own sound files.
var audio_clips = {

var created_audio = []

Let's go over these global variables.

  • audio_clips: A dictionary holding all the audio clips Globals.gd can play.

  • SIMPLE_AUDIO_PLAYER_SCENE: The simple audio player scene.

  • created_audio: A list to hold all the simple audio players Globals.gd has created.



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:

    if audio_clips.has(sound_name):
        var new_audio = SIMPLE_AUDIO_PLAYER_SCENE.instance()
        new_audio.should_loop = loop_sound


        new_audio.play_sound(audio_clips[sound_name], sound_position)


Let's go over what this function does.

Firstly, we check whether Globals.gd has an audio clip with the name sound_name in audio_clips. If it does not, we print an error message.


We then set should_loop, and add new_audio as a child of Globals.gd.



We add the new_audio into the created_audio list to hold all created audios.


Before we leave Globals.gd, we need to add a few lines of code to load_new_scene so when the player changes scenes, all the audio is destroyed.

Add the following to load_new_scene:

for sound in created_audio:
    if (sound != null):

Now, before Globals.gd changes scenes, it goes through each simple audio player in created_sounds and frees/destroys them. Once Globals.gd has gone through all the sounds in created_audio, we clear created_audio so it no longer holds any references to any (now freed/destroyed) 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 class variables since we will no longer be directly instancing/spawning sounds in 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 the arguments received.

Now all 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, pass in the name of the sound we want to play, whether we want it to loop or not, and the position from which to play the sound.

For example, if you want to play an explosion sound when the grenade explodes 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 the bodies within its blast radius.

Final notes


Now you have a fully working single player FPS!




You can download the finished project for the entire tutorial here: Godot_FPS_Part_6.zip






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 in the documentation.


You can download all the .blend files used in this tutorial here: Godot_FPS_BlenderFiles.zip



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 skybox was converted 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/


OpenGameArt.org, 360toolkit.co, the creator(s) of Titillium-Regular, StumpyStrust, and GameSounds.xyz are in no way involved in this tutorial.