Partie 6

Aperçu cette partie

Dans cette partie, nous allons ajouter un menu principal, un menu de pause, ajouter un système de respawn pour le joueur et changer/déplacer le système de son de façon à pouvoir l’utiliser dans n’importe quel script.

C’est la dernière partie du tutoriel FPS; à la fin de celui-ci, vous aurez de solides bases pour construire d’incroyables jeux FPS avec Godot !

../../../_images/FinishedTutorialPicture.png

Note

Vous êtes supposé avoir terminé Partie 5 avant de passer à cette partie du tutoriel. Le projet fini de Partie 5 sera le projet de départ de la partie 6

Commençons tout de suite !

Ajouter le menu principal

Premièrement, ouvrez Main_Menu.tscn et jetez un œil à la mise en place de la scène.

Le menu principal se décompose en trois différents panneaux, chacun représentant un “écran” différent de notre menu principal.

Note

Le nœud ``Background_Animation``est là pour rendre le fond du menu un peu plus intéressant qu’une simple couleur. C’est une caméra qui regarde autour dans le ciel, rien d’extraordinaire.

N’hésitez pas à développer tous les nœuds et à voir comment ces nœuds sont mis en place. Gardez en mémoire que seul Start_Menu doit être visible quand vous aurez terminé, car c’est ce que nous voulons montrer en premier quand nous entrons dans le menu principal.

Sélectionnez Main_Menu (la scène racine) et créez un nouveau script appelé Main_Menu.gd. Ajoutez ce qui suit :

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

    $Start_Menu/Button_Start.connect("pressed", self, "start_menu_button_pressed", ["start"])
    $Start_Menu/Button_Open_Godot.connect("pressed", self, "start_menu_button_pressed", ["open_godot"])
    $Start_Menu/Button_Options.connect("pressed", self, "start_menu_button_pressed", ["options"])
    $Start_Menu/Button_Quit.connect("pressed", self, "start_menu_button_pressed", ["quit"])

    $Level_Select_Menu/Button_Back.connect("pressed", self, "level_select_menu_button_pressed", ["back"])
    $Level_Select_Menu/Button_Level_Testing_Area.connect("pressed", self, "level_select_menu_button_pressed", ["testing_scene"])
    $Level_Select_Menu/Button_Level_Space.connect("pressed", self, "level_select_menu_button_pressed", ["space_level"])
    $Level_Select_Menu/Button_Level_Ruins.connect("pressed", self, "level_select_menu_button_pressed", ["ruins_level"])

    $Options_Menu/Button_Back.connect("pressed", self, "options_menu_button_pressed", ["back"])
    $Options_Menu/Button_Fullscreen.connect("pressed", self, "options_menu_button_pressed", ["fullscreen"])
    $Options_Menu/Check_Button_VSync.connect("pressed", self, "options_menu_button_pressed", ["vsync"])
    $Options_Menu/Check_Button_Debug.connect("pressed", self, "options_menu_button_pressed", ["debug"])

    Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)

    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":
        OS.shell_open("https://godotengine.org/")
    elif button_name == "options":
        options_menu.visible = true
        start_menu.visible = false
    elif button_name == "quit":
        get_tree().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":
        set_mouse_and_joypad_sensitivity()
        get_node("/root/Globals").load_new_scene(testing_area_scene)
    elif button_name == "space_level":
        set_mouse_and_joypad_sensitivity()
        get_node("/root/Globals").load_new_scene(space_level_scene)
    elif button_name == "ruins_level":
        set_mouse_and_joypad_sensitivity()
        get_node("/root/Globals").load_new_scene(ruins_level_scene)


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":
        pass


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

La grande majorité du code ici se rapport aux interfaces, ce qui est en dehors du propos de cette série de tutoriel. Nous allons seulement regardez brièvement le code relatif à l’UI.

Astuce

Voir Concevoir un écran titre et les tutoriels qui suivent pour avoir de meilleurs méthodes de créer des GUI et des UIs !

Examinons d’abord les variables de classe.

  • start_menu: Une variable utilisée pour stocker le Start_Menu Panel.
  • level_select_menu: Une variable utilisée pour stocker le Level_Select_Menu Panel.
  • options_menu: Une variable utilisée pour stocker le Options_Menu Panel.
  • testing_area_scene: Le chemin vers le fichier ``Testing_Area.tscn``de façon à ce que nous puissions le changer depuis la scène.
  • space_level_scene: Le chemin vers le fichier Space_Level.tscn de façon à ce que nous puissions le modifier depuis la scène.
  • ruins_level_scene: Le chemin vers le fichier Ruins_Level.tscn de façon à ce que nous puissions le modifier depuis la scène.

Avertissement

Vous devrez entrer les bons chemins d’accès aux bons fichiers dans l’éditeur avant de tester ce script ! Sinon, ça ne marchera pas !


Maintenant, voyons ce que fait _ready

Tout d’abord, nous récupérons tous les nœuds Panel et nous les assignons aux bonnes variables.

Ensuite, nous connectons tous les signaux pressed de boutons à leur fonction respectives [nom_panneau_ici]_button_pressed.

Nous assignons ensuite MOUSE_MODE_VISIBLE au nœud de la souris pour s’assurer que quand le joueur retourne sur cette scène, la souris soit visible.

Nous récupérons ensuite un singleton appelé Globals. On assigne ensuite des valeurs aux nœuds HSlider pour que leur valeurs correspondent à celles de la sensibilité de la souris et des contrôleurs dans le singleton.

Note

Nous n’avons pas encore créé le singleton Globals, pas de panique ! Nous allons le faire bientôt !


Dans start_menu_button_pressed, on vérifie quel bouton a été appuyé.

En se basant sur le bouton appuyé, soit nous changeons le panneau actuellement visible, soit on quitte l’application, soit on ouvre le site web de Godot.


Dans level_select_menu_button_pressed, on vérifie quel bouton a été appuyé.

Si le bouton back a été appuyé, on change les panneaux visibles pour retourner au menu principal.

Si un des boutons de changement de scène est appuyé, on commence par appeller set_mouse_and_joypad_sensitivity pour que le singleton (Globals.gd) ait les valeurs depuis les nœuds HSlider. Ensuite, nous demandons au singleton de changer ses nœuds en utilisant la fonction load_new_scene et en passant en paramètre le chemin d’accès à la scène que le joueur a demandé.

Note

Ne vous inquiétez pas pour le singleton, nous allons y venir !


Dans options_menu_button_pressed, on vérifie quel bouton est appuyé.

Si le bouton back a été appuyé, on change les panneaux visibles pour retourner au menu principal.

Si le bouton fullscreen est appuyé, on active/désactive le mode plein écran de :ref:`OS <class_OS>`en lui attribuant la valeur inverse de sa valeur actuelle.

Si le bouton vsync est appuyé, on active/désactive le mode vsync de :ref:`OS <class_OS>`en fonction du statut actuel du bouton de Vsync.


Enfin, on jette un œil à set_mouse_and_joypad_sensitivity.

Tout d’abord, on récupère le singleton Globals et on l’assigne à une variable locale.

On assigne ensuite aux variables mouse_sensitivity et joypad_sensitivity aux valeurs dans leur homologues HSlider respectifs.

Créer le singleton Globals

Maintenant, pour que tout ceci fonctionne, nous devons créer le singleton Globals. Créez un nouveau script dans l’onglet Script et appelez le Globals.gd.

Note

Pour créer le singleton Globals, allez dans l’onglet Script de l’éditeur, sélectionnez ensuite New et une boîte Create Script va apparaître. Ne changez rien à part Path où nous allons insérer le nom du script Globals.gd.

Ajoutez ce qui suit à 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)

Comme vous pouvez le voir, c’est court et simple. Au fur et à mesure que cette partie progresse, nous allons continuer d’ajouter de la logique plus complexe dans Globals.gd, mais pour l’heure, tout ce que ce singleton fait, c’est contenir deux variables de classe et définir de manière abstraite comment nous changeons de scènes.

  • mouse_sensitivity: La sensibilité actuelle de notre souris, afin que nous puissions la charger dans``Player.gd``.
  • joypad_sensitivity: La sensibilité actuelle de notre contrôleur, afin que nous puissions la charger dans Player.gd.

Pour l’instant, la seule utilité de Globals.gd est de contenir des variables à travers des scènes. Du fait que les sensibilités de nos souris et contrôleurs sont stockés dans Globals.gd, chaque changement fait dans une scène (comme dans Options_Menu) va affecter la sensibilité de notre joueur.

Tout ce que nous faisons dans load_new_scene, c’est d’appeler la fonction de change_scene de SceneTree, en passant en paramètre le chemin d’accès à la scène donné dans load_new_scene.

C’est tout le code nécessaire pour Globals.gd pour l’instant ! Avant de pouvoir tester le menu principal, nous devons d’abord définir Globals.gd comme un script chargé automatiquement.

Ouvrez l’onglet Project Settings et cliquez sur l’onglet AutoLoad.

../../../_images/AutoloadAddSingleton.png

Sélectionnez ensuite le chemin pour Globals.gd dans le champ Path en cliquant sur le bouton (..) à côté. Assurez-vous que le nom dans le champ Node Name est Globals. Si vous avez tout comme dans l’image ci-dessus, appuyez alors sur Add !

Cela rendra Globals.gd comme un singleton (script chargé automatiquement), ce qui nous permettra d’accéder à ce script depuis n’importe quelle scène.

Astuce

Pour plus d’informations sur les scripts singleton, regardez Singletons (Chargement Automatique).

Maintenant que Globals.gd est un singleton, vous pouvez tester le menu principal !

Vous voudrez peut-être changer la scène principale de Testing_Area.tscn à Main_Menu.tscn de sorte que lorsqu’on exporte le jeu, le joueur va commencer dans le menu principal. Vous pouvez faire cela en allant dans Project Settings, sous l’onglet General. Sélectionnez la catégorie Application, cliquez sur la sous-catégorie Run et vous pourrez alors changer la valeur de Main Scene.

Avertissement

Vous devrez entrer les bons chemins d’accès aux bons fichiers dans Main_Menu dans l’éditeur avant de tester le menu principal ! Autrement, vous ne serez pas en mesure de changer de scènes depuis le menu de sélection de niveaux.

Ajouter le menu de débogage

Ajoutons maintenant une simple scène de débogage de façon à pouvoir suivre des éléments tels que les IPS (Images Par Secondes) en jeu. Ouvrez Debug_Display.tscn.

Vous pouvez voir qu’un Panel est positionné dans le coin supérieur droit de l’écran. Il dispose de trois Labels, un pour afficher les IPS du jeu, un pour afficher sur quel OS le jeu tourne et un pour afficher quelle version de Godot est utilisée.

Ajoutons le code nécessaire pour remplir ces Labels. Sélectionnez Debug_Display et créez un nouveau script appelé Debug_Display.gd. Ajoutez ce qui suit :

extends Control

func _ready():
    $OS_Label.text = "OS: " + OS.get_name()
    $Engine_Label.text = "Godot version: " + Engine.get_version_info()["string"]

func _process(delta):
    $FPS_Label.text = "FPS: " + str(Engine.get_frames_per_second())

Passons en revue ce que fait ce code.


Dans _ready, on affecte au texte de OS_Label le nom fournit par OS en utilisant la fonction get_name. Cela va renvoyer le nom de l’OS (Système d’exploitation) sur lequel Godot a été compilé. Par exemple, si vous utilisez Windows, il va retourner Windows, si vous utilisez Linux, il va retourner X11.

Ensuite, nous au texte de Engine_Label les infos de versions fournies par Engine.get_version_info. Engine.get_version_info``renvoie un dictionnaire remplit d'informations utiles à propos de la version de Godot couramment utilisée. Nous ne nous intéressons qu'à la chaîne concernant la version, pour ce label en tout cas, afin de pouvoir assigner celui-ci au ``text de Engine_Label. Voir Engine pour plus d’informations sur les valeurs que retourne get_version_info.

Dans _process, on affecte au texte de FPS_Label la valeur de Engine.get_frames_per_second, mais parce que get_frames_per_second retourne un entier, nous devons le convertir en chaîne de caractère en utilisant str avant de l’ajouter au Label.


Maintenant, revenons au Main_Menu.gd et changeons ce qui suit dans options_menu_button_pressed :

elif button_name == "debug":
    pass

à ceci à la place :

elif button_name == "debug":
    get_node("/root/Globals").set_debug_display($Options_Menu/Check_Button_Debug.pressed)

Cela va appeler une nouvelle fonction appelée set_debug_display dans notre singleton, donc ajoutons cela maintenant !


Ouvrez Globals.gd et ajouter les variables de classe suivantes :

# ------------------------------------
# All the GUI/UI-related variables

var canvas_layer = null

const DEBUG_DISPLAY_SCENE = preload("res://Debug_Display.tscn")
var debug_display = null

# ------------------------------------
  • canvas_layer: Une couche du canvas afin que le GUI/UI créée dans ``Globals.gd``soit toujours affichée au dessus.
  • DEBUG_DISPLAY: La scène d’affichage de débogage sur laquelle nous avons travaillé plus tôt.
  • debug_display: Une variable utilisée pour stocker l’affichage de débogage quand/si il y en a une.

Maintenant que les variables de classe sont définies, nous devons ajouter quelques lignes à _ready pour que Globals.gd``ait une couche de canvas à utiliser (que nous allons stocker dans ``canvas_layer). Changez _ready par ce qui suit :

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

Nous allons maintenant dans _ready créer une nouvelle couche de canvas, l’assigner à canvas_layer et l’ajouter en tant que fils. Parce que Globals.gd est un singleton, Godot va créer un Node quand le jeu se lancera, qui aura Globals.gd d’attaché. Puisque Godot créer un Node, nous pouvons traiter Globals.gd comme n’importe quel nœud en ce qui concerne l’ajout et la suppressions de nœuds enfants.

La raison pour laquelle nous ajoutons un CanvasLayer est parce que nous voulons que tous nos nœuds d’UI/GUI que nous instantons dans Globals.gd se dessinent toujours au dessus de tout le reste.

Lorsque vous ajoutez des nœuds à un singleton, vous devez faire attention à ne pas perdre la référence des nœuds enfants. Nous devons faire cela car ces nœuds ne sont pas détruis lorsque l’on change de scène active, ce qui veut dire que vous rencontrerez des problèmes de mémoire si vous créer beaucoup de nœuds et que vous ne les détruisez pas.


Nous devons maintenant ajouter set_debug_display à Globals.gd :

func set_debug_display(display_on):
    if display_on == false:
        if debug_display != null:
            debug_display.queue_free()
            debug_display = null
    else:
        if debug_display == null:
            debug_display = DEBUG_DISPLAY_SCENE.instance()
            canvas_layer.add_child(debug_display)

Voyons ce qui se passe.

Nous vérifions d’abord si Globals.gd essaie d’allumer ou d’éteindre l’affichage de débogage.

Si Globals.gd est en train d’éteindre l’affichage, on vérifie alors si debug_display n’est pas égal à null. Si c’est le cas, Globals.gd doit avoir un affichage de débogage actif. Si Globals.gd a un affichage de débogage actif, on le libère en utilisant queue_free et on assigne null à debug_display.

Si Globals.gd allume l’affichage, on s’assure que Globals.gd n’a pas déjà un affichage de débogage actif. On s’en assure en vérifiant si debug_display est égal à null. Si c’est le cas, on instancie une nouvelle DEBUG_DISPLAY_SCENE et on l’ajoute en tant que fils de canvas_layer.


Ceci fait, on peut activer ou désactiver l’affichage en cochant ou décochant le CheckButton dans le panneau Options_Menu. Essayez !

Notez comme l’affichage de débogage reste même après que vous ayez changé de scène du Main_Menu.tscn à une autre scène (comme Testing_Area.tscn). C’est là toute la beauté de l’instanciation de nœuds dans un singleton et de les ajouter comme enfant de ce même singleton. N’importe quel nœud ajouté en tant qu’enfant du singleton va rester tant que le jeu tourne sans aucun travail supplémentaire de votre part !

Ajouter un menu de pause

Ajoutons un menu de pause afin de pouvoir retourner au menu principal quand on appuie sur l’action ui_cancel.

Ouvrez Pause_Popup.tscn.

Notez comment le nœud racine dans Pause_Popup est une WindowDialog. WindowDialog hérite de Popup, ce qui signifie que WindowDialog peut agir comme un popup.

Sélectionnez Pause_Popup et descendez jusqu’à atteindre le menu Pause dans l’inspecteur. Remarquez comme le mode de pause est mis à process au lieu de inherit qui est le paramètre par défaut. Cela fera en sorte qu’il est possible de continuer les traitements même quand le jeu est en pause (nous allons en avoir besoin pour interagir avec les éléments UI).

Maintenant que nous avons vu comment Pause_Popup.tscn est mis en place, écrivons le code pour le faire fonctionner. Nous voudrions normalement attacher un script au nœud racine de la scène, Pause_Popup dans notre cas mais parce que nous allons avoir besoin de recevoir des signaux dans Globals.gd, nous allons écrire tous le code pour le popup là dedans.

Ouvrez Globals.gd et ajouter les variables de classe suivantes :

const MAIN_MENU_PATH = "res://Main_Menu.tscn"
const POPUP_SCENE = preload("res://Pause_Popup.tscn")
var popup = null
  • MAIN_MENU_PATH: Le chemin vers la scène de menu principal.
  • POPUP_SCENE: La scène de popup que nous avons exploré tout à l’heure.
  • popup: Une variable pour stocker la scène de popup.

Maintenant, nous devons ajouter _process à Globals.gd de façon à ce qu’il puisse répondre quand l’action ui_cancel est appuyé. Ajoutez ce qui suit dans _process :

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

            popup.get_node("Button_quit").connect("pressed", self, "popup_quit")
            popup.connect("popup_hide", self, "popup_closed")
            popup.get_node("Button_resume").connect("pressed", self, "popup_closed")

            canvas_layer.add_child(popup)
            popup.popup_centered()

            Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)

            get_tree().paused = true

Voyons ce qui s’y passe.


Tout d’abord, nous devons vérifier si l’action ui_cancel est appuyé. Ensuite, nous nous assurons que Globals.gd``n'a pas de ``popup ouvert en vérifiant si popup est égal à null.

Si Globals.gd ne possède pas de popup ouvert, on instancie POPUP_SCENE et on l’assigne à popup.

On récupère ensuite le bouton quitter et on l’assigne son signal pressed à popup_quit qui sera ajouté bientôt.

Ensuite, on assigne le signal popup_hide de la WindowDialog et le signal pressed du bouton de reprise à popup_closed, que nous allons ajouter bientôt.

Ensuite, nous ajoutons popup en tant qu’enfant de canvas_layer de façon à ce qu’il se dessine au dessus. On dit ensuite au popup de s’afficher au centre de l’écran en utilisant popup_centered.

Nous nous assurons alors que le mode de la souris est MOUSE_MODE_VISIBLE pour que le joueur puisse interagir avec le popup. Si nous ne faisions pas cela, le joueur ne serait capable d’interagir avec le popup dans aucune scène où le mode de la souris est MOUSE_MODE_CAPTURED.

Enfin, on met en pause toute la SceneTree.

Note

Pour plus d’informations sur la pause dans Godot, voir Pausing games


Maintenant, nous devons ajouter des fonctions auxquelles sont connectés les signaux. Ajoutons popup_closed en premier.

ajoutez ce qui suit dans Globals.gd :

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

    if popup != null:
        popup.queue_free()
        popup = null

popup_closed va reprendre le jeu et détruire le popup s’il y en a un.

``popup_quit``est similaire, mais nous nous assurons que la souris soit visible et change de scène pour le menu principal.

ajoutez ce qui suit dans Globals.gd :

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

    Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)

    if popup != null:
        popup.queue_free()
        popup = null

    load_new_scene(MAIN_MENU_PATH)

popup_quit va reprendre le jeu, mettre le mode de la souris à MOUSE_MODE_VISIBLE pour s’assurer que la souris soit visible dans le menu principal, détruire le popup s’il y en a un et changer la scène pour le menu principal.


Avant de pouvoir tester le popup, nous devons changer une chose dans Player.gd.

Ouvrez Player.gd et dans process_input, changez le code pour capturer/libérer le curseur pour ce qui suit :

Au lieu de :

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

Vous ne laisserez que :

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

Maintenant, au lieu de capturer/libérer la souris, on vérifie si le mode courant de la souris est MOUSE_MODE_VISIBLE. Si c’est le cas, on le remet à MOUSE_MODE_CAPTURED.

Parce que le popup rend le mode de la souris MOUSE_MODE_VISIBLE, quand vous mettez le jeu en pause, nous n’avons plus à nous soucier de libérer et capturer le curseur dans Player.gd.


Maintenant, le popup du menu de pause est terminé. Vous pouvez mettre en pause à n’importe quel moment et retourner au menu principal !

Commencer le système de réapparition

Puise que le joueur peut perdre toute sa vie, il serait idéal que le joueur réapparaisse s’il meurt, c’est donc ce que nous allons faire !

Premièrement, ouvrez Player.tscn et développez HUD. Vous remarquerez qu’il existe un ColorRect appelé Death_Screen. Quand le joueur meurt, nous allons rendre Death_Screen visible et lui montrer combien de temps il doit attendre avant que le joueur puisse réapparaitre.

Ouvrez Player.gd et ajouter les variables de classe suivantes :

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

var globals
  • RESPAWN_TIME: Le temps qu’il faut (en secondes) pour que joueur réapparaisse.
  • dead_time: Une variable pour suivre depuis combien de temps le joueur est mort.
  • is_dead: Une variable pour savoir actuellement si le joueur est mort ou non.
  • globals: Une variable pour stocker le singleton Globals.gd.

Nous devons maintenant ajouter quelques lignes à _ready, de façon à pouvoir utiliser Globals.gd dans Player.gd. Ajoutez ce qui suit dans _ready :

globals = get_node("/root/Globals")
global_transform.origin = globals.get_respawn_position()

Nous allons maintenant récupérer le singleton Globals.gd et l’assigner à globals. On définit aussi la position globale du joueur en mettant son origine dans le Transform global du joueur à la position renvoyé par globals.get_respawn_position.

Note

Ne vous inquiétez pas, nous ajouterons get_respawn_position un petit peu plus bas !


Ensuite, nous avons besoin d’apporter quelques modifications à _physics_process. Changez _physics_process en ce qui suit :

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)

À présent, le joueur ne va plus interpréter les commandes de mouvement ou d’action quand celui-ci sera mort. Nous appelons maintenant également process_respawn.

Note

L’expression if !is_dead: est équivalente et fonctionne de la même façon que l’expression if is_dead == false:. En enlevant le signe ! de l’expression, on obtient l’expression opposée if is_dead == true:. C’est simplement une façon plus compacte d’écrire la même fonctionnalité.

Nous n’avons pas encore écrit process_respawn. Changeons cela maintenant.


Ajoutez ce qui suit à 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.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

Passons en revue ce que fait cette fonction.


Tout d’abord, on vérifie si le joueur vient de mourir en vérifiant si health``est inférieur ou égal à ``0 et si is_dead est false.

Si le joueur vient de mourir, on désactive les collision shapes du joueur. On fait cela pour s’assurer que le joueur ne bloque rien avec son cadavre.

Ensuite, on assigne true à changing_weapon ainsi que UNARMED à changing_weapon_name. Nous faisons cela pour que si le joueur utilise une arme, elle sera mise de côté quand il meurt.

On rend ensuite le ColorRect Death_Screen visible afin que le joueur ait un bel écran gris au dessus de tout le reste quand il meurt.On rend ensuite le reste de l’UI, les nœuds Panel et Crosshair invisibles.

On assigne alors RESPAWN_TIME à dead_time de façon à ce que le compte à rebours de « depuis quand le joueur est mort » démarre. On assigne aussi true à is_dead pour qu’on sache que le joueur est mort.

Si le joueur tient un objet quand il meurt, nous devons le jeter. On vérifie d’abord si le joueur tient un objet dans sa main ou non. Si c’est le cas, on jette cet objet en utilisant le même code que nous avons ajouté dans la Partie 5.

Note

La \n combination from the expression You have died\n est une commande utilisée pour afficher un texte suivi d’une nouvelle ligne en dessous. C’est toujours utile quand vous voulez des textes bien répartis en groupes de plusieurs lignes pour qu’il soit plus beau et plus lisible pour les joueurs de votre jeu.


On vérifie alors si le joueur est mort ou non. Si c’est le cas, on retire alors delta de dead_time.

On créer alors une nouvelle variable appelée dead_time_pretty, où nous convertissons dead_time en string en utilisant les trois premiers caractères en partant de la gauche. Cela donne au joueur une joli chaîne de caractères sur le temps qu’il lui reste avant de pouvoir réapparaître.

On change alors le Label dans Death_Screen pour montrer combien de temps il reste au joueur.

Nous vérifions ensuite si le joueur a attendu suffisamment longtemps pour pouvoir réapparaître. On fait cela en vérifiant si dead_time est égal à 0 ou moins.

Si le joueur a attendu suffisamment longtemps, on met la position du joueur à une nouvelle position de réapparition fournie par get_respawn_position.

On active alors les deux collision shapes du joueur pour qu’il puisse à nouveau entrer en contact avec l’environnement.

Ensuite, on rend le Death_Screen invisible et on rend le reste de l’UI, les nœuds Panel et ``Crosshair``visibles à nouveau.

On parcours alors chaque arme et on appelle sa fonction reset_weapon que nous allons ajouter bientôt.

Ensuite, on réinitialise health à sa valeur initiale de 100, grenade_amounts de même et current_grenade à Grenade. Cela ré-initialise complètement les variables à leur état d’origine.

Enfin, on met is_dead à false.


Avant de quitter Player.gd, nous devons ajouter une dernière petite chose à _input. Ajoutez ce qui suit au début de _input :

if is_dead:
    return

Maintenant, quand le joueur est mort, il ne peut plus regarder autour de lui avec la souris.

Terminer le système de réapparition

Premièrement, ouvrez Weapon_Pistol.gd et ajoutez la fonction reset_weapon. Ajoutez ce qui suit :

func reset_weapon():
    ammo_in_weapon = 10
    spare_ammo = 20

Maintenant, quand nous appelons reset_weapon, les munitions dans le pistolet et les munitions restantes seront réinitialisées.

Ajoutons maintenant reset_weapon à Weapon_Rifle.gd :

func reset_weapon():
    ammo_in_weapon = 50
    spare_ammo = 100

Et ajoutez ce qui suit à Weapon_Knife.gd :

func reset_weapon():
    ammo_in_weapon = 1
    spare_ammo = 1

Maintenant toutes les armes se réinitialiseront quand le joueur meurt.


Maintenant nous avons besoin d’ajouter des choses à Globals.gd. Tout d’abord, ajouter les variables de classe suivantes :

var respawn_points = null
  • respawn_points: Une variable utilisée pour stocker tous les points de réapparition du niveau

Du fait que nous allons récupérer un point de réapparition aléatoire à chaque fois, nous devons rendre aléatoire le générateur de nombre. Ajoutez ce qui suit à _ready :

randomize()

randomize va nous donner un nouveau seed aléatoire de façon à obtenir une chaîne de caractère (relativement) aléatoire quand nous utilisons une fonction de nombres aléatoires.

Ajoutons maintenant get_respawn_position à 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

Regardons son fonctionnement.


Premièrement, nous devons vérifier si Globals.gd a un respawn_points en vérifiant si respawn_points est null ou non.

Si respawn_points est null, on retourne la position d’un Vector 3 vide avec la position (0, 0, 0).

Si respawn_points n’est pas null, nous devons alors récupérer un nombre aléatoire entre 0 et le nombre d’éléments que nous avons dans respawn_points, en enlevant 1 puisque la plupart des langages de programmation (GDScript inclut) commencent à compter à partir de 0 quand vous accéder à un élément dans une liste.

On renvoie alors la position du nœud Spatial <class_Spatial>`à la position ``respawn_point` dans respawn_points.


Avant d’en avoir finit avec Globals.gd, nous devons ajouter ce qui suit à load_new_scene :

respawn_points = null

On met respawn_points à null pour que quand/si le joueur arrive à un niveau sans point de réapparition, on ne fait pas réapparaître le joueur aux points de réapparition du niveau précédent.


Tout ce dont nous avons besoin maintenant c’est d’une façon de définir les points de réapparition. Ouvrez Ruins_Level.tscn et sélectionnez Spawn_Points. Ajoutez un nouveau script appelé Respawn_Point_Setter.gd et attachez le à Spawn_Points. Ajoutez ce qui suit à Respawn_Point_Setter.gd :

extends Spatial

func _ready():
    var globals = get_node("/root/Globals")
    globals.respawn_points = get_children()

Maintenant, quand un nœud avec Respawn_Point_Setter.gd``a sa fonction ``_ready appelée , tous les nœuds enfants du nœud avec Respawn_Point_Setter.gd, Spawn_Points dans le cas de Ruins_Level.tscn, vont être ajoutés aux respawn_points de Globals.gd.

Avertissement

N’importe quel nœud avec Respawn_Point_Setter.gd doit être au dessus du joueur dans la SceneTree pour que les points de réapparition soient affectés avant que le joueur en ait besoin dans la fonction _ready du joueur.


Maintenant, quand un joueur meurt, il va réapparaître après avoir attendu 4 secondes !

Note

Aucun points de réapparition n’ont été mis en place pour les niveaux mise à part pour Ruins_Level.tscn ! Ajoutez des points de réapparition au Space_Level.tscn est un exercice qu’on vous laisse faire par vous même.

Écrire un système de son qui puisse être utilisé partout

Enfin, créons un système de son qui puisse jouer des sons de n’importe où, sans avoir à utiliser le joueur.

D’abord, ouvrer SimpleAudioPlayer.gd et changer le comme ce qui suit :

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 an AudioStreamPlayer3D, then uncomment these lines to set the position.
    #if audio_node is AudioStreamPlayer3D:
    #    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()

Il y a plusieurs changements par rapport à la version précédente. Premièrement, nous ne stockons plus les fichiers de son dans SimpleAudioPlayer.gd. Cela apporte de bien meilleures performances puisque nous n’avons plus à charger chaque clip audio quand nous créons un son. À la place, on force à passer un flux audio dans play_sound.

Un autre changement est une nouvelle variable de classe appelée should_loop. Au lieu de simplement détruire le lecteur audio quand il a finit de jouer, on vérifie à la place si le lecteur audio est définit comme boucle ou non. Cela va nous permettre de lancer des sons en boucle comme des musiques de fond qui se répètent sans avoir à faire apparaître un nouveau lecteur audio avec la musique quand l’ancien est terminé.

Enfin, à la place d’être instancié dans Player.gd, le lecteur audio va être instancié dans Globals.gd pour que nous puissions créer des sons de n’importe quelle scène. Maintenant, le lecteur audio va stocker le singleton Globals.gd pour que quand le lecteur audio est détruit, il puisse également le supprimer d’une liste dans Globals.gd.

Passons en revue les changements.


Pour les variables de classe, on retire toutes les variables audio_[insérer nom ici] puisque nous les passerons à la place dans Globals.gd.

Nous avons également ajouté deux nouvelles variables de classe, should_loop et globals. Nous utiliserons should_loop pour dire au lecteur audio s’il doit jouer le son à nouveau quand il est terminé et globals afin de stocker le singleton Globals.gd.

Le seul changement dans _ready est que le lecteur audio récupère le singleton Globals.gd et l’assigne à globals.

play_sound attend maintenant en paramètre un flux audio, appelé audio_stream, au lieu de sound_name. Au lieu de vérifier le nom du son et de paramétrer le flux pour le lecteur audio, on vérifie qu’un flux audio est passé en paramètre. Si aucun flux n’est passé en paramètre, on affiche un message d’erreur, retire le lecteur audio de la liste dans le singleton Globals.gd appelé created_audio et on libère le lecteur audio.

Enfin, dans sound_finished, on vérifie si le lecteur audio est supposé répéter ou non en utilisant should_loop. Si le lecteur audio doit répéter, on joue leson à nouveau depuis le début à la position 0.0. Au contraire, si le lecteur audio n’est pas sensé répéter, on retire le lecteur audio d’une liste dans le singleton Globals.gd appelé created_audio et on libère le lecteur audio.


Maintenant que nous avons fini nos modifications dans SimpleAudioPlayer.gd, nous devons tourner notre attention vers Globals.gd. Tout d’abord, ajoutez les variables de classe suivantes :

# All 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 = []

Voyons ce que font ces variables globales.

  • audio_clips: Un dictionnaire qui contient tous les clips audio que Globals.gd peut jouer.
  • SIMPLE_AUDIO_PLAYER_SCENE: La scène du lecteur audio.
  • created_audio: Une liste pour contenir tous les lecteurs audio que Globals.gd a créé.

Note

Si vous souhaitez ajouter des sons supplémentaires, vous devez les ajouter à audio_clips. Aucun sons ne seront fournis dans ce tutoriel, vous devrez donc vous les procurer vous même.

Un site que je vous recommande cependant est GameSounds.xyz. J’utilise le pack de son « Gamemaster audio gun sound pack » inclus dans le « Sonniss” GDC Game Audio bundle for 2017 ». Les pistes que j’ai utilisé (avec quelques éditions mineures) sont les suivantes :

  • gun_revolver_pistol_shot_04,
  • gun_semi_auto_rifle_cock_02,
  • gun_submachine_auto_shot_00_automatic_preview_01

Nous devons maintenant ajouter une nouvelle fonction appelée play_sound à 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!")

Regardons son fonctionnement.

Tout d’abord, nous devons vérifier si Globals.gd dispose d’un clip audio avec le nom sound_name dans audio_clips. Si il n’en a pas, on affiche un message d’erreur.

Si Globals.gd a un clip audio avec le nom sound_name, on instancie un nouveau SIMPLE_AUDIO_PLAYER_SCENE et on l’assigne à new_audio.

On paramètre alors should_loop et on ajoute new_audio en tant qu’enfant de Globals.gd.

Note

Souvenez vous, nous devons êtres prudent en ajoutant un nœud à un singleton, puisque ces nœuds ne seront pas détruit lors d’un changement de scène.

Nous appelons alors play_sound en passant en paramètre le clip audio associé au sound_name ainsi que la position du son.


Avant de quitter Globals.gd, nous devons ajouter quelques lignes de code à load_new_scene de façon à ce que quand le joueur change de scènes, tout l’audio soit détruit.

Ajoutez ce qui suit dans load_new_scene :

for sound in created_audio:
    if (sound != null):
        sound.queue_free()
created_audio.clear()

Maintenant, avant que Globals.gd ne change de scène, il va passer par tous les lecteurs audio dans created_sounds et les libérer. Une fois que Globals.gd a parcouru tous les sons de created_audio, on vide created_audio de façon à ce qu’il ne contienne plus aucune référence à aucun lecteurs audio (maintenant libérés).


Modifions create_sound dans Player.gd afin qu’il utilise ce nouveau système. D’abord, supprimez simple_audio_player des variables de classe de Player.gd puisque nous n’instancierons plus de sons dans Player.gd.

Maintenant, changez create_sound en ce qui suit :

func create_sound(sound_name, position=null):
    globals.play_sound(sound_name, false, position)

Désormais, à chaque appel de create_sound, nous appelons simplement play_sound dans Globals.gd en passant en paramètre les arguments reçus.


Tous les sons de notre FPS peuvent maintenant être joué de n’importe où. Tout ce que nous avons à faire, c’est de récupérer le singleton Globals.gd, appeler play_sound, passer en paramètre le nom du son que nous souhaitons jouer, s’il doit être joué en boucle ou non ainsi que la position à laquelle le son devrait être joué.

Par exemple, si vous voulez jouer un son d’explosion quand la grenade explose, vous devrez ajouter un nouveau son dans audio_clips de Globals.gd, récupérer le singleton Globals.gd et ensuite vous aurez simplement à ajouter quelque chose comme globals.play_sound("explosion", false, global_transform.origin) dans la fonction _process des grenades, juste après que les grenades aient infligé des dégâts à tous les corps de la zone.

Notes finales

../../../_images/FinishedTutorialPicture.png

Vous avez maintenant un jeu FPS solo complètement fonctionnel !

À ce stade, vous avez de solides bases pour créer des FPS plus compliqués.

Avertissement

Si jamais vous vous perdez, n’oubliez pas de relire le code !

Vous pouvez télécharger le projet terminé pour le tutoriel complet ici : Godot_FPS_Part_6.zip

Note

Les fichiers sources du projet terminé contiennent le même code, écrit dans un ordre différent. Ceci est du au fait que les fichiers sources du projet terminé sont ceux sur lesquels le tutoriel se base.

Le code du projet terminé a été écrit dans l’ordre où les fonctionnalités ont été créées, pas nécessairement dans un ordre idéal pour apprendre.

En dehors de ça, les source sont exactement les mêmes, simplement avec des commentaires utiles en plus pour expliquer ce que chaque partie fait.

Astuce

Les sources du projet terminé sont également stockés sur Github : https://github.com/TwistedTwigleg/Godot_FPS_Tutorial

Notez que les sources sur Github peuvent ou non êtres synchronisés avec la documentation de ce tutoriel.

Le code dans la documentation est certainement mieux géré et/ou plus à jour. Si vous n’êtes pas sûr de quelle version utiliser, utilisez le(s) projet(s) fournis dans la documentation car ceux-ci sont maintenus par la communauté de Godot.

Vous pouvez télécharger tous les fichiers .blend utilisés dans ce tutoriel ici : Godot_FPS_BlenderFiles.zip

Tous les assets fournis dans les assets standards (sauf s’il est écrit autrement) sont crées à l’origine par TwistedTwigleg, avec des changements/modifications par la communauté Godot. Tous les assets originaux fournis pour ce tutoriel sont sous la licence MIT.

N’hésitez pas à utiliser ces ressources comme vous le souhaitez ! Toutes les ressources originales appartiennent à la communauté Godot, les autres ressources appartenant à ceux énumérés ci-dessous :

La skybox a été créée par StumpyStrust et peut être trouvé sur OpenGameArt.org. https://opengameart.org/content/space-skyboxes-0. La skybox est sous licence CC0.

La police utilisée est Titillium-Regular, et est sous licence SIL Open Font License, Version 1.1.

La skybox a été convertie d’une projection cylindrique équidistante 360 en utilisant cet outil : https://www.360toolkit.co/convert-cubemap-to-spherical-equirectangular.html

Bien qu’aucun sons n’ont été fournis, vous pouvez trouver beaucoup de très bon sons à cette adresse : https://gamesounds.xyz/

Avertissement

OpenGameArt.org, 360toolkit.co, le(s) créateur(s) de Titillium-Regular, StumpyStrust, et GameSounds.xyz ne sont en aucune façon impliqués dans ce tutoriel.