Ce cours est la suite du cours d'introduction aux moteurs de jeux du semestre précédent. Maintenant que l'interface de Godot est maîtrisée, il est temps d'approfondir les possibilités de développement disponibles grâce à la rédaction de scripts. Pour l'instant, nous les rédigerons en GDScript.
Nous aurons en fil rouge de déveloper un jeu de stratégie en temps réel (STR ou RTS en anglais pour : real-time strategy). Pour commencer, vous pouvez télécharger la base de projet MyFirstRTS qui contient le minimum pour pouvoir commencer le développement mais pour vous dérouiller un peu, il n'est pas interdit de créer rapidement un environnement équivalent. Je tiens à préciser que les ressources graphiques que j'utilise ne sont pas de moi, elles proviennent de Kenney. J'ai notamment choisi le package Sci-Fi RTS pour les éléments graphiques et UI Pack: Space Expansion pour les éléments d'interface graphique.Voilà donc à quoi ressemble notre projet vu dans l'interface Godot lors du lancement :
Nous avons bien ajouté un noeud de type Camera2D à la scène mais pour l'instant aucun script n'y est attaché. Cependant, avant de se pencher sur le script, il y a quelques vérifications à effectuer dans l'inspecteur :
current doit être activée. Cette propriété active la caméra pour la scène courante. Une seule caméra peut-être courante dans une scène (obviously!).Drag Margin H/V sont activées la caméra ne se déplace que lorsque la souris atteint la marge de glissement. Ces propriétés peuvent rester inactives pour le moment mais peut-être seront nous plus tard amenés à vouloir glisser/déposer un élément d'un bout à l'autre de la map...Drag Center pour la propriété Anchor mode permettra de centrer la vue, sachant que dans ce cas les décalages et taille d'écran ou de fenêtre sont pris en compte. C'est la valeur qui doit être selectionnée car nous ne souhaitons pas fixer la caméra sur le coin supérieur gauche de notre fenêtre, ce qui se produit avec une valeur d'ancre à Fixed top-left
Il est maintenant temps d'ajouter un script à notre caméra, j'ai choisi de l'appeler Camera_movement.gd. Ce script est donc une classe qui hérite dans ce cas précis de Camera2D.
Nous allons tout d'abord gérer les déplacement de la caméra via les appuis sur les touches directionnelles.
speed initialisé à 15.0. On ajoutera le mot-clé export, cela sauvegarde la variable tant que la ressource à laquelle elle est attachée est en vie et lui permet d'être visible et modifiable dans l'éditeur._process lorsque vous avez besoin d'utiliser le delta de temps séparant deux images. Si le code qui met à jour les données doit être mis à jour aussi souvent que possible, c'est le bon endroit. Il est possible de savoir si une action est activée grâce à l'appel à la méthode Input.is_action_pressed("nom_action"). Dans notre cas, nous allons pouvoir détecter quand les appuis sur les touches directionnelles se produisent. Le nom de ces actions sont ui_right, ui_left, ui_down, ui_up. La fonction is_action_pressed retourne un booléen, qu'il est possible de caster en un entier de la manière suivante : int(Input.is_action_pressed("nom_action")). Dans la méthode _process (delta), déclarer deux variables locales qui permettront de stocker les actions sur l'axe horizontal input_x et sur l'axe vertical input_y. On attribuera à input_x la différence entre int(Input.is_action_pressed("ui_right")) et int(Input.is_action_pressed("ui_left")) . Procédez de manière similaire pour la valeur de input_y. Attention, pour les déplacement verticaux, n'oubliez pas que le sens des ordonnées positifs est vers le bas dans Godot.Camera2D hérite de Node2D, et donc de ses propriétés, en particulier, sa position qui est de type Vector2. Un Vector2 est donc une structure à 2 éléments qui est en général utilisée pour représenter des positions dans l'espace 2D mais qui peut servir à gérer n'importe quelle paire de données numériques. Déplacer la caméra consiste donc à changer les valeurs des champs x et y de la position. Pour lisser déplacement nous allons utiliser la méthode lerp qui effectue une interpolation linéaire entre deux valeurs, selon une valeur normalisée. La nouvelle position de la caméra sera calculée par interpolation linéaire entre sa précédente position (position.x si on considère l'abscisse) et la position à laquelle on ajoute les déplacement liés au appuis sur les touches directionneles multipliés par la vitesse de déplacement de la caméra (position.x + input_x*speed, par exemple pour la composante horizontale). Le poids de cette opération dépendra du temps écoulé depuis le précédent déplacement (delta) qu'on remultipliera par la vitesse de déplacement de la caméra.À ce stade, si on lance notre scène, les déplacements de la caméra sont bien pris en compte, vous pouvez ajuster la vitesse de déplacement en modifiant la valeur de la variable speed dans l'inteface.
La prochaine étape est de paramétrer le zoom. Les actions de zoom vont s'effectuer grâce à la molette de la souris et seront gérées dans la fonction _input(event) qui ne se déclenche que sur les frames dans lesquelles le moteur a effectivement détecté un événement en entrée. La classe Camera2D a une proriété zoom de type Vector2 qui définit le zoom de la caméra par rapport à la fenêtre. Les valeurs supérieures au vecteur (1, 1) effectuent un zoom arrière et les valeurs plus petites effectuent un zoom avant.
zoom_factor=1.0 et zoom_speed=10.0._input (event), on va alors chercher à savoir si une action sur la souris se produit. Pour cela on peut tester si l'événement récupéré et passé en argument event est de type InputEventMouseButton. Alors, on peut tester si cet event correspond à un appui de bouton en testant le retour de la méthode event.is_pressed(). Dans ce cas, il est encore possible d'affiner le test puisque nous effectuerons une action de zoom que si le bouton de souris effectivement actionné est la molette vers le haut (event.button_index == BUTTON_WHEEL_UP) ou vers le bas (event.button_index == BUTTON_WHEEL_DOWN). Nous choisirons un pas de zoom égal à 0.01*zoom_speed._process, utilisez à nouveau la méthode lerp pour modifier les champs x et y de la propriété zoom.is_zooming initialiséeà false.true dans la fonction_input lorsqu'une action sur la molette est détecteée et à false sinon._process, si la variable is_zooming est false, réinitialisez la valeur de zoom_factor.Le but de cette partie est de créer des objets selectionnables auxquels il sera possible par la suite d'appliquer des actions.
Unit.tscn dans notre projet.KinematicBody2D, renommez le Unit. N'oubliez pas d'aciver la propriété Pickable. Lui ajouter une CollisionShape2D et deux noeuds enfants de type Sprite. Nous en appelerons un Box et l'autre Sprite. Choisissez une texture dans le répertoire Unit pour le noeud Sprite (j'ai choisi scifiUnit_02.png). Pour le noeud Box, ajoutez la texture crossair_red.png dans le répertoire UI.
new RectangleShape2D à la CollisionShape2D. Ajustez la boîte englobante du rectangle au plus près de la texture de noeud Sprite. N'oubliez de rendre les enfants de Unit non sélectionnables, cela rend la manipulation de l'ensemble moins périlleuse.Notre unité est prête à être scriptée ! Voilà à quoi ressemble notre scène :

Unit.box qui récupère le noeud Box.setget fournit une syntaxe setter/getter. On l'utilise directement après une définition de la variable. La syntaxe est var ma_variable setget setter_func, getter_func. Dès que la valeur de ma_variable est modifiée par une source externe (c'est-à-dire qui ne provient pas de l'utilisation locale dans la classe), le modificateur setter_func sera appelé avant que la valeur soit modifiée. Le setter doit définir les actions à effectuer. Inversement, lorsqu'on accède à ma_variable, l'accesseur getter_func doit renvoyer la valeur souhaitée. setter_func (arg) prend obligatoirement et exactement 1 paramètre, getter_func () n'a pas de paramètre. Leur définition est obligatoire si elles ont été déclarées derrière le setget. Il faut savoir qu'il est possible de ne déclarer qu'un setter, dans ce cas la syntaxe est la suivante : var ma_variable setget setter_func. Pour ne déclarer qu'un getter : var ma_variable setget ,getter_func. L'accès local ne déclenchera pas le setter et le getter, pour forcer l'appel il faut soit les appeler explicitement : ma_variable.setter_func(valeur) soit utiliser self.ma_variable= valeur. Déclarez une variable globale selected initialisée à faux et pour laquelle on déclarera un modificateur (setter) nommé set_selected.set_selected, qui attribuera la valeur passée en argument à selected et qui rendra visible ou invisible le noeud Box. Pour cela, il suffit de modifier la valeur du champ visible de la variable box. Unit au signal input_event. Cela a pour conséquence directe de générer une fonction on_Unit_input_event (viewport, event, shape_idx).set_selected soit appelé avec pour argument true. En cas de click droit, l'unité doit être déselectionnée.Vous devrez me rendre ce travail avant le 10/02/2022 23h59. Le sujet de votre mail sera "[GODOT]Séance 1". Dans le corps du mail, vous n'oublierez pas de préciser vos nom, prénom, numéro d'étudiant.
Pour qu'une action sur un noeud puisse avoir des répercutions sur d'autre éléments du jeu, nous allons utiliser les signaux. Un signal est un message qui peut être émis par un objet afin qu'un autre puisse l'intercepter et y réagir. Je vous invite à lire leur principe de fonctionnement ici. Il est donc possible pour un noeud d'émettre un signal personnalisé, la syntaxe pour le déclarer est la suivante : signal mon_signal. Une fois déclaré, le signal apparaît dans l'inspecteur et peut être connecté de la même manière que les signaux intégrés d'un noeud. Le signal est émis par l'appel de la fonction emit_signal("mon_signal"). Il est possible de déclarer des arguments au signal : signal mon_signal (arg1, arg2, ...). On ajoute les valeurs de ces arguments à la suite du premier argument lors de l'émission : emit_signal ("mon_signal", val_arg1, val_arg2, ...). Il est également possible de connecter le signal dans le script : connect ("mon_signal", target, "method"). Cela permet donc au signal passé en premier paramètre d'être connecté à la méthode method de l'objet target.
Unit dans la scène World, nommez-les et dispersez-les sur la carte. Voici à quoi pourrait ressembler la scène World :

Base. Ce noeud, de type Control empêche les événements de type MouseInputButton d'atteindre les unités. Pour cela il faut changer la valeur de la propriété Disposition à l'écran du noeud Base. Fixez-la à Étendu en bas à droite, ce qui aura pour effet de rendre non-cliquables, uniquement les élements sous le NinePatchRect.

World, celui-ci permettra de gérer les actions et interactions du jeu.GDScript a un type Array qui représente un tableau générique permettant de stocker des objets de types différents. Les différents éléments sont accessibles par leur index à partir de 0 au moyen de l'opérateur []. Je vous invite vivement à lire la documentation de la classe Array. Ajoutez une variable selected_units à votre script qui permettra de stocker la liste à jour des unités sélectionnées. Cette variable sera donc un Array, vide à l'initialisation.func select_unit (unit) qui permet d'ajouter l'unité passée en paramètre à la liste des unités sélectionnées, si elle n'en fait pas déjà partie.func deselect_unit (unit) qui permet de supprimer l'unité passée en paramètre de la liste des unités sélectionnées, si elle en fait partie.Unit.gd Ajoutez deux signaux is_selected et is_deselected. Ces deux signaux ont un paramètre qu'on appelera unit, puisque cela permettra d'envoyer, en plus du signal, l'unité émettrice de ce signal.is_selected lorsqu'elle est sélectionnée. Ce signal aura un paramètre, qui doit être l'objet émetteur, en l'occurence nous-même._ready (), établissez la connection entre le signal is_selected, et la méthode select_unit de l'objet World. On pourra utiliser la méthode get_parent().is_deselected.print dans les méthodes select_unit et deselect_unit. Vous devriez normalement voir s'afficher le message de votre choix dans la console en cas de click droit ou gauche sur une unité. Notez que pour accéder au nom d'un noeud, nous disposons de la propriété name.float clamp (float value, float min, float max) zoom dans le calcul de la position au même titre que la vitesse.ctrl. On pourra récupérer la taille de la fenêtre grâce à la propriété OS.window_size. Cette propriété est un vecteur à deux dimensions renseignant la largeur et la hauteur de la fenêtre. L'idéal serait que la vitesse de déplacement augmente lorsque la souris s'approche du bord de la fenêtre.Vous devrez me rendre ce travail avant le 17/02/2022 23h59. Le sujet de votre mail sera "[GODOT]Séance 2". Dans le corps du mail, vous n'oublierez pas de préciser vos nom, prénom, numéro d'étudiant.
Notre but maintenant va être de lier nos éléments de jeu à l'interface graphique. Aujourd'hui, nous allons faire apparaître, dans la barre d'interface du bas de l'écran, un bouton avec le bon label, lors de la sélection d'une unité. Un clic sur ce bouton entrainera la déselection de l'unité correspondante.
TextureButton et choisissez une texture à lui attribuer. Ajoutez un noeud fils de type Label.TextureButton. Déclarez un signal button_clicked qui sera émis lors d'un click sur le bouton. L'émission du signal se fera dans la redéfinition de la méthode _pressed ( ) qui est appelée à la détection d'une action de click sur le bouton. connect_button. Elle prendra en paramètre l'objet qui provoque cette création d'instance et le nom de l'unité qui est attachée. Dans le corps de cette méthode, les propriétés name du bouton, ainsi que text du label se verront attribuer le nom de l'unité passée en paramètre. Il ne faut pas oublier de connecter notre signal button_clicked à la méthode qu'on appellera button_clicked dans le script attaché au monde, en ajoutant un argument correspondant au nom du bouton, afin de pouvoir le récupérer en argument dans la méthode appelée lors de l'émission du signal.Voilà pour la partie bouton. Sauvegardez votre scène. Voici à quoi elle pourrait ressembler :

World.tscn
World, afin de pouvoir créer des instances TextureButton, il est nécessaire de charger la scène précédemment créée. Je vous invite à lire la page sur la gestion des ressources pour comprendre comment fonctionnent les ressources dans Godot.buttons qui tiendra à jour la liste des boutons présents dans l'interface.Base. Commencez par écrire delete_buttons qui parcourt le tableau buttons et qui pour chaque élément, supprime le bouton de l'inteface s'il était présent. Ne pas oublier de vider le contenu de buttons. Je vous invite à regarder la doc de la classe Node et en particulier les méthodes : has_node(), get_node(), remove_child(), ou encore queue_free().create_buttons, elle commence par purger la barre des boutons existants, puis crée et ajoute les instances de boutons correspondant au contenu du tableau buttons. N'oubliez pas de connecter les boutons créés ! create_buttons lorsque une unité est sélectionnée ou désélectionnée.button_clicked de ce script. C'est donc le moment de coder cette méthode.Voici ce à quoi devrait ressembler votre jeu à cet instant :
Nous allons ajouter une barre de vie à nos unités et la faire apparaître en cas de sélection.
Unit.tscn. Ajoutez un noeud de type TextureProgress. Renommez le noeud Life. Il faut choisir deux textures : une texture pour la propriété Under (qui correspondra à ce qu'on aperçoit lorsque la barre se vide) et une texture pour la propriété Progress (correspond à ce qu'on observe lorsque la barre se remplit). J'ai choisi une texture transparente lorsque la barre est vide et une verte pour la barre pleine.Vous devrez me rendre ce travail avant le 5/03/2022 23h59. Le sujet de votre mail sera "[GODOT]Séance 3". Dans le corps du mail, vous n'oublierez pas de préciser vos nom, prénom, numéro d'étudiant.
Maintenant qu'il est possible de gérer la sélection de nos unités, nous allons pouvoir les déplacer de vers un même point de la carte. Les unités devront contourner les obstacles présent sur leur chemin.
Nous allons déjà enrichir notre monde afin de rendre les calcul de chemin de nos unités plus ardu. Pour cela, vous allez devoir créer deux scènes : une première qui reprend les éléments présent dans le noeud "Factory" du projet initial. Et une deuxième qui ressemblerait à celle ci-dessous :

Pour calculer un chemin entre deux position de la carte, nous allons utiliser un objet Navigation2D. La première chose à faire est donc de lire attentivement la documentation liée à cet objet et à son utilisation. Puis, regarder l'exemple fourni en lien sur cette même page 2D Navigation Demo. Ensuite, créer un noeud Navigation2D et dessinez le NavigationPolygonInstance en tenant compte des éléments de notre map. Voilà à quoi cela pourrait ressembler à ce stade :

Le but est que les unités sélectionnées se déplacent vers un point de la carte en cas de doubleclick. Voilà une proposition de marche à suivre :
Camera_movement.gd, ajoutez la détection du doubleclick dans la méthode _input. Dans ce cas, un signal start_move_unitssera émis avec en paramètre les coordonnées de la souris à cet instant._init, connectez ce signal à méthode start_move_units du noeud World.World afin d'y créer la méthode start_move_units qui reçoit la position de la souris en paramètre. Dans cette fonction, on parcourt la liste des unités actuellement sélectionnées et on appelle la méthode move_to_point du noeud Unit. Il ne faut pas oublier de passer les coordonnées du point vers lequel on souhaite envoyer cette unité.
Unit.gd, en vous inspirant de l'exemple vu en début de séance, écrivez la méthode move_to_point (destination).Le but de cet exercice est d'élaborer la scène qui va permettre d'instancier autant de boules de feu que désiré. La première étape est donc de créer une nouvelle FireBall.tscn Cette scène sera composée de la manière suivante :
Faites en sorte que les tours éméttent des boules de feu
Vous devrez me rendre ce travail avant le 17/03/2022 23h59. Le sujet de votre mail sera "[GODOT]Séance 4". Dans le corps du mail, vous n'oublierez pas de préciser vos nom, prénom, numéro d'étudiant.
En reprenant la scène Fireball.tscn, nous allons lui ajouter un script, afin gérer le lancement et la trajectoire d'une boule de feu par les tours. Il faut commencer par créer un champ speed initialisé à 20. Ensuite, il faut ajouter deux champs de type Vector2, chacun disposant d'un setter. Ainsi, à sa création, une boule de feu aura un vecteur de direction et une destination. Dans la méthode _process, gérer le déplacement de la boule de feu jusqu'à sa destination, puis la supprimer une fois l'objectif atteint.
Nous allons compléter la scène Tower.tscn. Nous allons ajouter deux noeuds fils au noeud 2D principal :
Timer, afin de pouvoir choisir la fréquence d'émission des boules de feu. Pour cela, un Timer, émet un signal chaque fois qu'il atteint 0. Il commence le décompte depuis le nombre de secondes (par défaut 1) fixée par la propriété wait_time. J'ai choisi cinq secondes.Position2D que l'on va placer dans l'éditeur à l'endroit où l'on veut que soient émises les boules de feu.Tower à cet instant :

Pour émettre les boules de feu, nous avons besoin d'ajouter un script à la tour. Il est judicieux de l'ajouter au noeud racine, nous aurons ainsi accès facilement aux noeuds Timer et Position2D. Dans un premier temps, nous allons faire tourner les tours sur elles-même et les faire tirer toutes les cinq secondes. Voici les actions à réaliser :
_ready, appliquer une rotation aléatoire à la tour, afin que chacune soit orientée différemment au démarrage du jeu. Puis, démarrer le timer en avec un délai aléatoire afin que les tours ne tirent pas toutes exactement en même temps. Connecter son signal timeout à la méthode _on_Timer_timeout. World, fixer sa position au niveau du canon de la tour (i.e. la Position2D) et faîtes en sorte que la direction du tir soit donnée par le vecteur formé par le centre de la tour et la position de son canon. Pour la destination, on choisira que la boule de feu peut parcourir une distance de 200 avant de disparaître. _process.Nous allons faire en sorte que les unités prennent des dommages lorsqu'elles se font toucher par une boule de feu. Pour cela, nous allons créer une fonctio take_fb_damage dans le script Unit.gd. Dans cette fonction, il faudra dimnuer la valeur associée au noeud Life. Si cette valeur, n'est plus supérieure à 0, l'unité doit donc disparaître...
Il ne reste plus qu'à ajouter un signal body_entered sur l'area2D. Ce signal, est connecté à la méthode _on_FireBall_body_entered(body). Dans cette fonction, il suffira de tester si l'élément entré en contact avec la boule de feu possède une méthode qui gère la perte d'énergie due à la collision avec la boule de feu (take_fb_damage dans notre cas). Si c'est le cas, on appellera la méthode en question.
Voici une vidéo de l'objectif à atteindre :
Et maintenant, vous pouvez faire en sorte que les tours aient également des points de vie, et que les unités puissent les attaquer et les détruire. Vous êtes libres de choisir comment se déroule l'attaque (juste le contact, tir de boules de feu également, épée...).
Vous devrez me rendre ce travail avant le 24/03/2022 23h59. Le sujet de votre mail sera "[GODOT]Séance 5". Dans le corps du mail, vous n'oublierez pas de préciser vos nom, prénom, numéro d'étudiant.
Pour commencer, je vous recommande de lire très attentivement la page suivante.
Commencez par créer une scène dédiée au choix du username, à la création d'un serveur ou à la connexion à un serveur existant.

Une fois capable de créer un serveur et d'y acceuillir des joueurs, configurer un jeu simple ou les joueurs se déplacent sur une map et peuvent se lancer des boules de feu.
Essayez, maintenant d'intégrer une gestion du multijoueur dans le jeu que nous avons développé depuis le début du semestre. Dans un premier temps, vous pourrez limiter à deux joueurs, chacun aura ses unités et ses tours, le but étant de détruire toutes les tours de l'autre joueur.