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.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.Notre but maintenant va être de lier nos éléments de jeu à l'interface graphique. Dans un premier temps, 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 maintenant 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.