Partie 4

Aperçu cette partie

Dans cette partie, nous allons ajouter des bonus de santé, des bonus de munitions, des cibles que le joueur peut détruire, le support des manettes ainsi que la possibilité de changer d’arme avec la molette de la souris.

../../../_images/PartFourFinished.png

Note

Vous êtes supposés avoir terminé la doc_fps_tutorial_part_three`avant de pouvoir continuer sur cette partie du tutoriel. Le projet fini de :ref:`doc_fps_tutorial_part_three sera le projet de départ de la partie 4

Commençons tout de suite !

Ajout des contrôles de manette

Note

Dans Godot, n’importe quelle manette est appelée joypad. Cela inclut: manettes de console, Joysticks (venant de simulateurs de vol), volants (venant de simulateurs de conduite automobile), manettes VR, et plus encore !

Premièrement, nous devons changer certaines choses dans nos paramètres d’entrée de projet. Ouvrez les paramètres de projet et sélectionnez l’onglet Input Map.

Nous devons maintenant associer des boutons de contrôleurs à nos différentes actions. Cliquez sur l’icône plus et sélectionnez Joy Button.

../../../_images/ProjectSettingsAddKey.png

N’hésitez pas à utiliser la disposition de bouton que vous préférez. Faites attention à ce que l’appareil sélectionné est réglé sur 0. Dans le projet terminé, nous utiliserons ce qui suit :

  • movement_sprint : Device 0, Button 4 (L, L1)
  • fire : Device 0, Button 0 (PS Cross, XBox A, Nintendo B)
  • reload : Device 0, Button 0 (PS Square, XBox X, Nintendo Y)
  • flashlight : Device 0, Button 12 (D-Pad Up)
  • shift_weapon_positive : Device 0, Button 15 (D-Pad Right)
  • shift_weapon_negative : Device 0, Button 14 (D-Pad Left)
  • fire_grenade : Device 0, Button 1 (PS Circle, XBox B, Nintendo A).

Note

Ceux-ci sont déjà configurés pour vous si vous avez téléchargé les ressources de démarrage

Lorsque vous êtes satisfaits de vos entrées, fermez les paramètres de projet et sauvegardez.


Ouvrez maintenant Player.gd et ajoutez une entrée de contrôleur.

D’abord, nous devons définir de nouvelles variables de classe. Ajoutez les suivantes dans Player.gd :

# You may need to adjust depending on the sensitivity of your joypad
var JOYPAD_SENSITIVITY = 2
const JOYPAD_DEADZONE = 0.15

Parcourons les rôles de chacun d’entre eux :

  • JOYPAD_SENSITIVITY : C’est la vitesse à laquelle les joysticks du contrôleur vont bouger la caméra.
  • JOYPAD_DEADZONE : Les zones mortes du contrôleur. Vous pouvez les ajuster en fonction de son type.

Note

Beaucoup de contrôleurs tremblent à un moment donné. Pour contrer cela, nous ignorons le mouvement dans un certain rayon du JOYPAD_DEADZONE. Si nous n’ignorions pas ce mouvement, la caméra tremblerait.

De plus, nous définissons JOYPAD_SENSITIVITY comme une variable plutôt que comme une constante car nous allons la changer un peu plus tard.

Nous sommes maintenant prêts à manipuler les entrées de contrôleur !


Dans process_input, ajoutez le code suivant juste avant input_movement_vector = input_movement_vector.normalized() :

# Add joypad input if one is present
if Input.get_connected_joypads().size() > 0:

    var joypad_vec = Vector2(0, 0)

    if OS.get_name() == "Windows":
        joypad_vec = Vector2(Input.get_joy_axis(0, 0), -Input.get_joy_axis(0, 1))
    elif OS.get_name() == "X11":
        joypad_vec = Vector2(Input.get_joy_axis(0, 1), Input.get_joy_axis(0, 2))
    elif OS.get_name() == "OSX":
        joypad_vec = Vector2(Input.get_joy_axis(0, 1), Input.get_joy_axis(0, 2))

    if joypad_vec.length() < JOYPAD_DEADZONE:
        joypad_vec = Vector2(0, 0)
    else:
        joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))

    input_movement_vector += joypad_vec
# Add joypad input if one is present
if Input.get_connected_joypads().size() > 0:

    var joypad_vec = Vector2(0, 0)

    if OS.get_name() == "Windows" or OS.get_name() == "X11":
        joypad_vec = Vector2(Input.get_joy_axis(0, 0), -Input.get_joy_axis(0, 1))
    elif OS.get_name() == "OSX":
        joypad_vec = Vector2(Input.get_joy_axis(0, 1), Input.get_joy_axis(0, 2))

    if joypad_vec.length() < JOYPAD_DEADZONE:
        joypad_vec = Vector2(0, 0)
    else:
        joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))

    input_movement_vector += joypad_vec

Détaillons ce que nous faisons.

Tout d’abord, nous vérifions si un contrôleur est connecté.

Si un contrôleur est connecté, nous récupérons alors les axes droite/gauche et haut/bas pour le stick gauche. Étant donné qu’une manette de Xbox 360 a un mapping de ses axes basé sur l’OS, nous utiliserons différents axes en fonction de l’OS.

Avertissement

Nous supposons dans ce tutoriel que vous utilisez une manette de Xbox 360 ou une manette Playstation branchée. Je n’ai également (pour le moment) pas accès à un Mac donc les axes de joystick peuvent changer. Si c’est le cas, ouvrez un ticket Github sur le dépôt de la documentation Godot ! Merci !

Nous devons ensuite vérifier si la norme du vecteur du contrôleur est comprise dans le rayon JOYPAD_DEADZONE. Si c’est le cas, on affecte un Vector2 vide à joypad_vec. Si ce n’est pas le cas, on utilise une zone morte radiale à l’échelle pour des calculs précis de zone morte.

Note

Vous pouvez trouver un bon article qui explique tout sur la façon de gérer les zones mortes des contrôleurs ici : http://www.third-helix.com/2013/04/12/doing-thumbstick-dead-zones-right.html

Nous utilisons une translation du vecteur radial de zone morte mis à l’échelle fourni dans cet article. Celui-ci est très intéressant et je vous suggère d’aller y jeter un œil !

Enfin, nous ajoutons joypad_vec à input_movement_vector.

Astuce

Vous vous souvenez que nous avons normalisé input_movement_vector ? Voilà pourquoi ! Si nous n’avions pas normalisé input_movement_vector, le joueur pourrait se déplacer plus vite s’il bougeait dans la même direction avec un clavier et un contrôleur !


Écrivez une nouvelle fonction appelée process_view_input et ajoutez ce qui suit :

func process_view_input(delta):

    if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
        return

    # NOTE: Until some bugs relating to captured mice are fixed, we cannot put the mouse view
    # rotation code here. Once the bug(s) are fixed, code for mouse view rotation code will go here!

    # ----------------------------------
    # Joypad rotation

    var joypad_vec = Vector2()
    if Input.get_connected_joypads().size() > 0:

        if OS.get_name() == "Windows":
            joypad_vec = Vector2(Input.get_joy_axis(0, 2), Input.get_joy_axis(0, 3))
        elif OS.get_name() == "X11":
            joypad_vec = Vector2(Input.get_joy_axis(0, 3), Input.get_joy_axis(0, 4))
        elif OS.get_name() == "OSX":
            joypad_vec = Vector2(Input.get_joy_axis(0, 3), Input.get_joy_axis(0, 4))

        if joypad_vec.length() < JOYPAD_DEADZONE:
            joypad_vec = Vector2(0, 0)
        else:
            joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))

        rotation_helper.rotate_x(deg2rad(joypad_vec.y * JOYPAD_SENSITIVITY))

        rotate_y(deg2rad(joypad_vec.x * JOYPAD_SENSITIVITY * -1))

        var camera_rot = rotation_helper.rotation_degrees
        camera_rot.x = clamp(camera_rot.x, -70, 70)
        rotation_helper.rotation_degrees = camera_rot
    # ----------------------------------
func process_view_input(delta):

   if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
       return

   # NOTE: Until some bugs relating to captured mice are fixed, we cannot put the mouse view
   # rotation code here. Once the bug(s) are fixed, code for mouse view rotation code will go here!

   # ----------------------------------
   # Joypad rotation

   var joypad_vec = Vector2()
   if Input.get_connected_joypads().size() > 0:

       if OS.get_name() == "Windows" or OS.get_name() == "X11":
           joypad_vec = Vector2(Input.get_joy_axis(0, 2), Input.get_joy_axis(0, 3))
       elif OS.get_name() == "OSX":
           joypad_vec = Vector2(Input.get_joy_axis(0, 3), Input.get_joy_axis(0, 4))

       if joypad_vec.length() < JOYPAD_DEADZONE:
           joypad_vec = Vector2(0, 0)
       else:
           joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))

       rotation_helper.rotate_x(deg2rad(joypad_vec.y * JOYPAD_SENSITIVITY))

       rotate_y(deg2rad(joypad_vec.x * JOYPAD_SENSITIVITY * -1))

       var camera_rot = rotation_helper.rotation_degrees
       camera_rot.x = clamp(camera_rot.x, -70, 70)
       rotation_helper.rotation_degrees = camera_rot
   # ----------------------------------

Regardons ce qui ce passe :

Nous allons tout d’abord vérifier le nœud de la souris. Si celui-ci n’est pas MOUSE_MODE_CAPTURED, nous allons faire un return, ce qui va passer tout le code en dessous.

Ensuite, nous définissons un nouveau Vector2 appelé joypad_vec. Il va stocker la position du joystick droit. En se basant sur l’OS, nous allons définir sa valeur de façon à ce qu’il soit mappé aux bons axes du joystick droit.

Avertissement

Comme indiqué ci-dessus, je n’ai (actuellement) pas accès à un ordinateur Mac donc les axes de joystick peuvent changer. Si c’est le cas, veuillez ouvrir un ticket Github sur le dépôt de la documentation ! Merci !

Nous tenons compte ensuite de la zone morte du contrôleur exactement comme dans process_input.

Ensuite, nous faisons tourner rotation_helper et le KinematicBody du joueur en utilisant joypad_vec.

Notez comment le code qui gère la rotation du joueur et de rotation_helper est exactement le même code que celui dans _input. Tout ce que nous avons fait, c’est de changer les valeurs en utilisant joypad_vec et JOYPAD_SENSITIVITY.

Note

A cause de certains bugs de souris sur Windows, nous ne pouvons pas mettre le rotation de la souris dans process_view non plus. Une fois que ces bugs seront réglés, nous mettrons probablement à jour ce tutoriel pour placer la rotation de la souris dans process_view_input aussi.

Enfin, nous limitons la valeur de rotation de la caméra de façon à ce que le joueur ne puisse pas regarder à l’envers.


La dernière chose que nous devons faire, c’est d’ajouter process_view_input à _physics_process.

Une fois que process_view_input est inclus dans _physics_process, nous devrions être en mesure de jouer avec un contrôleur !

Note

J’ai décidé de ne pas utiliser de déclencheurs de contrôleurs car nous aurions alors à gérer plus de choses avec les axes et aussi car je préfère utiliser un bumper pour tirer.

Si vous souhaitez utiliser les déclencheurs pour tirer, vous aurez besoin de changer le fonctionnement du tir dans process_input. Vous devez récupérer la valeur des axes pour les déclencheurs et vérifier si ceux-ci sont au dessus d’une certaine valeur telle que ``0.8``par exemple. Si c’est le cas, ajoutez le même code que quand la touche ``fire``a été actionnée.

Ajouter une entrée pour la molette de la souris

Ajoutons une dernière fonctionnalité liée eux touches avant de commencer à travailler sur les bonus et les cibles. Ajoutons la possibilité de changer d’arme en utilisant la molette de la souris.

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

var mouse_scroll_value = 0
const MOUSE_SENSITIVITY_SCROLL_WHEEL = 0.08

Passons en revue l’utilité de chaque variable :

  • mouse_scroll_value : La valeur de la molette de la souris.
  • MOUSE_SENSITIVITY_SCROLL_WHEEL : De combien une seule action de défilement augmente mouse_scroll_value

Maintenant, ajoutons ce qui suit à _input :

if event is InputEventMouseButton and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
    if event.button_index == BUTTON_WHEEL_UP or event.button_index == BUTTON_WHEEL_DOWN:
        if event.button_index == BUTTON_WHEEL_UP:
            mouse_scroll_value += MOUSE_SENSITIVITY_SCROLL_WHEEL
        elif event.button_index == BUTTON_WHEEL_DOWN:
            mouse_scroll_value -= MOUSE_SENSITIVITY_SCROLL_WHEEL

        mouse_scroll_value = clamp(mouse_scroll_value, 0, WEAPON_NUMBER_TO_NAME.size() - 1)

        if changing_weapon == false:
            if reloading_weapon == false:
                var round_mouse_scroll_value = int(round(mouse_scroll_value))
                if WEAPON_NUMBER_TO_NAME[round_mouse_scroll_value] != current_weapon_name:
                    changing_weapon_name = WEAPON_NUMBER_TO_NAME[round_mouse_scroll_value]
                    changing_weapon = true
                    mouse_scroll_value = round_mouse_scroll_value

Voyons ce qui se passe ici :

Tout d’abord, nous devons vérifier si l’événement est un événement de type InputEventMouseButton et si le mode de la souris est MOUSE_MODE_CAPTURED. Ensuite, nous vérifions que l’index du bouton est soit BUTTON_WHEEL_UP, soit BUTTON_WHEEL_DOWN.

Si l’index de l’événement est bien un index de bouton de la molette, alors nous vérifions qu’il est de type BUTTON_WHEEL_UP ou BUTTON_WHEEL_DOWN. En fonction de si c’est un déplacement vers le haut ou vers le bas, on ajoute ou soustrait MOUSE_SENSITIVITY_SCROLL_WHEEL à mouse_scroll_value ou inversement.

Nous limitons ensuite les valeurs du défilement de la souris afin de s’assurer qu’il se situe dans la plage des armes sélectionnables.

Nous vérifions ensuite si le joueur est en train de changer d’arme ou de recharger. Si celui-ci ne fait ni l’une ni l’autre, on arrondi mouse_scroll_value et on le transforme en int.

Note

Nous convertissons mouse_scroll_value en int de façon à pouvoir l’utiliser comme clef dans notre dictionnaire. Si nous le laissons en float, nous aurions une erreur en tentant de lancer le projet.

Nous vérifions ensuite si le nom de l’arme à round_mouse_scroll_value``n'est pas égale au nom courant de l'arme en utilisant ``WEAPON_NUMBER_TO_NAME. Si l’arme est différence de celle que le joueur utilise, on assigne changing_weapon_name, on attribue true à changing_weapon afin que le joueur change d’arme dans process_changing_weapon et on attribue mouse_scroll_value à round_mouse_scroll_value.

Astuce

La raison pour laquelle on règle mouse_scroll_value``à la valeur arrondie du défilement est parce que nous ne voulons pas que le joueur garde sa molette de souris entre deux valeurs, ce qui lui donnerai l'opportunité de changer d'arme extrêmement rapidement. En assignant ``mouse_scroll_value à round_mouse_scroll_value, on s’assure que chaque arme a besoin du même montant de défilement que les autre pour en changer.


Nous devons changer une toute dernière chose dans process_input. Dans le code pour changer d’arme, ajoutez ce qui suit juste après la ligne changing_weapon = true :

mouse_scroll_value = weapon_change_number

Maintenant, la valeur de défilement va changer avec les entrées du clavier. Si nous ne changions pas ça, la valeur de défilement serait désynchronisée. Si c’était le cas, faire défiler en bas ou en haut ne ferais pas la transition vers la prochaine/dernière arme mais plutôt à la prochaine/dernière arme du défilement à la souris.


Vous pouvez maintenant changer d’arme en utilisant la molette de la souris ! Essayez !

Ajouter des vies ramassables

Maintenant que le joueur a de la santé et des munitions, nous voudrions idéalement un moyen de regagner ces ressources.

Ouvrez Health_Pickup.tscn.

Développez Holder``si ce n'est pas déjà fait. Remarquez le fait que nous avons deux nœuds Spatial. Le premier est appelé ``Health_Kit et l’autre Health_Kit_Small.

C’est parce que nous allons en fait faire deux tailles de bonus de santé : une petite et une grande/normale. Health_Kit et Health_Kit_Small ont seulement un simple MeshInstance comme enfant.

Développez ensuite Health_Pickup_Trigger. Il s’agit d’un nœud Area que nous allons utiliser pour vérifier si le joueur a marché suffisamment près pour récupérer le kit de santé. Si vous le développez, vous trouverez deux collision shapes, une pour chaque taille. Nous utiliserons une taille de collision shape différente en fonction de la taille du bonus de santé, de sorte que le plus petit kit de santé a une collision shape plus proche de sa taille.

La dernière chose à noter est la façon dont nous avons un nœud AnimationPlayer de sorte que la trousse de soin ballote et tourne lentement.

Sélectionnez Health_Pickup et ajoutez un nouveau script appelé Health_Pickup.gd. Ajoutez ce qui suit :

extends Spatial

export (int, "full size", "small") var kit_size = 0 setget kit_size_change

# 0 = full size pickup, 1 = small pickup
const HEALTH_AMOUNTS = [70, 30]

const RESPAWN_TIME = 20
var respawn_timer = 0

var is_ready = false

func _ready():

    $Holder/Health_Pickup_Trigger.connect("body_entered", self, "trigger_body_entered")

    is_ready = true

    kit_size_change_values(0, false)
    kit_size_change_values(1, false)
    kit_size_change_values(kit_size, true)


func _physics_process(delta):
    if respawn_timer > 0:
        respawn_timer -= delta

        if respawn_timer <= 0:
            kit_size_change_values(kit_size, true)


func kit_size_change(value):
    if is_ready:
        kit_size_change_values(kit_size, false)
        kit_size = value
        kit_size_change_values(kit_size, true)
    else:
        kit_size = value


func kit_size_change_values(size, enable):
    if size == 0:
        $Holder/Health_Pickup_Trigger/Shape_Kit.disabled = !enable
        $Holder/Health_Kit.visible = enable
    elif size == 1:
        $Holder/Health_Pickup_Trigger/Shape_Kit_Small.disabled = !enable
        $Holder/Health_Kit_Small.visible = enable


func trigger_body_entered(body):
    if body.has_method("add_health"):
        body.add_health(HEALTH_AMOUNTS[kit_size])
        respawn_timer = RESPAWN_TIME
        kit_size_change_values(kit_size, false)

Passons en revue ce que fait ce script, en commençant par ses variables de classe :

  • kit_size : La taille du bonus de santé. Remarquez comment nous utilisons une fonction setget pour savoir s’il a changé.
  • HEALTH_AMMOUNTS : Le montant de santé de donne chaque kit par rapport à leur taille.
  • RESPAWN_TIME : La durée, en seconde, qu’il faut pour que le bonus réapparaisse
  • respawn_timer : Une variable utilisé pour suivre le temps qu’a mis le bonus de santé pour réapparaître.
  • is_ready : Une variable pour suivre si la fonction _ready a été appelée ou non.

Nous utilisons is_ready car les fonctions setget sont appelées avant _ready. Nous devons ignorer le premier appel de kit_size_change car nous ne pouvons pas accéder aux nœuds enfants tant que _ready``n'a pas été appelé. Si nous n'ignorions pas le premier appel ``setget, nous obtiendrons plusieurs erreurs dans le « debugger ».

Notez également comment nous utilisons une variable exporté. Nous faisons cela de façon à pouvoir changer la taille des bonus de santé dans l’éditeur. Cela nous permet de ne pas avoir à créer deux scènes pour les deux tailles, étant donné que nous pouvons facilement changer les tailles dans l’éditeur en utilisant la variable exportée.

Astuce

Pour plus de détails, regardez GDScript basics et descendez à la section Exports pour avoir une liste de tous les indices d’export que vous pouvez utiliser.


Regardons maintenant _ready :

Premierement , nous connectons le signale « body_entered » provenans de « Health_Pickup_Trigger » à la fonction « trigger_body_entered ». Cela permet que n’importe quel corp qui penettre :ref:`Area <class_Area> déclenche la fonction « trigger_body_entered ».

Ensuite, nous assignons à is_ready la valeur true afin de pouvoir utiliser la fonction setget.

Nous cachons ensuite tout les kits existants ainsi que leur « collision shapes » en utilisant kit_size_change_values. Le premier argument est la taille du kit, quand au second, il correspond l’activation ou non du « collision shape » ainsi que du « mesh » à cette taille.

Ensuite, nous faisons en sorte que seul le kit sélectionné soit visible en appelant kit_size_change_values et en lui passant kit_size et true en paramètre afin que la taille à kit_size soit activée.


Ensuite, regardons kit_size_change.

La première chose que nous faisons, est de vérifier si is_ready est à true.

Si is_ready est à true, alors nous rendons n’importe quel kit déjà assigné à kit_size désactivé en utilisant kit_size_change_values et en passant kit_size et false en paramètres.

Nous assignons ensuite kit_size à la nouvelle valeur passé en paramètre value. Puis, nous appelons kit_size_change_values``en passant ``kit_size en paramètre mais en ajoutant cette fois-ci true en tant que second argument afin de l’activer. Parce que nous avons changé kit_size à la valeur passée en paramètre, cela va rendre le kit passé en paramètre visible.

Si is_ready n’est pas à true, on assigne simplement la valeur passé en paramètre value à kit_size.


Maintenant, jetons un œil à kit_size_change_values.

La première chose que nous faisons est de vérifier quelle taille a été passé en paramètre. En fonction de la taille que nous voulons activer, nous voulons différents nœuds.

On récupère la collision shape pour le nœud correspondant à la size et on la désactive grâce au paramètre enabled passé en argument/variable.

Note

Pourquoi utilisons nous !enable au lieu de enable ? C’est ainsi car quand nous souhaitons activer le nœud, nous pouvons passer true en paramètre mais parce que CollisionShape utilise disabled au lieu de enabled, nous devons l’inverser. En faisant cela, nous pouvons activer la collision shape et rendre le mesh visible quand true est passé en paramètre.

Nous récupérons alors la bon nœud Spatial qui contient le mesh et nous changeons sa visibilité en enable.

Cette fonction peut être déroute; essayez de penser comme ceci : On active/désactive les nœuds appropriés pour size en utilisant enabled. Ainsi, nous ne pouvons pas récupérer de la santé pour une taille qui n’est pas visible et seul les mesh avec la taille appropriés sont visibles.


Enfin, regardons trigger_body_entered.

La première chose que nous vérifions est si oui ou non le corps qui vient d’entrer dispose d’une méthode/fonction appelée add_health. Si c’est le cas, alors nous appelons add_health et passons en paramètre la santé donnée par la taille de kit actuelle.

Ensuite, on assigne RESPAWN_TIME à respawn_timer de façon à ce que le joueur doive attendre avant de pouvoir regagner de la vie. Enfin, appelez kit_size_change_values en passant kit_size et false en paramètres de sorte que kit_size soit invisible jusqu’à ce qu’il ait attendu suffisamment longtemps pour réapparaître.


La dernière chose que nous devons faire avant que le joueur puisse utiliser ces bonus de santé, c’est de compléter Player.gd.

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

const MAX_HEALTH = 150
  • MAX_HEALTH: Le montant maximum de santé que le joueur peut avoir.

Maintenant, nous devons ajouter la fonction add_health au joueur. Ajoutez ce qui suit au Player.gd :

func add_health(additional_health):
    health += additional_health
    health = clamp(health, 0, MAX_HEALTH)

Passons en revue rapidement son utilité.

Nous ajoutons d’abord additional_health à la santé actuelle du joueur. Nous limitons ensuite la santé de façon à ce que cette valeur ne dépasse pas la valeur de MAX_HEALTH, ou bien une valeur inférieure à 0.


Ceci fait, le joueur peut maintenant récupérer de la santé ! Allez placer quelques scènes Health_Pickup dans la carte et essayez par vous-même. Vous pouvez changer la taille des bonus de santé dans l’éditeur quand un Health_Pickup instancié dans la scène est sélectionné à partir d’une liste déroulante très pratique.

Ajouter des munitions ramassables

Bien que l’ajout de santé soit bénéfique, nous ne pouvons pas en voir les effets (actuellement) car rien ne peut nous faire des dégâts. Ajoutons quelques bonus de munitions ensuite !

Ouvrez Ammo_Pickup.tscn. Notez que la structure est exactement la même que Health_Pickup.tscn, mais que les meshes et les déclencheurs collision shapes sont changé un petit peu pour prendre en compte les différences de tailles des meshes.

Sélectionnez Ammo_Pickup et ajoutez un nouveau script appelé Ammo_Pickup.gd. Ajoutez ce qui suit :

extends Spatial

export (int, "full size", "small") var kit_size = 0 setget kit_size_change

# 0 = full size pickup, 1 = small pickup
const AMMO_AMOUNTS = [4, 1]

const RESPAWN_TIME = 20
var respawn_timer = 0

var is_ready = false

func _ready():

    $Holder/Ammo_Pickup_Trigger.connect("body_entered", self, "trigger_body_entered")

    is_ready = true

    kit_size_change_values(0, false)
    kit_size_change_values(1, false)

    kit_size_change_values(kit_size, true)


func _physics_process(delta):
    if respawn_timer > 0:
        respawn_timer -= delta

        if respawn_timer <= 0:
            kit_size_change_values(kit_size, true)


func kit_size_change(value):
    if is_ready:
        kit_size_change_values(kit_size, false)
        kit_size = value

        kit_size_change_values(kit_size, true)
    else:
        kit_size = value


func kit_size_change_values(size, enable):
    if size == 0:
        $Holder/Ammo_Pickup_Trigger/Shape_Kit.disabled = !enable
        $Holder/Ammo_Kit.visible = enable
    elif size == 1:
        $Holder/Ammo_Pickup_Trigger/Shape_Kit_Small.disabled = !enable
        $Holder/Ammo_Kit_Small.visible = enable


func trigger_body_entered(body):
    if body.has_method("add_ammo"):
        body.add_ammo(AMMO_AMOUNTS[kit_size])
        respawn_timer = RESPAWN_TIME
        kit_size_change_values(kit_size, false)

Vous avez peut être remarqué que ce code ressemble presque exactement à celui des bonus de santé. Cela est du au fait qu’il lui ressemble énormément. Seuls quelques détails ont changés et ce que nous allons examiner.

Premièrement, remarquez le changement de HEALTH_AMMOUNTS``à ``AMMO_AMOUNTS. AMMO_AMOUNTS va correspondre au nombre de chargeurs que le bonus ajoute à votre arme courante. (Contrairement au cas de HEALTH_AMMOUNTS qui représente le nombre de points de santé ajouté, nous ajoutons ici un chargeur entier à larme actuelle au lieu d’une quantité brute de munitions)

La seule autre chose à remarquer est dans trigger_body_entered. Nous vérifions l’existence et nous appelons la fonction appelée add_ammo et non pas add_health.

En dehors de ces deux petits changements, tout le reste est exactement pareil que pour le bonus de santé !


Tout ce que nous devons faire pour faire fonctionner ces bonus de munitions, c’est d’ajouter une nouvelle fonction au joueur. Ouvrez Player.gd et ajoutez la fonction suivante :

func add_ammo(additional_ammo):
    if (current_weapon_name != "UNARMED"):
        if (weapons[current_weapon_name].CAN_REFILL == true):
            weapons[current_weapon_name].spare_ammo += weapons[current_weapon_name].AMMO_IN_MAG * additional_ammo

Regardons son fonctionnement.

La première chose que nous vérifions c’est si le joueur est UNARMED. Nous devons vérifier ceci car UNARMED``ne dispose pas d'un nœud/script et nous devons nous assurer que le joueur n'est pas ``UNARMED avant de récupérer le nœud/script attaché à current_weapon_name.

Ensuite, nous vérifions que l’arme courante peut être remplie. Si c’est le cas, nous lui ajoutons un chargeur entier de munitions en multipliant la valeur AMMO_IN_MAG par combien de chargeurs nous voulons lui ajouter (additional_ammo).


Ceci fait, vous devriez pouvoir obtenir de nouvelles munitions ! Placez quelques bonus de munition dans une/deux/toutes les scènes et essayez par vous même !

Note

Notez que nous ne limitons pas le nombre de munitions que nous pouvons porter. Pour limiter le nombre de munition que vous pouvez porter pour chaque arme, vous devrez ajouter une variable supplémentaire dans chaque script d’arme et limiter la variable spare_ammo après avoir ajouté des munitions dans add_ammo.

Ajouter des cibles destructibles

Avant de terminer cette partie, ajoutons quelques cibles.

Ouvrez Target.tscn et jetez un œil aux scènes dans l’arborescence des scènes.

Tout d’abord, remarquez que nous n’utilisons pas un nœud RigidBody, mais un StaticBody. La raison derrière ce choix est que nos cibles pas encore détruites ne vont bouger nul part; utiliser un :ref:`RigidBody <class_RigidBody>`n’en voudrait pas la peine puisque que tout ce que cette cible va faire c’est rester immobile.

Astuce

Nous gagnons également un tout petit peu de performances en utilisant un StaticBody à la place d’un RigidBody.

L’autre chose à noter c’est que nous utilisons un nœud appelé Broken_Target_Holder. Ce nœud va contenir une scène instancié appelée Broken_Target.tscn. Ouvrez Broken_Target.tscn.

Remarquez comme la cible se détruit en 5 morceaux distincts, chacun étant un nœud RigidBody. Nous allons faire apparaître cette scène quand la cible prend trop de dégâts et doit être détruite. Ensuite, nous cacherons les cibles pas encore détruites de façon à avoir une cible qui se détruise plutôt qu’une cible qui apparaît détruite.

Tant que vous avez encore Broken_Target.tscn``ouvert, ajoutez ``RigidBody_hit_test.gd à tous les nœuds RigidBody. Cela va faire en sorte que le joueur puisse tirer sur les pièces détruites et qu’elles réagissent aux balles.

Très bien, maintenant repassez à Target.tscn, sélectionnez le nœud StaticBody Target et créez un nouveau script appelé Target.gd.

Ajoutez le code suivant à Target.gd :

extends StaticBody

const TARGET_HEALTH = 40
var current_health = 40

var broken_target_holder

# The collision shape for the target.
# NOTE: this is for the whole target, not the pieces of the target.
var target_collision_shape

const TARGET_RESPAWN_TIME = 14
var target_respawn_timer = 0

export (PackedScene) var destroyed_target

func _ready():
    broken_target_holder = get_parent().get_node("Broken_Target_Holder")
    target_collision_shape = $Collision_Shape


func _physics_process(delta):
    if target_respawn_timer > 0:
        target_respawn_timer -= delta

        if target_respawn_timer <= 0:

            for child in broken_target_holder.get_children():
                child.queue_free()

            target_collision_shape.disabled = false
            visible = true
            current_health = TARGET_HEALTH


func bullet_hit(damage, bullet_transform):
    current_health -= damage

    if current_health <= 0:
        var clone = destroyed_target.instance()
        broken_target_holder.add_child(clone)

        for rigid in clone.get_children():
            if rigid is RigidBody:
                var center_in_rigid_space = broken_target_holder.global_transform.origin - rigid.global_transform.origin
                var direction = (rigid.transform.origin - center_in_rigid_space).normalized()
                # Apply the impulse with some additional force (I find 12 works nicely).
                rigid.apply_impulse(center_in_rigid_space, direction * 12 * damage)

        target_respawn_timer = TARGET_RESPAWN_TIME

        target_collision_shape.disabled = true
        visible = false

Passons en revue ce que fait ce script, en commençant par les variables de classe :

  • TARGET_HEALTH: Le montant de dégâts nécessaires afin de détruire une cible intacte.
  • current_health: La santé actuelle de la cible.
  • broken_target_holder: Une variable pour stocker le nœud Broken_Target_Holder afin de pouvoir l’utiliser facilement.
  • target_collision_shape: Une variable pour stocker la CollisionShape pour les cibles non détruites.
  • TARGET_RESPAWN_TIME: La durée en seconde correspondante au temps nécessaire pour que la cible réapparaisse.
  • target_respawn_timer: Une variable utilisée pour suivre la durée durant laquelle la cible a été détruite.
  • destroyed_target: Une PackedScene Pour sauvegarder la scène de cible cassée.

Remarque que nous utilisons une variable exporté (une PackedScene) pour récupérer la scène de cible détruite au lieu d’utiliser preload. En utilisant une variable exporté, nous pouvons choisir la scène depuis l’éditeur et si nous avons besoin de sélectionner une autre scène, il suffit de sélectionner une autre scène dans l’éditeur; nous n’avons pas besoin d’aller dans le code pour changer la scène à utiliser.


Regardons maintenant _ready.

La première chose que nous devons faire, c’est récupérer le conteneur de cible cassé et l’assigner à broken_target_holder. Notez que nous utilisons get_parent().get_node() ici au lieu de $. Si vous vouliez utiliser $, vous devriez changer get_parent().get_node() en $"../Broken_Target_Holder".

Note

Au moment où cela a été écrit, je n’ai pas réalisé que l’on pouvait utiliser $"../NomDuNoeud" pour récupérer le nœud parent en utilisant $, c’est pourquoi get_parent().get_node() est utilisé à la place.

Ensuite, nous récupérons la collision shape et nous lui assignons target_collision_shape. La raison pour laquelle nous avons besoin de la collision shape est parce que même si le mesh est invisible, la collision shape existe toujours dans le monde physique. Cela permettrait au joueur de pouvoir interagir avec une cible non cassée même si celle-ci est invisible ce qui n’est pas souhaitable. Pour contourner ce soucis, désactivons/réactivons la collision shape et nous rendons le mesh visible/invisible.


Ensuite, regardons _physics_process.

Nous n’utiliserons que _physics_process pour faire apparaître et par conséquent, la première chose à vérifier est si target_respawn_timer est supérieur à 0.

Si c’est le cas, nous soustrayons delta de celui-ci.

Nous vérifions ensuite si target_respawn_timer est 0``ou moins. La raison derrière cela est qu'étant donné que nous avons retiré ``delta de target_respawn_timer, si c’est égal à 0 ou moins, alors la cible vient d’arriver, ce qui nous permet de faire ce que l’on doit faire une fois le timer terminé.

Dans ce cas, nous voulons faire réapparaître la cible.

La première chose que nous faisons, c’est retirer tous les enfants du conteneur de cible cassé. On fait ça en parcourant tous les fils de broken_target_holder et en les supprimant en utilisant queue_free.

Puis, nous activons les collision shape en mettant leur booléen disabled à false.

Nous rendons alors la cible ainsi que tous ces enfants visibles à nouveau.

Enfin, nous réinitialisons la santé de la cible (current_health) en TARGET_HEALTH.


Terminons par un coup d’œil à bullet_hit.

La première chose que nous faisons est de soustraire le nombre de dégâts de la balle a fait à la santé de la cible.

Ensuite, nous vérifions si la santé de la cible est à 0 ou moins. Si c’est le cas, la cible vient d’être détruite et nous devons faire apparaître une cible détruite.

Nous commençons par instancier une nouvelle scène de cible détruite et on lui assigne une nouvelle variable, un clone.

Nous ajoutons le clone en tant que fils du conteneur de cible détruite.

Pour un effet bonus, nous voulons que toutes les pièces de cibles explosent vers l’extérieur. Pour faire cela, nous parcourons tous les enfants dans clone.

Pour chaque enfant, nous vérifions d’abord si c’est un nœud RigidBody. Si c’est le cas, on calcule alors la position centrale de la cible relative au nœud enfant. Ensuite, on détermine dans quelle direction le nœud enfant est relatif au centre. En utilisant ces variables calculées, on pousse l’enfant du centre calculé vers une direction éloignée du centre en utilisant les dégâts de la balle comme force.

Note

On multiplie les dégâts par 12 de façon à avoir un effet plus dramatique. Vous pouvez changer ceci à une valeur plus basse ou plus élevée en fonction du degré d’explosion souhaité pour vos cibles détruites.

Ensuite, on fixe la minuterie de réapparition de la cible. On fixe celle-ci à TARGET_RESPAWN_TIME de façon à ce qu’il prenne TARGET_RESPAWN_TIME en secondes avant de réapparaître.

Nous désactivons ensuite les collision shapes des cibles non détruites et on met la visibilité de la cible à false.


Avertissement

Assurez-vous d’assigner une valeur à celle exportée destroyed_target de Target.tscn dans l’éditeur ! Dans le cas contraire, les cibles ne seront pas détruites et vous aurez une erreur !

Ceci fait, allez placer quelques instances de ``Target.tscn``autour de un/deux/tous les niveaux. Vous devriez les voir exploser en 5 morceaux après avoir subi suffisamment de dégâts. Après un certain temps, elles vont réapparaître en une cible intacte à nouveau.

Notes finales

../../../_images/PartFourFinished.png

Vous pouvez dès à présent utiliser un contrôleur, changer d’armes avec la molette de la souris, reconstituer votre santé ou vos munitions et casser des cibles avec vos armes.

Dans la prochaine partie, Partie 5, nous allons ajouter des grenades à notre joueur, lui donner la capacité d’attraper et de lancer des objets et nous ajouterons également des tourelles !

Avertissement

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

Vous pouvez télécharger le projet terminé pour cette partie ici : Godot_FPS_Part_4.zip