Partie 5

Aperçu cette partie

Dans cette partie, nous allons ajouter des grenades au joueur, lui donner la capacité de lancer et attraper des objets et pour finir ajouter des tourelles !

../../../_images/PartFiveFinished.png

Note

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

Commençons tout de suite !

Ajouter les grenades

Tout d’abord, donnons au joueur quelques grenades avec lesquelles jouer. Ouvrez Grenade.tscn.

Il y a quelques points à noter ici, le premier et le plus important étant que les grenades vont utiliser des nœuds RigidBody. Nous allons utiliser des nœuds RigidBody pour nos grenades de façon à ce qu’elles rebondissent dans le monde de façon (à peu près) réaliste.

La seconde chose à noter est Blast_Area. C’est un nœud Area qui va représenter le rayon d’explosion de la grenade.

Enfin, la dernière chose à explorer est Explosion. C’est un nœud Particles qui va émettre un effet d’explosion quand la grenade va exploser. One chose à noter ici, c’est que nous avons activé One shot. Cela fait en sorte que nous émettions toutes les particules à fois. Les particules sont émises en utilisant les coordonnées du monde et non pas des coordonées locales, c’est pour cela que nous avons décoché Local Coords.

Note

Si vous le voulez, vous pouvez voir comment les particules sont paramétrés en regardant dans Process Material et Draw Passes des particules.

Écrivons le code nécessaire pour les grenades. Sélectionnez Grenade et créez un nouveau script appelé Grenade.gd. Ajoutez ce qui suit :

extends RigidBody

const GRENADE_DAMAGE = 60

const GRENADE_TIME = 2
var grenade_timer = 0

const EXPLOSION_WAIT_TIME = 0.48
var explosion_wait_timer = 0

var rigid_shape
var grenade_mesh
var blast_area
var explosion_particles

func _ready():
    rigid_shape = $Collision_Shape
    grenade_mesh = $Grenade
    blast_area = $Blast_Area
    explosion_particles = $Explosion

    explosion_particles.emitting = false
    explosion_particles.one_shot = true

func _process(delta):

    if grenade_timer < GRENADE_TIME:
        grenade_timer += delta
        return
    else:
        if explosion_wait_timer <= 0:
            explosion_particles.emitting = true

            grenade_mesh.visible = false
            rigid_shape.disabled = true

            mode = RigidBody.MODE_STATIC

            var bodies = blast_area.get_overlapping_bodies()
            for body in bodies:
                if body.has_method("bullet_hit"):
                    body.bullet_hit(GRENADE_DAMAGE, body.global_transform.looking_at(global_transform.origin, Vector3(0, 1, 0)))

            # This would be the perfect place to play a sound!


        if explosion_wait_timer < EXPLOSION_WAIT_TIME:
            explosion_wait_timer += delta

            if explosion_wait_timer >= EXPLOSION_WAIT_TIME:
                queue_free()

Faisons le point sur ce qui se passe, en commençant par les variables de classe :

  • GRENADE_DAMAGE: Le montant de dégâts qu’inflige les grenades quand elles explosent.
  • GRENADE_TIME: Le temps que la grenade met (en secondes) pour exploser une fois qu’elle est créée/jetée.
  • grenade_timer: Une variable utilisée pour savoir depuis combien de temps la grenade a été créée/jetée.
  • EXPLOSION_WAIT_TIME: Le temps nécessaire (en seconde) d’attente avant que nous détruisions la scène de la grenade après l’explosion
  • explosion_wait_timer: Une variable utilisée pour suivre le temps passé après que l’explosion soit arrivée.
  • rigid_shape: La CollisionShape pour le RigidBody de la grenade.
  • grenade_mesh: La MeshInstance pour la grenade.
  • blast_area: La Area de l’explosion utilisée pour faire des dégâts aux objets quand la grenade explose.
  • explosion_particles: Les Particles qui sortent quand les grenades explosent.

Remarquez comme EXPLOSION_WAIT_TIME est un nombre assez atypique (0.48). C’est parce que nous voulons que EXPLOSION_WAIT_TIME soit égal à la durée d’émission des particules d’explosion, donc quand les particules disparaissent, nous détruisons les grenades. On calcule EXPLOSION_WAIT_TIME en prenant la durée de vie de la particule et en la divisant par l’échelle de vitesse de la particule. Cela nous donnera la durée de vie exacte des particules.


Concentrons maintenant notre attention sur _ready.

Tout d’abord, nous récupérons tous les nœuds dont nous avons besoin et nous les assignons aux bonnes variables de classe.

Nous avons besoin de récupérer la CollisionShape et la MeshInstance car de la même façon que dans la Partie 4, nous aurons besoin de cacher la mesh des grenades et de désactiver les collision shapes quand les grenades vont exploser.

La raison pour laquelle nous devons récupérer la Area de l’explosion est parce que nous allons faire des dégâts à tous les éléments présents dans la zone quand la grenade va exploser. Nous allons utiliser un code similaire à celui du couteau dans le joueur. Nous avons besoin des Particles de façon à pouvoir émettre des particules quand les grenades vont exploser.

après avoir récupéré tous les nœuds et les avoir assignés à leur variables de classe, on s’assure alors que les particules d’explosions ne sont pas émises et qu’elles sont paramétrés sur One shot. C’est une précaution pour s’assurer qu’elles se comporte comme elles le devrait.


Regardons maintenant _process.

Premièrement, nous vérifions que grenade_timer est inférieur à GRENADE_TIME. Si c’est le cas, on ajoute delta et on le renvoi. De cette façon, les grenades vont devoir attendre GRENADE_TIME secondes avant d’exploser, permettant au RigidBody de pouvoir se déplacer autour.

Si grenade_timer est supérieur ou égal à GRENADE_TIMER, nous vérifions alors si la grenade a attendu suffisamment longtemps pour exploser. On fait cela en vérifiant si explosion_wait_timer est égal à 0 ou moins. Étant donné que nous allons ajouter delta à explosion_wait_timer juste après, le code juste en dessous de cette vérification va être appelé une fois uniquement, juste quand la grenade a attendu suffisamment longtemps et doit exploser.

Si la grenade a attendue suffisamment longtemps pour exploser, on dit aux explosion_particles qu’elles peuvent émettent. Nous rendons ensuite grenade_mesh invisible et on désactive rigid_shape, ce qui cachera la grenade.

Nous réglons alors le mode du RigidBody à MODE_STATIC afin que la grenade ne bouge pas.

Nous récupérons ensuite tous les corps dans la blast_area, nous vérifions qu’ils ont la méthode/fonction et si ils l’ont, on l’appelle et on passe en argument GRENADE_DAMAGE ainsi que le transform du corps touché par la grenade. Cela fera en sorte que les corps touchés par la grenade explosent vers l’extérieur de la position de la grenade.

Nous vérifions ensuite si explosion_wait_timer est plus petit que EXPLOSION_WAIT_TIME. Si c’est le cas, on ajoute delta à explosion_wait_timer.

Ensuite, nous vérifions si explosion_wait_timer est supérieur ou égal à EXPLOSION_WAIT_TIME. Parce que nous avons ajouté delta, cela va être appelée une seule fois. Si explosion_wait_timer est supérieur ou égal à EXPLOSION_WAIT_TIME, les grenades ont attendues suffisamment longtemps pour laisser les Particles se lancer et nous pouvons détruire/libérer la grenade comme nous n’en avons plus besoin.


Paramétrons rapidement la grenade collante également. Ouvrez Sticky_Grenade.tscn.

Sticky_Grenade.tscn``est presque identique à ``Grenade.tscn, avec une petite addition. Nous avons maintenant une seconde Area, appelée Sticky_Area. Nous allons utiliser Stick_Area pour détecter quand les grenades collantes sont entrés en collision avec l’environnement et quand elles ont besoin de coller quelque chose.

Sélectionnez Sticky_Grenade et créez un nouveau script appelé Sticky_Grenade.gd. Ajoutez ce qui suit :

extends RigidBody

const GRENADE_DAMAGE = 40

const GRENADE_TIME = 3
var grenade_timer = 0

const EXPLOSION_WAIT_TIME = 0.48
var explosion_wait_timer = 0

var attached = false
var attach_point = null

var rigid_shape
var grenade_mesh
var blast_area
var explosion_particles

var player_body

func _ready():
    rigid_shape = $Collision_Shape
    grenade_mesh = $Sticky_Grenade
    blast_area = $Blast_Area
    explosion_particles = $Explosion

    explosion_particles.emitting = false
    explosion_particles.one_shot = true

    $Sticky_Area.connect("body_entered", self, "collided_with_body")


func collided_with_body(body):

    if body == self:
        return

    if player_body != null:
        if body == player_body:
            return

    if attached == false:
        attached = true
        attach_point = Spatial.new()
        body.add_child(attach_point)
        attach_point.global_transform.origin = global_transform.origin

        rigid_shape.disabled = true

        mode = RigidBody.MODE_STATIC


func _process(delta):

    if attached == true:
        if attach_point != null:
            global_transform.origin = attach_point.global_transform.origin

    if grenade_timer < GRENADE_TIME:
        grenade_timer += delta
        return
    else:
        if explosion_wait_timer <= 0:
            explosion_particles.emitting = true

            grenade_mesh.visible = false
            rigid_shape.disabled = true

            mode = RigidBody.MODE_STATIC

            var bodies = blast_area.get_overlapping_bodies()
            for body in bodies:
                if body.has_method("bullet_hit"):
                    body.bullet_hit(GRENADE_DAMAGE, body.global_transform.looking_at(global_transform.origin, Vector3(0, 1, 0)))

            # This would be the perfect place to play a sound!


        if explosion_wait_timer < EXPLOSION_WAIT_TIME:
            explosion_wait_timer += delta

            if explosion_wait_timer >= EXPLOSION_WAIT_TIME:
                if attach_point != null:
                    attach_point.queue_free()
                queue_free()

Le code au dessus, est presque identique au code pour les Grenade.gd, passons seulement en revue les changements.

Premièrement, nous avons quelques variables de classe en plus :

  • attached: Une variable utilisé pour savoir si oui ou non, la grenade collante est attachée à un PhysicsBody.
  • attach_point: Une variable qui stocke un Spatial qui sera à la position où la grenade collante est entrée en collision.
  • player_body: Le KinematicBody du joueur.

Ils ont été ajoutés pour permettre à la grenade collante de s’accrocher à n’importe quel PhysicsBody qu’il pourrait toucher. Nous devons également besoin du KinematicBody du joueur pour que la grenade collante ne s’accroche pas au joueur quand il la lance.


Maintenant, regardons les petits changements dans _ready. Nous avons ajouté une ligne de code de façon à ce que quand n’importe quel corps rentre dans Stick_Area, la fonction collided_with_body soit appelée.


Jetons maintenant un œil à``collided_with_body``.

Tout d’abord, nous nous assurons que la grenade collante ne rentre pas en collision avec elle-même. Parce que la Area collante ne sait pas si elle est attachée au RigidBody de la grenade, nous devons nous assurer qu’il ne va pas se coller à lui-même en vérifiant que le corps n’est pas rentré en collision avec lui-même. Si c’est le cas, on ignore en faisant un renvoi.

Nous vérifions ensuite si quelque chose est assigné au player_body, et si le corps avec lequel la grenade collante est entré en contact est le joueur. Si le corps avec lequel la grenade collante est entré en contact est bien le player_body, on l’ignore et on fait un renvoi.

Ensuite, nous vérifions si la grenade collante s’est déjà attachée à quelque chose ou non.

Si la grenade n’est pas attachée, on assigne alors true à attached de façon à savoir que la grenade collante est attachée à quelque chose.

Nous créons alors un nouveau nœud Spatial et on le met en tant qu’enfant du corps auquel s’est accroché la grenade. Nous assignons alors la position du :ref:`Spatial <class_Spatial>`à la position courante globale de la grenade.

Note

Parce que nous avons ajouté le Spatial en tant qu’enfant du corps attaché à la grenade collante, celui-ci va suivre le corps. Nous pouvons alors utiliser ce Spatial pour assigner la position de la grenade collante de façon à ce qu’elle ait tout le temps la même position relative au corps avec lequel elle est entrée en contact.

Nous désactivons ensuite la rigid_shape``de façon à ce que la grenade collante ne déplace pas constamment l’objet avec lequel elle est en contact. Enfin, nous mettons le mode à ``MODE_STATIC afin que la grenade ne bouge pas.


Enfin, passons en revue les quelques changements dans _process.

Nous vérifions désormais si la grenade est attachée ou non tout en haut de la fonction _process.

Si la grenade collante est attachée, nous nous assurons que le point sur lequel elle est attachée n’est pas null. Si le point attaché n’est pas égal à null, on assigne à la position globale de la grenade (en utilisant l’origine globale de son Transform) à la position globale du Spatial assigné au attach_point (en utilisant l’origine globale de son Transform).

Le seul autre changement est avant la destruction de la grenade collante pour vérifier si celle-ci a un point attaché. Si c’est le cas, on appelle aussi queue_free sur ce point afin qu’il soit également détruit.

Ajouter des grenades au joueur

Nous devons maintenant ajouter un petit peu de code au Player.gd afin de pouvoir utiliser les grenades.

Commencez par ouvrir Player.tscn et développez l’arbre des nœuds jusqu’à ce que vous obteniez Rotation_Helper. Remarquez que nous avons un noeud appelé Grenade_Toss_Pos dans Rotation_Helper. C’est ici que nous allons faire apparaître les grenades.

Remarquez également que le nœud est légèrement incliné sur l’axe X de façon à ce qu’il ne pointe pas tout droit mais plutôt un peu en l’air. En changeant la rotation de Grenade_Toss_Pos, vous pouvez également changer l’angle où les grenades sont projetées.

Très bien, faisons maintenant en sorte que les grenades fonctionnent avec le joueur. Ajoutez les variables de classe suivantes au Player.gd :

var grenade_amounts = {"Grenade":2, "Sticky Grenade":2}
var current_grenade = "Grenade"
var grenade_scene = preload("res://Grenade.tscn")
var sticky_grenade_scene = preload("res://Sticky_Grenade.tscn")
const GRENADE_THROW_FORCE = 50
  • grenade_amounts: Le nombre de grenades que le joueur porte actuellement (pour chaque type de grenade).
  • current_grenade : Le nom de la grenade que le joueur utilise actuellement.
  • grenade_scene: La scène de grenade sur laquelle nous avons travaillé plus tôt.
  • sticky_grenade_scene: La scène de grenade collante sur laquelle nous avons travaillé plus tôt.
  • GRENADE_THROW_FORCE: La force avec laquelle le joueur va lancer les grenades.

La plupart de ces variables sont similaires à la façon dont nous avons mis en place nos armes.

Astuce

Bien qu’il soit possible de faire un système de grenade plus modulaire, j’ai trouvé qu’il n’était pas nécessaire d’ajouter de la complexité pour seulement deux grenades. Si vous étiez amené à créer un FPS plus complexe avec plus de grenades, vous devriez probablement utiliser un système de grenade similaire à celui utilisé pour les armes.


Nous avons maintenant besoin “ajouter un peu de code dans _process_input. Ajoutez ce qui suit :

# ----------------------------------
# Changing and throwing grenades

if Input.is_action_just_pressed("change_grenade"):
    if current_grenade == "Grenade":
        current_grenade = "Sticky Grenade"
    elif current_grenade == "Sticky Grenade":
        current_grenade = "Grenade"

if Input.is_action_just_pressed("fire_grenade"):
    if grenade_amounts[current_grenade] > 0:
        grenade_amounts[current_grenade] -= 1

        var grenade_clone
        if current_grenade == "Grenade":
            grenade_clone = grenade_scene.instance()
        elif current_grenade == "Sticky Grenade":
            grenade_clone = sticky_grenade_scene.instance()
            # Sticky grenades will stick to the player if we do not pass ourselves
            grenade_clone.player_body = self

        get_tree().root.add_child(grenade_clone)
        grenade_clone.global_transform = $Rotation_Helper/Grenade_Toss_Pos.global_transform
        grenade_clone.apply_impulse(Vector3(0, 0, 0), grenade_clone.global_transform.basis.z * GRENADE_THROW_FORCE)
# ----------------------------------

Voyons ce qui s’y passe.

Premièrement, nous allons vérifier si l’action change_grenade vient d’être activée. Si c’est le cas, nous vérifions alors qu’elle grenade le joueur utilise. En fonction du nom de la grenade que le joueur utilise, on change current_grenade au nom de la grenade opposée.

Nous vérifions ensuite si l’action fire_grenade vient d’être activée. SI c’est le cas, on vérifie alors si le joueur a plus de 0 grenades pour le type de grenade actuellement sélectionnée.

Si le joueur a plus de 0 grenades, on retire un du nombre actuellement porté pour le type de grenade sélectionné. Ensuite, en se basant sur la grenade actuellement utilisée, on instancie la scène de la grenade est on l’assigne à grenade_clone.

Nous ajoutons ensuite grenade_clone en tant que fils du nœud à la racine et on assigne son Transform global au Transform global de Grenade_Toss_Pos. Enfin, on applique une impulsion à la grenade de façon à ce qu’elle soit lancée devant, de façon relative au vecteur directionnel Z de grenade_clone.


Maintenant, le joueur peut utiliser les deux types de grenades mais il reste encore quelques éléments que nous devrions probablement ajouter avant de passer à l’ajout de nouvelles fonctionnalités.

Il nous faut une manière d’indiquer au joueur combien de grenades il lui reste et nous devrions probablement ajouter une façon de récupérer plus de grenades quand le joueur ramasse des munitions.

Tout d’abord, changeons un peu le code dans Player.gd de manière à afficher combien de grenades il lui reste. Changez process_UI de cette façon process_UI :

func process_UI(delta):
    if current_weapon_name == "UNARMED" or current_weapon_name == "KNIFE":
        # First line: Health, second line: Grenades
        UI_status_label.text = "HEALTH: " + str(health) + \
                "\n" + current_grenade + ": " + str(grenade_amounts[current_grenade])
    else:
        var current_weapon = weapons[current_weapon_name]
        # First line: Health, second line: weapon and ammo, third line: grenades
        UI_status_label.text = "HEALTH: " + str(health) + \
                "\nAMMO: " + str(current_weapon.ammo_in_weapon) + "/" + str(current_weapon.spare_ammo) + \
                "\n" + current_grenade + ": " + str(grenade_amounts[current_grenade])

Nous allons maintenant montrer de combien de grenades le joueur dispose dans l’UI.

Tant que nous sommes dans Player.gd, créons une fonction pour ajouter des grenades au joueur. Ajoutez ka fonction suivante dans Player.gd :

func add_grenade(additional_grenade):
    grenade_amounts[current_grenade] += additional_grenade
    grenade_amounts[current_grenade] = clamp(grenade_amounts[current_grenade], 0, 4)

Nous pouvons maintenant ajouter une grenade en utilisant add_grenade, et la valeur sera automatiquement limité à un maximum de 4 grenades.

Astuce

Vous pouvez changer le 4 en une constante si vous le souhaitez. Vous auriez besoin de créer une nouvelle constante globale, quelque chose comme MAX_GRENADES et ensuite limiter la valeur de clamp(grenade_amounts[current_grenade], 0, 4) à clamp(grenade_amounts[current_grenade], 0, MAX_GRENADES)

Si vous ne voulez pas limiter combien de grenades le joueur peut porter, enlevez simplement la ligne qui limite les grenades !

Maintenant que nous avons une fonction pour ajouter des grenades, ouvrez AmmoPickup.gd et utilisez la !

Ouvrez AmmoPickup.gd et allez dans la fonction trigger_body_entered. Changer son contenu en ceci :

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)

    if body.has_method("add_grenade"):
        body.add_grenade(GRENADE_AMOUNTS[kit_size])
        respawn_timer = RESPAWN_TIME
        kit_size_change_values(kit_size, false)

Maintenant, nous vérifions également si le corps dispose de la fonction add_grenade. Si c’est le cas, nous l’appelons comme nous appelons add_ammo.

Vous avez peut être remarqué que nous utilisons une nouvelle constante que nous n’avons pas encore définie, GRENADE_AMOUNTS. Implémentons la ! Ajoutez les variables de classes suivantes dans AmmoPickup.gd avec les autres :

const GRENADE_AMOUNTS = [2, 0]
  • GRENADE_AMOUNTS: Le montant de grenades que chaque bonus contient.

Remarquez que le second élément dans GRENADE_AMOUNTS est 0. Nous avons fait cela afin que le plus petit bonus de munitions ne donne pas de grenades additionnelles au joueur.


Vous devriez être en mesure de lancer des grenades ! Faites un essai !

Ajout de la capacité d’attraper et lancer des nœuds RigidBody au joueur

Ensuite, donnons au joueur la capacité de ramasser et lancer des nœuds RigidBody.

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

var grabbed_object = null
const OBJECT_THROW_FORCE = 120
const OBJECT_GRAB_DISTANCE = 7
const OBJECT_GRAB_RAY_DISTANCE = 10
  • grabbed_object: Une variable pour stocker le nœud RigidBody attrapé.
  • OBJECT_THROW_FORCE: La force avec laquelle le joueur va lancer l’objet attrapé.
  • OBJECT_GRAB_DISTANCE: La distance entre la caméra et l’objet attrapé par le joueur.
  • OBJECT_GRAB_RAY_DISTANCE: La distance parcourue par le Raycast. C’est la distance à laquelle le joueur peut attraper des objets.

Ceci fait, tout ce que nous devons ajouter, c’est un peu de code dans process_input :

# ----------------------------------
# Grabbing and throwing objects

if Input.is_action_just_pressed("fire") and current_weapon_name == "UNARMED":
    if grabbed_object == null:
        var state = get_world().direct_space_state

        var center_position = get_viewport().size / 2
        var ray_from = camera.project_ray_origin(center_position)
        var ray_to = ray_from + camera.project_ray_normal(center_position) * OBJECT_GRAB_RAY_DISTANCE

        var ray_result = state.intersect_ray(ray_from, ray_to, [self, $Rotation_Helper/Gun_Fire_Points/Knife_Point/Area])
        if ray_result != null:
            if ray_result["collider"] is RigidBody:
                grabbed_object = ray_result["collider"]
                grabbed_object.mode = RigidBody.MODE_STATIC

                grabbed_object.collision_layer = 0
                grabbed_object.collision_mask = 0

    else:
        grabbed_object.mode = RigidBody.MODE_RIGID

        grabbed_object.apply_impulse(Vector3(0, 0, 0), -camera.global_transform.basis.z.normalized() * OBJECT_THROW_FORCE)

        grabbed_object.collision_layer = 1
        grabbed_object.collision_mask = 1

        grabbed_object = null

if grabbed_object != null:
    grabbed_object.global_transform.origin = camera.global_transform.origin + (-camera.global_transform.basis.z.normalized() * OBJECT_GRAB_DISTANCE)
# ----------------------------------

Voyons ce qui se passe.

Premièrement, nous devons vérifier si l’action déclenchée est l’action fire et que le joueur utilise « l’arme » UNARMED. Nous vérifions cela, car nous voulons que le joueur puisse récupérer et lancer des objets seulement quand il n’utilise aucune arme. C’est un choix de design et cela donne du sens à UNARMED.

Vérifions ensuite si grabbed_object est null.


Si grabbed_object est null, nous voulons voir si nous pouvons ramasser un RigidBody.

Nous récupérons d’abord l’état spatial direct du World actuel. Cela nous permettra de lancer un rayon entièrement depuis le code, au lieu d’avoir à utiliser un nœud Raycast.

Note

voir Ray-casting pour plus d’informations sur le raycasting dans Godot.

Ensuite, nous récupérons le centre de l’écran en divisant la taille de la Viewport par deux. Nous obtenons ensuite le point d’origine et d’arrivée du rayon en utilisant project_ray_origin````project_ray_normal de la caméra. Si vous voulez en savoir plus sur le fonctionnement de ces fonctions, regardez Ray-casting.

Nous envoyons ensuite le rayon dans l’espace 3D et nous vérifions si nous avons un résultat. Nous ajoutons la Area du couteau et du joueur comme exception de façon à ce que le joueur ne puisse pas se porter lui même ou la Area du couteau.

La prochaine étape est de vérifier si nous avons obtenu un résultat du rayon. Si c’est le cas, nous vérifions si l’objet qui est entré en contact est un RigidBody.

Si le rayon est rentré en contact avec un RigidBody, on l’assigne au ``grabbed_object`. On met ensuite le mode du RigidBody avec lequel nous sommes rentré en contact en ``MODE_STATIC``de façon à ce qu’il ne bouge pas dans nos mains.

Enfin, nous assignons au collision layer et au collision mask du RigidBody attrapé à 0. Cela fera en sorte que le RigidBody n’ait pas de collision layer ou mask, ce qui veut dire qu’il ne pourra pas rentrer en collision tant qu’on le tiendra.


Si grabbed_object n’est pas null, nous devons alors lancer le RigidBody que le joueur tient.

Nous définissons d’abord le mode du RigidBody que nous mettons sur MODE_RIGID.

Note

Nous faisons une plutôt grande hypothèse que tous les rigidbodies utilisent MODE_RIGID. Bien que ce soit le cas pour cette série de tutoriels, cela ne sera pas forcément le cas dans d’autres projets.

Si vous avez des rigidbodies avec différents modes, vous devrez peut-être stocker le mode du RigidBody que vous avez ramassé dans une variable de classe afin de lui rétablir son mode au moment où vous l’avez ramassé.

Nous lui appliquons ensuite une impulsion pour le propulser devant. Nous le faisons voelr dans la direction où la caméra fait face, en utilisant la force que nous avons assigné à la variable OBJECT_THROW_FORCE.

Nous assignons alors 1 au collision et layer mask du RigidBody attrapé, afin qu’il puisse rentre en collision avec d’autres objets à nouveau.

Note

Nous faisons, encore une fois, une plutôt grande hypothèse en partant du principe que tous les rigidbodies auront leur collisions mask et layer sur 1. Si vous utilisez ce script sur d’autres projets, vous devrez peut-être stocker les collision layer/mask des RigidBody attrapés dans une variables avant de les changer à 0, de façon à leur rendre leur valeur initiale une fois lâchés.

Enfin, on assigne null au grabbed_object car le joueur a lancé avec succès l’objet.


La dernière chose que nous devons faire, c’est de vérifier si oui ou non grabbed_object est égal à null en dehors du code relatif à attraper/jeter un objet.

Note

Bien que techniquement ce n’est pas relié aux entrées, il est assez facile de placer le code de déplacement des objets attrapés ici car ce n’est que deux lignes et que tout le code pour attraper/lancer un objet se trouve au même endroit

Si le joueur tient un objet, on lui assigne sa position globale à la position de la caméra + OBJECT_GRAB_DISTANCE dans la direction où la caméra fait face.


Avant de tester cela, nous devons changer quelque chose dans _physics_process. Quand le joueur tient un objet, nous ne voulons pas que le joueur puisse changer d’armes ou recharger. Changez _physics_process à ce qui suit :

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

    if grabbed_object == null:
        process_changing_weapons(delta)
        process_reloading(delta)

    # Process the UI
    process_UI(delta)

Maintenant, le joueur ne peut pas changer d’arme ou recharger tant qu’il tient un objet.

Vous êtes désormais capable d’attraper et lancer des nœuds RigidBody tant que vous êtes dans l’état UNARMED ! Faites un essai !

Ajouter une tourelle

Créons une tourelle capable de tirer sur le joueur !

Ouvrez Turret.tscn. Développez Turret si ce n’est pas encore le cas.

Remarquez comme la tourelle se décompose en plusieurs parties : Base, Head, Vision_Area, et un nœud de Particles Smoke.

Ouvrez Base et vous y trouverez son StaticBody et son mesh. Ouvrez Head et vous y trouverez plusieurs mesh, un StaticBody et un nœud Raycast.

Une chose à noter avec Head, c’est que le raycast correspond à l’endroit où les balles seront tirées si nous utilisons du raycast. Nous avons également deux mesh appelés Flash et Flash_2. Il y aura un effet de lumière sur le bout du canon qui va apparaître lorsque la tourelle tire.

Vision_Area est une Area que nous allons utiliser pour donner une vision à la tourelle. Quand quelque chose entre la Vision_Area, on suppose que la tourelle pourra le voir.

Smoke est un nœud Particles qui va se jouer quand la tourelle est détruite et en cours de réparation.


Maintenant que nous avons vu comment la scène est mise en place, commençons à écrire le code pour la tourelle. Sélectionnez Turret et créez un nouveau script appelé Turret.gd. Ajoutez ce qui suit dans ce script :

extends Spatial

export (bool) var use_raycast = false

const TURRET_DAMAGE_BULLET = 20
const TURRET_DAMAGE_RAYCAST = 5

const FLASH_TIME = 0.1
var flash_timer = 0

const FIRE_TIME = 0.8
var fire_timer = 0

var node_turret_head = null
var node_raycast = null
var node_flash_one = null
var node_flash_two = null

var ammo_in_turret = 20
const AMMO_IN_FULL_TURRET = 20
const AMMO_RELOAD_TIME = 4
var ammo_reload_timer = 0

var current_target = null

var is_active = false

const PLAYER_HEIGHT = 3

var smoke_particles

var turret_health = 60
const MAX_TURRET_HEALTH = 60

const DESTROYED_TIME = 20
var destroyed_timer = 0

var bullet_scene = preload("Bullet_Scene.tscn")

func _ready():

    $Vision_Area.connect("body_entered", self, "body_entered_vision")
    $Vision_Area.connect("body_exited", self, "body_exited_vision")

    node_turret_head = $Head
    node_raycast = $Head/Ray_Cast
    node_flash_one = $Head/Flash
    node_flash_two = $Head/Flash_2

    node_raycast.add_exception(self)
    node_raycast.add_exception($Base/Static_Body)
    node_raycast.add_exception($Head/Static_Body)
    node_raycast.add_exception($Vision_Area)

    node_flash_one.visible = false
    node_flash_two.visible = false

    smoke_particles = $Smoke
    smoke_particles.emitting = false

    turret_health = MAX_TURRET_HEALTH


func _physics_process(delta):

    if is_active == true:

        if flash_timer > 0:
            flash_timer -= delta

            if flash_timer <= 0:
                node_flash_one.visible = false
                node_flash_two.visible = false

        if current_target != null:

            node_turret_head.look_at(current_target.global_transform.origin + Vector3(0, PLAYER_HEIGHT, 0), Vector3(0, 1, 0))

            if turret_health > 0:

                if ammo_in_turret > 0:
                    if fire_timer > 0:
                        fire_timer -= delta
                    else:
                        fire_bullet()
                else:
                    if ammo_reload_timer > 0:
                        ammo_reload_timer -= delta
                    else:
                        ammo_in_turret = AMMO_IN_FULL_TURRET

    if turret_health <= 0:
        if destroyed_timer > 0:
            destroyed_timer -= delta
        else:
            turret_health = MAX_TURRET_HEALTH
            smoke_particles.emitting = false


func fire_bullet():

    if use_raycast == true:
        node_raycast.look_at(current_target.global_transform.origin + Vector3(0, PLAYER_HEIGHT, 0), Vector3(0, 1, 0))

        node_raycast.force_raycast_update()

        if node_raycast.is_colliding():
            var body = node_raycast.get_collider()
            if body.has_method("bullet_hit"):
                body.bullet_hit(TURRET_DAMAGE_RAYCAST, node_raycast.get_collision_point())

        ammo_in_turret -= 1

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

        clone.global_transform = $Head/Barrel_End.global_transform
        clone.scale = Vector3(8, 8, 8)
        clone.BULLET_DAMAGE = TURRET_DAMAGE_BULLET
        clone.BULLET_SPEED = 60

        ammo_in_turret -= 1

    node_flash_one.visible = true
    node_flash_two.visible = true

    flash_timer = FLASH_TIME
    fire_timer = FIRE_TIME

    if ammo_in_turret <= 0:
        ammo_reload_timer = AMMO_RELOAD_TIME


func body_entered_vision(body):
    if current_target == null:
        if body is KinematicBody:
            current_target = body
            is_active = true


func body_exited_vision(body):
    if current_target != null:
        if body == current_target:
            current_target = null
            is_active = false

            flash_timer = 0
            fire_timer = 0
            node_flash_one.visible = false
            node_flash_two.visible = false


func bullet_hit(damage, bullet_hit_pos):
    turret_health -= damage

    if turret_health <= 0:
        smoke_particles.emitting = true
        destroyed_timer = DESTROYED_TIME

C’est pas mal de code, alors nous allons décomposer le script fonction par fonction. Jetons d’abord un œil aux variables de classe :

  • use_raycast: Un booléen exporté que nous allons pouvoir changer quand la tourelle utilise des objets ou tire des rayons pour des balles.
  • TURRET_DAMAGE_BULLET: Le montant de dégâts d’une seule scène de balle.
  • TURRET_DAMAGE_RAYCAST: Le montant de dégats qu’une seule balle Raycast fait.
  • FLASH_TIME: La durée (en seconde) pendant laquelle le flash sur le canon est visible.
  • flash_timer: Une variable utilisée pour savoir combien de temps a passé depuis qu’on a rendu visible le flash du canon.
  • FIRE_TIME: Le temps nécessaire (en secondes) pour tirer une balle.
  • fire_timer: Une variable utilisée pour savoir combien de temps a passé depuis qu’une balle a été tirée.
  • node_turret_head: Une variable utilisée pour stocker le nœud Head.
  • node_raycast: Une variable nous permettant de stocker le nœud Raycast attaché à la tête de la tourelle.
  • node_flash_one: Une variable utilisée pour stocker le premier :ref:`MeshInstance <class_MeshInstance>`du flash du canon.
  • node_flash_two: Une variable utilisée pour stocker le second :ref:`MeshInstance <class_MeshInstance>`du flash du canon.
  • ammo_in_turret: Le nombre de balles dont la tourelle dispose actuellement.
  • AMMO_IN_FULL_TURRET: Le nombre de balle dont une tourelle pleine dispose.
  • AMMO_RELOAD_TIME: Le temps qu’il faut pour qu’une tourelle recharge.
  • ammo_reload_timer: Une variable utilisée pour le temps qu’a passer une tourelle a recharger.
  • current_target: La cible courante de la tourelle.
  • is_active: Une variable pour savoir si la tourelle peut tirer sur la cible ou non.
  • PLAYER_HEIGHT: La hauteur que nous ajoutons afin que la tourelle ne tire pas sur les pieds de la cible.
  • smoke_particles: Une variable pour stocker les nœuds de particule de fumée.
  • turret_health: La santé actuelle de la tourelle.
  • MAX_TURRET_HEALTH: La quantité de santé dont une tourelle intacte dispose.
  • DESTROYED_TIME: Le temps qu’il faut (en seconde) pour une tourelle détruite pour se réparer elle-même.
  • destroyed_timer: Une variable pour suivre le temps pendant lequel une tourelle a été détruite.
  • bullet_scene: La scène de la balle que doit tirer la tourelle (même scène que celle pour le pistolet du joueur)

Hé bien, ça fait pas mal de variables de classes !


Passons en revue _ready maintenant.

Premièrement, nous allons récupérer la zone de vision et connecter les signaux body_entered et body_exited à body_entered_vision et body_exited_vision, respectivement.

Nous récupérons ensuite tous les nœuds et nous les assignons à leur variables respectives.

Ensuite, nous ajoutons quelques exceptions au Raycast de façon à ce que la tourelle ne puisse pas se faire de dégâts.

Nous rendons ensuite les deux mesh de flash invisible au début car nous n’allons pas tirer pendant _ready.

Nous obtenons ensuite le nœud de particules de fumée et nous l’assignons à la variable smoke_particles. Nous attribuons false à emitting pour s’assurer que les particules ne s’émettent pas tant que la tourelle n’est pas détruite.

Enfin, nous assignons MAX_TURRET_HEALTH à la vie de la tourelle afin qu’elle commence avec toute sa vie.


Regardons maintenant _physics_process.

Premièrement, nous vérifions si la tourelle est active. Si c’est le cas, nous voulons activer le code de tir.

Ensuite, si flash_timer est supérieur à zéro, ce qui veut dire que les mesh de flash sont visibles, nous voulons supprimer delta de flash_timer. Si flash_timer atteint zéro ou moins après avoir soustrait delta, nous voulons cacher les deux mesh de flash.

Ensuite, nous vérifions si la tourelle a une cible. Si c’est le cas, nous faisons en sorte que le tourelle la regarde en lui ajoutant PLAYER_HEIGHT de façon à ce qu’elle ne fixe pas les pieds du joueur.

Nous vérifions ensuite si la santé de la tourelle est supérieur à zéro. Si c’est le cas, nous vérifions si la tourelle a encore des munitions.

S’il y en a, nous vérifions alors si fire_timer est supérieur à zéro. Si c’est le cas, la tourelle ne peut pas tirer et nous devons soustraire delta de fire_timer. Si fire_timer est inférieur ou égal à zéro, la tourelle peut alors tirer une balle, nous appelons donc la fonction fire_bullet.

S’il n’y a plus aucune munitions dans la tourelle, nous vérifions si ammo_reload_timer est supérieur à zéro. Si c’est le cas, nous soustrayons delta de ammo_reload_timer. Si ammo_reload_timer est inférieur ou égal à zéro, on assigne ammo_in_turret à AMMO_IN_FULL_TURRET car la tourelle a attendu suffisamment longtemps pour se réapprovisionner en munitions.

Ensuite, nous vérifions si la vie de tourelle est inférieur ou égale à 0 en dehors du fait qu’elle est active ou non. Si la santé de la tourelle est zéro ou moins, nous vérifions alors si destroyed_timer est supérieur à zéro. Si c’est le cas, nous soustrayons delta de destroyed_timer.

Si destroyed_timer est inférieur ou égal à zéro, on assigne turret_health à MAX_TURRET_HEALTH et on stoppe les émissions de particules de fumée en assignant false à smoke_particles.emitting.


Regardons maintenant fire_bullet.

Tout d’abord, nous vérifions si la tourelle utilise un raycast.

Le code pour utiliser un raycast est presque identique à celui présent dans le fusil de la Partie 2, je vais donc passer dessus très brièvement.

Nous allons d’abord tourner le raycast vers la cible, en s’assurant que celui-ci va toucher sa cible si rien n’est sur le chemin. On force ensuite le raycast à se mettre à jour de façon à obtenir une collision à la frame près. Nous vérifions ensuite si le raycast est entré en collision avec quelque chose. Si c’est le cas, nous vérifions ensuite si le corps entré en collision a une méthode bullet_hit. Si elle l’a, on l’appelle et on lui passe les dommages qu’inflige un simple raycast de balle ainsi que le Transform du raycast. On soustrait ensuite 1 de ammo_in_turret.

Si la tourelle n’utilise pas de raycast, on fait apparaître une balle à la place. Ce code est pratiquement le même que celui du pistolet dans Partie 2, donc comme pour le code du raycast, je vais le passer en revue rapidement.

Nous créons d’abord une clone de balle en lui assignant clone. On l’ajoute ensuite en tant qu’enfant du nœud racine. On assigne la position du bout du canon au Transform global de la balle, on ajuste un peu sa taille car c’est trop petit et on lui assigne ses dommages et sa vitesse en utilisant les variables de classe constantes de la tourelle. On soustrait ensuite 1 de ammo_in_turret.

Ensuite, quelque soit la méthode utilisée pour la balle, on rend les deux mesh du flash visible. On assigne FLASH_TIME à flash_timer et FIRE_TIME à fire_timer. On vérifie ensuite si la tourelle a utilisé sa dernière balle. Si c’est le cas, on affecte AMMO_RELOAD_TIME à ammo_reload_timer de façon à ce que la tourelle recharge.


Regardons maintenant body_entered_vision, et heureusement, c’est relativement court.

Nous commençons par vérifier si la tourelle a une cible en vérifiant si current_target est égal à null. Si la tourelle n’a pas de cible, on vérifie alors si le corps qui est entré dans le Area de vision est un KinematicBody.

Note

Nous supposons que la tourelle ne devrait cibler que les nœuds :ref:`KinematicBody <class_KinematicBody>`puisque que le joueur utilise ces nœuds.

Si le corps qui vient d’entrer dans la vision Area est un KinematicBody, on assigne le corps à current_target, et on met is_active à true.


Regardons maintenant body_exited_vision.

Premièrement, on vérifie si la tourelle a une cible. Si c’est le cas, on vérifie alors si le corps qui vient de sortir de la vision de la tourelle Area est la cible de la tourelle.

Si le corps qui vient de sortir de la vision Area est la cible de la tourelle, on assigne null à current_target, false à is_active et on ré-initialise toutes les variables liées au tir de la tourelle puisque la tourelle n’a plus de cibles sur laquelle tirer.


Terminons par un coup d’œil à bullet_hit.

Nous soustrayons d’abord les dommages que cause la balle à la santé de la tourelle.

Ensuite, on vérifie si la tourelle est détruite ou non (sa santé atteint zéro ou moins). Si la tourelle est détruite, on commence à émettre le signal d’émission des particules de fumée et on assigne DESTROYED_TIME à destroyed_timer afin que la tourelle attende pour être réparée.


Whew, maintenant que tout ceci est fait, il ne nous reste plus qu’une dernière chose à faire avant que la tourelle soit prête à l’utilisation. Ouvrez Turret.tscn si ce n’est pas encore le cas et sélectionner un des nœuds StaticBody de Base ou Head Créez un nouveau script appelé TurretBodies.gd et attacher n’importe quel :ref:`StaticBody <class_StaticBody>`que vous avez sélectionné.

Ajoutez le code suivant à TurretBodies.gd :

extends StaticBody

export (NodePath) var path_to_turret_root

func _ready():
    pass

func bullet_hit(damage, bullet_hit_pos):
    if path_to_turret_root != null:
        get_node(path_to_turret_root).bullet_hit(damage, bullet_hit_pos)

Tout ce que ce code fait, c’est d’appeler bullet_hit sur n’importe quel nœud où mène path_to_turret_root. Revenez à l’éditeur et assignez le NodePath au nœud Turret.

Sélectionnez maintenant l’autre nœud StaticBody <class_StaticBody>`(que ce soit dans ``Body` ou dans Head) et assignez lui le script TurretBodies.gd. Une fois le script attaché, assignez de nouveau le NodePath au nœud Turret.


La dernière chose que nous devons ajouter, c’est une façon pour le joueur d’être blessé. Puisque toutes les balles utilisent la fonction bullet_hit, nous devons ajouter cette fonction au joueur.

Ouvrez Player.gd et ajoutez ce qui suit :

func bullet_hit(damage, bullet_hit_pos):
    health -= damage

Avec tout ceci fait, vous avez désormais des tourelles complètement opérationnelles ! Allez en placer quelques unes dans un/deux ou toutes les scènes et essayez par vous-même !

Notes finales

../../../_images/PartFiveFinished.png

Vous pouvez maintenant récupérer des nœuds RigidBody et lancer des grenades. Nous avons également des tourelles qui tirent sur le joueur.

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

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_5.zip