Partie 3

Aperçu cette partie

Dans cette partie, nous allons limité les armes du joueur en lui ajoutant un nombre de munitions. Nous allons également donner a notre joueur l’aptitude de recharger, et nous allons ajouter les bruitages des tirs quand l’arme est utilisée.

../../../_images/PartThreeFinished.png

Note

Vous êtes supposez avoir fini :ref:”doc_fps_tutorial_part_two” avant de commencer cette partie du tutoriel. Le projet final de :ref:`doc_fps_tutorial_part_two” sera le projet de départ pour la troisième partie

Commençons tout de suite !

Changement des niveaux

Maintenant que nous avons un FPS qui fonctionne totalement, passons à un niveau ressemblant plus a un FPS (FPS-like).

Ouvrez « Space_Level.tscn » (« assets/Space_Level_Objects/Space_Level.tscn ») and/ou « Ruins_Level.tscn » (« assets/Ruin_Level_Objects/Ruins_Level.tscn »).

« Space_Level.tscn » et « Ruins_Level.tscn » sont des niveaux FPS complètement customisés crée pour les besoins de ce tutoriel. Appuyez sur « F6 » pour jouer la scène d’entrée, ou appuyez sur le bouton « jouer la scène courante », et essayez chaque méthode.

Avertissement

Space_Level.tscn``est plus demandant graphiquement sur le GPU que ``Ruins_Level.tscn. Si votre PC n’arrive pas à donner un rendu stable, essayez ``Ruins_Level.tscn``à la place.

Vous avez peut-être remarquez qu’il y a plusieurs noeuds :ref:”RigiBody<class_RigiBody>” placés au travers du niveau. Nous pouvons placer « RigiBody_hit_test.gd » sur eux et alors ils réagiront au fait d’être touché par des balles, c’est parti !

Suivez l’instruction en dessous pour une (ou les deux) des scènes que vous voulez utiliser

Expand "Other_Objects" and then expand "Physics_Objects".

Expand one of the "Barrel_Group" nodes and then select "Barrel_Rigid_Body" and open it using
the "Open in Editor" button.
This will bring you to the "Barrel_Rigid_Body" scene. From there, select the root node and
scroll the inspector down to the bottom.
Select the drop down arrow under the "Node" tab, and then select "Load". Navigate to
"RigidBody_hit_test.gd" and select "Open".

Return back to "Space_Level.tscn".

Expand one of the "Box_Group" nodes and then select "Crate_Rigid_Body" and open it using the
"Open in Editor" button.
This will bring you to the "Crate_Rigid_Body" scene. From there, select the root node and
scroll the inspector down to the bottom.
Select the drop down arrow under the "Node" tab, and then select "Load". Navigate to
"RigidBody_hit_test.gd" and select "Open".

Return to "Space_Level.tscn".
Expand "Misc_Objects" and then expand "Physics_Objects".

Select all the "Stone_Cube" RigidBodies and then in the inspector scroll down to the bottom.
Select the drop down arrow under the "Node" tab, and then select "Load". Navigate to
"RigidBody_hit_test.gd" and select "Open".

Return to "Ruins_Level.tscn".

Maintenant vous pouvez tirer sur tout les « rigid bodies » dans n’importe quel niveau et ils réagiront aux balles les touchant !

Ajout des munitions

Maintenant que notre joueur possède des armes à feu, donnons leurs un nombre de munitions limité.

Tout d’abord, nous devons définir quelques variables dans chacun de nos scripts d’armes à feu.

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

var ammo_in_weapon = 10
var spare_ammo = 20
const AMMO_IN_MAG = 10
  • ammo_in_weapon: Le nombre de munitions actuellement dans le pistolet
  • spare_ammo: Le nombre de munitions que nous avons en réserve (disponible pour le rechargement de l’arme) pour le pistolet
  • AMMO_IN_MAG: Le nombre de munitions dans une arme ou un chargeur, quand il/elle est entièrement chargé de munitions

Tout ce qu’il ne reste à faire est d’ajouter une seule ligne de code à fire_weapon.

Ajoutez ce qui suit juste en dessous : Clone.BULLET_DAMAGE = DAMAGE: ammo_in_weapon -= 1

Ceci décrémente un de « ammo_in_weapon » à chaque fois que le jouer tire. Notez que nous ne vérifions pas si le joueur a assez de munitions ou non dans « fire_weapon ». Nous vérifions si le joueur a assez de munitions dans « Player.gd ».


Maintenant nous allons devoir ajouter des munitions pour le fusil et le couteau.

Note

Vous vous demandez peut-être pourquoi nous ajoutons des munitions pour le couteau étant donné qu’il ne consomme pas de munitions. Nous voulons en ajouter pour avoir une interface consistante pour toutes nos armes.

Si nous n’ajoutons pas de munitions au couteau, nous allons devoir ajouter des vérifications pour le couteau. En ajoutant une variable munitions pour le couteau, nous n’aurons pas à nous préoccuper de si oui ou non toute nos armes ont les mêmes variables.

Ajoutez la variable de classe suivante à « Weapon_Rifle.gd » :

var ammo_in_weapon = 50
var spare_ammo = 100
const AMMO_IN_MAG = 50

Puis ajoutez la ligne suivante à « fire_weapon » : « ammo_in_weapon -= 1 » . Assurez-vous que « ammo_in_weapon -= 1 » soit hors de la vérification « if ray.is_colliding() » pour que le joueur ne perdent des munitions qu’importe si il touche quelque chose ou non.

Maintenant il ne nous reste que le couteau. Ajoutez l’élément suivant à « Weapon_Knife.gd » :

var ammo_in_weapon = 1
var spare_ammo = 1
const AMMO_IN_MAG = 1

Comme le couteau ne consomme pas de munitions c’est tout ce que nous avons besoin d’ajouter.


Maintenant nous avons besoin de changer une chose dans « Player.gd », c’est-à-dire,

comment nous faisons feu avec les armes dans « process_input » . Changez le code pour faire feu avec les armes par le suivant :

# ----------------------------------
# Firing the weapons
if Input.is_action_pressed("fire"):
    if changing_weapon == false:
        var current_weapon = weapons[current_weapon_name]
        if current_weapon != null:
            if current_weapon.ammo_in_weapon > 0:
                if animation_manager.current_state == current_weapon.IDLE_ANIM_NAME:
                    animation_manager.set_animation(current_weapon.FIRE_ANIM_NAME)
# ----------------------------------

Maintenant les armes ont une quantité limité de munitions, et arrêterons de faire feu lorsque le joueur n’en aura plus.


Idéalement, nous aimerions que le joueur puisse savoir combien de munitions il lui reste. Faisons une nouvelle fonction appelé « process_UI » .

Premièrement, ajoutez « process_UI(delta) » à « _physics_process » .

Maintenant ajoutez ce qui suit à « Player.gd » :

func process_UI(delta):
    if current_weapon_name == "UNARMED" or current_weapon_name == "KNIFE":
        UI_status_label.text = "HEALTH: " + str(health)
    else:
        var current_weapon = weapons[current_weapon_name]
        UI_status_label.text = "HEALTH: " + str(health) + \
                "\nAMMO: " + str(current_weapon.ammo_in_weapon) + "/" + str(current_weapon.spare_ammo)

Regardons ce qui ce passe :

Premièrement, nous vérifions si l’arme actuelle est soit « UNARMED » ou « KNIFE » . Si elle l’ait, alors nous changeons le texte de « UI_status_label » pour seulement afficher la vie du joueur vu « UNARMED » et « KNIFE » ne consomme pas de munitions.

Si le joueur utilise une arme qui consume des armes, nous obtenons d’abord le noeud de l’arme.

Puis nous changeons le texte « UI_status_label » pour afficher la vie du joueur, tout en affichant combien de munitions il reste au joueur dans son arme et combien de munitions il possède dans so ninventaire pour cette arme.

Maintenant nous pouvons voir combien de munitions le joueur possède au travers du HUD.

Ajout du rechargement des armes

Maintenant que le joueur peut arriver à court de munitions, nous devons lui donner un moyen de recharger. Ajoutons le rechargement maintenant !

Pour recharger, nous avons besoin d’ajouter quelque variables en plus et une fonction pour chaque arme.

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

const CAN_RELOAD = true
const CAN_REFILL = true

const RELOADING_ANIM_NAME = "Pistol_reload"
  • « CAN_RELOAD » : Un booléen pour savoir si oui ou non l’arme à la capacité de recharger
  • « CAN_REFILL » : Un booléen pour savoir si oui ou non nous pouvons recharger l’arme avec des munitions de l’inventaire. Nous n’utiliserons pas « CAN_REFILL » dans cette partie, mais nous le ferons dans la prochaine partie !
  • « RELOADING_ANIM_NAME » : Le nom de l’animation de chargement pour cette arme.

Maintenant nous devons ajouter une fonction pour gérer le rechargement de l’arme. Ajoutez la fonction suivante à « Weapon_Pistol.gd » :

func reload_weapon():
    var can_reload = false

    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        can_reload = true

    if spare_ammo <= 0 or ammo_in_weapon == AMMO_IN_MAG:
        can_reload = false

    if can_reload == true:
        var ammo_needed = AMMO_IN_MAG - ammo_in_weapon

        if spare_ammo >= ammo_needed:
            spare_ammo -= ammo_needed
            ammo_in_weapon = AMMO_IN_MAG
        else:
            ammo_in_weapon += spare_ammo
            spare_ammo = 0

        player_node.animation_manager.set_animation(RELOADING_ANIM_NAME)

        return true

    return false

Regardons ce qui ce passe :

Premièrement nous définissons une variable pour voir si on peut recharger notre arme ou non.

Puis nous vérifions si l’arme du joueur est dans « idle » animation car on ne veut pouvoir recharger que si le joueur ne tire pas, ne l’équipe pas et ne la déséquipe pas.

Ensuite nous vérifions si le joueur a assez de munitions dans l’inventaire, et si les munitions présente dans l’arme sont équales à l’arme rechargée au maximum. De cette façon nous nous assurons que le joueur ne peut pas recharger si il n’a plus de munitions ou si il est déjà au maximum.

Si nous pouvons encore recharger, alors nous calculons la quantité de munitions nécessaire pour recharger l’arme.

Si le joueur a assez de munitions pour remplir l’arme, nous enlevons les munitions nécessaires de spare_ammo et ensuite nous réglons ammo_in_weapon sur un chargeur d’arme complet.

Si le joueur n’a pas assez de munitions, nous ajoutons toutes les munitions restantes dans spare_ammo, puis réglons spare_ammo sur 0.

Ensuite nous jouons l’animation de rechargement de cette arme, puis nous retournons true.

Si le joueur n’a pas pu recharger, nous retournons false.


Maintenant nous avons besoin d’ajouter le rechargement du fusil. Ouvrez Weapon_Rifle.gd et ajouter les variables de classe suivantes :

const CAN_RELOAD = true
const CAN_REFILL = true

const RELOADING_ANIM_NAME = "Rifle_reload"

Ces variables sont exactement les mêmes que celles du pistolet, seul RELOADING_ANIM_NAME change pour l’animation de rechargement du fusil.

Maintenant nous devons ajouter reload_weapon à Weapon_Rifle.gd :

func reload_weapon():
    var can_reload = false

    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        can_reload = true

    if spare_ammo <= 0 or ammo_in_weapon == AMMO_IN_MAG:
        can_reload = false

    if can_reload == true:
        var ammo_needed = AMMO_IN_MAG - ammo_in_weapon

        if spare_ammo >= ammo_needed:
            spare_ammo -= ammo_needed
            ammo_in_weapon = AMMO_IN_MAG
        else:
            ammo_in_weapon += spare_ammo
            spare_ammo = 0

        player_node.animation_manager.set_animation(RELOADING_ANIM_NAME)

        return true

    return false

Ce code est exactement le même que celui du pistolet.


Le dernier morceau que nous devons faire pour les armes est d’ajouter le “rechargement” du couteau. Ajoutez les variables de classe suivantes à Weapon_Knife.gd :

const CAN_RELOAD = false
const CAN_REFILL = false

const RELOADING_ANIM_NAME = ""

Comme nous ne pouvons recharger ou remplir un couteau, nous mettons à false les deux constantes. On définit aussi RELOADING_ANIM_NAME comme une chaîne vide, vu que le couteau n’a pas d’animation de rechargement.

Maintenant nous devons ajouter reloading_weapon :

func reload_weapon():
    return false

Comme nous ne pouvons pas recharger un couteau, nous retournons toujours false.

Ajout du rechargement au joueur

Maintenant nous devons ajouter quelques choses à Player.gd. D’abord nous devons définir une nouvelle variable de classe :

var reloading_weapon = false
  • reloading_weapon : Une variable pour traquer si le joueur essaie de recharger ou non.

Ensuite nous devons ajouter un nouvel appel de fonction à _physics_process.

Ajoutez process_reloading(delta) à _physics_process. Maintenant _physics_process devrait ressembler à ça :

func _physics_process(delta):
    process_input(delta)
    process_movement(delta)
    process_changing_weapons(delta)
    process_reloading(delta)
    process_UI(delta)

Maintenant nous devons ajouter process_reloading. Ajoutez la fonction suivante à Player.gd :

func process_reloading(delta):
    if reloading_weapon == true:
        var current_weapon = weapons[current_weapon_name]
        if current_weapon != null:
            current_weapon.reload_weapon()
        reloading_weapon = false

Voyons ce qui s’y passe.

D’abord, on s’assure que le joueur essaie de recharger.

Si le joueur essaie de recharger, on récupère ensuite l’arme actuelle. Si l’arme actuelle n’est pas à égale à null, on appelle sa fonction reload_weapon.

Note

Si l’arme actuelle est égale à null, alors l’arme actuelle est UNARMED.

Enfin, nous définissons reloading_weapon à false car, sans tenir compte du fait que le joueur aie correctement rechargé, nous avons essayé de recharger et nous n’avons plus besoin de continuer d’essayer.


Avant que l’on puisse laisser le joueur recharger, nous devons changer quelques choses dans process_input.

La première chose à modifier se trouve dans l’implémentation du changement d’arme. Nous avons besoin d’ajouter un test additionnel (if reloading_weapon == false:) pour voir si le joueur recharge.

if changing_weapon == false:
    # New line of code here!
    if reloading_weapon == false:
        if WEAPON_NUMBER_TO_NAME[weapon_change_number] != current_weapon_name:
            changing_weapon_name = WEAPON_NUMBER_TO_NAME[weapon_change_number]
            changing_weapon = true

Cela permet d’empêcher le joueur de changer d’arme lorsqu’il recharge.

Maintenant nous devons ajouter le code pour déclencher un rechargement lorsque le joueur appuie sur le bouton d’action reload. Ajoutez le code suivant à process_input :

# ----------------------------------
# Reloading
if reloading_weapon == false:
    if changing_weapon == false:
        if Input.is_action_just_pressed("reload"):
            var current_weapon = weapons[current_weapon_name]
            if current_weapon != null:
                if current_weapon.CAN_RELOAD == true:
                    var current_anim_state = animation_manager.current_state
                    var is_reloading = false
                    for weapon in weapons:
                        var weapon_node = weapons[weapon]
                        if weapon_node != null:
                            if current_anim_state == weapon_node.RELOADING_ANIM_NAME:
                                is_reloading = true
                    if is_reloading == false:
                        reloading_weapon = true
# ----------------------------------

Voyons ce qui s’y passe.

D’abord on s’assure que le joueur n’est pas déjà en train de recharger, ou que le joueur essaie de changer d’arme.

Ensuite on vérifie si l’action reload à été pressée.

Si le joueur a appuyé sur reload`, on regarde quel est l’arme courante et on vérifie qu’elle n’est pas null. Ensuite on utilise la constante CAN_RELOAD de l’arme pour savoir si celle-ci peut recharger ou non.

Si l’arme peut être rechargée, on récupère alors l’état d’animation courant, et faisons une variable pour traquer si le joueur est déjà en train de recharger ou pas.

On fait ensuite le tour de chaque arme pour s’assurer que le joueur n’est pas déjà en train de jouer l’animation de recharge de l’arme.

If the player is not reloading any weapon, we set reloading_weapon to true.


Une chose que j’aime ajouter est le rechargement automatique de l’arme lorsque l’on essaye de tirer alors qu’elle est déchargée.

On a besoin d’ajouter une nouvelle condition (is_reloading_weapon == false:) pour que le joueur ne puisse pas tirer avec son arme actuelle lorsqu’il recharge.

Let’s change our firing code in process_input so it reloads when trying to fire an empty weapon:

# ----------------------------------
# Firing the weapons
if Input.is_action_pressed("fire"):
    if reloading_weapon == false:
        if changing_weapon == false:
            var current_weapon = weapons[current_weapon_name]
            if current_weapon != null:
                if current_weapon.ammo_in_weapon > 0:
                    if animation_manager.current_state == current_weapon.IDLE_ANIM_NAME:
                        animation_manager.set_animation(current_weapon.FIRE_ANIM_NAME)
                else:
                    reloading_weapon = true
# ----------------------------------

Now we check to make sure the player is not reloading before we fire the weapon, and when we have 0 or less ammo in the current weapon, we set reloading_weapon to true if the player tries to fire.

This will make it so the player will try to reload when attempting to fire an empty weapon.


With that done, the player can now reload! Give it a try! Now you can fire all the spare ammo for each weapon.

Adding sounds

Finally, let’s add some sounds that accompany the player firing, reloading and changing weapons.

Astuce

There are no game sounds provided in this tutorial (for legal reasons). https://gamesounds.xyz/ is a collection of « royalty free or public domain music and sounds suitable for games ». I used Gamemaster’s Gun Sound Pack, which can be found in the Sonniss.com GDC 2017 Game Audio Bundle.

Open up Simple_Audio_Player.tscn. It is simply a Spatial with an AudioStreamPlayer as its child.

Note

The reason this is called a “simple” audio player is because we are not taking performance into account and because the code is designed to provide sound in the simplest way possible.

If you want to use 3D audio, so it sounds like it’s coming from a location in 3D space, right click the AudioStreamPlayer and select « Change type ».

This will open the node browser. Navigate to AudioStreamPlayer3D and select « change ». In the source for this tutorial, we will be using AudioStreamPlayer, but you can optionally use AudioStreamPlayer3D if you desire, and the code provided below will work regardless of which one you chose.

Create a new script and call it Simple_Audio_Player.gd. Attach it to the Spatial in Simple_Audio_Player.tscn and insert the following code:

extends Spatial

# All of the audio files.
# You will need to provide your own sound files.
var audio_pistol_shot = preload("res://path_to_your_audio_here")
var audio_gun_cock = preload("res://path_to_your_audio_here")
var audio_rifle_shot = preload("res://path_to_your_audio_here")

var audio_node = null

func _ready():
    audio_node = $Audio_Stream_Player
    audio_node.connect("finished", self, "destroy_self")
    audio_node.stop()


func play_sound(sound_name, position=null):

    if audio_pistol_shot == null or audio_rifle_shot == null or audio_gun_cock == null:
        print ("Audio not set!")
        queue_free()
        return

    if sound_name == "Pistol_shot":
        audio_node.stream = audio_pistol_shot
    elif sound_name == "Rifle_shot":
        audio_node.stream = audio_rifle_shot
    elif sound_name == "Gun_cock":
        audio_node.stream = audio_gun_cock
    else:
        print ("UNKNOWN STREAM")
        queue_free()
        return

    # 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()


func destroy_self():
    audio_node.stop()
    queue_free()

Astuce

By setting position to null by default in play_sound, we are making it an optional argument, meaning position doesn’t necessarily have to be passed in to call play_sound.

Voyons ce qui se passe ici :


In _ready, we get the AudioStreamPlayer and connect its finished signal to the destroy_self function. It doesn’t matter if it’s an AudioStreamPlayer or AudioStreamPlayer3D node, as they both have the finished signal. To make sure it is not playing any sounds, we call stop on the AudioStreamPlayer.

Avertissement

Make sure your sound files are not set to loop! If it is set to loop, the sounds will continue to play infinitely and the script will not work!

The play_sound function is what we will be calling from Player.gd. We check if the sound is one of the three possible sounds, and if it is one of the three sounds we set the audio stream in AudioStreamPlayer to the correct sound.

If it is an unknown sound, we print an error message to the console and free the audio player.

If you are using an AudioStreamPlayer3D, remove the # to set the position of the audio player node so it plays at the correct position.

Finally, we tell the AudioStreamPlayer to play.

When the AudioStreamPlayer is finished playing the sound, it will call destroy_self because we connected the finished signal in _ready. We stop the AudioStreamPlayer and free the audio player to save on resources.

Note

This system is extremely simple and has some major flaws:

One flaw is we have to pass in a string value to play a sound. While it is relatively simple to remember the names of the three sounds, it can be increasingly complex when you have more sounds. Ideally, we’d place these sounds in some sort of container with exposed variables so we do not have to remember the name(s) of each sound effect we want to play.

Another flaw is we cannot play looping sounds effects, nor background music, easily with this system. Because we cannot play looping sounds, certain effects, like footstep sounds, are harder to accomplish because we then have to keep track of whether or not there is a sound effect and whether or not we need to continue playing it.

One of the biggest flaws with this system is we can only play sounds from Player.gd. Ideally we’d like to be able to play sounds from any script at any time.


With that done, let’s open up Player.gd again. First we need to load the Simple_Audio_Player.tscn. Place the following code in the class variables section of the script:

var simple_audio_player = preload("res://Simple_Audio_Player.tscn")

Now we need to instance the simple audio player when we need it, and then call its play_sound function and pass the name of the sound we want to play. To make the process simpler, let’s create a create_sound function in Player.gd:

func create_sound(sound_name, position=null):
    var audio_clone = simple_audio_player.instance()
    var scene_root = get_tree().root.get_children()[0]
    scene_root.add_child(audio_clone)
    audio_clone.play_sound(sound_name, position)

Let’s walk through what this function does:


The first line instances the Simple_Audio_Player.tscn scene and assigns it to a variable named audio_clone.

The second line gets the scene root, and this has a large (though safe) assumption.

We first get this node’s SceneTree, and then access the root node, which in this case is the Viewport this entire game is running under. Then we get the first child of the Viewport, which in our case happens to be the root node in Test_Area.tscn or any of the other provided levels. We are making a huge assumption that the first child of the root node is the root scene that the player is under, which may not always be the case.

If this doesn’t make sense to you, don’t worry too much about it. The second line of code only does not work reliably if you have multiple scenes loaded as children of the root node at a time, which will rarely happen for most projects and will not be happening in this tutorial series. This is only potentially a issue depending on how you handle scene loading.

The third line adds our newly created Simple_Audio_Player scene to be a child of the scene root. This works exactly the same as when we are spawning bullets.

Finally, we call the play_sound function and pass in the arguments passed in to create_sound. This will call Simple_Audio_Player.gd’s play_sound function with the passed in arguments.


Now all that is left is playing the sounds when we want to. Let’s add sound to the pistol first!

Open up Weapon_Pistol.gd.

Now, we want to make a noise when the player fires the pistol, so add the following to the end of the fire_weapon function:

player_node.create_sound("Pistol_shot", self.global_transform.origin)

Now when the player fires the pistol, we’ll play the Pistol_shot sound.

To make a sound when the player reloads, we need to add the following right under player_node.animation_manager.set_animation(RELOADING_ANIM_NAME) in the reload_weapon function:

player_node.create_sound("Gun_cock", player_node.camera.global_transform.origin)

Now when the player reloads, we’ll play the Gun_cock sound.


Now let’s add sounds to the rifle. Open up Weapon_Rifle.gd.

To play sounds when the rifle is fired, add the following to the end of the fire_weapon function:

player_node.create_sound("Rifle_shot", ray.global_transform.origin)

Now when the player fires the rifle, we’ll play the Rifle_shot sound.

To make a sound when the player reloads, we need to add the following right under player_node.animation_manager.set_animation(RELOADING_ANIM_NAME) in the reload_weapon function:

player_node.create_sound("Gun_cock", player_node.camera.global_transform.origin)

Now when the player reloads, we’ll play the Gun_cock sound.

Notes finales

../../../_images/PartThreeFinished.png

Now you have weapons with limited ammo that play sounds when you fire them!

At this point, we have all the basics of an FPS game working. There are still a few things that would be nice to add, and we’re going to add them in the next three parts!

For example, right now we have no way to add ammo to our spares, so we’ll eventually run out. Also, we don’t have anything to shoot at outside of the RigidBody nodes.

In Part 4 we’ll add some targets to shoot at, along with some health and ammo pick ups! We’re also going to add joypad support, so we can play with wired Xbox 360 controllers!

Avertissement

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

You can download the finished project for this part here: Godot_FPS_Part_3.zip