Tutoriel FPS - Partie 5 : Ajouter des grenades et des tourelles

Répondre
Avatar du membre
keltwookie
Admin du site
Messages : 81
Enregistré le : mer. avr. 04, 2018 5:42 pm
Localisation : Kashyyyk
Contact :

Tutoriel FPS - Partie 5 : Ajouter des grenades et des tourelles

Message par keltwookie » ven. mai 11, 2018 5:17 am

Aperçu de la partie 5

Dans cette partie, nous allons ajouter des grenades à notre joueur, donner à notre joueur la possibilité de saisir et de lancer des objets, et ajouter des tourelles !

Image

Remarque :
Vous êtes supposé avoir terminé Tutoriel FPS - Partie 4 : Joypads, collectes et cibles avant de passer à cette partie du tutoriel.Le projet terminé de la partie 4 sera le projet de départ de la partie 5.


Commençons !


Ajouter des grenades

Donnons d'abord à notre joueur des grenades. Ouvrez Grenade.tscn.

Il y a quelques points à noter ici, le premier et le plus important étant que nos grenades vont utiliser des nœuds RigidBody. Nous allons les utiliser afin que nos grenades rebondissent autour du monde d'une manière quelque peu réaliste.

La deuxième chose à noter est Blast_Area. Il s'agit d'un nœud Area qui représentera le rayon de souffle de notre grenade.

Enfin, la dernière chose à noter est Explosion. C'est un nœud Particles qui émettra un effet d'explosion lorsque les grenades éclateront. Une chose à noter ici est que nous avons One shot activé. C'est ainsi que nous émettrons toutes nos particules en même temps. Nous émettons également des coordonnées globales au lieu de coordonnées locales, Local Coords ne sera donc pas coché.

Remarque :
Si vous voulez, vous pouvez voir comment les particules sont installées en regardant à travers Process Material et Draw Passes.


Ecrivons le code nécessaire pour notre grenade. Sélectionnez le nœud correspondant et créez un nouveau script appelé Grenade.gd puis ajouter ce qui suit :

Code : Tout sélectionner

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, global_transform.origin)

            # 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()
Revoyons ce qui se passe, en commençant par les variables globales :
  • GRENADE_DAMAGE : La quantité de dommages que la grenade cause lorsqu'elle explose.
  • GRENADE_TIME : Le temps qu'il faut à la grenade (en secondes) pour exploser une fois qu'elle a été créée/jetée.
  • grenade_timer : Variable permettant de savoir depuis combien de temps la grenade a été créée/jetée.
  • EXPLOSION_WAIT_TIME : Le temps nécessaire (en secondes) pour attendre avant de détruire la scène de la grenade après l'explosion.
  • explosion_wait_timer : Variable permettant de savoir combien de temps s'est écoulé depuis l'explosion de la grenade.
  • rigid_shape : Le CollisionShape pour le RigidBody de la grenade.
  • grenade_mesh : Le MeshInstance pour la grenade.
  • blast_area : L’ Area d’explosion servant à endommager les objets lorsque la grenade explose.
  • explosion_particles : Les Particles émises lorsque la grenade explose.
Remarquez comment EXPLOSION_WAIT_TIME est plutôt étrange (0.48). C'est parce que nous voulons que EXPLOSION_WAIT_TIME soit la durée d'émission des particules, donc quand les particules sont produites, nous détruisons la grenade. Nous calculons 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 donne le temps exact que dureront les particules de l'explosion.

Maintenant, tournons notre attention vers _ready.

Tout d'abord, nous obtenons tous les nœuds dont nous aurons besoin et nous les assignons aux variables globales appropriées.

Nous devons obtenir les nœuds CollisionShape et MeshInstance parce que, de la même manière que la cible dans la partie 4, nous cacherons le maillage de la grenade et désactiverons la forme de collision lorsque la grenade explosera.

La raison pour laquelle nous devons obtenir l’Area d’explosion est que nous pouvons endommager tout ce qui s'y trouve lorsqu'elle explose. Nous utiliserons un code similaire à celui de l'arme « couteau » de notre joueur. Nous avons besoin de Particles pour pouvoir émettre ces particules quand les grenades explosent.

Après avoir obtenu tous les nœuds et les avoir assigné à leurs variables globales, nous nous assurons que les particules d'explosion n'émettent pas, et qu'elles sont réglées pour émettre en une seule fois.

Regardons maintenant _process.

Tout d'abord, nous vérifions si grenade_timer est inférieur à GRENADE_TIME. Si c'est le cas, nous ajoutons delta et return. C'est pourquoi nous devons attendre GRENADE_TIME en secondes, permettant à notre RigidBody de se déplacer.

Si grenade_timer est égal à GRENADE_TIME ou plus, nous devons alors vérifier si nous avons attendu assez longtemps et si la grenade doit exploser. Pour ce faire, nous vérifions si explosion_wait_timer est égal ou inférieur à 0. Puisque nous ajouterons delta à explosion_wait_timer juste après, n'importe quel code sous la vérification ne sera appelé qu'une seule fois, juste au moment où nous avons attendu assez longtemps pour que la grenade n'explose.

Si nous avons attendu assez longtemps, nous disons d'abord à explosion_particles d'émettre. Ensuite, nous rendons grenade_mesh invisible et désactivons rigid_shape, cachant ainsi notre grenade.

Nous réglons ensuite le mode de RigidBody sur MODE_STATIC pour que la grenade ne bouge pas.

Ensuite, nous obtenons tous les corps dans la zone blast_area, vérifions s'ils ont la méthode bullet_hit, et s'ils le font, nous l'appelons et passons dans GRENADE_DAMAGE et la position de la grenade.

Nous vérifions ensuite si explosion_wait_timer est inférieur à EXPLOSION_WAIT_TIME. Si c'est le cas, nous ajoutons delta à explosion_wait_timer.

Ensuite, nous vérifions si explosion_wait_timer est supérieur ou égal à EXPLOSTION_WAIT_TIME. Parce que nous avons ajouté delta, ceci ne sera appelé qu'une seule fois. Si explosion_wait_timer est plus ou égal à EXPLOSION_WAIT_TIME, nous avons attendu assez longtemps pour laisser les particules émettre.

Mettons rapidement en place la grenade collante. Ouvrez Sticky_Grenade.tscn.

Sticky_Grenade.tscn est presque identique à Grenade.tscn, avec un petit ajout. Nous avons maintenant une deuxième zone, appelée Sticky_Area. Nous l’utiliserons pour détecter quand nous sommes entrés en collision avec l'environnement et que la grenade « colle » à quelque chose.

Sélectionnez le nœud RigidBody Sticky_Grenade et créez un nouveau script appelé Sticky_Grenade.gd. Ajouter ce qui suit :

Code : Tout sélectionner

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, global_transform.origin)

            # 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 ci-dessus est presque identique au code de Grenade.gd, alors passons en revue ce qui a changé.
Tout d'abord, nous avons quelques variables globales supplémentaires :
  • attached : Une variable pour savoir si nous nous sommes attachés ou non à un PhysicsBody.
  • attach_point : Une variable pour gérer un Spatial qui sera à la position où nous sommes entrés en collision.
  • player_body : Le KinematicBody du joueur.
Ces ajouts sont là pour que la grenade puisse coller à n'importe quel PhysicsBody que nous touchons. Nous avons aussi besoin du KinematicBody du joueur pour que la grenade ne colle pas à celui-ci.


Regardons maintenant le petit changement dans _ready. Nous y avons ajouté une ligne de code pour que lorsque n'importe quel corps entrant dans Sticky_Area appelle la fonction collided_with_body.


Ensuite, jetons un coup d'oeil à collided_with_body.


D'abord, nous nous assurons que la grenade n’est pas entrée en collision avec le joueur. Parce que notre Area ne sait pas qu'il est attaché au RigidBody de la grenade, nous devons nous assurer que nous n'allons pas coller la grenade au joueur. Si le joueur est entré en collision avec, nous l'ignorons avec return.


Nous vérifions ensuite si nous avons quelque chose assigné à player_body, et si le corps avec lequel la grenade est entrée en collision est le joueur. Si c’est le cas, nous l'ignorons avec return.


Ensuite, nous vérifions si la grenade est collée ou pas.


Si ce n’est pas le cas, nous réglons attached à true, de sorte que nous savons que la grenade est collée à quelque chose.


Nous faisons alors un nouveau nœud Spatial, et en faisons un nœud enfant du corps avec lequel nous sommes entrés en collision. Nous réglons ensuite la position du nœud Spatial à notre position actuelle.


Remarque :
Parce que nous avons ajouté le nœud Spatial en tant que nœud enfant du corps avec lequel nous sommes entrés en collision, il suivra avec ledit corps. Nous pouvons alors utiliser ce nœud Spatial pour établir notre position, de sorte que nous sommes toujours à la même, par rapport au corps avec lequel la grenade a interagit.


Nous désactivons ensuite rigid_shape pour que nous ne soyons pas constamment en mouvement, quel que soit le corps avec lequel la grenade est entrée en collision. Enfin, nous réglons notre mode sur MODE_STATIC pour que la grenade ne bouge pas.

Enfin, passons en revue les quelques changements dans _process.

Maintenant, nous vérifions si nous sommes attachés (attached) au début de _process.

Si c’est le cas, nous nous assurons alors que le point attaché n'est pas égal à null. Si ce n’est pas le cas, nous fixons notre position globale (en utilisant global_transform.origin) à la position globale du Spatial assigné à attach_point (toujours en utilisant global_transform.origin).

Le seul autre changement est que maintenant, avant de détruire la grenade, nous vérifions si nous avons un point d'attache. Si nous en avons un, nous appelons aussi queue_free, et il sera donc détruit de la même manière.


Ajouter des grenades au joueur



Nous avons maintenant besoin d’ajouter un peu de code à Player.gd pour que le joueur puisse utiliser nos grenades.

Tout d'abord, ouvrez Player.tscn et développez l'arbre nodal jusqu'à ce que vous arriviez à Rotation_Helper. Remarquez que nous avons un nœud appelé Grenade_Toss_Pos. C'est ici que nous allons générer les grenades.

Notez aussi qu'il est légèrement tourné sur l'axe X, de sorte qu'il ne pointe pas en ligne droite, mais plutôt légèrement vers le haut. En changeant la rotation de Grenade_Toss_Pos, vous pouvez changer l'angle sous lequel les grenades sont lancées.

Ok, maintenant commençons à faire fonctionner les grenades avec notre joueur. Ajoutez les variables globales suivantes à Player.gd :

Code : Tout sélectionner

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 : La quantité de grenades que nous transportons actuellement pour chaque type de grenade.
  • current_grenade : Le nom du type de grenade que nous utilisons actuellement.
  • grenade_scene : La scène de la 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 nous lançons la grenade.

La plupart de ces variables sont similaires à la façon dont nous avons configuré nos armes.


Astuce :
Bien qu'il soit possible de fabriquer un système de grenade plus modulaire, j'ai estimé que cela ne valait pas la peine d’une complexité supplémentaire pour seulement deux types de grenades. Si vous vouliez faire un FPS plus complexe avec plus de grenades, vous voudriez probablement faire un système de grenades similaire à celui que nous avons mis en place pour les armes.



Nous devons maintenant ajouter du code dans _process_input. Ajoutez-y ce qui suit :

Code : Tout sélectionner

# ----------------------------------
# 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 se passe ici.

Tout d'abord, nous vérifions si l'action change_grenade vient d'être activée. Si c'est le cas, nous vérifions ensuite quelle grenade nous utilisons actuellement. Basé sur le nom de la grenade que nous utilisons actuellement, nous changeons current_grenade pour le nom de la grenade opposée.

Ensuite, nous vérifions si l'action fire_grenade vient d'être activée. Si c'est le cas, nous vérifions ensuite si nous avons plus de 0 grenades pour la grenade actuelle sélectionnée.
Si nous avons plus de 0 grenades, nous en retirons une de la quantité pour la grenade actuelle. Ensuite, en nous basant sur la grenade utilisée, nous instancions la scène de grenade appropriée et l'assignons à grenade_clone.

Ensuite, nous ajoutons grenade_clone en tant que nœud enfant à la racine, et réglons sa transformation globale à Grenade_Toss_Pos. Enfin, nous appliquons une impulsion à la grenade pour qu'elle soit lancée vers l'avant, par rapport au vecteur directionnel Z de grenade_clone.

Maintenant, nous pouvons utiliser les deux types de grenades, mais il y a quelques choses que nous devrions probablement additionner avant de passer à d’autres ajouts.

Nous avons encore besoin d'un moyen de voir combien de grenades il nous reste, et probablement d’un moyen pour obtenir plus de grenades lorsque nous ramassons des munitions.

Tout d'abord, changeons le code dans Player.gd pour voir combien de grenades il nous reste. Modifier process_UI comme suit :

Code : Tout sélectionner

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])
Maintenant, nous allons montrer combien de grenades il nous reste dans notre interface utilisateur.

Tant que nous sommes encore dans Player.gd, créons une fonction pour y ajouter des grenades :

Code : Tout sélectionner

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

Maintenant nous pouvons ajouter une grenade en utilisant add_grenade, et elle sera automatiquement clampée à un maximum de 4 grenades.

Astuce :
Vous pouvez changer le 4 en constante si vous le souhaitez. Vous auriez besoin de faire une nouvelle constante globale, quelque chose comme MAX_GRENADES, puis de changer clamp(grenade_amounts[current_grenade], 0, 4)
pour
clamp(grenade_amounts[current_grenade], 0, MAX_GRENADES).


Si vous ne voulez pas limiter le nombre de grenades que vous pouvez porter, enlevez la ligne qui clampe les grenades !

Maintenant que nous avons une fonction pour ajouter des grenades, utilisons-la et ouvrons AmmoPickup.gd.

Allez à la fonction trigger_body_entered et ajoutez ce qui suit :

Code : Tout sélectionner

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 aussi que le corps a 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. Ajoutez la variable globale suivante aux autres dans AmmoPickup.gd :

Code : Tout sélectionner

const GRENADE_AMOUNTS = [2, 0]
  • GRENADE_AMOUNTS : La quantité de grenades que chaque collecte de chaque taille contient.
Remarquez que le deuxième élément dans GRENADE_AMOUNTS est 0, de sorte que la petite collecte de munitions ne donne pas de grenades supplémentaires à notre joueur.

Maintenant vous devriez être capable de lancer des grenades ! Essayez !


Ajouter la possibilité de saisir et de lancer des nœuds RigidBody pour le joueur

Ouvrez Player.gd et ajoutez les variables globales suivantes :

Code : Tout sélectionner

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 gérer le nœud RigidBody saisi.
  • OBJECT_THROW_FORCE : La force à laquelle nous lançons l'objet saisi.
  • OBJECT_GRAB_DISTANCE : La distance par rapport à la caméra à laquelle nous gérons l'objet saisi.
  • OBJECT_GRAB_RAY_DISTANCE : La distance parcourue par le Raycast. C'est notre distance de saisie d’objet.
Une fois cet ajout effectué, tout ce que nous avons à faire est d'ajouter du code à process_input :

Code : Tout sélectionner

# ----------------------------------
# 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:
            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.

Nous vérifions d'abord si l'action est celle de tir, et si nous utilisons l'arme UNARMED. C'est parce que nous ne voulons pouvoir ramasser et lancer des objets que lorsque nous n'utilisons pas d'armes. C'est un choix de conception, mais je pense que cela donne une utilité à UNARMED.

Ensuite, nous vérifions si grabbed_object est null ou non.

Si oui, nous voulons voir si nous pouvons ramasser un RigidBody.

Nous obtenons d'abord l'état direct de l'espace à partir du monde actuel. C'est ainsi que nous pouvons lancer un rayon entièrement à partir du code, au lieu d'avoir à utiliser un noeud Raycast.

Remarque :
voir Ray-casting dans la documentation Godot pour plus d'informations sur le sujet.


Ensuite, nous obtenons le centre de l'écran en divisant la taille actuelle de la fenêtre en deux. Nous obtenons ensuite le point d'origine et le point final du rayon en utilisant project_ray_origin et project_ray_normal depuis la caméra. Si vous voulez en savoir plus sur le fonctionnement de ces fonctions, voir Ray-casting.

Ensuite, nous envoyons notre rayon dans l'espace pour voir si nous obtenons un résultat. Nous nous ajoutons nous-même ainsi que la zone du couteau comme deux exceptions, de sorte que nous ne pouvons pas, nous-mêmes ou la zone de collision du couteau, nous transporter.

Si le rayon est entré en collision avec un RigidBody, nous avons mis grabbed_object sur le collisionneur avec lequel le rayon est entré en collision. Nous avons ensuite réglé le mode sur le RigidBody avec lequel nous sommes entrés en collision sur MODE_STATIC pour qu'il ne soit pas déplacé.

Enfin, nous réglons sa couche et son masque de collision à 0, ce qui signifie qu'il n'aura ni l’un ni l’autre et qu'il ne pourra pas entrer en collision avec quoi que ce soit.

Si grabbed_object n'est pas null, alors nous devons lancer le RigidBody que nous tenons.

Nous réglons d'abord le mode RigidBody que nous tenons MODE_RIGID.

Remarque :
C’est faire preuve d'un postulat assez large que de dire que tous les corps rigides utiliseront MODE_RIGID. Bien que ce soit le cas pour cette série de tutoriels, ce n'est peut-être pas le cas pour d'autres projets.
Si vous avez des RigidBody avec différents modes, vous pouvez avoir besoin de stocker le mode du RigidBody que vous avez ramassé dans une variable globale afin de pouvoir le ramener au mode dans lequel il était avant que vous l'ayez ramassé.


Ensuite, nous appliquons une impulsion pour l'envoyer devant nous et dans la direction de la caméra avec OBJECT_THROW_FORCE.

Nous avons ensuite réglé la couche de collision et le masque de RigidBody à 1, afin qu'il puisse à nouveau entrer en collision avec n'importe quoi sur la couche 1.


Remarque :
Il s'agit, une fois de plus, d'un postulat générique selon lequel tous les corps rigides ne se trouveront que sur la couche de collision 1, et tous les masques de collision se trouveront sur la couche 1. Si vous utilisez ce script dans d'autres projets, vous devrez peut-être stocker la couche de collision/masque de RigidBody avant de les changer à 0.


Enfin, nous avons mis grabbed_object à null puisque nous avons réussi à lancer l'objet.

La dernière chose que nous faisons est de vérifier si grabbed_object est égal à null, en dehors du code de « ramassage/lancement ».

Remarque :
Bien que techniquement non lié à l'entrée, il est assez facile de placer le code en déplaçant l'objet saisi ici parce qu’il ne s’agit seulement que de deux lignes, et donc tout le code de saisie/lancer est à un seul endroit.


Si nous tenons un objet, nous réglons sa position globale à la position de la caméra plus OBJECT_GRAB_DISTANCE dans la direction de la caméra.

Avant de tester ceci, nous devons changer quelque chose dans _physics_process . Pendant que nous tenons un objet, nous ne voulons pas pouvoir changer d'armes ou recharger, nous devons donc le modifier avec ce qui suit :

Code : Tout sélectionner

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)
Grâce à cela, nous ne pourrons pas changer d'armes ou recharger tout en tenant un objet.

Maintenant vous pouvez attraper et lancer des nœuds RigidBody alors que vous êtes dans un état UNARMED ! Essayez !


Ajouter une tourelle

Créons une tourelle pour tirer sur notre joueur !

Ouvrez Turret.tscn. Agrandir Turret si ce n’est déjà fait.

Remarquez comment notre tourelle est divisée en plusieurs parties. Nous avons une base (Base), une tête (Head), une aire visuelle (Vision_Area) et des particules (Particles) Smoke.

Ouvrez Base et vous verrez que c'est un StaticBody et un maillage. Ouvrez Head et vous trouverez plusieurs maillages, un StaticBody et un nœud Raycast.

Une chose à noter avec Head est que le Raycast sera l'endroit d'où nos balles tireront si nous utilisons le raycasting. Nous avons également deux maillages appelés Flash et Flash_2. Ce sera le flash d’éjection qui montre brièvement quand la tourelle tire.

Vision_Area est un noeud [urlhttp://docs.godotengine.org/en/stable/classes/class_area.html]Area[/url] que nous utiliserons pour que la tourelle puisse voir. Quand quelque chose entre dans Vision_Area, nous supposerons que la tourelle peut voir cette chose.

Smoke est un nœud de particules qui s’activera lorsque la tourelle sera détruite et en cours de réparation.

Maintenant que nous avons regardé 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-y ce qui suit :

Code : Tout sélectionner

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 == false:

        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

    else:
        node_raycast.look_at(current_target.global_transform.origin + PLAYER_HEIGHT, 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

    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

Cela fait pas mal de code, alors décomposons-le fonction par fonction et examinons d'abord les variables globales
  • use_raycast : Un booléen exporté pour que nous puissions changer afin que la tourelle puisse utiliser des objets ou du raycasting pour les balles.
  • TURRET_DAMAGE_BULLET : L'ampleur des dommages causés par une seule scène de balle.
  • TURRET_DAMAGE_RAYCAST : L'ampleur des dommages causés par une seule balle Raycast.
  • FLASH_TIME : La durée (en secondes) pendant laquelle les maillages du flash d’éjection sont visibles.
  • flash_timer : Variable permettant de savoir depuis combien de temps les maillages du flash d’éjection sont visibles.
  • FIRE_TIME : Le temps (en secondes) nécessaire pour tirer une balle.
  • fire_timer : Variable permettant de savoir combien de temps s'est écoulé depuis le dernièr tir de la tourelle.
  • node_turret_head : Variable permettant de gérer le nœud Head.
  • node_raycast : Une variable pour gérer le nœud Raycast attaché à la tête de la tourelle.
  • node_flash_one : Une variable pour gérer le premier flash d’éjection MeshInstance.
  • node_flash_two : Une variable pour gérer le second flash d’éjection MeshInstance.
  • ammo_in_turret: La quantité de munitions actuellement dans la tourelle.
  • AMMO_IN_FULL_TURRET : La quantité de munitions dans une tourelle remplie.
  • AMMO_RELOAD_TIME : Le temps qu'il faut à la tourelle pour recharger.
  • ammo_reload_timer : Variable de suivi de la durée de rechargement de la tourelle.
  • [current_target : La cible courante de la tourelle.
  • is_active : Variable permettant de déterminer si la tourelle est capable de tirer sur la cible.
  • PLAYER_HEIGHT : La hauteur que nous ajoutons à la cible pour ne pas tirer sur ses pieds.
  • smoke_particles : Une variable pour gérer le nœud de particules de fumée.
  • turret_health : La quantité de santé que la tourelle a actuellement.
  • MAX_TURRET_HEALTH : La capacité de santé maximale de la tourelle.
  • DESTROYED_TIME : Le temps (en secondes) qu'il faut pour qu'une tourelle détruite se répare d'elle-même.
  • destroyed_timer : Variable permettant de suivre le temps pendant lequel une tourelle a été détruite.
  • bullet_scene : La scène de la balle que la tourelle tire (même scène que le pistolet du joueur).
Ouf, ça fait pas mal de variables globales !

Regardons ensuite _ready.

Nous obtenons d'abord l’aire visuelle et connectons les signaux body_entered et body_exited aux signaux body_entered_vision et body_exited_vision respectivement.

Nous obtenons alors tous les nœuds et nous les assignons à leurs variables respectives.

Ensuite, ajoutez quelques exceptions au nœud Raycast pour que la tourelle ne puisse pas se blesser elle-même.

Ensuite, nous rendons les deux maillages de flashs d’éjection invisibles pour commencer, puisque nous n'allons pas tirer pendant _ready.

On obtient alors le nœud de particules de fumée et on l'affecte au nœud Smoke_particles. Nous avons également réglé l'émission sur false pour s'assurer qu'elle n'émet pas jusqu'à ce que la tourelle soit détruite.

Enfin, nous réglons la santé de la tourelle à MAX_TURRET_HEALTH pour qu'elle commence en pleine santé.

Maintenant, allons à _physics_process.

Nous vérifions d'abord si la tourelle est active. Si la tourelle est active, nous voulons traiter le code de tir.

Ensuite, nous vérifions si flash_timer est supérieur à zéro, ce qui signifie que les maillages de flash sont visibles, nous voulons supprimer delta de flash_timer. Si celui-ci arrive à zéro ou moins après avoir soustrait delta, nous voulons cacher les deux maillages de flash d’éjection.

Ensuite, nous vérifions si nous avons une cible ou non. Si nous avons une cible, nous faisons regarder la tête de la tourelle, en ajoutant PLAYER_HEIGHT pour ne pas viser les pieds du joueur.

Nous vérifions ensuite si la santé de la tourelle est supérieure à zéro. Si c'est le cas, nous vérifions alors s'il y a des munitions dans la tourelle.

S'il y a des munitions dans la tourelle, nous vérifions alors si fire_timer est supérieur à zéro. Si c’est le cas, nous ne pouvons pas tirer et nous devons retirer delta de fire_timer. Si celui-ci est égal ou inférieur à zéro, nous voulons tirer une balle, c'est pourquoi nous appelons la fonction fire_bullet.

S'il n'y a pas de munitions dans la tourelle, nous vérifions si ammo_reload_timer. est supérieur à zéro. Si c’est le cas, nous soustrayons le delta. de celui-ci. Si ammo_reload_timer. est égal ou inférieur à zéro, nous réglons ammo_in_turret. sur AMMO_IN_FULL_TURRET. parce que nous avons attendu assez longtemps pour remplir la tourelle.

Ensuite, nous vérifions si la santé de la tourelle est inférieure ou égale à 0, en dehors de savoir si elle est active ou non. Si la santé de la tourelle est égale ou inférieure à zéro, nous vérifions alors si destroy_timer est supérieur à zéro. Si c’est le cas, nous soustrayons delta de celui-ci.

Si destroy_timer est inférieur ou égal à zéro, nous réglons turret_health sur MAX_TURRET_HEALTH et arrêtons d'émettre des particules de fumée en réglant smoke_particles.emittingsur false.

Ensuite, regardons fire_bullet.

Tout d'abord, nous vérifions si nous utilisons un raycast ou non.

Le code pour l'utilisation d'un raycast est presque entièrement le même que le code pour le fusil de la partie 2, donc je vais seulement le revoir brièvement.

Nous commençons par faire en sorte que le raycast regarde la cible, en s'assurant que nous atteindrons celle-ci. Nous forçons ensuite le raycast à se mettre à jour afin d'obtenir un contrôle de collision parfait. Nous vérifions ensuite si le raycast est entré en collision avec quoi que ce soit. Si le raycast est entré en collision avec quelque chose, nous vérifions ensuite si le corps entré en collision a la fonction bullet_hit. Si c'est le cas, nous l'appelons et transmettons dans les dommages qu'une seule balle de raycast produit. Nous enlevons ensuite 1 de ammo_in_turret.


Ensuite, quelle que soit la méthode que nous ayons utilisée, nous rendons les deux maillages de flash visibles. Nous réglons flash_timer et fire_timer sur FLASH_TIME et FIRE_TIME respectivement. Nous vérifions ensuite si nous avons utilisé la dernière balle dans la tourelle. Si c’est la cas, nous réglons ammo_reload_timer sur AMMO_RELOAD_TIME.

Regardons maintenant body_entered_vision, et heureusement c'est plutôt court.

Nous vérifions d'abord si nous avons actuellement une cible en vérifiant si current_target est égal à null. Si nous n'avons pas de cible, nous vérifions si le corps qui vient d'entrer dans la zone de vision est un KinematicBody.

Remarque :
Nous supposons que la tourelle ne devrait tirer que sur les nœuds KinematicBody, puisque c'est ce que notre/nos joueur(s) utilise(nt).


Si le corps qui n'est que la zone visuelle est un KinematicBody, nous fixons current_target au corps, et set is_active à true.

Regardons maintenant body_exited_vision.

D'abord, nous vérifions si nous avons une cible. Si nous avons une cible, nous vérifions ensuite si le corps qui vient de quitter notre zone de vision est notre cible.

Si le corps qui vient de quitter la zone est la cible courante, nous réglons current_target à null, set is_active à false, et réinitialisons toutes les variables liées au tir de la tourelle, puisque nous n'avons plus de cible sur laquelle tirer.

Enfin, regardons bullet_hit.

Nous enlevons d'abord tous les dommages que nous avons reçus depuis la santé de la tourelle.

Ensuite, nous vérifions si nous avons été détruits. Si c'est le cas, nous générons l’émission des particules de fumée et réglons destroyed_timer à DESTROYED_TIME, de sorte que nous devons attendre pour réparer la tourelle.

Ouf, avec tout cela fait et codé, nous n'avons qu'une dernière chose à faire avant que nos tourelles soient prêtes à l'emploi. Ouvrez Turret.tscn si ce n’est déjà fait et sélectionnez l'un des nœuds StaticBody dans Body ou Base. Créez un nouveau script appelé TurretBodies.gd et attachez-le à n'importe quel StaticBody sélectionné.

Ajoutez le code suivant :

Code : Tout sélectionner

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 fait ce code est d'appeler bullet_hit quel que soit le nœud vers lequel path_to_turret_root conduit. Retournez à l'éditeur et affectez le NodePath au nœud Turret.


La dernière chose que nous devons faire est d'ajouter un moyen pour que le joueur soit blessé. Puisque toutes nos balles utilisent la fonction bullet_hit, nous devons l'ajouter à notre joueur.

Ouvrez Player.gd et ajoutez ce qui suit :

Code : Tout sélectionner

func bullet_hit(damage, bullet_hit_pos):
    health -= damage
Avec tout cela, vous devriez avoir des tourelles entièrement opérationnelles ! Allez en placer quelques unes dans une/les deux/toutes les scènes et essayez-les !


Remarques finales

Image

Maintenant vous pouvez ramasser les nœuds RigidBody et lancer des grenades. Nous avons maintenant aussi des tourelles pour tirer sur notre joueur.

Dans la partie 6, nous allons ajouter un système de régénération pour le joueur, et changer/déplacer le système sonore pour que nous puissions l'utiliser à partir de n'importe quel script.

Attention :
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

Tutoriel FPS - Partie 5 - Version PDF téléchargeable
- Le projet"XPlore"
- Tutos Blender
- Tutos Godot Game Engine

“ L'artiste est un malade qui essaie de se soigner en créant, mais plus il se soigne, plus il est malade. Et plus il est malade, plus il est content, vu qu'il n'a aucune envie de guérir." Philippe Geluck