Partie 2

Aperçu cette partie

Dans cette partie, nous donnerons à notre joueur des armes avec lesquelles jouer.

../../../_images/PartTwoFinished.png

À la fin de cette partie, vous aurez un joueur qui pourra tirer avec un pistolet, un fusil et attaquer avec un couteau. Le joueur aura aussi maintenant des animations avec des transitions, et les armes interagiront avec les objets dans l’environnement.

Note

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

Commençons tout de suite !

Réalisation d’un système de traitement des animations

Tout d’abord, nous avons besoin d’un moyen de gérer les animations changeantes. Ouvrez Player.tscn et sélectionnez le AnimationPlayer <class_AnimationPlayer>`Node (``Player` -> Rotation_Helper -> Model -> Animation_Player).

Créez un nouveau script appelé AnimationPlayer_Manager.gd et attachez-le au AnimationPlayer.

Ajoutez le code suivant dans AnimationPlayer_Manager.gd :

extends AnimationPlayer

# Structure -> Animation name :[Connecting Animation states]
var states = {
    "Idle_unarmed":["Knife_equip", "Pistol_equip", "Rifle_equip", "Idle_unarmed"],

    "Pistol_equip":["Pistol_idle"],
    "Pistol_fire":["Pistol_idle"],
    "Pistol_idle":["Pistol_fire", "Pistol_reload", "Pistol_unequip", "Pistol_idle"],
    "Pistol_reload":["Pistol_idle"],
    "Pistol_unequip":["Idle_unarmed"],

    "Rifle_equip":["Rifle_idle"],
    "Rifle_fire":["Rifle_idle"],
    "Rifle_idle":["Rifle_fire", "Rifle_reload", "Rifle_unequip", "Rifle_idle"],
    "Rifle_reload":["Rifle_idle"],
    "Rifle_unequip":["Idle_unarmed"],

    "Knife_equip":["Knife_idle"],
    "Knife_fire":["Knife_idle"],
    "Knife_idle":["Knife_fire", "Knife_unequip", "Knife_idle"],
    "Knife_unequip":["Idle_unarmed"],
}

var animation_speeds = {
    "Idle_unarmed":1,

    "Pistol_equip":1.4,
    "Pistol_fire":1.8,
    "Pistol_idle":1,
    "Pistol_reload":1,
    "Pistol_unequip":1.4,

    "Rifle_equip":2,
    "Rifle_fire":6,
    "Rifle_idle":1,
    "Rifle_reload":1.45,
    "Rifle_unequip":2,

    "Knife_equip":1,
    "Knife_fire":1.35,
    "Knife_idle":1,
    "Knife_unequip":1,
}

var current_state = null
var callback_function = null

func _ready():
    set_animation("Idle_unarmed")
    connect("animation_finished", self, "animation_ended")

func set_animation(animation_name):
    if animation_name == current_state:
        print ("AnimationPlayer_Manager.gd -- WARNING: animation is already ", animation_name)
        return true


    if has_animation(animation_name):
        if current_state != null:
            var possible_animations = states[current_state]
            if animation_name in possible_animations:
                current_state = animation_name
                play(animation_name, -1, animation_speeds[animation_name])
                return true
            else:
                print ("AnimationPlayer_Manager.gd -- WARNING: Cannot change to ", animation_name, " from ", current_state)
                return false
        else:
            current_state = animation_name
            play(animation_name, -1, animation_speeds[animation_name])
            return true
    return false


func animation_ended(anim_name):

    # UNARMED transitions
    if current_state == "Idle_unarmed":
        pass
    # KNIFE transitions
    elif current_state == "Knife_equip":
        set_animation("Knife_idle")
    elif current_state == "Knife_idle":
        pass
    elif current_state == "Knife_fire":
        set_animation("Knife_idle")
    elif current_state == "Knife_unequip":
        set_animation("Idle_unarmed")
    # PISTOL transitions
    elif current_state == "Pistol_equip":
        set_animation("Pistol_idle")
    elif current_state == "Pistol_idle":
        pass
    elif current_state == "Pistol_fire":
        set_animation("Pistol_idle")
    elif current_state == "Pistol_unequip":
        set_animation("Idle_unarmed")
    elif current_state == "Pistol_reload":
        set_animation("Pistol_idle")
    # RIFLE transitions
    elif current_state == "Rifle_equip":
        set_animation("Rifle_idle")
    elif current_state == "Rifle_idle":
        pass;
    elif current_state == "Rifle_fire":
        set_animation("Rifle_idle")
    elif current_state == "Rifle_unequip":
        set_animation("Idle_unarmed")
    elif current_state == "Rifle_reload":
        set_animation("Rifle_idle")

func animation_callback():
    if callback_function == null:
        print ("AnimationPlayer_Manager.gd -- WARNING: No callback function for the animation to call!")
    else:
        callback_function.call_func()

Revoyons ce que fait ce script :


Commençons par les variables de classe de ce script :

  • states : Un dictionnaire pour tenir nos états d’animation. (Plus d’explications ci-dessous)
  • animation_speeds : Un dictionnaire pour contenir toutes les vitesses auxquelles nous voulons jouer nos animations.
  • current_state : Une variable pour contenir le nom de l’état de l’animation dans lequel nous nous trouvons actuellement.
  • callback_function : Variable de maintien de la fonction de rappel. (Plus d’explications ci-dessous)

Si vous êtes familier avec les machines à états, vous avez peut-être remarqué que states est structuré comme une machine à états de base. Voici en gros comment les states sont établis :

states est un dictionnaire dont la clé est le nom de l’état courant, et la valeur est un tableau contenant toutes les animations (états) vers lesquelles nous pouvons faire la transition. Par exemple, si nous sommes actuellement dans l’état Idle_unarmed, nous ne pouvons passer qu’à Knife_equip, Pistol_equip, Rifle_equip et Idle_unsarmed.

Si nous essayons de passer à un état qui n’est pas inclus dans les états de transition possibles pour l’état dans lequel nous nous trouvons, alors nous recevons un message d’avertissement et l’animation ne change pas. Nous pouvons aussi passer automatiquement de certains états à d’autres, comme nous l’expliquerons plus loin dans animation_ended

Note

Pour que ce tutoriel reste simple, nous n’utilisons pas une machine d’état “correcte”. Si vous souhaitez en savoir plus sur les machines d’état, consultez les articles suivants :

animation_speeds est la vitesse à laquelle chaque animation sera lue. Certaines animations sont un peu lentes et dans un effort pour que tout ait l’air lisse, nous avons besoin de les jouer à des vitesses plus rapides.

Astuce

Notez que toutes les animations de tir sont plus rapides que leur vitesse normale. Souvenez-vous de ça pour plus tard !

current_state contiendra le nom de l’état de l’animation dans lequel nous sommes actuellement.

Enfin, callback_function sera un FuncRef passé par le joueur pour générer des balles à la bonne image de l’animation. Un FuncRef nous permet de passer une fonction en argument, nous permettant effectivement d’appeler une fonction depuis un autre script, ce que nous utiliserons plus tard.


Regardons maintenant _ready.

Tout d’abord, nous réglons notre animation sur Idle_unarmed à l’aide de la fonction set_animation ; nous commençons donc avec cette animation.

Ensuite, nous connectons le signal animation_finished à ce script et l’assignons à appeler animation_ended. Cela signifie que chaque fois qu’une animation est terminée, animation_ended sera appelé.


Regardons set_animation suivant.

set_animation change l’animation par l’animation nommée animation_name si on la transition est possible. En d’autres termes, si l’état de l’animation dans lequel nous nous trouvons a le nom de l’état de l’animation passé dans states, alors nous allons passer à cette animation.

Tout d’abord, nous vérifions si le nom de l’animation passée dans l’animation est le même que celui de l’animation en cours de lecture. Si ce sont les mêmes, alors nous écrivons un avertissement à la console et nous retournons « true ».

Deuxièmement, nous voyons si AnimationPlayer a l’animation avec le nom animation_name en utilisant has_animation. Si ce n’est pas le cas, nous retournons false.

Troisièmement, nous vérifions si current_state est défini. Si current_state n’est pas actuellement défini, alors nous définissons current_state au nom passé dans l’animation et nous indiquons AnimationPlayer pour commencer l’animation avec un mélange de -1 à la vitesse définie dans``animation_speeds`` et ensuite nous retournons true.

Note

Le temps de mélange est la durée pendant laquelle les deux animations doivent être mélangées ensemble.

En mettant une valeur de -1, la nouvelle animation joue instantanément, remplaçant toute animation déjà en cours de lecture.

Si vous mettez une valeur de 1, pendant une seconde la nouvelle animation jouera avec une force croissante, mélangeant les deux animations ensemble pendant une seconde avant de jouer uniquement la nouvelle animation. Il en résulte une transition en douceur entre les animations, ce qui est parfait lorsque vous passez d’une animation de marche à une animation de course.

Nous réglons le temps de mélange sur -1 parce que nous voulons changer instantanément les animations.

Si nous avons un état dans current_state, alors nous obtenons tous les états possibles vers lesquels nous pouvons changer.

Si le nom de l’animation se trouve dans la liste des transitions possibles, nous réglons current_state sur l’animation passée en animation (animation_name), AnimationPlayer pour lire l’animation avec un temps de mélange -1 à la vitesse définie dans animation_speeds et retourner true.


Regardons maintenant animation_ended.

animation_ended est la fonction qui sera appelée par AnimationPlayer quand la lecture d’une animation est terminée.

Pour certains états d’animation, il se peut que nous ayons besoin de passer à un autre état quand il sera terminé. Pour cela, nous vérifions chaque état d’animation possible. S’il le faut, nous ferons la transition vers un autre État.

Avertissement

Si vous utilisez vos propres modèles animés, assurez-vous qu’aucune des animations n’est réglée sur boucle. Les animations en boucle n’envoient pas le signal animation_finished lorsqu’elles arrivent à la fin de l’animation et sont sur le point de boucler à nouveau.

Note

Les transitions dans animation_ended feraient idéalement partie des données dans states, mais dans un effort pour rendre le tutoriel plus facile à comprendre, nous allons coder chaque transition d’état dans animation_ended`.


Enfin, il y a le animation_callback. Cette fonction sera appelée par une piste de méthode d’appel dans nos animations. Si nous avons un FuncRef assigné à callback_function, alors nous appelons cela passé en fonction. Si nous n’avons pas de FuncRef assigné à callback_function, nous imprimons un avertissement sur la console.

Astuce

Essayez d’exécuter ``Testing_Area.tscn``pour vous assurer qu’il n’y a pas de problème d’exécution. Si le jeu fonctionne mais que rien ne semble avoir changé, alors tout fonctionne correctement.

Préparation des animations

Maintenant que nous avons un gestionnaire d’animation fonctionnel, nous devons l’appeler à partir de notre script joueur. Avant cela, cependant, nous avons besoin de définir quelques pistes de rappel d’animation dans nos animations de tir.

Ouvrez Player.tscn si vous ne l’avez pas ouvert et naviguez jusqu’au nœud AnimationPlayer (Player -> Rotation_Helper -> Model` ->`Animation_Player``).

Nous avons besoin d’attacher une piste de méthode d’appel à trois de nos animations : L’animation de tir pour le pistolet, le fusil et le couteau. Commençons par le pistolet. Cliquez sur la liste déroulante de l’animation et sélectionnez « Pistol_fire ».

Faites maintenant défiler vers le bas de la liste des pistes d’animation. Le dernier élément de la liste devrait s’appeler Armature/Skeleton:Left_UpperPointer. Maintenant au-dessus de la liste, cliquez sur le bouton « Add track », à gauche de la ligne de temps

../../../_images/AnimationPlayerAddTrack.png

Ceci ouvrira une fenêtre avec quelques choix. Nous voulons ajouter une piste de méthode d’appel, alors cliquez sur l’option qui s’appelle « Call Method Track ». Cela ouvrira une fenêtre montrant l’arborescence de nœud en entier. Naviguez jusqu’au nœud AnimationPlayer, sélectionnez-le et appuyez sur OK.

../../../_images/AnimationPlayerCallFuncTrack.png

Maintenant au bas de la liste des pistes d’animation, vous aurez une voie verte qui s’appelle « AnimationPlayer ». Maintenant, nous devons ajouter le point où nous voulons appeler notre fonction de rappel. Faites défiler la ligne de temps jusqu’à ce que vous atteignez le point où le museau de l’arme commence à clignoter.

Note

La ligne de temps est la fenêtre dans laquelle tous les points de notre animation sont stockés. Chacun des petits points représente un point de données d’animation.

Faire défiler la ligne de temps signifie se déplacer à travers l’animation. Ainsi, lorsque nous disons « Faites défiler la ligne de temps jusqu’à ce que vous atteigniez un point », ce que nous voulons dire c’est se déplacer dans la fenêtre d’animation jusqu’à ce que vous atteigniez le point sur la ligne de temps.

En outre, le museau d’une arme à feu est le point final où la balle sort. Le flash du museau est le flash de lumière qui s’échappe du museau lorsqu’une balle est tirée. Le museau est aussi parfois appelé le canon de l’arme à feu.

Astuce

Pour un contrôle plus précis lors du défilement de la ligne de temps, appuyez sur control et faites défiler vers l’avant avec la molette de la souris pour effectuer un zoom avant. Le défilement vers l’arrière permet d’effectuer un zoom arrière.

Vous pouvez également changer la façon dont le défilement de la ligne de temps s’effectue en changeant la valeur dans ``Step(s)``à une valeur inférieure/supérieure.

Une fois que vous arrivez à un point que vous aimez, cliquez avec le bouton droit de la souris sur la ligne pour « Animation Player » et appuyez sur la touche d’insertion. Dans le champ de nom vide, entrez animation_callback et appuyez sur enter.

../../../_images/AnimationPlayerInsertKey.png

Maintenant, lorsque nous jouons cette animation, la piste de la méthode d’appel sera déclenchée à ce point spécifique de l’animation.


Répétons le processus pour les animations de tir au fusil et au couteau !

Note

Parce que le processus est exactement le même que le pistolet, le processus va être expliqué un peu moins en profondeur. Suivez les étapes d’au-dessus si vous vous perdez ! C’est exactement la même chose, juste sur une animation différente.

Allez dans l’animation « Rifle_fire » à partir du menu déroulant de l’animation. Ajoutez la piste de la méthode d’appel une fois que vous atteignez le bas de la liste des pistes d’animation en cliquant sur le bouton « Add Track » au-dessus de la liste. Trouvez le point où le museau de l’arme commence à clignoter et faites un clic droit et appuyez sur Insert Key pour ajouter un point de piste de la méthode d’appel à cet endroit sur la piste.

Tapez « animation_callback » dans le champ du nom de la fenêtre pop up qui s’est ouverte et appuyez sur enter.

Nous devons maintenant appliquer la piste de la méthode callback à l’animation du couteau. Sélectionnez l’animation « Knife_fire » et faites défiler jusqu’en bas des pistes d’animation. Cliquez sur le bouton « Add Track » au-dessus de la liste et ajoutez une piste de méthode. Trouvez ensuite un point autour du premier tiers de l’animation pour placer le point de la méthode de rappel de l’animation.

Note

Nous n’allons pas vraiment tirer au couteau, et l’animation est une animation poignardante plutôt qu’une animation de tir. Pour ce tutoriel, nous réutilisons la logique de tir au fusil pour notre couteau, donc l’animation a été nommée dans un style qui est cohérent avec les autres animations.

De là, cliquez avec le bouton droit de la souris sur la ligne de temps et cliquez sur « Insert Key ». Mettez « animation_callback » dans le champ de nom et appuyez sur enter.

Astuce

Assurez-vous de sauvegarder votre travail !

Cela fait, nous sommes presque prêts à ajouter la possibilité de tirer à notre script de joueur ! On doit monter une dernière scène : La scène de l’objet balle.

Créer la scène balle

Il existe plusieurs façons de manipuler les balles d’une arme à feu dans les jeux vidéo. Dans cette série de tutoriels, nous explorerons deux des méthodes les plus courantes : Objets, et raycasts.


L’une des deux façons est d’utiliser un objet balle. Ce sera un objet qui voyage à travers le monde et manipule son propre code de collision. Dans cette méthode, nous créons un objet balle dans la direction de notre fusil, puis il se déplacera vers l’avant.

Cette méthode présente plusieurs avantages. Le premier étant que nous n’avons pas besoin de stocker les balles dans notre joueur. Nous pouvons simplement créer la balle, puis passer à autre chose, et la balle elle-même vérifiera les collisions, envoyant le ou les signaux appropriés à l’objet avec lequel elle entre en collision, et se détruisant d’elle-même.

Un autre avantage est que nous pouvons avoir des mouvements de balles plus complexes. Si nous voulons faire tomber la balle légèrement au fur et à mesure que le temps passe, nous pouvons faire que le script de contrôle de balle pousse lentement la balle vers le sol. L’utilisation d’un objet implique aussi que la balle prend du temps pour atteindre sa cible, elle ne touche pas instantanément ce vers quoi elle est pointée. Cela semble plus réaliste parce que rien dans la vie réelle ne bouge instantanément d’un point à l’autre.

L’un des inconvénients majeurs est la performance. Bien que le fait que chaque balle calcule sa propre trajectoire et gère sa propre collision offre beaucoup de souplesse, cela se fait au détriment de la performance. Avec cette méthode, nous calculons le mouvement de chaque balle à chaque étape, et bien que ce ne soit pas un problème pour quelques dizaines de balles, cela peut devenir un énorme problème lorsque vous avez potentiellement plusieurs centaines de balles.

Malgré le coût en terme de performance, de nombreux jeux de tir à la première personne incluent une certaine forme d’objet balle. Les lance-roquettes en sont un excellent exemple, car dans de nombreux jeux de tir à la première, les roquettes n’explosent pas instantanément à la position prise pour cible. Vous pouvez également souvent trouver des balles comme objets avec les grenades parce qu’elles rebondissent généralement dans le monde avant d’exploser.

Note

Bien que je ne puisse pas dire avec certitude que c’est le cas, ces jeux utilisent probablement des objets balles sous une forme ou une autre : (Ce sont entièrement de mes observations. J’ai peut-être tout à fait tort. Je n’ai travaillé sur aucun des jeux suivants)

  • Halo (lance-roquettes, grenades à fragmentation, fusils de sniper, balles brutes, et plus)
  • Destiny (lance-roquettes, grenades, fusils à fusion, fusils sniper, super mouvements, et plus)
  • Call of Duty (lance-roquettes, grenades, couteaux balistiques, arbalètes et autres)
  • Battlefield (lance-roquettes, grenades, mortiers, et plus)

Un autre inconvénient des objets balle est la mise en réseau. Les objets balle doivent synchroniser les positions (au moins) avec tous les clients connectés au serveur.

Bien que nous n’implémentons aucune forme de réseau (le réseau aura une série de tutoriels spécifique), c’est une considération à garder à l’esprit lors de la création de votre jeu de tir à la première personne, surtout si vous prévoyez d’ajouter une forme de réseau dans l’avenir.


L’autre façon de gérer les collisions de balles que nous allons examiner est le raycasting.

Cette méthode est extrêmement courante dans les armes à feu qui ont des balles qui se déplacent rapidement et qui changent rarement de trajectoire avec le temps.

Au lieu de créer un objet de balle et de l’envoyer dans l’espace, nous envoyons plutôt un rayon à partir du canon/museau de l’arme vers l’avant. Nous réglons l’origine du raycast à la position de départ de la balle, et en fonction de la longueur, nous pouvons ajuster la distance à laquelle la balle “voyage” dans l’espace.

Note

Bien que je ne puisse pas dire avec certitude que c’est le cas, ces jeux utilisent probablement des raycasts sous une forme ou une autre : (Ceci est entièrement tirés de mes observations. J’ai peut-être tout à fait tort. Je n’ai jamais travaillé sur aucun des jeux suivants)

  • Halo (fusils d’assaut, DMR, fusils de combat, carabine d’engagement, laser spartan, et plus)
  • Destiny (Auto fusils, fusils à impulsion, fusils scouts, canons à main, mitrailleuses, et plus)
  • Call of Duty (fusils d’assaut, mitrailleuses légères, mitrailleuses, pistolets, et plus)
  • Battlefield (fusils d’assaut, SMG, carabines, pistolets, et plus)

L’un des grands avantages de cette méthode est qu’elle est légère en termes de performances. Envoyer quelques centaines de rayons dans l’espace est beaucoup plus facile à calculer pour l’ordinateur que d’envoyer quelques centaines d’objets balles.

Un autre avantage est que nous pouvons savoir instantanément si nous avons touché quelque chose ou pas exactement au moment où nous l’appelons. Pour le réseau, c’est important parce que nous n’avons pas besoin de synchroniser les mouvements des balles sur Internet, nous n’avons qu’à envoyer si oui ou non le raycast touche.

Le raycasting a cependant quelques inconvénients. Un inconvénient majeur est que nous ne pouvons pas facilement lancer un rayon dans autre chose qu’une ligne linéaire. Cela signifie que nous ne pouvons tirer qu’en ligne droite, quelle que soit la longueur de notre rayon. Vous pouvez créer l’illusion d’un mouvement de balle en projetant plusieurs rayons à différentes positions, mais non seulement c’est difficile à mettre en œuvre en code, mais c’est aussi plus lourd sur les performances.

Un autre inconvénient est que nous ne pouvons pas voir la balle. Avec les objets à balles, nous pouvons voir la balle se déplacer dans l’espace si nous y attachons un maillage, mais comme les raycasts se produisent instantanément, nous n’avons pas une façon décente de montrer les balles. Vous pourriez tracer une ligne à partir de l’origine du raycast jusqu’au point où le raycast est entré en collision, et c’est une façon populaire de montrer les raycasts. Une autre façon est tout simplement de ne pas dessiner le raycast du tout, parce que théoriquement les balles se déplacent si vite que nos yeux ne pourraient pas le voir de toute façon.


Préparons l’objet balle. C’est ce que notre pistolet va créer lorsque la fonction de rappel d’animation « Pistol_fire » est appelée.

Ouvrez Bullet_Scene_Scene.tscn. La scène contient un nœud Spatial appelé bullet, avec un MeshInstance et un Area avec un CollisionShape enfant.

Créez un nouveau script appelé Bullet_script.gd et joignez-le au Bullet` Spatial.

Nous allons déplacer tout l’objet balle à la racine (« Bullet »). Nous utiliserons le Area pour vérifier si nous avons ou non heurté quelque chose

Note

Pourquoi utilisons-nous un Area et non un RigidBody ? La raison principale pour laquelle nous n’utilisons pas un RigidBody est que nous ne voulons pas que la balle interagisse avec d’autres nœuds RigidBody. En utilisant un Area nous nous assurons qu’aucun des autres nœuds RigidBody, y compris les autres balles, ne sera affecté.

Une autre raison est simplement qu’il est plus facile de détecter les collisions avec un Area !

Voici le script qui va contrôler notre balle :

extends Spatial

var BULLET_SPEED = 70
var BULLET_DAMAGE = 15

const KILL_TIMER = 4
var timer = 0

var hit_something = false

func _ready():
    $Area.connect("body_entered", self, "collided")


func _physics_process(delta):
    var forward_dir = global_transform.basis.z.normalized()
    global_translate(forward_dir * BULLET_SPEED * delta)

    timer += delta
    if timer >= KILL_TIMER:
        queue_free()


func collided(body):
    if hit_something == false:
        if body.has_method("bullet_hit"):
            body.bullet_hit(BULLET_DAMAGE, global_transform)

    hit_something = true
    queue_free()

Parcourons le script :


Nous définissons d’abord quelques variables de classe :

  • BULLET_SPEED : La vitesse à laquelle la balle se déplace.
  • BULLET_DAMAGE : Les dommages que la balle causera à tout ce avec quoi elle entre en collision.
  • KILL_TIMER : Combien de temps la balle existe sans toucher quoi que ce soit.
  • timer : Un flottant pour savoir depuis combien de temps la balle existe.
  • hit_something : Un booléen pour savoir si on a touché quelque chose ou non.

À l’exception de timer et hit_something, toutes ces variables changent la façon dont la balle interagit avec le monde.

Note

La raison pour laquelle nous utilisons un kill timer est qu’il n’y a pas de cas où une balle se déplace indéfiniment. En utilisant un kill timer, nous pouvons nous assurer qu’aucune balle ne voyagera éternellement,consommant des ressources.

Astuce

Comme dans Partie 1, nous avons quelques variables de classe en majuscules. La raison derrière ceci est la même que celle donnée dans Partie 1 : Nous voulons traiter ces variables comme des constantes, mais nous voulons pouvoir les modifier. Dans ce cas, nous devrons plus tard modifier les dommages et la vitesse des balles, nous avons donc besoin qu’elles soient des variables et non des constantes.


Dans _ready nous réglons le signal body_entered de la zone pour qu’il appelle la fonction ``collided``quand un corps entre dans la zone.


_physics_process obtient l’axe Z local de la balle. Si vous regardez la scène en mode local, vous verrez que la balle fait face à l’axe local positif Z.

Ensuite, nous faisons se déplacer la balle entière dans cette direction, en multipliant notre vitesse et notre temps delta.

Ensuite, nous ajoutons le temps delta à notre timer et vérifions si le timer a atteint une valeur aussi grande ou plus grande que notre constante KILL_TIME. Si c’est le cas, nous utilisons queue_free pour libérer la balle.


Dans collided, nous vérifions si nous avons déjà touché quelque chose.

Rappelez-vous que collided n’est appelé que lorsqu’un corps est entré dans le nœud Area. Si la balle n’est pas déjà entrée en collision avec quelque chose, nous vérifions ensuite si le corps avec lequel la balle est entrée en collision a une fonction/méthode appelée bullet_hit. Si c’est le cas, nous l’appelons et nous passons les dommages de la balle et la transformation globale de la balle afin que nous puissions obtenir la rotation et la position de la balle.

Note

Dans collided, le passé dans le corps peut être un StaticBody, RigidBody, ou KinematicBody

Nous avons réglé la variable hit_something de Bullet sur true parce que, que le corps avec lequel la balle est entrée en collision ait ou non la fonction/méthode bullet_hit, elle a touché quelque chose et nous devons donc nous assurer que la balle ne touche pas autre chose.

Ensuite, nous libérons la balle en utilisant queue_free.

Astuce

Vous vous demandez peut-être pourquoi nous avons une variable hit_something si nous libérons la balle en utilisant queue_free dès qu’elle frappe quelque chose.

La raison pour laquelle nous devons savoir si nous avons touché quelque chose ou non est que queue_free ne libère pas immédiatement le nœud, de sorte que la balle pourrait entrer en collision avec un autre corps avant que Godot ait une chance de la libérer. En vérifiant si la balle a touché quelque chose, nous pouvons nous assurer qu’elle ne touchera qu’un seul objet.


Avant de recommencer à programmer le joueur, jetons un coup d’œil rapide sur Player.tscn. Ouvrez à nouveau Player.tscn.

Développez Rotation_Helper et remarquez comment il a deux nœuds : Gun_Fire_Points et Gun_Aim_Point.

Gun_aim_point est le point sur lequel les balles viseront. Remarquez comment il est aligné avec le centre de l’écran et tiré à une certaine distance vers l’avant sur l’axe Z. Gun_aim_point servira de point de collision avec les balles au fur et à mesure qu’elles entreront en collision.

Note

Il y a un maillage invisible à des fins de débogage. Le maillage est une petite sphère qui indique visuellement vers qu’elle cible les balles vont être dirigées.

Ouvrez Gun_Fire_Points et vous trouverez trois autres nœuds Spatial, un pour chaque arme.

Ouvrez Rifle_Point et vous trouverez un nœud Raycast. C’est ici que nous enverrons les raycast pour les balles de notre fusil. La longueur du raycast dictera la distance à laquelle nos balles voyageront.

Nous utilisons un nœud Raycast pour manipuler la balle du fusil car nous voulons tirer beaucoup de balles rapidement. Si nous utilisons des objets Balle, il est fort possible que nous rencontrions des problèmes de performance sur des machines plus anciennes.

Note

Si vous vous demandez d’où viennent les positions des pointes, ce sont les positions brutes des extrémités de chaque arme. Vous pouvez le voir en allant dans AnimationPlayer, en sélectionnant l’une des animations de tir et en parcourant la ligne de temps. La pointe de chaque arme doit être alignée avec l’extrémité de chaque arme.

Ouvrez Knife_Point et vous trouverez un nœud Area. Nous utilisons un Area pour le couteau parce que nous nous occupons seulement de tous les corps proches de nous, et parce que notre couteau ne tire pas dans l’espace. Si nous fabriquions un couteau à lancer, nous produirions probablement un objet de balle qui ressemble à un couteau.

Enfin, nous avons Pistol_Point. C’est à ce moment-là que nous allons créer/instancier nos objets balles. Nous n’avons pas besoin de nœuds supplémentaires ici, car la balle gère sa propre détection de collision.

Maintenant que nous avons vu comment nous allons manipuler nos autres armes et où nous allons fabriquer les balles, commençons à travailler pour qu’elles fonctionnent.

Note

Vous pouvez également regarder les nœuds HUD si vous le souhaitez. Il n’y a rien d’extravagant et à part l’utilisation d’un seul Label, nous ne toucherons aucun de ces nœuds. Vérifier Conception d’interfaces avec les nœuds Control pour un tutoriel sur l’utilisation des nœuds GUI.

Créer la première arme

Écrivons le code de chacune de nos armes, en commençant par le pistolet.

Sélectionnez Pistolet_Point (Player -> Rotation_Helper -> Gun_Fire_Points -> Pistol_Point) et créez un nouveau script appelé Weapon_Pistol.gd.

Ajoutez le code suivant à Weapon_Pistol.gd :

extends Spatial

const DAMAGE = 15

const IDLE_ANIM_NAME = "Pistol_idle"
const FIRE_ANIM_NAME = "Pistol_fire"

var is_weapon_enabled = false

var bullet_scene = preload("Bullet_Scene.tscn")

var player_node = null

func _ready():
    pass

func fire_weapon():
    var clone = bullet_scene.instance()
    var scene_root = get_tree().root.get_children()[0]
    scene_root.add_child(clone)

    clone.global_transform = self.global_transform
    clone.scale = Vector3(4, 4, 4)
    clone.BULLET_DAMAGE = DAMAGE

func equip_weapon():
    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        is_weapon_enabled = true
        return true

    if player_node.animation_manager.current_state == "Idle_unarmed":
        player_node.animation_manager.set_animation("Pistol_equip")

    return false

func unequip_weapon():
    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        if player_node.animation_manager.current_state != "Pistol_unequip":
            player_node.animation_manager.set_animation("Pistol_unequip")

    if player_node.animation_manager.current_state == "Idle_unarmed":
        is_weapon_enabled = false
        return true
    else:
        return false

Voyons comment fonctionne le script.


Tout d’abord, nous définissons quelques variables de classe dont nous aurons besoin dans le script :

  • DAMAGE : L’ampleur des dommages causés par une seule balle.
  • IDLE_ANIM_NAME : Le nom de l’animation d’inactivité du pistolet.
  • FIRE_ANIM_NAME : Le nom de l’animation de tir du pistolet.
  • is_weapon_enabled : Variable permettant de vérifier si cette arme est utilisée/activée.
  • bullet_scene : La scène de la balle sur laquelle on a travaillé plus tôt.
  • player_node : Une variable pour contenir Player.gd.

La raison pour laquelle nous définissons la plupart de ces variables est que nous pouvons les utiliser dans Player.gd.

Chacune des armes que nous fabriquerons aura toutes ces variables (moins bullet_scene) donc nous avons une interface cohérente pour interagir avec Player.gd. En utilisant les mêmes variables/fonctions dans chaque arme, nous pouvons interagir avec elles sans avoir à savoir quelle arme nous utilisons, ce qui rend notre code beaucoup plus modulaire parce que nous pouvons ajouter des armes sans avoir à changer une grande partie du code dans ``Player.gd``et ça fonctionne.

Nous pourrions écrire tout le code dans Player.gd, mais ensuite Player.gd sera de plus en plus difficile à gérer à mesure que nous ajouterons des armes. En utilisant une conception modulaire avec une interface cohérente, nous pouvons garder Player.gd agréable et soigné, tout en facilitant l’ajout, la suppression et la modification d’armes.


Dans _ready, nous ne faisons que passer.

Il y a une chose à noter cependant, l’hypothèse que nous allons remplir Player.gd à un moment.

Nous allons supposer que Player.gd passe lui-mêmes en avant l’appel à des fonctions de Weapon_Pistol.gd.

Bien que cela puisse conduire à des situations où le joueur ne se passe pas lui-même (parce que nous oublions), nous aurions besoin d’une longue chaîne d’appels get_parent pour parcourir l’arbre de scène pour récupérer le joueur. Cela n’a pas l’air joli (get_parent().get_parent().get_parent().get_parent()``et ainsi de suite) et il est relativement sûr de supposer que nous allons nous rappeler de nous passer à chaque arme dans ``Player.gd`.


Ensuite regardons fire_weapon :

La première chose que nous faisons, c’est instancier la scène balles que nous avons faite tout à l’heure.

Astuce

En instanciant la scène, nous créons un nouveau nœud contenant tous les nœuds de la scène que nous avons instanciée, clonant effectivement cette scène.

Ensuite, nous ajoutons un clone au premier nœud enfant de la racine de la scène dans laquelle nous sommes actuellement. En faisant cela, nous en faisons un enfant du nœud racine de la scène actuellement chargée.

En d’autres termes, nous ajoutons un clone en tant qu’enfant du premier nœud (ce qui se trouve en haut de l’arbre de scène) dans la scène actuellement chargée/ouverte. Si la scène actuellement chargée/ouverte est Testing_Area.tscn, nous ajouterions notre clone```en tant qu'enfant de ``Testing_Area, le nœud racine dans cette scène.

Avertissement

Comme mentionné plus loin dans la section sur l’ajout de sons, cette méthode fait une hypothèse. Ceci sera expliqué plus loin dans la section sur l’ajout de sons dans doc_fps_tutorial_part_three_part_3

Ensuite, nous réglons la transformation globale du clone sur la transformation globale du Pistol_Aim_Point. La raison pour laquelle nous faisons cela, c’est pour que la balle soit crée à l’extrémité du pistolet.

Vous pouvez voir que Pistol_Aim_Point est positionné à la fin du pistolet en cliquant sur le bouton AnimationPlayer et en faisant défiler Pistol_fire. Vous verrez que la position est plus ou moins à l’extrémité du pistolet quand il tire.

Ensuite, nous mettons à l’échelle d’un facteur de 4 parce que la scène balle est un peu trop petite par défaut.

Ensuite, nous fixons les dommages de la balle (BULLET_DAMAGE) à la quantité de dommages causés par une seule balle de pistolet (DAMAGE).


Regardons maintenant equip_weapon :

La première chose que nous faisons est de vérifier si le gestionnaire d’animation est dans l’animation d’inactivité du pistolet. Si nous sommes dans l’animation d’inactivité du pistolet, nous réglons is_weapon_enabled sur true et retournons true parce que le pistolet a été équipé avec succès.

Parce que nous savons que l’animation equip de notre pistolet passe automatiquement à l’animation d’inactivité du pistolet, si nous sommes dans l’animation d’inactivité du pistolet, le pistolet doit logiquement avoir terminé de jouer l’animation d’équipement.

Note

Nous savons que ces animations seront en transition parce que nous avons écrit le code pour les faire transitionner dans Animation_Manager.gd

Ensuite, nous vérifions si le joueur est dans l’état d’animation Idle_unarmed. Parce que toutes les animations non équipées vont dans cet état, et parce que n’importe quelle arme peut être équipée à partir de cet état, nous changeons les animations en Pistol_equip si le joueur est dans l’état Idle_unarmed.

Puisque nous savons que Pistol_equip passera à Pistol_idle`, nous n’avons pas besoin de faire d’autres traitements supplémentaires pour équiper les armes, mais comme nous n’avons pas encore pu équiper le pistolet, nous retournons « faux ».


Enfin, jetons un coup d’œil à unequip_weapon :

unequip_weapon est semblable à equip_weapon, mais nous vérifions les choses à l’inverse.

Tout d’abord, nous vérifions si le joueur est dans l’état d’animation d’inactivité. Ensuite, nous vérifions que le joueur n’est pas dans l’animation Pistol_unequip. Si le joueur n’est pas dans l’animation Pistol_unequip, nous voulons jouer l’animation pistol_unequip.

Note

Vous vous demandez peut-être pourquoi nous vérifions si le joueur est dans l’animation d’inactivité du pistolet, puis nous nous assurons que le joueur n’est pas en train de se déséquiper juste après. La raison de cette vérification supplémentaire est que nous pourrions (dans de rares cas) appeler unequip_weapon deux fois avant d’avoir eu la chance de traiter set_animation, alors nous ajoutons cette vérification supplémentaire pour nous assurer que l’animation déséquipée est jouée.

Ensuite, nous vérifions si le joueur est dans Idle_unarmed, qui est l’état de l’animation dans lequel nous allons passer depuis Pistol_unequip. Si le joueur se trouve dans Idle_unarmed, nous réglons alors is_weapon_enabled sur false puisque nous n’utilisons plus cette arme, et nous retournons true parce que nous avons réussi à déséquiper le pistolet.

Si le joueur n’est pas dans « Idle_unarmed », nous retournons false parce que nous n’avons pas encore réussi à déséquiper le pistolet.

Création des deux autres armes

Maintenant que nous avons tout le code nécessaire pour le pistolet, ajoutons le code pour le fusil et le couteau.

Sélectionnez Rifle_Point (Player -> Rotation_Helper -> Gun_Fire_Points -> Rifle_Point) et créez un nouveau script appelé Weapon_Rifle.gd, puis ajoutez ce qui suit :

extends Spatial

const DAMAGE = 4

const IDLE_ANIM_NAME = "Rifle_idle"
const FIRE_ANIM_NAME = "Rifle_fire"

var is_weapon_enabled = false

var player_node = null

func _ready():
    pass

func fire_weapon():
    var ray = $Ray_Cast
    ray.force_raycast_update()

    if ray.is_colliding():
        var body = ray.get_collider()

        if body == player_node:
            pass
        elif body.has_method("bullet_hit"):
            body.bullet_hit(DAMAGE, ray.global_transform)

func equip_weapon():
    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        is_weapon_enabled = true
        return true

    if player_node.animation_manager.current_state == "Idle_unarmed":
        player_node.animation_manager.set_animation("Rifle_equip")

    return false

func unequip_weapon():

    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        if player_node.animation_manager.current_state != "Rifle_unequip":
            player_node.animation_manager.set_animation("Rifle_unequip")

    if player_node.animation_manager.current_state == "Idle_unarmed":
        is_weapon_enabled = false
        return true

    return false

C’est exactement la même chose que Weapon_Pistol.gd, alors nous allons seulement regarder ce qui a changé : fire_weapon.

La première chose que nous faisons est d’obtenir le nœud Raycast, qui est un enfant de Rifle_Point.

Ensuite, nous forçons le Raycast à se mettre à jour en utilisant force_raycast_update. Cela forcera le Raycast à détecter les collisions lorsque nous l’appelons, ce qui signifie que nous obtenons un contrôle parfait des collisions avec le monde physique 3D.

Ensuite, nous vérifions si le Raycast est entré en collision avec quelque chose.

Si le Raycast est entré en collision avec quelque chose, on obtient d’abord le corps de collision avec lequel il est entré en collision. Cela peut être un StaticBody, RigidBody, ou un KinematicBody.

Ensuite, nous voulons nous assurer que le corps avec lequel nous sommes entrés en collision n’est pas le joueur, puisque nous ne voulons (probablement) pas lui donner la possibilité de se tirer dans le pied.

Si le corps n’est pas le joueur, nous vérifions ensuite s’il a une fonction/méthode appelée bullet_hit. Si c’est le cas, nous l’appelons et passons le montant des dégâts que cette balle fait (DAMAGE), et la transformation globale de la Raycast afin que nous puissions dire de quelle direction la balle est venue.


Il ne nous reste plus qu’à écrire le code du couteau.

Sélectionnez Rifle_Point (Player -> Rotation_Helper -> Gun_Fire_Points -> Rifle_Point) et créez un nouveau script appelé Weapon_Rifle.gd, puis ajoutez ce qui suit :

extends Spatial

const DAMAGE = 40

const IDLE_ANIM_NAME = "Knife_idle"
const FIRE_ANIM_NAME = "Knife_fire"

var is_weapon_enabled = false

var player_node = null

func _ready():
    pass

func fire_weapon():
    var area = $Area
    var bodies = area.get_overlapping_bodies()

    for body in bodies:
        if body == player_node:
            continue

        if body.has_method("bullet_hit"):
            body.bullet_hit(DAMAGE, area.global_transform)

func equip_weapon():
    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        is_weapon_enabled = true
        return true

    if player_node.animation_manager.current_state == "Idle_unarmed":
        player_node.animation_manager.set_animation("Knife_equip")

    return false

func unequip_weapon():

    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        player_node.animation_manager.set_animation("Knife_unequip")

    if player_node.animation_manager.current_state == "Idle_unarmed":
        is_weapon_enabled = false
        return true

    return false

Comme dans le cas de Weapon_Rifle.gd, les seules différences se situent au niveau de fire_weapon, alors regardons cela :

La première chose que nous faisons est d’obtenir le Area nœud enfant de Knife_Point.

Ensuite, nous voulons obtenir tous les corps de collision à l’intérieur de la Area en utilisant get_overlapping_bodies. Ceci retournera une liste de tous les corps qui touchent le Area.

Nous voulons ensuite passer en revue chacun de ces corps.

Tout d’abord, nous vérifions que le corps n’est pas le joueur, parce que nous ne voulons pas que le joueur soit capable de se poignarder. Si le corps est le joueur, nous utilisons continue pour sauter et regarder le corps suivant dans bodies.

Si nous n’avons pas sauté au corps suivant, nous vérifions alors si le corps a la fonction/méthode bullet_hit. Si c’est le cas, nous l’appelons, en passant par le montant des dommages causés par un simple coup de couteau (DAMAGE) et la transformation globale de la:ref:Area <class_Area>.

Note

Bien que nous puissions essayer de calculer un emplacement approximatif pour l’endroit exact où le couteau a frappé, nous n’allons pas le faire parce que l’utilisation de la position de Area fonctionne assez bien et le temps supplémentaire nécessaire pour calculer une position approximative pour chaque corps ne vaut pas la peine.

Faire fonctionner les armes

Commençons à faire fonctionner les armes dans Player.gd.

Commençons d’abord par ajouter quelques variables de classe dont nous aurons besoin pour les armes :

# Place before _ready
var animation_manager

var current_weapon_name = "UNARMED"
var weapons = {"UNARMED":null, "KNIFE":null, "PISTOL":null, "RIFLE":null}
const WEAPON_NUMBER_TO_NAME = {0:"UNARMED", 1:"KNIFE", 2:"PISTOL", 3:"RIFLE"}
const WEAPON_NAME_TO_NUMBER = {"UNARMED":0, "KNIFE":1, "PISTOL":2, "RIFLE":3}
var changing_weapon = false
var changing_weapon_name = "UNARMED"

var health = 100

var UI_status_label

Voyons ce que ces nouvelles variables vont faire :

  • animation_manager : Cela va contenir le nœud AnimationPlayer et son script, que nous avons écrit précédemment.
  • current_weapon_name : Le nom de l’arme que nous utilisons actuellement. Il a quatre valeurs possibles : UNARMED, KNIFE, PISTOL et RIFLE.
  • weapons : Un dictionnaire qui contiendra tous les nœuds de l’arme.
  • WEAPON_NUMBER_TO_NAME : Un dictionnaire permettant de convertir le numéro d’une arme en son nom. On s’en servira pour changer d’armes.
  • WEAPON_NAME_TO_NUMBER : Un dictionnaire permettant de convertir le nom d’une arme en son numéro. On s’en servira pour changer d’armes.
  • changing_weapon : Un booléen pour savoir si oui ou non nous changeons d’armes.
  • changing_weapon_name : Le nom de l’arme que nous voulons changer.
  • health : Quel quantité vie a notre joueur. Dans cette partie du tutoriel, nous ne l’utiliserons pas.
  • UI_status_label : Une étiquette pour montrer combien nous avons de vie et combien de munitions nous avons dans notre fusil et en réserve.

Ensuite, nous devons ajouter quelques choses dans le _ready. Voici le nouveau _ready de la fonction :

func _ready():
    camera = $Rotation_Helper/Camera
    rotation_helper = $Rotation_Helper

    animation_manager = $Rotation_Helper/Model/Animation_Player
    animation_manager.callback_function = funcref(self, "fire_bullet")

    Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

    weapons["KNIFE"] = $Rotation_Helper/Gun_Fire_Points/Knife_Point
    weapons["PISTOL"] = $Rotation_Helper/Gun_Fire_Points/Pistol_Point
    weapons["RIFLE"] = $Rotation_Helper/Gun_Fire_Points/Rifle_Point

    var gun_aim_point_pos = $Rotation_Helper/Gun_Aim_Point.global_transform.origin

    for weapon in weapons:
        var weapon_node = weapons[weapon]
        if weapon_node != null:
            weapon_node.player_node = self
            weapon_node.look_at(gun_aim_point_pos, Vector3(0, 1, 0))
            weapon_node.rotate_object_local(Vector3(0, 1, 0), deg2rad(180))

    current_weapon_name = "UNARMED"
    changing_weapon_name = "UNARMED"

    UI_status_label = $HUD/Panel/Gun_label
    flashlight = $Rotation_Helper/Flashlight

Voyons ce qui a changé.

On obtient d’abord le nœud AnimationPlayer et on l’affecte à la variable animation_manager. Ensuite, nous réglons la fonction de rappel sur FuncRef qui appellera la fonction fire_bullet du joueur. Pour l’instant, nous n’avons pas encore écrit la fonction fire_bullet, mais nous y arriverons bientôt.

Ensuite, nous obtenons tous les nœuds d’armes et nous les assignons à weapons. Cela nous permettra d’accéder aux nœuds d’armes uniquement avec leur nom (KNIFE, PISTOL, or RIFLE).

Nous obtenons alors la position globale de Gun_Aim_Point pour que nous puissions faire pivoter les armes du joueur pour viser.

Ensuite, nous passons en revue chaque arme dans weapons.

On prend d’abord le nœud de l’arme. Si le nœud de l’arme n’est pas null, on met alors sa variable player_node` à ce script (Player.gd). Ensuite, nous l’observons à l’aide de la fonction look_at, puis nous la faisons tourner de 180 degrés sur l’axe Y.

Note

Nous faisons pivoter tous ces points d’arme de 180 degrés sur leur axe Y parce que notre caméra est dirigée vers l’arrière. Si nous ne faisions pas pivoter tous ces points d’arme de 180 degrés, toutes les armes tireraient à l’envers.

Ensuite, nous réglons current_weapon_name et changing_weapon_name sur UNARMED.

Enfin, nous obtenons le UI Label de notre HUD.


Ajoutons un nouvel appel de fonction à _physics_process pour pouvoir changer d’arme. Voici le nouveau code :

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

Maintenant, nous allons appeler process_changing_weapons.


Ajoutons maintenant tout le code d’entrée joueur pour les armes dans process_input. Ajouter le code suivant :

# ----------------------------------
# Changing weapons.
var weapon_change_number = WEAPON_NAME_TO_NUMBER[current_weapon_name]

if Input.is_key_pressed(KEY_1):
    weapon_change_number = 0
if Input.is_key_pressed(KEY_2):
    weapon_change_number = 1
if Input.is_key_pressed(KEY_3):
    weapon_change_number = 2
if Input.is_key_pressed(KEY_4):
    weapon_change_number = 3

if Input.is_action_just_pressed("shift_weapon_positive"):
    weapon_change_number += 1
if Input.is_action_just_pressed("shift_weapon_negative"):
    weapon_change_number -= 1

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

if changing_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
# ----------------------------------

# ----------------------------------
# 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 animation_manager.current_state == current_weapon.IDLE_ANIM_NAME:
                animation_manager.set_animation(current_weapon.FIRE_ANIM_NAME)
# ----------------------------------

Passons en revue les ajouts, en commençant par la façon dont nous changeons les armes.

On obtient d’abord le numéro de l’arme courante et on l’assigne à weapon_change_number.

Ensuite, nous vérifions si l’une des touches numériques (touches 1 à 4) est activée. Si c’est le cas, nous réglons weapon_change_number sur la valeur mappée à cette clé.

Note

La raison pour laquelle la clé 1 est mappée à 0 est que le premier élément d’une liste est mappé à zéro, pas à un. La plupart des accesseurs de listes/tableaux dans la plupart des langages de programmation commencent à 0 au lieu de 1. Voir https://en.wikipedia.org/wiki/Zero-based_numbering pour plus d’informations.

Ensuite, nous vérifions si shift_weapon_positive ou shift_weapon_negative est pressé. Si l’un d’eux l’est, on ajoute/soustrait 1 au numéro_de_changement_d'arme.

Parce que le joueur a peut-être changé weapon_change_number en dehors du nombre d’armes que le joueur possède, nous le fixons de sorte qu’il ne peut pas dépasser le nombre maximum d’armes que le joueur a et il assure que weapon_change_number vaut 0 ou plus.

Ensuite, nous vérifions que le joueur ne change pas déjà d’arme. Si ce n’est pas le cas, nous vérifions si l’arme pour laquelle le joueur veut changer est une nouvelle arme et non celle qu’il utilise actuellement. Si l’arme pour laquelle le joueur changer est une nouvelle arme, nous réglons alors changing_weapon_name sur weapon_change_number et réglons changing_weapon sur vrai.

Pour tirer, nous vérifions d’abord si l’action fire est pressée. Ensuite, nous vérifions que le joueur ne change pas d’arme. Ensuite, nous obtenons le nœud d’arme pour l’arme actuelle.

Si le nœud d’arme courant n’est pas nul, et que le joueur est dans son état IDLE_ANIM_NAME, l’animation du joueur est définie sur FIRE_ANIM_NAME de l’arme courante.


Ajoutons process_changing_weapons suivant.

Ajoutez le code suivant :

func process_changing_weapons(delta):
    if changing_weapon == true:

        var weapon_unequipped = false
        var current_weapon = weapons[current_weapon_name]

        if current_weapon == null:
            weapon_unequipped = true
        else:
            if current_weapon.is_weapon_enabled == true:
                weapon_unequipped = current_weapon.unequip_weapon()
            else:
                weapon_unequipped = true

        if weapon_unequipped == true:

            var weapon_equipped = false
            var weapon_to_equip = weapons[changing_weapon_name]

            if weapon_to_equip == null:
                weapon_equipped = true
            else:
                if weapon_to_equip.is_weapon_enabled == false:
                    weapon_equipped = weapon_to_equip.equip_weapon()
                else:
                    weapon_equipped = true

            if weapon_equipped == true:
                changing_weapon = false
                current_weapon_name = changing_weapon_name
                changing_weapon_name = ""

Voyons ce qui se passe ici :

La première chose que nous faisons est de nous assurer que nous avons reçu les informations pour changer les armes. Pour ce faire, nous nous assurons que le changing_weapons est true.

Ensuite, nous définissons une variable (weapon_unequipped) pour pouvoir vérifier si l’arme courante a été correctement déséquipée ou non.

Ensuite, nous obtenons l’arme actuelle depuis weapons.

Si l’arme actuelle n’est pas null, il faut alors vérifier si l’arme est activée. Si l’arme est activée, nous appelons sa fonction unequip_weapon pour qu’elle lance l’animation de déséquipement. Si l’arme n’est pas activée, nous réglons « arme_non_équipée » sur « vrai » parce que l’arme n’a pas été correctement équipée.

Si l’arme actuelle est null, nous pouvons simplement régler weapon_unequipped sur true. La raison pour laquelle nous faisons cette vérification est qu’il n’y a pas de script/nœud d’arme pour UNARMED, mais il n’y a pas non plus d’animations pour UNARMED, donc nous pouvons juste commencer à équiper l’arme pour laquelle le joueur veut changer.

Si le joueur a réussi à déséquiper l’arme actuelle (weapon_unequipped == true), nous devons équiper la nouvelle arme.

Tout d’abord, nous définissons une nouvelle variable (weapon_equipped) pour savoir si le joueur a réussi à équiper la nouvelle arme ou non.

Ensuite, nous obtenons l’arme pour laquelle le joueur veut changer. Si l’arme pour laquelle le joueur veut changer n’est pas null, nous vérifions alors si elle est activée ou non. S’il n’est pas activé, on appelle sa fonction equip_weapon pour qu’il commence à équiper l’arme. Si l’arme est activée, nous réglons weapon_equipped sur true.

Si l’arme pour laquel le joueur veut changer est null, nous réglons simplement weapon_equipped sur true parce que nous n’avons pas de nœud/script pour UNARMED, ni d’animations.

Enfin, nous vérifions si le joueur a bien équipé la nouvelle arme. S’il l’a fait, nous réglons changing_weapon sur false parce que le joueur ne change plus d’arme. Nous réglons aussi current_weapon_name sur changing_weapon_name puisque l’arme courante a changé, puis nous réglons changing_weapon_name sur un string vide.


Maintenant, nous avons besoin d’ajouter une nouvelle fonction au joueur, et après le joueur pourra commencer a tirer !

On a besoin d’ajouter fire_bullet, qui peut être appelée par AnimationPlayer aux points que nous avons fixé précédemment dans l”:ref:`AnimationPlayer <class_AnimationPlayer> :

func fire_bullet():
    if changing_weapon == true:
        return

    weapons[current_weapon_name].fire_weapon()

Voyons ce que fait cette fonction :

Premièrement on vérifie que le joueur est en train de changer d’arme. Et si le joueur change d’arme, on ne veut pas tirer, donc on return.

Astuce

L’appel de return empêche le reste de la fonction d’être appelé. Dans ce cas, nous ne retournons pas de variable parce que nous sommes seulement intéressés à ne pas exécuter le reste du code, et parce que nous n’attendons pas non plus de variable retournée lorsque nous appelons cette fonction.

Ensuite nous disons que le joueur est en train de tirer avec l’arme actuelle en appelant sa fonction fire_weapon.

Astuce

Rappelez-vous que nous avons mentionné que la vitesse des animations pour le tir était plus rapide que les autres animations ? En changeant les vitesses d’animation de tir, vous pouvez changer la vitesse à laquelle l’arme tire des balles !


Avant de pouvoir tester nos nouvelles armes, on a encore un peu de pain sur la planche.

Création de quelques sujets de tests

Crée un nouveau script en allant dans la fenêtre de script, puis clique sur « fichier », et sélectionne nouveau. Nomme ce script RigidBody_hit_test et soit sur qu’il hérite de RigidBody.

Maintenant nous avons besoin d’ajouter ce code :

extends RigidBody

const BASE_BULLET_BOOST = 9;

func _ready():
    pass

func bullet_hit(damage, bullet_global_trans):
    var direction_vect = bullet_global_trans.basis.z.normalized() * BASE_BULLET_BOOST;

    apply_impulse((bullet_global_trans.origin - global_transform.origin).normalized(), direction_vect * damage)

Voyons comment bullet_hit fonctionne :

D’abord, on obtient le vecteur directionnel vers l’avant de la balle. C’est ainsi que nous pouvons dire dans quelle direction la balle va frapper le RigidBody. Nous l’utiliserons pour pousser le RigidBody dans la même direction que la balle.

Note

Nous devons booster le vecteur directionnel par BASE_BULLET_BOOST pour que les balles aient un peu plus de punch et déplacent les nœuds RigidBody d’une manière visible. Vous pouvez simplement régler BASE_BULLET_BOOST sur des valeurs inférieures ou supérieures si vous voulez une réaction plus ou moins importante lorsque les balles entrent en collision avec le RigidBody.

Puis on applique une impulsion en utilisant apply_impulse.

Premièrement, nous avons besoin de calculer la position de l’impulsion. Car apply_impulse prends en entrée un vecteur relatif au RigidBody , nous avons besoin de calculer la distance du RigidBody à la balle. Nous calculons cela en soustrayant la position globale du RigidBody. Nous normalisons ce vecteur pour que la taille du collisionneur n’affecte pas l’influence de la balle sur le RigidBody.

Finalement, nous avons besoin de calculer la force de l’impulsion. Pour cela, on utilise la direction vers laquelle est orienté la balle et nous la multiplions par les dégâts de la balle. Cela donne un bon résultat et pour des balles plus puissantes on obtient un résultat plus important.


Maintenant, nous devons attacher ce script à tous les nœuds RigidBody que l’on veut affecter.

Ouvrez Testing_Area.tscn et sélectionnez tous les cubes parentés au nœud Cubes node.

Astuce

Si vous sélectionnez le cube supérieur, puis maintenez la touche shift et sélectionnez le dernier cube, Godot sélectionnera tous les cubes entre les deux !

Une fois que vous avez sélectionné tous les cubes, faites défiler vers le bas dans l’inspecteur jusqu’à ce que vous obtenez à la section « scripts ». Cliquez sur la liste déroulante et sélectionnez « charger ». Ouvrez le script RigidBody_hit_test.gd nouvellement créé.

Notes finales

../../../_images/PartTwoFinished.png

C’était beaucoup de code! Mais maintenant, avec tout ce qui est fait, vous pouvez tester vos armes !

Vous devriez maintenant être capable de tirer autant de balles que vous le voulez sur les cubes, et ces derniers bougeront en fonction des balles qui entrent en collision avec ces derniers.

ICI Partie 3, nous ajouterons des munitions aux armes, mais aussi des bruitages !

Avertissement

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

Vous pouvez télécharger le projet fini pour cette partie ICI : download:Godot_FPS_Part_2.zip <files/Godot_FPS_Part_2.zip>