Votre premier jeu¶
Vue d'ensemble¶
Ce tutoriel vous guidera dans la réalisation de votre premier projet Godot. Vous apprendrez comment fonctionne l'éditeur de Godot, comment structurer un projet et comment réaliser un jeu 2D.
Note
Ce projet est une introduction au moteur Godot. Il suppose que vous avez déjà une certaine expérience en programmation. Si vous n'avez jamais programmé, vous devriez commencer ici : Les scripts.
Le jeu s'appelle "Dodge the Creeps !". Votre personnage doit se déplacer et éviter les ennemis le plus longtemps possible. Voici un aperçu du résultat final :

Pourquoi 2D ? Les jeux 3D sont beaucoup plus complexes que les jeux 2D. Vous devriez vous en tenir à la 2D jusqu'à ce que vous ayez une bonne compréhension du processus de développement d'un jeu et de la façon d'utiliser Godot.
Configuration du projet¶
Lancez Godot et créez un nouveau projet. Ensuite, téléchargez dodge_assets.zip
. Il contient les images et les sons que vous utiliserez pour créer le jeu. Dézippez ces fichiers dans le dossier de votre projet.
Note
Pour ce tutoriel, nous supposerons que vous êtes familier avec l'éditeur de Godot. Si vous n'avez pas lu Des scènes et des nœuds, faites-le maintenant pour une explication sur la mise en place d'un projet et l'utilisation de l'éditeur.
Ce jeu est conçu pour le mode portrait, nous devons donc ajuster la taille de la fenêtre de jeu. Cliquez sur Projet -> Paramètres du projet -> Display-> Window et réglez "Width" à 480
et "Height" à 720
.
Toujours dans cette section, sous les options "Stretch", réglez Mode
sur 2d
et Aspect
sur "keep". Cela permet de s'assurer que le jeu s'adapte de manière cohérente sur des écrans de différentes tailles.
Organisation du projet¶
Dans ce projet, nous allons créer 3 scènes indépendantes : Player
, Mob
et HUD
, que nous combinerons dans la scène Main
du jeu. Dans un projet de plus grande envergure, il peut être utile de créer des dossiers pour contenir les différentes scènes et leurs scripts, mais pour ce jeu relativement petit, vous pouvez sauvegarder vos scènes et scripts dans le dossier racine, appelé res://
. Vous pouvez voir vos dossiers de projet dans le dock Système de fichiers dans le coin supérieur gauche :

Scène Player¶
La première scène définira l'objet Player
. L'un des avantages de la création d'une scène Player séparée est que nous pouvons la tester séparément, avant même d'avoir créé d'autres parties du jeu.
Structure des nœuds¶
Pour commencer, nous devons choisir un nœud racine pour l'objet joueur. En règle générale, le nœud racine d'une scène doit refléter la fonctionnalité souhaitée de l'objet - ce que l'objet est. Cliquez sur le bouton "Other Node" et ajoutez un nœud Area2D à la scène.

Godot affichera une icône d'avertissement près du nœud dans l'arbre de scène. Vous pouvez l'ignorer pour le moment. Nous y reviendrons plus tard.
Avec Area2D
, nous pouvons détecter les objets qui chevauchent ou heurtent le joueur. Changez le nom du nœud en Player
en double-cliquant sur son nom. Maintenant que nous avons défini le nœud racine de la scène, nous pouvons ajouter des nœuds supplémentaires pour lui donner plus de fonctionnalités.
Avant d'ajouter des enfants au nœud Player
, nous voulons nous assurer de ne pas les déplacer ou les redimensionner accidentellement en cliquant dessus. Sélectionnez le nœud et cliquez sur l'icône à droite du cadenas ; son info-bulle indique "Rendre la sélection des enfants de l'objet impossible."

Sauvegardez la scène. Cliquez sur Scène -> Enregistrer la scène, ou appuyez sur Ctrl + S sous Windows/Linux ou Cmd + S sous macOS.
Note
Pour ce projet, nous suivrons les conventions de nommage de Godot.
- GDScript : Les classes (nœuds) utilisent PascalCase, les variables et les fonctions utilisent snake_case, et les constantes utilisent ALL_CAPS (Voir Guide de style GDScript).
- C# : Les classes, les variables d'exportation et les méthodes utilisent PascalCase, les attributs privés utilisent _camelCase, les variables locales et les paramètres utilisent camelCase (Voir Guide de style C#). Attention à taper les noms des méthodes précisément lorsque vous connectez des signaux.
Animation du sprite¶
Cliquez sur le nœud Player
et ajoutez un nœud AnimatedSprite comme nœud enfant. AnimatedSprite
s'occupera de l'apparence et des animations pour notre joueur. Notez qu'il y a un symbole d'avertissement à côté du nœud. Un AnimatedSprite
nécessite une ressource SpriteFrames, qui contient la liste des animations qu'il peut afficher. Pour en créer une, trouvez la propriété Frames
dans l'Inspecteur et cliquez sur "[vide]" -> "Nouveau SpriteFrames". Cliquez à nouveau pour ouvrir le panneau "SpriteFrames" :

A gauche, vous trouverez une liste d'animations. Cliquez sur "default" et renommez-la en "walk". Cliquez ensuite sur le bouton "New Animation" pour créer une deuxième animation nommée "up". Trouvez les images du joueur dans l'onglet "Système de fichiers" - elles se trouvent dans le dossier art
que vous avez décompressé plus tôt. Faites glisser les deux images pour chaque animation, nommées playerGrey_up[1/2]
et playerGrey_walk[1/2]
, dans le côté "Animation Frames" du panneau pour l'animation correspondante :

Les images du joueur sont un peu trop grandes pour la fenêtre de jeu, nous devons donc les réduire. Cliquez sur le nœud AnimatedSprite
et réglez la propriété Scale
sur (0.5, 0.5)
. Vous pouvez la trouver dans l'Inspecteur sous la catégorie Node2D
.

Enfin, ajoutez un CollisionShape2D en tant qu'enfant de Player
. Ceci déterminera la "hitbox" du joueur, soit les limites de sa zone de collision. Pour ce personnage, un nœud CapsuleShape2D
donne le meilleur ajustement, donc à côté de "Shape" dans l'Inspecteur, cliquez sur "[vide]" -> "Nouveau CapsuleShape2D". En utilisant les poignées, redimensionnez la forme pour couvrir le sprite :

Lorsque vous avez terminé, votre scène Player
devrait ressembler à ceci :

Veillez à sauvegarder à nouveau la scène après ces changements.
Déplacer le joueur¶
Maintenant, nous devons ajouter des fonctionnalités que nous ne pouvons pas obtenir à partir d'un nœud intégré, nous allons donc ajouter un script. Cliquez sur le nœud Player
et cliquez sur le bouton "Attacher un script" :

Dans la fenêtre de réglages du script, vous pouvez laisser les paramètres par défaut. Cliquez simplement sur "Créer" :
Note
Si vous créez un script en C# ou un autre langage, sélectionnez le language dans le menu déroulant langage avant de cliquer sur créer.

Note
Si c'est la première fois que vous rencontrez GDScript, veuillez lire Les scripts avant de continuer.
Commencez par déclarer les variables membres dont cet objet aura besoin :
extends Area2D
export var speed = 400 # How fast the player will move (pixels/sec).
var screen_size # Size of the game window.
public class Player : Area2D
{
[Export]
public int Speed = 400; // How fast the player will move (pixels/sec).
private Vector2 _screenSize; // Size of the game window.
}
L'utilisation du mot-clé export
sur la première variable SPEED
nous permet de définir sa valeur dans l'inspecteur. Cela peut être pratique pour les valeurs que vous voulez pouvoir ajuster de la même manière que les propriétés intégrées d'un nœud. Cliquez sur le nœud Player
et vous verrez maintenant apparaître la section "Script Variables" dans l'inspecteur. Notez que, si vous changez la valeur ici, cela remplacera la valeur écrite dans le script.
Avertissement
Si vous utilisez C#, vous devez (re)compiler les assemblages du projet chaque fois que vous voulez voir de nouvelles variables d'exportation ou des nouveaux signaux. Cette compilation peut être déclenchée manuellement en cliquant sur le mot "Mono" au bas de la fenêtre de l'éditeur pour afficher le panneau Mono, puis en cliquant sur le bouton "Compiler Projet".

La fonction _ready()
est appelée lorsqu'un nœud entre dans l'arbre de scène, ce qui est un bon moment pour trouver la taille de la fenêtre de jeu :
func _ready():
screen_size = get_viewport_rect().size
public override void _Ready()
{
_screenSize = GetViewport().Size;
}
Maintenant nous pouvons utiliser la fonction _process()
pour définir ce que le joueur va faire. _process()
est appelée à chaque image, nous l'utiliserons donc pour mettre à jour les éléments de notre jeu qui vont changer souvent. Pour le joueur, nous devons faire ce qui suit :
- Vérifier les entrées.
- Se déplacer dans la direction donnée.
- Jouer l'animation appropriée.
Tout d'abord, nous devons vérifier les entrées - le joueur appuie-t-il sur une touche ? Pour ce jeu, nous avons 4 entrées de direction à vérifier. Les actions d'entrées sont définies dans les Paramètres du projet sous "Contrôles". Vous pouvez définir des événements personnalisés et leur affecter des touches, des événements souris ou d'autres entrées. Pour cette démo, nous utiliserons les événements par défaut qui sont assignés aux touches fléchées du clavier.
Vous pouvez détecter si une touche est pressée en utilisant Input.is_action_pressed()
, qui retourne true
s'il elle est pressée ou false
si elle ne l'est pas.
func _process(delta):
var velocity = Vector2() # The player's movement vector.
if Input.is_action_pressed("ui_right"):
velocity.x += 1
if Input.is_action_pressed("ui_left"):
velocity.x -= 1
if Input.is_action_pressed("ui_down"):
velocity.y += 1
if Input.is_action_pressed("ui_up"):
velocity.y -= 1
if velocity.length() > 0:
velocity = velocity.normalized() * speed
$AnimatedSprite.play()
else:
$AnimatedSprite.stop()
public override void _Process(float delta)
{
var velocity = new Vector2(); // The player's movement vector.
if (Input.IsActionPressed("ui_right"))
{
velocity.x += 1;
}
if (Input.IsActionPressed("ui_left"))
{
velocity.x -= 1;
}
if (Input.IsActionPressed("ui_down"))
{
velocity.y += 1;
}
if (Input.IsActionPressed("ui_up"))
{
velocity.y -= 1;
}
var animatedSprite = GetNode<AnimatedSprite>("AnimatedSprite");
if (velocity.Length() > 0)
{
velocity = velocity.Normalized() * Speed;
animatedSprite.Play();
}
else
{
animatedSprite.Stop();
}
}
Nous commençons par régler velocity
à (0, 0)
- par défaut le joueur ne doit pas bouger. Ensuite nous vérifions chaque entrées et les ajoutons ou les soustrayons à la velocity
pour obtenir une direction totale. Par exemple, si vous maintenez right
et down
en même temps, le vecteur velocity
résultant sera (1, 1)
. Dans ce cas, puisque nous ajoutons un mouvement horizontal et un mouvement vertical, le joueur se déplacerait plus vite que s'il se déplaçait horizontalement.
Nous pouvons empêcher cela si nous normalisons la vitesse, ce qui signifie que nous réglons sa longueur à 1
, et la multiplions par la vitesse désirée. Cela signifie qu'il n'y a plus de mouvement diagonal rapide.
Astuce
Si vous n'avez jamais utilisé les mathématiques vectorielles auparavant, ou si vous avez besoin d'un rafraîchissement, vous pouvez voir une explication de l'utilisation des vecteurs dans Godot à Mathématiques vectorielles. C'est bon à savoir mais ça ne sera pas nécessaire pour le reste de ce tutoriel.
Nous vérifions également si le joueur se déplace afin de pouvoir appeler play()
ou stop()
sur l'AnimatedSprite.
$
est un raccourci pourget_node()
. Dans le code ci-dessus,$AnimatedSprite.play()
est donc identique àget_node("AnimatedSprite").play()
.
Astuce
En GDScript, $
retourne le nœud au chemin relatif depuis ce nœud, ou retourne null
si le nœud n'est pas trouvé. Puisque AnimatedSprite est un enfant du nœud courant, nous pouvons utiliser $AnimatedSprite
.
Maintenant que nous avons une direction de mouvement, nous pouvons mettre à jour la position du joueur. Nous pouvons aussi utiliser clamp()
pour l'empêcher de quitter l'écran. Clamping une valeur signifie la limiter à une plage donnée. Ajoutez ce qui suit au bas de la fonction _process
(assurez-vous que ce n'est pas indenté sous le else) :
position += velocity * delta
position.x = clamp(position.x, 0, screen_size.x)
position.y = clamp(position.y, 0, screen_size.y)
Position += velocity * delta;
Position = new Vector2(
x: Mathf.Clamp(Position.x, 0, _screenSize.x),
y: Mathf.Clamp(Position.y, 0, _screenSize.y)
);
Astuce
Le paramètre delta de la fonction _process() fait référence à la longueur de l'image - le temps qu'à mis l'image précédente pour se terminer. Utiliser cette valeur assure que le mouvement restera constant même si le taux d'images par seconde varie.
Cliquez sur "Lancer la scène" (F6
) et vérifiez que vous pouvez déplacer le joueur autour de l'écran dans toutes les directions.
Avertissement
Si vous obtenez une erreur le panneau "Debugger" qui dit
Attempt to call function 'play' in base 'null instance' on a null instance
cela signifie probablement que vous avez mal orthographié le nom du nœud AnimatedSprite. Les noms de nœuds sont sensibles à la casse et $NodeName
doit correspondre au nom que vous voyez dans l'arbre de scènes.
Choisir les animations¶
Maintenant que le joueur peut se déplacer, nous devons changer l'animation que l'AnimatedSprite joue en fonction de la direction. Nous avons une animation "walk", qui montre le joueur marchant vers la droite. Cette animation doit être retournée horizontalement en utilisant la propriété flip_h
pour le mouvement vers la gauche, et une animation "up", qui doit être retournée verticalement avec flip_v
pour le mouvement vers le bas. Rajoutez ce code à la fin de notre fonction _process()
:
if velocity.x != 0:
$AnimatedSprite.animation = "walk"
$AnimatedSprite.flip_v = false
# See the note below about boolean assignment
$AnimatedSprite.flip_h = velocity.x < 0
elif velocity.y != 0:
$AnimatedSprite.animation = "up"
$AnimatedSprite.flip_v = velocity.y > 0
if (velocity.x != 0)
{
animatedSprite.Animation = "walk";
animatedSprite.FlipV = false;
// See the note below about boolean assignment
animatedSprite.FlipH = velocity.x < 0;
}
else if (velocity.y != 0)
{
animatedSprite.Animation = "up";
animatedSprite.FlipV = velocity.y > 0;
}
Note
Les affectations booléennes dans le code ci-dessus sont un raccourci courant pour les programmeurs. Puisque nous faisons un test de comparaison (booléen) et aussi assignons une valeur booléenne, nous pouvons faire les deux en même temps. Considérez ce code par rapport à l'affectation booléenne d'une ligne ci-dessus :
if velocity.x < 0:
$AnimatedSprite.flip_h = true
else:
$AnimatedSprite.flip_h = false
if (velocity.x < 0)
{
animatedSprite.FlipH = true;
}
else
{
animatedSprite.FlipH = false;
}
Relancez la scène et vérifiez que les animations sont correctes dans chacune des directions.
Astuce
Une erreur courante consiste ici à mal taper les noms des animations. Les noms des animations dans le panneau SpriteFrames doivent correspondre à ce que vous tapez dans le code. Si vous avez nommé l'animation "Walk"
, vous devez également utiliser un "W" majuscule dans le code.
Lorsque vous êtes sûr que le mouvement fonctionne correctement, ajoutez cette ligne à _ready()
, afin que le joueur soit caché au début du jeu :
hide()
Hide();
Préparation pour les collisions¶
Nous voulons que Player
détecte quand il est touché par un ennemi, mais nous ne n’avons pas encore créé d'ennemis ! Ce n'est pas grave, car nous allons utiliser la fonctionnalité signal de Godot pour le faire fonctionner.
Ajoutez ce qui suit en haut du script, après extends Area2D
:
signal hit
// Don't forget to rebuild the project so the editor knows about the new signal.
[Signal]
public delegate void Hit();
Ceci définit un signal personnalisé appelé "hit" que notre joueur émettra lorsqu'il entre en collision avec un ennemi. Nous utiliserons Area2D
pour détecter la collision. Sélectionnez le nœud Player
et cliquez sur l'onglet "Nœud" à côté de l'onglet Inspecteur pour voir la liste des signaux que le joueur peut émettre :

Notez que notre signal personnalisé "hit" est là aussi ! Puisque nos ennemis vont être des nœuds RigidBody2D
, nous avons besoin du signal body_entered(body: Node)
; celui-ci sera émis lorsqu'un objet percute le joueur. Cliquez sur "Connecter..." et la fenêtre "Connecter un signal" apparaîtra. Nous n'avons pas besoin de modifier ces paramètres, alors cliquez à nouveau sur "Connect". Godot va automatiquement créer une fonction dans le script de votre joueur.

Notez l'icône verte indiquant qu'un signal est connecté à cette fonction. Ajoutez ce code à la fonction :
func _on_Player_body_entered(body):
hide() # Player disappears after being hit.
emit_signal("hit")
$CollisionShape2D.set_deferred("disabled", true)
public void OnPlayerBodyEntered(PhysicsBody2D body)
{
Hide(); // Player disappears after being hit.
EmitSignal("Hit");
GetNode<CollisionShape2D>("CollisionShape2D").SetDeferred("disabled", true);
}
Chaque fois qu'un ennemi frappe le joueur, le signal sera émis. Nous devons désactiver la collision du joueur afin de ne pas déclencher le signal hit
plus d'une fois.
Note
Désactiver la forme de la zone de collision peut provoquer une erreur si cela se produit pendant le traitement des collisions par le moteur. L'utilisation de set_deferred()
indique à Godot d'attendre pour désactiver la forme jusqu'à ce que l'on puisse le faire en toute sécurité.
La dernière étape consiste à ajouter une fonction que nous pouvons appeler pour réinitialiser le joueur au début d'une nouvelle partie.
func start(pos):
position = pos
show()
$CollisionShape2D.disabled = false
public void Start(Vector2 pos)
{
Position = pos;
Show();
GetNode<CollisionShape2D>("CollisionShape2D").Disabled = false;
}
Scène de l'ennemi¶
Il est temps maintenant de créer des ennemis que notre joueur devra esquiver. Leur comportement ne sera pas très complexe : des monstres vont apparaître au hasard aux bords de l'écran et se déplacer dans une direction aléatoire en ligne droite, puis disparaître lorsqu'ils sortent de l'écran.
Nous allons créer une scène Mob
, que nous pouvons ensuite instancier pour créer un nombre quelconque de monstres indépendants dans le jeu.
Note
Voyez Instanciation pour en apprendre davantage à propos de l'instanciation.
Configuration du nœud¶
Cliquez sur Scène -> Nouvelle scène et ajoutez les nœuds suivants :
N'oubliez pas de configurer les enfants pour qu'ils ne puissent pas être sélectionnés, comme vous l'avez fait avec la scène Player.
Dans les propriétés du RigidBody2D, réglez Gravity Scale
sur 0
, afin que le monstre ne tombe pas vers le bas. De plus, sous la section PhysicsBody2D
, cliquez sur la propriété Mask
et décochez la première case. Cela permettra de s'assurer que les monstres n'entrent pas en collision les uns avec les autres.

Configurez l'AnimatedSprite comme vous l'avez fait pour le joueur. Cette fois, nous avons 3 animations : fly
, swim
, et walk
. Il y a deux images pour chaque animation dans le dossier art.
Ajustez la vitesse "Speed (FPS)" à 3
pour toutes les animations.

Mettez la propriété Playing
dans l'inspecteur sur “On”.
Nous allons choisir une de ces animations au hasard pour que les monstres aient une certaine variété.
Comme les images du joueur, ces images de monstre doivent être réduites. Réglez la propriété Scale
d' AnimatedSprite
sur (0.75, 0.75)
.
Comme dans la scène Player
, ajouter un CapsuleShape2D
pour la collision. Pour aligner la forme avec l'image, vous aurez besoin de paramétrer la propriété Rotation Degrees
à 90
(sous "Transform" dans l'inspecteur).
Enregistrer la scène.
Script de l'ennemi¶
Ajouter un script au Mob
et ajouter les variables membres suivantes :
extends RigidBody2D
export var min_speed = 150 # Minimum speed range.
export var max_speed = 250 # Maximum speed range.
public class Mob : RigidBody2D
{
// Don't forget to rebuild the project so the editor knows about the new export variables.
[Export]
public int MinSpeed = 150; // Minimum speed range.
[Export]
public int MaxSpeed = 250; // Maximum speed range.
}
Quand nous faisons apparaître un monstre, Nous allons prendre une valeur aléatoire entre min_speed
et max_speed
pour la vitesse à laquelle chaque monstre se déplacera (ce serait ennuyant s'ils bougeaient tous à la même vitesse).
Maintenant, regardons le reste du script. Dans _ready()
, nous choisissons aléatoirement l'un des trois types d'animations :
func _ready():
var mob_types = $AnimatedSprite.frames.get_animation_names()
$AnimatedSprite.animation = mob_types[randi() % mob_types.size()]
// C# doesn't implement GDScript's random methods, so we use 'System.Random' as an alternative.
static private Random _random = new Random();
public override void _Ready()
{
var animSprite = GetNode<AnimatedSprite>("AnimatedSprite");
var mobTypes = animSprite.Frames.GetAnimationNames();
animSprite.Animation = mobTypes[_random.Next(0, mobTypes.Length)];
}
Tout d'abord, nous obtenons la liste des noms d'animation à partir de la propriété frames
de l'AnimatedSprite. On obtient ainsi un tableau contenant les trois noms d'animation : ["walk", "swim", "fly"]
pour "marche", "nage", et "vole".
Nous devons ensuite choisir un nombre aléatoire entre 0
et 2
pour sélectionner l'un de ces noms dans la liste (les indices des listes commencent à 0
). La fonction randi() % n
sélectionne un nombre entier aléatoire entre 0
et n-1
.
Note
Vous devez utiliser randomize()
si vous voulez que votre séquence de nombres "aléatoires" soit différente à chaque fois que vous exécutez la scène. Nous allons utiliser randomize()
dans notre scène Main
, alors nous n'en aurons pas besoin ici.
La dernière étape est de faire en sorte que les monstres se suppriment eux-mêmes lorsqu'ils quittent l'écran. Connecter le signal screen_exited()
du nœud VisibilityNotifier2D
et ajouter ce code :
func _on_VisibilityNotifier2D_screen_exited():
queue_free()
public void OnVisibilityNotifier2DScreenExited()
{
QueueFree();
}
Cela complète la scène Mob.
Scène principale¶
Maintenant, il est temps de tout rassembler ensemble. Créer une nouvelle scène et ajouter un Node nommé Main
. Assurez-vous de créer un Node, pas un Node2D. Cliquez sur le bouton "Instance" et sélectionnez le fichier Player.tscn
que vous avez sauvegardé.

Ajoutez maintenant les nœuds suivants en tant qu'enfants de Main
, et nommez-les comme indiqué (les valeurs sont en secondes) :
- Timer (nommé
MobTimer
) - pour contrôler à quelle fréquence les ennemis apparaissent - Timer (nommé
ScoreTimer
) - pour incrémenter le score à chaque seconde - Timer (nommé
StartTimer
) - pour ajouter un délai avant le début - Position2D (named
StartPosition
) - pour indiquer la position de départ du joueur
Réglez la propriété Wait Time
de chacun des nœuds Timer
comme suit :
MobTimer
:0.5
ScoreTimer
:1
StartTimer
:2
En outre, mettez la propriété One Shot
de StartTimer
sur "On" et réglez la Position
du nœud StartPosition
sur (240, 450)
.
Générer des monstres¶
Le nœud principal va générer de nouveaux monstres, et nous voulons qu'ils apparaissent à un endroit aléatoire sur le bord de l'écran. Ajouter un nœud Path2D nommé MobPath
comme un enfant de Main
. Lorsque vous sélectionnez Path2D
, vous verrez de nouveaux boutons en haut de l'éditeur :

Sélectionnez celui du milieu ("Ajouter un point") et tracez le chemin en cliquant pour ajouter les points aux coins montrés. Pour que les points s'accrochent à la grille, assurez-vous que "Utiliser l’aimantation à la grille" et "Utiliser l’aimantation intelligente" sont sélectionnés. Cette option se trouve à gauche du bouton "Verrouiller", apparaissant comme un aimant à côté de lignes qui se croisent.

Important
Tracez le chemin dans le sens des aiguilles d'une montre, ou vos monstres pointeront vers l'extérieur au lieu de vers l'intérieur !

Après avoir placé le point 4
dans l'image, cliquez sur le bouton "Fermer la courbe" et votre courbe sera terminée.
Maintenant que le chemin est défini, ajoutez un nœud PathFollow2D en tant qu'enfant de MobPath
et nommez-le MobSpawnLocation
. Ce nœud tournera automatiquement et suivra le chemin au fur et à mesure qu'il se déplace, de sorte que nous pouvons l'utiliser pour sélectionner une position et une direction aléatoires le long du chemin.
Votre scène devrait ressembler à ceci :

Script principal¶
Ajoutez un script à Main
. Au début du script nous utilisons export (PackedScene)
pour nous permettre de choisir la scène du monstre que nous voulons instancier.
extends Node
export (PackedScene) var Mob
var score
func _ready():
randomize()
public class Main : Node
{
// Don't forget to rebuild the project so the editor knows about the new export variable.
[Export]
public PackedScene Mob;
private int _score;
// We use 'System.Random' as an alternative to GDScript's random methods.
private Random _random = new Random();
public override void _Ready()
{
}
// We'll use this later because C# doesn't support GDScript's randi().
private float RandRange(float min, float max)
{
return (float)_random.NextDouble() * (max - min) + min;
}
}
Cliquez sur le nœud Main
et vous verrez la propriété Mob
dans l'inspecteur sous "Script Variables".
Vous pouvez affecter la valeur de cette propriété de deux façons :
- Faites glisser
Mob.tscn
depuis le panneau "Système de fichiers" et déposez-le sur la propriétéMob
. - Cliquez sur la flèche vers le bas à côté de "[empty]" et choisissez "Load". Sélectionnez
Mob.tscn
.
Ensuite, sélectionnez le nœud Player
dans le dock Scène, et accédez au dock Nœud dans la barre latérale. Assurez-vous que l'onglet Signaux est sélectionné dans le dock Nœud.
Vous devriez voir une liste des signaux pour le nœud Player
. Trouvez et double-cliquez sur le signal hit
dans la liste (ou faites un clic droit et sélectionnez "Connect..."). Cela ouvrira le dialogue de connexion des signaux. Nous voulons créer une nouvelle fonction appelée game_over
, qui gérera ce qui doit se passer quand une partie se termine. Tapez "game_over" dans la case "Receiver Method" en bas du dialogue de connexion des signaux et cliquez sur "Connect". Ajoutez le code suivant à la nouvelle fonction, ainsi qu'une fonction new_game
qui configurera tout pour une nouvelle partie :
func game_over():
$ScoreTimer.stop()
$MobTimer.stop()
func new_game():
score = 0
$Player.start($StartPosition.position)
$StartTimer.start()
public void GameOver()
{
GetNode<Timer>("MobTimer").Stop();
GetNode<Timer>("ScoreTimer").Stop();
}
public void NewGame()
{
_score = 0;
var player = GetNode<Player>("Player");
var startPosition = GetNode<Position2D>("StartPosition");
player.Start(startPosition.Position);
GetNode<Timer>("StartTimer").Start();
}
Maintenant, connectez le signal timeout()
de chacun des nœuds Timer (StartTimer
, ScoreTimer
et MobTimer
) au script principal. StartTimer
démarrera les deux autres timers. ScoreTimer
incrémentera le score de 1.
func _on_StartTimer_timeout():
$MobTimer.start()
$ScoreTimer.start()
func _on_ScoreTimer_timeout():
score += 1
public void OnStartTimerTimeout()
{
GetNode<Timer>("MobTimer").Start();
GetNode<Timer>("ScoreTimer").Start();
}
public void OnScoreTimerTimeout()
{
_score++;
}
Dans _on_MobTimer_timeout()
nous allons créer une instance de monstre, choisir un emplacement de départ aléatoire le long du Path2D
, et mettre le monstre en mouvement. Le nœud PathFollow2D
tournera automatiquement puisqu'il suit le chemin, donc nous l'utiliserons pour sélectionner la direction du monstre ainsi que sa position.
Notez qu'une nouvelle instance doit être ajoutée à la scène en utilisant add_child()
.
func _on_MobTimer_timeout():
# Choose a random location on Path2D.
$MobPath/MobSpawnLocation.offset = randi()
# Create a Mob instance and add it to the scene.
var mob = Mob.instance()
add_child(mob)
# Set the mob's direction perpendicular to the path direction.
var direction = $MobPath/MobSpawnLocation.rotation + PI / 2
# Set the mob's position to a random location.
mob.position = $MobPath/MobSpawnLocation.position
# Add some randomness to the direction.
direction += rand_range(-PI / 4, PI / 4)
mob.rotation = direction
# Set the velocity (speed & direction).
mob.linear_velocity = Vector2(rand_range(mob.min_speed, mob.max_speed), 0)
mob.linear_velocity = mob.linear_velocity.rotated(direction)
public void OnMobTimerTimeout()
{
// Choose a random location on Path2D.
var mobSpawnLocation = GetNode<PathFollow2D>("MobPath/MobSpawnLocation");
mobSpawnLocation.Offset = _random.Next();
// Create a Mob instance and add it to the scene.
var mobInstance = (RigidBody2D)Mob.Instance();
AddChild(mobInstance);
// Set the mob's direction perpendicular to the path direction.
float direction = mobSpawnLocation.Rotation + Mathf.Pi / 2;
// Set the mob's position to a random location.
mobInstance.Position = mobSpawnLocation.Position;
// Add some randomness to the direction.
direction += RandRange(-Mathf.Pi / 4, Mathf.Pi / 4);
mobInstance.Rotation = direction;
// Choose the velocity.
mobInstance.LinearVelocity = new Vector2(RandRange(150f, 250f), 0).Rotated(direction);
}
Important
Pourquoi PI
? Dans les fonctions nécessitant des angles, GDScript utilise des radians et non des degrés. Si vous êtes plus à l'aise avec les degrés, vous devrez utiliser les fonctions deg2rad()
et rad2deg()
pour convertir les angles entre les deux.
Tester la scène¶
Maintenant, testons la scène pour s'assurer que tout fonctionne. Ajoutez cela à _ready()
:
func _ready():
randomize()
new_game()
public override void _Ready()
{
NewGame();
}
}
Assignons également Main
comme "Main Scene" - celle qui s'exécute automatiquement au lancement du jeu. Appuyez sur le bouton "Play" et sélectionnez Main.tscn
lorsque vous y êtes invité.
Vous devriez être capable de bouger le joueur, voir les monstres apparaître, et voir le joueur disparaître quand il est touché par un monstre.
Quand vous êtes sûr que tout fonctionne, supprimez l'appel à new_game()
depuis _ready()
.
HUD¶
La dernière partie dont notre jeu a besoin est une interface utilisateur : une interface pour afficher des choses comme le score, un message "game over" et un bouton pour recommencer. Créez une nouvelle scène, et ajoutez un nœud CanvasLayer nommé HUD
. "HUD" signifie "heads-up display", un écran d'information qui est affiché superposé à la vue du jeu.
Le nœud CanvasLayer nous permet de dessiner nos éléments de l'interface utilisateur sur un calque au-dessus du reste du jeu, de sorte que les informations qu'il affiche ne sont couvertes par aucun élément du jeu comme le joueur ou les monstres.
Le HUD doit afficher les informations suivantes :
- Le score, modifié par
ScoreTimer
. - Un message, tel que "Game Over" ou "Get Ready"
- Un bouton "Démarrer" pour commencer le jeu.
Le nœud de base pour les éléments de l'interface utilisateur est Control. Pour créer notre interface utilisateur, nous utiliserons deux types de nœuds Control : Label et Button.
Créez les éléments suivants en tant qu'enfants du nœud HUD
:
- Un Label nommé
ScoreLabel
. - Un Label nommé
Message
. - Un Button nommé
StartButton
. - Un Timer nommé
MessageTimer
.
Cliquez sur le ScoreLabel
et dans l'inspecteur, entrez un nombre dans le champ Text
. La police par défaut pour les nœuds Control
est petite et ne s'ajuste pas correctement. Il existe un fichier de police inclus dans les ressources du jeu appelé "Xolonium-Regular.ttf". Pour utiliser cette police, procédez comme suit :
- Sous "Polices personnalisées", choisissez "Nouveau DynamicFont"

- Cliquez sur le "DynamicFont" que vous avez ajouté, et sous "Font/Font Data", choisissez "Charger" et sélectionnez le fichier "Xolonium-Regular.ttf". Vous devez également définir
Size
la taille de la police de caractères. Un réglage à64
fonctionne bien.

Une fois que vous avez fait cela sur le ScoreLabel
, vous pouvez cliquer sur la flèche vers le bas à côté de la propriété DynamicFont et choisir "Copier", puis "Coller" au même endroit sur les deux autres nœuds Control.
Note
Ancres et marges : Les nœuds Control
ont une position et une taille, mais ils ont aussi des ancres et des marges. Les ancres définissent l'origine - le point de référence pour les bords du nœud. Les marges se mettent à jour automatiquement lorsque vous déplacez ou redimensionnez un nœud control. Ils représentent la distance entre les bords du nœud control et son ancrage. Voir Conception d'interfaces avec les nœuds Control pour plus de détails.
Disposez les nœuds comme indiqué ci-dessous. Cliquez sur le bouton "Layout" pour définir la disposition d'un nœud de contrôle :

Vous pouvez faire glisser les nœuds pour les placer manuellement ou, pour un placement plus précis, utiliser les paramètres suivants :
ScoreLabel¶
- Layout / Disposition sur l'écran : "Top Wide"
- Text :
0
- Align : "Center"
Message¶
- Layout / Disposition sur l'écran : "HCenter Wide"
- Text :
Dodge the Creeps!
- Align : "Center"
- Autowrap : "On"
StartButton¶
- Text :
Start
- Layout / Disposition sur l'écran : "Center Bottom"
- Margin :
- Top :
-200
- Bottom :
-100
- Top :
Sur le MessageTimer
, mettez le Wait Time
à 2
et mettez la propriété One Shot
à "On".
Ajoutez à présent ce script au HUD
:
extends CanvasLayer
signal start_game
public class HUD : CanvasLayer
{
// Don't forget to rebuild the project so the editor knows about the new signal.
[Signal]
public delegate void StartGame();
}
Le signal start_game
indique au nœud Main
que le bouton a été pressé.
func show_message(text):
$Message.text = text
$Message.show()
$MessageTimer.start()
public void ShowMessage(string text)
{
var message = GetNode<Label>("Message");
message.Text = text;
message.Show();
GetNode<Timer>("MessageTimer").Start();
}
Cette fonction est appelée lorsque nous voulons afficher temporairement un message, tel que "Get Ready".
func show_game_over():
show_message("Game Over")
# Wait until the MessageTimer has counted down.
yield($MessageTimer, "timeout")
$Message.text = "Dodge the\nCreeps!"
$Message.show()
# Make a one-shot timer and wait for it to finish.
yield(get_tree().create_timer(1), "timeout")
$StartButton.show()
async public void ShowGameOver()
{
ShowMessage("Game Over");
var messageTimer = GetNode<Timer>("MessageTimer");
await ToSignal(messageTimer, "timeout");
var message = GetNode<Label>("Message");
message.Text = "Dodge the\nCreeps!";
message.Show();
await ToSignal(GetTree().CreateTimer(1), "timeout");
GetNode<Button>("StartButton").Show();
}
Cette fonction est appelée lorsque le joueur perd. Elle affichera "Game Over" pendant 2 secondes, puis reviendra à l'écran de titre et affichera le bouton "Start".
Note
Quand vous avez besoin d'une courte pause, une alternative à l'utilisation d'un nœud Timer est d'utiliser la fonction create_timer()
de l'arborescence de scène. Ceci peut être très utile pour créer un délais, comme dans le code ci-dessus, où nous voulons attendre un peu avant de faire apparaître le bouton "Start".
func update_score(score):
$ScoreLabel.text = str(score)
public void UpdateScore(int score)
{
GetNode<Label>("ScoreLabel").Text = score.ToString();
}
Cette fonction est appelée dans Main
chaque fois que le score change.
Connectez le signal timeout()
de MessageTimer
et le signal pressed()
de StartButton
et ajoutez le code suivant aux nouvelles fonctions :
func _on_StartButton_pressed():
$StartButton.hide()
emit_signal("start_game")
func _on_MessageTimer_timeout():
$Message.hide()
public void OnStartButtonPressed()
{
GetNode<Button>("StartButton").Hide();
EmitSignal("StartGame");
}
public void OnMessageTimerTimeout()
{
GetNode<Label>("Message").Hide();
}
Connecter le HUD à Main¶
Maintenant que nous avons fini de créer la scène du HUD
, sauvegardez-la et retournez à Main
. Instanciez la scène HUD
dans Main
comme vous l'avez fait dans la scène Player
. L'arbre de scène devrait ressembler à ça, alors assurez-vous de ne rien manquer :

Nous devons maintenant connecter la fonctionnalité HUD
à notre script Main
. Cela nécessite quelques ajouts à la scène Main
:
Dans l'onglet Nœud, connectez le signal start_game
du HUD à la fonction new_game()
du nœud Main en tapant "new_game" dans la "Receiver Method" de la fenêtre "Connect a Signal". Vérifiez que l'icône de connexion verte apparaît maintenant à côté de func new_game()
dans le script.
Dans new_game()
, mettez à jour l'affichage des scores et affichez le message "Get Ready" :
$HUD.update_score(score)
$HUD.show_message("Get Ready")
var hud = GetNode<HUD>("HUD");
hud.UpdateScore(_score);
hud.ShowMessage("Get Ready!");
Dans game_over()
nous devons appeler la fonction HUD
correspondante :
$HUD.show_game_over()
GetNode<HUD>("HUD").ShowGameOver();
Enfin, ajoutez ceci à on_ScoreTimer_timeout()
pour que l'affichage reste synchronisé avec le changement de score :
$HUD.update_score(score)
GetNode<HUD>("HUD").UpdateScore(_score);
Vous êtes maintenant prêt à jouer ! Cliquez sur le bouton "Lancer le projet". Il vous sera demandé de sélectionner une scène principale, choisissez alors Main.tscn
.
Supprimer les vieux Creeps¶
Si vous jouez jusqu'au "Game Over" et que vous commencez une nouvelle partie immédiatement, les monstres de la partie précédente sont toujours à l'écran. Il vaudrait mieux qu'ils disparaissent tous au début d'une nouvelle partie. Il nous faut juste un moyen de dire à tous les monstres de se supprimer. Nous pouvons le faire grâce à la fonction "groupe".
Dans la scène Mob
, sélectionnez le nœud racine et cliquez sur l'onglet "Nœud" à côté de l'inspecteur (le même endroit où vous trouvez les signaux du nœud). À côté de "Signaux", cliquez sur "Groupes" et vous pouvez taper un nouveau nom de groupe et cliquer sur "Ajouter".

Désormais, toutes les monstres feront partie du groupe des "mobs" (monstres). Nous pouvons alors ajouter la ligne suivante à la fonction game_over()
dans Main
:
get_tree().call_group("mobs", "queue_free")
GetTree().CallGroup("mobs", "queue_free");
La fonction call_group()
appelle la fonction nommée sur chaque nœud d'un groupe - dans ce cas nous disons à chaque mob de s'effacer eux-même.
Pour terminer¶
Nous avons maintenant terminé toutes les fonctionnalités de notre jeu. Ci-dessous sont quelques étapes restantes pour ajouter un peu plus de "jus" pour améliorer l'expérience de jeu. N'hésitez pas à développer le gameplay avec vos propres idées.
Arrière-plan¶
Le fond gris par défaut n'est pas très attrayant, changeons donc sa couleur. Une façon de le faire est d'utiliser un nœud ColorRect. Faites-en le premier nœud sous Main
de sorte qu'il soit dessiné derrière les autres nœuds. ColorRect
n'a qu'une seule propriété : Color
. Choisissez une couleur que vous aimez et sélectionnez "Layout" -> "Full Rect" pour qu'elle couvre l'écran.
Vous pouvez également ajouter une image de fond, si vous en avez une, en utilisant un nœud TextureRect
à la place.
Effets sonores¶
Le son et la musique peuvent être le moyen le plus efficace d'ajouter de l'attrait à l'expérience de jeu. Dans votre dossier de ressources de jeu, vous avez deux fichiers son : "House In a Forest Loop.ogg" pour la musique de fond, et "gameover.wav" pour quand le joueur perd.
Ajouter deux nœuds AudioStreamPlayer en tant qu'enfants de Main
. Nommez l'un d'eux Music
et l'autre DeathSound
. Sur chacun d'eux, cliquez sur la propriété Stream
, sélectionnez "Charger", et choisissez le fichier audio correspondant.
Pour jouer de la musique, ajouter $Music.play()
dans la fonction new_game()
et $Music.stop()
dans la fonction game_over()
.
Enfin, ajoutez $DeathSound.play()
dans la fonction game_over()
.
Raccourci clavier¶
Puisque le jeu se joue avec les touches du clavier, il serait pratique si nous pouvions également commencer le jeu en appuyant sur une touche du clavier. Une façon d'y parvenir est d'utiliser la propriété "Raccourci" du nœud Button
.
Dans la scène HUD
, sélectionnez le StartButton
et trouvez sa propriété Shortcut dans l'Inspecteur. Sélectionnez "New Shortcut" et cliquez sur l'élément "Shortcut". Une deuxième propriété Shortcut apparaîtra. Sélectionnez "New InputEventAction" et cliquez sur le nouveau "InputEventAction". Enfin, dans la propriété Action, tapez le nom ui_select
. Il s'agit de l'événement d'entrée(input event) par défaut associé à la barre d'espace.

Maintenant, lorsque le bouton de démarrage apparaît, vous pouvez soit cliquer dessus, soit appuyer sur Space pour démarrer le jeu.
Fichiers du projet¶
- Vous pouvez trouver une version complète de ce projet ici :