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 :

../../_images/dodge_preview.gif

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 :

../../_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, 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.

../../_images/add_node.png

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

../../_images/lock_children.png

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

../../_images/spriteframes_panel.png

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 :

../../_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

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 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().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 pour get_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 :

../../_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(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.

../../_images/player_signal_connection.png

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.

../../_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. Il y a deux images pour chaque animation dans le dossier art.

Ajustez la vitesse "Speed (FPS)" à 3 pour toutes les animations.

../../_images/mob_animations.gif

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. Cliquez sur le bouton "Instance" et sélectionner votre Player.tscn sauvegardé.

../../_images/instance_scene.png

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 à gauche du bouton "Verrouiller", apparaissant comme un aimant à côté de lignes qui se croisent.

../../_images/grid_snap_button.png

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 !

../../_images/draw_path2d.gif

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 :

../../_images/main_scene_nodes.png

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 :

  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 Size la taille de la police de caractères. Un réglage à 64 fonctionne bien.
../../_images/custom_font2.png

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 :

../../_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

  • 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

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 :

../../_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() 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".

../../_images/group_tab.png

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

../../_images/start_button_shortcut.png

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 :