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

../../_images/dodge_preview.gif

Pourquoi en 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.

Configuration du projet

Lancez Godot et créez un nouveau projet. Ensuite, téléchargez dodge_assets.zip - 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. 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 utilisera 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.

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 :

../../_images/filesystem_dock.png

Scène Player

La première scène que nous allons faire définit 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, cliquez sur le bouton « Ajouter un noeud enfant/Ajouter un nouveau nœud » et ajoutez un nœud Area2D à la scène.

../../_images/add_node.png

Avec Area2D, nous pouvons détecter les objets qui se chevauchent ou se heurtent au joueur. Changez son nom en Player en cliquant sur le nom du nœud. C’est le nœud racine de la scène. Nous pouvons ajouter des nœuds supplémentaires au joueur pour ajouter des 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 de la serrure ; son info-bulle indique « Rendre la sélection des enfants de l’objet impossible. »

../../_images/lock_children.png

Sauvegardez la scène. Cliquez sur Scène -> Enregistrer la scène, ou appuyez sur Ctrl+S sur Windows/Linux ou Command+S sur Mac.

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 est une liste des animations qu’il peut afficher. Pour en créer un, trouvez la propriété Frames dans l’Inspecteur et cliquez sur « [vide] » -> « Nouveau SpriteFrames ». Ensuite, au même endroit, cliquez sur <SpriteFrames>, ensuite cliquez sur « Ouvrir l’éditeur » pour ouvrir le panneau « SpriteFrames » :

../../_images/spriteframes_panel.png

Sur la gauche se trouve une liste d’animations. Cliquez sur « default » et renommez-le en « right ». Cliquez ensuite sur le bouton « Ajouter » pour créer une deuxième animation nommée « up ». Faites glisser les deux images de chaque animation, nommées playerGrey_up[1/2] et playerGrey_walk[1/2], dans le côté « Trames d’animation » du panneau :

../../_images/spriteframes_panel2.png

Les images du joueur sont un peu trop grandes pour la fenêtre de jeu, nous devons donc les réduire. Cliquez sur le noeud 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.

../../_images/player_scale.png

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 :

../../_images/player_coll_shape.png

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

../../_images/player_scene_nodes.png

Déplacer le joueur

Maintenant, nous devons ajouter des fonctionnalités que nous ne pouvons pas obtenir à partir d’un nœud fourni, nous allons donc ajouter un script. Cliquez sur le noeud Player et cliquez sur le bouton « Attacher un script » :

../../_images/add_script_button.png

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.

../../_images/attach_node_window.png

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 ».

../../_images/export_variable.png

La fonction _ready() est appelée lorsqu’un noeud 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().GetSize();
}

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 peut pas bouger. Ensuite nous vérifions chaque entrées et les ajoutons ou les soustrayons de 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 à Vector math. 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 démarrer ou arrêter l’animation de l’AnimatedSprite.

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.

$ est un raccourci pour get_node(). Dans le code ci-dessus, $AnimatedSprite.play() est donc identique à get_node("AnimatedSprite").play().

Maintenant que nous avons une direction de mouvement, nous pouvons mettre à jour la position de Player et utiliser clamp() pour l’empêcher de quitter l’écran en ajoutant ce qui suit à la fin de la fonction _process :

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

Clamper une valeur signifie la limiter à un intervalle donné.

Cliquez sur « Lancer la scène » (F6) et confirmez que vous pouvez déplacer le joueur à travers l’écran dans toutes les directions. La sortie de la console qui s’ouvre lors de la lecture de la scène peut être fermée en cliquant sur Sortie (qui devrait être surligné en bleu) dans le coin inférieur gauche du panneau inférieur.

Avertissement

Si vous obtenez une erreur dans le panneau « Débogueur » qui se réfère à une « instance nulle », cela signifie probablement que vous avez mal orthographié le nom du noeud. Les noms de nœuds sont sensibles à la casse et $NomDuNoeud ou get_node("NomDuNoeud") doivent correspondre au nom que vous voyez dans l’arbre des 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 « droite », qui doit être retournée horizontalement en utilisant la propriété flip_h pour le mouvement vers la gauche, et une animation « vers le haut », 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 = "right"
    $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 = "right";
    // See the note below about boolean assignment
    animatedSprite.FlipH = velocity.x < 0;
    animatedSprite.FlipV = false;
}
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. Considérez ce code par rapport à l’affectation booléenne raccourcie 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. 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éparer aux 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 :

../../_images/player_signals.png

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( Object body ); celui-ci sera émis lorsqu’un objet percute le joueur. Cliquez sur « Connecter… » puis sur « Connecter » dans la fenêtre « Connecter un signal ». Nous n’avons pas besoin de changer les paramètres - Godot créera automatiquement une fonction dans le script du joueur. Cette fonction sera appelée à chaque que le signal sera émis - elle traite le signal.

Astuce

Lorsque vous connectez un signal, au lieu de demander à Godot de créer une fonction pour vous, vous pouvez également donner le nom d’une fonction existante à laquelle vous voulez relier le signal.

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 du moteur de collision. L’utilisation de set_deferred() nous permet de faire attendre Godot pour qu’il attende de désactiver la forme jusqu’à ce que ce soit sans danger pour le faire.

La dernière étape pour notre joueur est d’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 construire une scène Mob, que nous pouvons ensuite instancier pour créer un nombre quelconque de monstres indépendants dans le jeu.

Configuration du nœud

Cliquez sur Scène -> Nouvelle scène et créons le monstre.

La scène Mob utilisera 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 de 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.

../../_images/set_collision_mask.png

Configurez l”AnimatedSprite comme vous l’avez fait pour le joueur. Cette fois, nous avons 3 animations : fly, swim, et walk. Réglez la propriété Playing de l’Inspecteur sur « On » et réglez le paramètre « Speed (FPS) » comme indiqué ci-dessous. Nous sélectionnerons une de ces animations au hasard pour que les monstres aient une certaine variété.

../../_images/mob_animations.gif

fly devrait être réglé à 3 FPS, et swim et walk à 4 FPS.

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 Node2D.

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.
var mob_types = ["walk", "swim", "fly"]
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.

    private String[] _mobTypes = {"walk", "swim", "fly"};
}

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). Nous avons également un tableau qui contient les noms des trois animations, que nous utiliserons pour en choisir une aléatoirement. Faites attention de les avoir nommées de la même façon dans le script et dans la ressource SpriteFrames.

Maintenant, regardons le reste du script. Dans _ready(), nous choisissons aléatoirement l’un des trois types d’animations :

func _ready():
    $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()
{
    GetNode<AnimatedSprite>("AnimatedSprite").Animation = _mobTypes[_random.Next(0, _mobTypes.Length)];
}

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. randi() % n est la manière habituelle d’obtenir un entier entre 0 et n-1.

Le dernier morceau 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 Visibility et ajouter ce code :

func _on_Visibility_screen_exited():
    queue_free()
public void OnVisibilityScreenExited()
{
    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. Cliquez sur le bouton « Instance » et sélectionner votre Player.tscn sauvegardé.

../../_images/instance_scene.png

Note

Voyez Instancier pour en apprendre davantage à propos de l’instanciation.

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

../../_images/path2d_buttons.png

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 « Accrocher à la grille » est coché. Cette option se trouve sous le bouton « Options du magnétisme » à gauche du bouton « Verrouiller », apparaissant comme une série de trois points verticaux.

../../_images/draw_path2d.gif

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.

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;
    }
}

Faites glisser Mob.tscn depuis le panneau « Système de fichiers » et déposez-le sur la propriété Mob dans les variables de script du nœud Main.

Ensuite, cliquez sur Player et connectez le signal hit. Nous voulons créer une nouvelle fonction nommée game_over, qui s’occupera de ce qui doit se passer à la fin d’une partie. Tapez « game_over » dans le champ « Méthode dans le nœud » en bas de la fenêtre « Connecter le signal ». Ajoutez le code suivant, ainsi qu’une fonction new_game pour configurer 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().

Maintenant cliquez sur MobTimer dans la vue scène et rendez-vous dans la vue de l’Inspecteur, passez à la vue nœud puis cliquez sur timeout() et connectez le signal.

Ajoutez le code suivant :

func _on_MobTimer_timeout():
    # Choose a random location on Path2D.
    $MobPath/MobSpawnLocation.set_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.SetOffset(_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.SetLinearVelocity(new Vector2(RandRange(150f, 250f), 0).Rotated(direction));
}

Important

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 unités.

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 affiche 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é MessageLabel.
  • Un Button nommé StartButton.
  • Un Timer nommé MessageTimer.

Cliquez sur le ScoreLabel et dans l’inspecteur, tapez un nombre dans _Text_ field . La police par défaut pour les nœuds Control est petite et ne s’agrandit pas correctement. Il y a un fichier de police inclus dans les ressources du jeu appelé « Xolonium-Regular.ttf ». Pour utiliser cette police, procédez comme ce qui suit pour chacun des trois nœuds Control :

  1. Sous « Polices personnalisées », choisissez « Nouveau DynamicFont »
../../_images/custom_font1.png
  1. 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 la taille de la police de caractères. Un réglage à « 64 » fonctionne bien.
../../_images/custom_font2.png

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 de contrôle. Ils représentent la distance entre les bords du nœud de contrôle 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 « Ancrer » pour définir l’ancre d’un nœud Control :

../../_images/ui_anchor.png

Vous pouvez faire glisser les nœuds pour les placer manuellement ou, pour un placement plus précis, utiliser les paramètres suivants :

ScoreLabel

  • Text : 0
  • Layout / Disposition sur l’écran : « Top Wide »
  • Align : « Center »

MessageLabel

  • Text : Dodge the Creeps!
  • Layout / Disposition sur l’écran : « HCenter Wide »
  • Align : « Center »

StartButton

  • Text : Start
  • Layout / Disposition sur l’écran : « Center Bottom »
  • Margin :
    • Top : -200
    • Bottom : -100

Ajoutez à présent ce script à 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):
    $MessageLabel.text = text
    $MessageLabel.show()
    $MessageTimer.start()
public void ShowMessage(string text)
{
    var messageLabel = GetNode<Label>("MessageLabel");
    messageLabel.Text = text;
    messageLabel.Show();

    GetNode<Timer>("MessageTimer").Start();
}

Cette fonction est appelée lorsque nous voulons afficher temporairement un message, tel que « Get Ready ». Sur le MessageTimer, mettez Wait Time sur 2 et la propriété One Shot sur « On ».

func show_game_over():
    show_message("Game Over")
    yield($MessageTimer, "timeout")
    $MessageLabel.text = "Dodge the\nCreeps!"
    $MessageLabel.show()
    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 messageLabel = GetNode<Label>("MessageLabel");
    messageLabel.Text = "Dodge the\nCreeps!";
    messageLabel.Show();

    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() du MessageTimer et le signal pressed() du StartButton.

func _on_StartButton_pressed():
    $StartButton.hide()
    emit_signal("start_game")

func _on_MessageTimer_timeout():
    $MessageLabel.hide()
public void OnStartButtonPressed()
{
    GetNode<Button>("StartButton").Hide();
    EmitSignal("StartGame");
}

public void OnMessageTimerTimeout()
{
    GetNode<Label>("MessageLabel").Hide();
}

Connecter le HUD au Main

Maintenant que nous avons fini de créer la scène du HUD, sauvegardez-la et retournez à Main. Instancez la scène HUD dans Main comme vous l’avez fait dans la scène Player et placez-la au bas de l’arbre. L’arbre complet devrait ressembler à ceci, alors assurez-vous de ne rien manquer :

../../_images/completed_main_scene.png

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().

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.

Removing old creeps

If you play until « Game Over » and then start a new game the creeps from the previous game are still on screen. It would be better if they all disappeared at the start of a new game.

We’ll use the start_game signal that’s already being emitted by the HUD node to remove the remaining creeps. We can’t use the editor to connect the signal to the mobs in the way we need because there are no Mob nodes in the Main scene tree until we run the game. Instead we’ll use code.

Start by adding a new function to Mob.gd. queue_free() will delete the current node at the end of the current frame.

func _on_start_game():
    queue_free()

Then in Main.gd add a new line inside the _on_MobTimer_timeout() function, at the end.

$HUD.connect("start_game", mob, "_on_start_game")

This line tells the new Mob node (referenced by the mob variable) to respond to any start_game signal emitted by the HUD node by running its _on_start_game() function.

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 faites glisser la taille du ColorRect pour qu’il couvre l’écran.

Vous pouvez aussi ajouter une image de fond, si vous en avez une, en utilisant un nœud Sprite.

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 « Bouton ».

Dans la scène HUD, sélectionnez le ``StartButton``et trouvez _Shortcut_ property dans l’inspecteur. Sélectionnez « New Shortcut » et cliquez sur l’élément « Shortcut ». Une deuxième _Shortcut_ property apparaîtra. Sélectionnez « New InputEventAction » et cliquez sur le nouveau « InputEvent ». Enfin, dans _Action_ property, tapez le nom « ui_select ». C’est l’événement d’entrée par défaut associé à la barre d’espace.

../../_images/start_button_shortcut.png

Maintenant, lorsque le bouton de démarrage apparaît, vous pouvez cliquer dessus ou appuyer sur la barre d’espace pour démarrer le jeu.

Fichiers du projet

Vous pouvez trouver une version complète de ce projet ici :