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 : La suite consiste à ajouter un script pour permettre de sélectionner l'unité par un click.
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
.
Normalement, vous pouvez, maintenant, cliquer sur vos unités et en constater à nouveau les effets .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 :
Nous allons maintenant retourner dans notre scène 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 : Une fois créées, instanciez chacune d'elles plusieurs fois (au moins 5) et placez où vous le souhaitez. La carte est ainsi enrichie.
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_units
sera é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.