Licence CC BY-NC

Apprenez à développer des jeux en Ruby avec Gosu

Gem, gem, gem Gosu

Publié :
Auteur :
Catégorie :
Temps de lecture estimé : 51 minutes

Ce contenu devait être un tutoriel. La première partie, celle publiée ici, est quasiment terminée. Celle-ci porte sur les bases de Gosu (comment ouvrir une fenêtre, comment gérer les événements en provenance de la souris ou encore comment écrire à l’écran). N’ayant pas la foi de le finir pour le moment, je le poste en l’état ici pour qu’il puisse être utile. Si vous êtes intéressé pour le finir, n’hésitez pas à me MP.

Vous voulez apprendre à créer des jeux vidéo et vous connaissez déjà Ruby ? Cela tombe bien, car à travers ce tutoriel, nous allons explorer Gosu, une bibliothèque de jeux 2D, multi-plateforme et utilisable avec ce langage de façon orientée objet.

Bien que plutôt méconnu, Gosu n’en demeure pas moins intéressant. En effet, sa légèreté en fait un outil rapide à prendre en main. Il peut être particulièrement utile pour se familiariser avec le développement de jeux vidéo par exemple. Ou encore pour développer des jeux vidéo dans un temps relativement court, comme lors d’une Game Jam. En revanche, cette légèreté implique aussi que Gosu est moins complet que d’autres outils. Mais pas de panique, car il y a déjà de quoi faire comme nous le verrons d’ici quelques instants !

Cette bibliothèque étant publiée sous licence MIT, cela signifie que vous pouvez notamment réaliser des jeux commerciaux ou non ou encore modifier Gosu, le tout librement, tant que vous mentionnez les auteurs initiaux avec le copyright.

Avant de continuer, vous devez avoir Gosu d’installé (ce qui implique Ruby avant toute chose). Pour l’installer, les utilisateurs de Windows peuvent se contenter d’un gem install gosu tandis qu’il y a quelques manipulations supplémentaires à effectuer sous Linux et OS X.

Tout au long de ce tutoriel, n’hésitez pas à pratiquer et à vous reporter à la documentation.

Prérequis
Connaissances en programmation et en orienté objet
Connaissances en Ruby

Objectifs
Apprendre à développer des jeux vidéo
Se familiariser avec Gosu

Première fenêtre

Pour commencer, il convient d’importer Gosu :

1
require 'gosu'

Cela fait, nous pouvons ouvrir notre première fenêtre.

Construction et affichage de la fenêtre

Pour créer une fenêtre, nous devons instancier un objet de type Window. Logique, non ? ;)

En paramètres, il nous faut spécifier les dimensions (largeur puis hauteur) en pixels. De plus, nous pouvons indiquer si nous souhaitons que notre fenêtre soit en plein écran (auquel cas, les dimensions correspondent à la résolution) ou non, ainsi qu’un temps de rafraîchissement (nous reviendrons d’ici peu sur ce terme). Dans la version basique, cela donne :

1
window = Gosu::Window.new(640, 480) # Création fenêtre large de 640px et haute de 480px

Après l’instanciation, nous pouvons modifier le titre de notre fenêtre grâce à l’attribut caption :

1
window.caption = "Un titre peu ordinaire" # Modification titre

Jusqu’à présent, vous avez sans doute été déçu si vous avez testé le code. En effet, il ne se rien passé ! Ou plutôt, il ne s’est rien passé de visible. En fait, nous n’avons pas encore indiqué à notre fenêtre qu’elle pouvait se montrer, il est donc normal qu’elle soit restée cachée ! Pour l’afficher, il suffit de faire appel à la méthode show :

1
window.show # Affichage de la fenêtre

La fenêtre entre alors en exécution et vous devriez voir quelque comme ce qui suit apparaître :

Notre première fenêtre.

Boucle de jeu et méthodes associées

Derrière un jeu vidéo, il y a tout une suite d’opérations nécessaires, qui se font dans un certain ordre. Ainsi, il faut prendre en compte les événements (lorsque le joueur appuie sur son clavier par exemple), mettre à jour l’état du jeu (en modifiant la position du joueur par exemple), et afficher le nouvel état du jeu (pour que le joueur se voit bouger par exemple).

Cela est placé dans ce que l’on appelle une boucle de jeu, c’est-à-dire une boucle infinie qui s’exécutera à intervalles de temps plus ou moins réguliers, et où ces actions se succéderont, jusqu’à ce que le joueur décide de quitter. Voici ci-dessous un schéma illustrant cela :

La boucle d’un jeu.

Dans le cas de Gosu, après avoir créé la fenêtre, nous lançons son exécution avec la méthode show.

Ensuite, nous pouvons gérer les événements à travers différentes méthodes telles que button_down. Celle-ci permet de capturer l’appui sur un élément (clavier, souris ou manette) et de savoir lequel est-ce à travers un argument nommé id. Pour mettre à jour l’état du jeu, il existe une méthode update. Enfin, il existe une méthode draw en ce qui concerne l’affichage. Pour l’anecdote, ces méthodes sont dites de rappel (callback).

Au passage, vous vous rappelez de update_interval ? Eh bien, c’est le temps qui s’écoulera entre chaque appel à la méthode update. Peut-être que ce n’est pas très parlant comme ça, mais c’est grâce à ce temps qu’il est possible de mettre à jour un jeu (la position d’un personnage par exemple) indépendamment de la puissance de l’ordinateur (l’affichage ne dépend pas de l’exécution d’une boucle). Nous en reparlerons un peu plus loin.

Pour quitter l’exécution, il suffit d’appeler close. De base, elle est appelée automatiquement lors d’un clique sur la croix.

Toutes les méthodes énoncées ci-dessus sont présentes dans la classe Window, mais il nous faut les redéfinir pour les utiliser comme nous le voulons (certaines ne faisant rien sinon). De ce fait, nous allons créer une classe héritant de Window et redéfinir les méthodes qui nous intéressent. Par exemple, pour s’assurer de sauvegarder une partie lorsque le joueur quitte, nous pourrions redéfinir close :

1
2
3
4
5
def close
  # Sauvegarde de l'état du jeu...
  puts "Partie sauvegardée !"
  super # Appel à la méthode close de la classe parente
end

Le squelette de code :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
require 'gosu'

class GameWindow < Gosu::Window

  def initialize(width, height, fullscreen = false, update_interval = 16.666666)
    super
    self.caption = "Un titre peu ordinaire"
  end

  def draw
    # Là où l'on va dessiner dans la fenêtre
  end

  def update
    # Là où l'on va mettre à jour l'état du jeu
  end

  def button_down(id)
    # Là où l'on va tester si un élément (clavier, souris, manette) a été enfoncé
  end

  def close
    # Sauvegarde de l'état du jeu...
    puts "Partie sauvegardée !"
    super # Appel à la méthode close de la classe parente
  end

  # D'autres méthodes
  # ...

end

window = GameWindow.new(640, 480)
window.show

Les mêmes causes produisant les mêmes effets, vous devriez voir la même fenêtre apparaître lors de l’exécution.

Affichage du curseur

Pour terminer cette section, vous aurez sans doute remarqué que le curseur de la souris n’apparaît pas lorsque nous déplaçons celle-ci à l’intérieur de la fenêtre. Cela est dû à la méthode need_cursor? qui renvoie un booléen selon que nous ayons besoin du curseur ou non. Comme nous voulons l’afficher, nous allons la redéfinir et lui faire renvoyer la valeur true.

1
2
3
def needs_cursor?
  true
end

Notons qu’il serait préférable d’avoir un attribut pour stocker ce booléen si nous voulions faire évoluer cette valeur au cours du jeu.

Tracer et dessiner

Maintenant que nous savons gérer une fenêtre, nous pouvons essayer d’afficher des choses dedans. Comme vous le savez après la lecture de la section précédente, nous allons travailler principalement dans la méthode draw.

Les coordonnées

Vous conviendrez que pour placer les éléments, il nous connaître leurs positions, donc avoir un système de coordonnées… Et il y en a un ! Le monde est bien fait, n’est-ce pas ?

Dans les bibliothèques de jeu, l’origine du repère des coordonnées se trouve très souvent dans le coin haut gauche. De plus, l’axe des abscisses est orienté vers la droite tandis que l’axe des ordonnées est orienté vers le bas. Ainsi, les coordonnées du coin haut gauche sont (0, 0) tandis que celles du coin bas droit sont (largeur, hauteur). En outre, les éléments sont situés avec la position de leur coin haut gauche. Par exemple, si un point a une position en y qui vaut presque la hauteur de la fenêtre, il sera proche du bas de celle-ci. De même, si sa position en x vaut 0, alors il touchera le bord gauche de la fenêtre.

Voici un exemple d’illustration avec un rectangle en (posX, posY) pour bien comprendre :

Repère de coordonnées.

Les couleurs

Avant de s’attaquer aux différents tracés, il est à noter que Gosu propose une classe spécifique pour les couleurs, à savoir Color.

Grâce à celle-ci, nous pouvons instancier des couleurs en fournissant l’opacité (Alpha) ainsi que les taux de rouge (Red), de vert (Green) et de bleu (Blue). Pour ceux qui ne sont pas familiers avec cela, je vous invite à lire cet article. Nous pouvons passer ces paramètres sous la forme d’entiers de 0 à 255 inclus. Remarquons que si nous ne fournissons que trois paramètres, alors le constructeur considérera que ce sont les taux de couleurs et l’opacité sera initialisée au maximum. Par ailleurs, nous pouvons aussi fournir une forme hexadécimale (quelques uns sont fournis dans la documentation) de l’ensemble au constructeur.

Exemple :

1
2
3
red = Gosu::Color.new(255, 255, 0, 0) # Couleur rouge, forme ARGB ; a=255, r=255, g=0, b=0
blue = Gosu::Color.new(0, 0, 255) # Couleur bleue, forme RGB ; a=255, r=0, g=0, b=255
white = Gosu::Color.new(0xff_ffffff) # Couleur blanche, forme hexadécimale ; a=255, r=255, g=255, b=255

Nous avons vu une façon de créer des couleurs, mais ce n’est pas la seule. Je vous renvoie donc à la documentation si vous voulez creuser.

Les traits et les formes

Bon, la théorie est terminée maintenant, je vous l’assure ! Nous allons enfin pouvoir commencer à afficher des choses.

Durant la fin de cette section, les méthodes qui vont nous intéresser se trouvent à la racine du module Gosu.

Avant de nous y mettre, regroupons les quelques couleurs de tout à l’heure dans un Hash constant, car elles vont nous être utiles :

1
2
3
4
5
COLORS = {
  red: Gosu::Color.new(255, 255, 0, 0),
  blue: Gosu::Color.new(0, 0, 255),
  white: Gosu::Color.new(0xff_ffffff)
}

Traits

Pour tracer un trait, il suffit d’appeler la méthode draw_line , en lui fournissant le point de départ, sa couleur, le point d’arrivée ainsi que sa couleur. Si les deux couleurs sont différentes, il y aura un dégradé.

Exemple :

1
2
3
4
def draw
  Gosu::draw_line(0, 0, COLORS[:red], width, height, COLORS[:blue]) # Trait avec dégradé
  Gosu::draw_line(0, height, COLORS[:white], width, 0, COLORS[:white]) # Trait sans dégradé
end

Résultat :

Exemple de traits.

La documentation indique que l’utilisation de cette méthode n’est pas recommandée, car il peut y avoir des pixels manquants au début ou à la fin du trait. Pour notre utilisation, ça ne nous dérangera pas.

Formes

Les traits c’est sympa, mais nous sommes vite limités. Nous pourrions très bien tracer différentes figures en en assemblant mais inutile de réinventer la roue !

Triangle

Pour dessiner un triangle, il suffit d’utiliser la méthode draw_triangle. Celle-ci prend en paramètres non pas deux, mais trois coordonnées de points accompagnés de leurs couleurs. De la même façon que pour les traits, il y aura un dégradé si les couleurs sont différentes.

Exemple :

1
2
3
4
def draw
  Gosu::draw_triangle(45, 55, COLORS[:blue], 195, 240, COLORS[:white], 300, 180, COLORS[:red]) # Triangle avec dégradé
  Gosu::draw_triangle(300, 400, COLORS[:white], 500, 300, COLORS[:white], 600, 400, COLORS[:white]) # Triangle sans dégradé
end

Résultat :

Exemple de triangles.
Rectangle

Dans le cas d’un rectangle, ce n’est pas bien compliqué non plus. La méthode draw_rect est là pour ça. Nous devons lui passer les coordonnées de son coin haut gauche, sa largeur, sa hauteur ainsi que sa couleur. Il n’y a donc pas de dégradé possible ici.

Exemple :

1
2
3
4
def draw
  Gosu::draw_rect(0, 0, width, height, COLORS[:white]) # Rectangle blanc
  Gosu::draw_rect(width/3, height/3, width/3, height/3, COLORS[:red]) # Rectangle rouge
end

Résultat :

Exemple de rectangles.
Quadrilatère

Dernière roue du carrosse, il est possible dessiner n’importe quel quadrilatère (qui est en fait l’assemblage de deux triangles) avec la méthode draw_quad. Celle-ci prend quatre points accompagnés de leurs couleurs.

Exemple :

1
2
3
4
def draw
  Gosu::draw_quad(25, 25, COLORS[:red], 200, 25, COLORS[:red], 200, 200, COLORS[:blue], 25, 200, COLORS[:blue]) # Quadrilatère avec dégradé
  Gosu::draw_quad(300, 300, COLORS[:white], 400, 200, COLORS[:white], 500, 300, COLORS[:white], 600, 150, COLORS[:white]) # Quadrilatère sans dégradé
end

Résultat :

Exemple de quadrilatères.

Remarquez que si nous voulons un dégradé dans notre rectangle, il nous faut donc utiliser cette méthode là, comme nous l’avons fait dans l’exemple.

Nous n’en avons pas parlé jusqu’à présent, mais comme vous avez dû vous en apercevoir si vous êtes allé regarder la documentation, les méthodes que nous venons d’utiliser pour les traits et les formes peuvent prendre deux paramètres supplémentaires : le Z-order (qui vaut zéro par défaut) et le mode (défaut par défaut). Nous reviendrons sur ces deux paramètres dans la section sur les images.

Avant de passer à la section suivante, je vous invite à pratiquer afin d’intégrer le fonctionnement des méthodes vues.

Gérer les événements (1/2)

Nous rentrons maintenant dans une des sections les plus intéressantes puisqu’il va y avoir de l’action ! :pirate:

Enfin, presque… :euh:

Quand je parle d’action, je parle d’événement. Ici, nous allons nous intéresser à ceux en provenance de la souris.

Clique enfoncé ou relâché

Avec Gosu, nous pouvons tester si un élément est enfoncé grâce à la méthode button_down, ou relâché (après avoir été enfoncé) grâce à la méthode button_up. Ces deux méthodes ont pour argument l’identifiant du bouton enfoncé. En regardant les différentes constantes dans le module, nous pouvons établir le tableau suivant :

Identifiant Correspondance
MS_LEFT Bouton gauche souris
MS_MIDDLE Bouton central souris
MS_RIGHT Bouton droit souris
MS_WHEEL_DOWN Défilement vers le bas
MS_WHEEL_UP Défilement vers le haut
MS_OTHER_0…MS_OTHER_7 Autres boutons souris

Exemple :

1
2
3
4
5
6
7
def button_down(id)  # Détection élément enfoncé
  puts "Clique gauche enfoncé." if id == Gosu::MS_LEFT
end

def button_up(id) # Détection élément relâché
  puts "Clique droit relâché." if id == Gosu::MS_RIGHT
end

Comme vous pouvez le voir, ce morceau de code sert à détecter lorsque le bouton gauche de la souris est enfoncé ainsi que lorsque le bouton droit de la souris est relâché.

Pour tester si un bouton est couramment enfoncé, nous pouvons faire appel à la méthode button_down? en fournissant l’identifiant du bouton en question.

Exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
require 'gosu'

class GameWindow < Gosu::Window

  def initialize(width, height, fullscreen = false, update_interval = 16.666666)
    super
    self.caption = "Détection élément enfoncé"
  end

  def update
    if button_down?(Gosu::MS_LEFT) # Détection bouton gauche souris enfoncé
      puts "Et tu cliques, cliques, cliques"
    end
  end

  def needs_cursor?
    true
  end
end

window = GameWindow.new(640, 480)
window.show

En testant les codes ci-dessus, vous avez pu vous rendre compte que lorsque nous effectuons la détection dans update, il semble y avoir un effet de continuité contrairement au résultat produit par button_down ou button_up. En effet, dans le premier cas « Et tu cliques, cliques, cliques » s’affiche tant que nous pressons le bouton gauche de la souris tandis que dans le second cas « Clique gauche enfoncé » s’affiche lorsque la touche gauche passe à l’état enfoncé. C’est normal puisque update étant appelé régulièrement, nous testons régulièrement l’état de la touche avec button_down?, tandis que button_down prend compte le passage à l’état enfoncé uniquement. Cette différence peut être très utile comme nous le verrons par la suite.

Déplacement souris

Sachant que les attributs mouse_x et mouse_y de la classe Window nous renseignent sur la position de la souris, nous pouvons suivre le déplacement de celle-ci en calculant la différence entre la précédente position et la nouvelle.

Exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
require 'gosu'

class GameWindow < Gosu::Window

  def initialize(width, height, fullscreen = false, update_interval = 16.666666)
    super
    @previous_x, @previous_y = mouse_x, mouse_y # Initialisation ancienne position souris
    self.caption =  "Déplacement souris"
  end

  def update
    dx = mouse_x - @previous_x # Calcul déplacement horizontal
    dy = mouse_y - @previous_y # Calcul déplacement vertical
    if dx != 0
      puts "Déplacement de #{dx} en abscisse"
      @previous_x = mouse_x
    end
    if dy != 0
      puts "Déplacement de #{dy} en ordonnée"
      @previous_y = mouse_y
    end
  end

  def needs_cursor?
    true
  end
end

window = GameWindow.new(640, 480)
window.show

Ayant connaissance de la position de la souris, nous pouvons nous amuser à dessiner un rectangle dont le centre serait le curseur, comme dans le code ci-dessous.

Exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
require 'gosu'

class GameWindow < Gosu::Window

  def initialize(width, height, fullscreen = false, update_interval = 16.666666)
    super
    @width_rect = width/10 # Largeur rectangle
    @height_rect = height/10 # Hauteur rectangle
    @color_rect = Gosu::Color.new(0xff_ffffff) # Couleur rectangle
    self.caption = "Rectangle suivant déplacement souris"
  end

  def draw
    # Dessin d'un rectangle centré sur le curseur de la souris
    Gosu::draw_rect(mouse_x-@width_rect/2, mouse_y-@height_rect/2, @width_rect, @height_rect, @color_rect)
  end

  def needs_cursor?
    true
  end
end

window = GameWindow.new(640, 480)
window.show

Écrire à l'écran

Dessiner c’est bien, mais écrire dans la fenêtre c’est encore mieux. Si, si ! :D

Pour cela, deux possibilités s’offrent à nous.

En utilisant un objet Font

Tout d’abord, nous pouvons instancier un objet de type Font qui permet comme son nom l’indique de gérer une police d’écriture.

Pour construire une police, nous pouvons soit simplement passer une taille auquel cas le nom de police choisi sera celui par défaut du système, ou nous pouvons aussi indiquer un nom de police auquel cas l’ordre change un peu et il faut aussi indiquer une fenêtre.

Exemple :

1
2
3
4
font1 = Gosu::Font.new(50) # Construction avec taille police
puts font1.name == Gosu::default_font_name # Affiche 'true'
window = Gosu::Window.new(640, 480) # Construction d'une fenêtre
font2 = Gosu::Font.new(window, "Arial", 18) # Construction avec fenêtre, nom police et taille police

Comme vous pouvez le voir, c’est la méthode default_font_name qui nous retourne le nom de la police par défaut du système.

Une fois l’objet créé, nous pouvons nous en servir avec sa méthode draw. Celle-ci doit prendre en paramètres le texte à écrire, la position de celui-ci (en abscisse puis en ordonnée) ainsi qu’une valeur de Z-order, que nous expliquerons un peu plus loin encore une fois. Par ailleurs, nous pouvons aussi spécifier d’autres paramètres tels qu’un facteur d’échelle horizontale (1 par défaut), un facteur d’échelle verticale (1 par défaut) ou encore une couleur (blanche par défaut). Je vous laisse jeter un coup d’œil à la documentation si besoin.

Exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
require 'gosu'

class GameWindow < Gosu::Window

  def initialize(width, height, fullscreen = false, update_interval = 16.666666)
    super
    self.caption = "L'écriture à l'écran"
    @font1 = Gosu::Font.new(50)
    @font2 = Gosu::Font.new(self, "Arial", 36)
    @pos_x = (width-@font1.text_width("Salut les Zesteurs")) / 2
  end

  def draw
    @font1.draw("Salut les Zesteurs", @pos_x, height/8, 0)
    @font2.draw("Je suis de couleur bleue !", width/8, height/3, 0, 1, 1, 0xff_0000ff)
    @font2.draw("Je suis de couleur rose !", width/8, height/3*1.2, 0, 1, 1, 0xff_ff66ff)
    @font2.draw("Je suis de couleur jaune !", width/8, height/3*1.4, 0, 1, 1, 0xff_ffff00)
  end
end

window = GameWindow.new(640, 480)
window.show

Résultat :

Exemple d’écriture avec des objets de type Font.

Remarquez que nous avons centré le premier texte en largeur en connaissant sa largeur estimée par la méthode text_width.

En passant par une image

Une autre façon de procéder est de passer par un objet de type Image, que nous étudierons plus spécialement dans la seconde partie. Dans notre cas, nous allons créer un tel objet à partir d’un texte avec la méthode from_text. Cette dernière a plusieurs signatures, comme vous pourrez le constater en vous reportant à la documentation. Nous allons nous contenter de lui passer un texte ainsi qu’une taille, mais sachez qu’il est aussi possible de spécifier un nom de police par exemple.

L’avantage de cette méthode, c’est qu’elle prend en compte les retours à la ligne dans le texte. Il est donc possible d’afficher un message sur plusieurs lignes au lieu d’afficher chaque ligne de façon séparée.

Une fois l’image de notre texte créée, il ne nous reste plus qu’à l’afficher à l’aide de sa méthode draw dont le prototype est fort similaire à celle de la classe Font, seul le paramètre du texte à écrire disparaissant.

Exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
require 'gosu'

class GameWindow < Gosu::Window

  def initialize(width, height, fullscreen = false, update_interval = 16.666666)
    super
    self.caption = "L'écriture à l'écran"
    @message = ""
    15.times do |line|
      5.times do |word_per_line|
        @message += "I like trains "
      end
      @message += "\n" # Fin de ligne dans le message
    end
    @image = Gosu::Image.from_text(@message, 32) # Construction image
  end

  def draw
    @image.draw(0, 0, 0) # Affichage de l'image
  end
end

window = GameWindow.new(640, 480)
window.show

Résultat :

Exemple d’écriture avec une image.

S'informer du temps écoulé

Comme dit dans la première section, la boucle de jeu s’exécute un certain nombre de fois par seconde.

Encore une fois, connaître le temps écoulé est important puisque celui-ci nous permet de mettre à jour un jeu indépendamment de la puissance de l’ordinateur.

Comme nous le savons désormais, nous pouvons choisir le temps qui s’écoulera entre deux appels à update en modifiant update_interval. Par défaut, ce dernier vaut 16.666666 ms, ce qui fait 10000/16.666666 donc environ 60 rafraîchissements par seconde, ou FPS dans le jargon.

En plus de cette valeur, il existe aussi une méthode du module nous permettant de connaître le temps écoulé en millisecondes depuis la création de la fenêtre : milliseconds. Celle-ci nous retournant un entier, il peut donc y avoir des différences de précision.

D’après la documentation, cette valeur est susceptible d’être réinitialisée à zéro en cas de grande durée.

Exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
require 'gosu'

class GameWindow < Gosu::Window

  def initialize(width, height, fullscreen = false, update_interval = 16.666666)
    # Il semblerait que 16.666666 soit le minimum possible, donc 1000/16.66666 = 60 fps
    super
    self.caption = "S'informer du temps écoulé"
    @previous_time = Gosu.milliseconds
  end

  def update
    time = Gosu.milliseconds
    puts "update_interval = #{update_interval}, temps calculé = #{time-@previous_time}"
    @previous_time = time
  end

end

window = GameWindow.new(640, 480)
window.show

Remarquons que 60 images par seconde semble être le maximum possible, car en paramétrant un interval plus court, nous retombons sur les mêmes valeurs.

TP : un jeu de memory

Pour clore cette première partie en beauté, nous allons réaliser un jeu de memory.

Dans ce type de jeu, il y a un certain nombre de cartes allant par paires identiques (plus un intrus lorsque le total est impair) qui sont mélangées et masquées. Le joueur doit alors retrouver les paires identiques en faisant appel à sa mémoire pour se souvenir des positions des cartes qu’il a retournées. Lorsque le joueur trouve une paire, celle-ci reste définitivement retournée. Lorsqu’il se trompe il perd une vie. Quand il n’a plus de vie, il perd la partie.

Si vous rencontrez des difficultés au cours de cet exercice, n’hésitez pas à revoir les sections précédentes ou à poser vos questions sur les forums. De plus, si vous avez du mal à savoir où aller, tenter de décomposer votre programme sur papier (que doit-il faire en premier ? puis ensuite ? etc.).

Cahier des charges

Notre jeu fonctionnera en plein écran. Pour quitter, il faudra presser le bouton droit de la souris.

Petite astuce, pour que la résolution de votre fenêtre colle avec la taille de votre écran, vous pouvez utiliser les valeurs retournées par les méthodes screen_width et screen_height, qui se trouvent à la racine du module.

Ensuite, il y a aura neuf cartes au total (soit quatre paires identiques plus une intruse). Les cartes seront représentées par des rectangles colorés. Sur ce même écran, il faudra afficher au joueur le nombre de vies qu’il lui reste.

Lorsque la partie se terminera, un écran final affichera si le joueur a gagné ou perdu. Il indiquera aussi au joueur qu’il peut recommencer en pressant le clique gauche de la souris.

Comme vous l’aurez compris, le but est de se servir de ce que nous avons vu précédemment, c’est pourquoi il ne faut pas hésiter à reparcourir les sections précédentes encore une fois.

Voici un rendu de mon programme, si ça peut vous inspirer :

Jeu de memory (1)
Jeu de memory (2)

Correction

Voici la correction que je propose. Si votre code est différent du mien, pas de panique, le principal est qu’il soit fonctionnel.

Fichier constants.rb

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
require 'gosu'

# Module quelques variables et fonctions
module Constants

  # Le nombre de cartes
  CARDS_COUNT = 9

  # Les couleurs des paires
  COLORS_CARDS = {
    grey: Gosu::Color.new(0xff_808080),
    white: Gosu::Color.new(0xff_ffffff),
    aqua: Gosu::Color.new(0xff_00ffff),
    red: Gosu::Color.new(0xff_ff0000),
    green: Gosu::Color.new(0xff_00ff00),
    blue: Gosu::Color.new(0xff_0000ff),
    yellow: Gosu::Color.new(0xff_ffff00)
  }

  # Les autres couleurs
  COLORS_OTHERS = {
    orange: Gosu::Color.new(254, 199, 115), # Couleur pour le fond
    brown: Gosu::Color.new(174, 121, 51), # Couleur pour le recto des cartes
    black: Gosu::Color.new(0xff_000000) # Couleur pour l'intrus
  }

  # Fonction générant une liste de couleurs pour les cartes, avec un intrus si besoin
  def self.get_random_colors
    count_cards = (CARDS_COUNT%2 == 0) ? CARDS_COUNT : CARDS_COUNT-1
    random_colors = []
    color = nil

    while random_colors.length != count_cards
      color = COLORS_CARDS[COLORS_CARDS.keys.sample]
      2.times { random_colors.push(color) } unless random_colors.include?(color)
    end
    random_colors.push(COLORS_OTHERS[:black]) if count_cards != CARDS_COUNT

    random_colors.shuffle!
  end

  # Fonction permettant d'écrire centré
  def self.print_center(window, message, pos_y = -1, size_police = 45, color = COLORS_OTHERS[:black])
    img = Gosu::Image.from_text(message, size_police, {})
    pos_x = (window.width-img.width) / 2
    pos_y = (window.height-img.height) / 2 if pos_y == -1
    img.draw(pos_x, pos_y, 0, 1.0, 1.0, color)
  end

  # Fonction permettant d'écrire aligné à droite
  def self.print_right_align(window, message, pos_y = -1, size_police = 40, color = COLORS_OTHERS[:black])
    img = Gosu::Image.from_text(message, size_police, {})
    pos_x = window.width - img.width
    pos_y = window.height - img.height if pos_y == -1
    img.draw(pos_x, pos_y, 0, 1.0, 1.0, color)
  end

  # Fonction permettant d'écrire aligné à gauche
  def self.print_left_align(window, message, pos_y = -1, size_police = 40, color = COLORS_OTHERS[:black])
    img = Gosu::Image.from_text(message, size_police, {})
    pos_x = 0
    pos_y = window.height - img.height if pos_y == -1
    img.draw(pos_x, pos_y, 0, 1.0, 1.0, color)
  end

end

Fichier card.rb

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
require 'gosu'
require_relative 'constants'

# Classe pour les cartes
class Card

  attr_accessor :is_revealed
  attr_reader :color_recto

  def initialize(pos_x, pos_y, width, height, color_recto)
    @pos_x = pos_x # Position x point haut gauche
    @pos_y = pos_y # Position y point haut gauche
    @width = width # Largeur de la carte
    @height = height # Hauteur de la carte
    @color_recto = color_recto # Couleur recto
    @color_verso = Constants::COLORS_OTHERS[:brown]  # Couleur verso
    @is_revealed = false # Si la carte est retournée ou non
  end

  # Fonction permettant de savoir si une carte contient un point donné
  def contain?(pos_x, pos_y)
    (pos_x >= @pos_x && pos_x < @pos_x+@width) && (pos_y >= @pos_y && pos_y < @pos_y+@height)
  end

  def draw
    if @is_revealed
      Gosu::draw_rect(@pos_x, @pos_y, @width, @height, @color_recto)
    else
      Gosu::draw_rect(@pos_x, @pos_y, @width, @height, @color_verso)
    end
  end

end

Fichier window.rb

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
require 'gosu'
require_relative 'constants'
require_relative 'card'

# Classe pour le jeu
class GameWindow < Gosu::Window

  def initialize(width, height, fullscreen = false, update_interval = 16.666666)
    super
    initialize_game
  end

  def initialize_game
    @lives = 3 # Nombre de vies initiales
    @cards = [] # Array pour stocker les cartes
    @current_pair = [] # Array pour stocker la paire retournée par le joueur
    @timer = 0 # Temps écoulé
    @state = :game # État de la partie :game => partie en cours, :end => fin de la partie
    initialize_cards
  end

  # Fonction permettant d'initialiser les cartes
  def initialize_cards
    random_colors = Constants.get_random_colors
    elements_per_group = Math.sqrt(Constants::CARDS_COUNT).to_i # Éléments par ligne ou par colonne
    width_allowed = 0.7 * width # Espace attribué en largeur aux cartes
    height_allowed = 0.8 * height # Espace attribué en hauteur aux cartes
    step_x = (width-width_allowed) / (1+elements_per_group) # Écart horizontal entre cartes
    step_y = (height-height_allowed) / (1+elements_per_group) # Écart vertical entre cartes
    pos_x = step_x # Position en abscisse de la première carte
    pos_y = step_y # Position en ordonnée de la première carte
    width_card = width_allowed / elements_per_group # Largeur carte
    height_card = height_allowed / elements_per_group # Hauteur carte

    elements_per_group.times do |line|
      elements_per_group.times do |column|
        @cards.push(Card.new(pos_x, pos_y, width_card, height_card, random_colors[line*elements_per_group+column]))
        pos_x += (width_card+step_x)
      end
      pos_x = step_x
      pos_y += (height_card+step_y)
    end
  end

  def update
    @timer += update_interval
    case @state
    when :game # Écran de jeu
      if @current_pair.length == 2 && @timer > 400
        test_equality
        test_end
        @current_pair.clear
        @timer = 0
      end
    when :end # Écran de fin
    end
  end

  def draw
    Gosu.draw_rect(0, 0, width, height, Constants::COLORS_OTHERS[:orange])
    case @state
    when :game # Écran de jeu
      Constants.print_right_align(self, "Nombre de vies : #{@lives}", 0)
      @cards.each { |card| card.draw }
      Constants.print_left_align(self, "Clique gauche : sélectionner / Clique droit : quitter", -1, 25)
    when :end # Écran de fin
      if @lives != 0
        Constants.print_center(self, "Bravo, vous avez gagné !", height/3, 70)
      else
        Constants.print_center(self, "Dommage, vous avez perdu !", height/3, 70)
      end
      Constants.print_center(self, "Clique gauche : recommencer / Clique droit : quitter", height/3*1.4, 40)
    end
  end

  def button_up(id)
    case @state
    when :game # Écran de jeu
      if @current_pair.length < 2 && id == Gosu::MsLeft # Pour retourner une carte
        card = return_card(mouse_x, mouse_y)
        if card
          @current_pair.push(card)
          @timer = 0 if @current_pair.length == 2
        end
      end
    when :end # Écran de fin
      if id == Gosu::MsLeft && @timer > 400 # Pour relancer une partie
        initialize_game
        @state = :game
      end
    end
  end

  def button_down(id)
    if id == Gosu::MS_RIGHT # Pour quitter la partie avec un clique droit
      close
    end
  end

  def needs_cursor?
    true
  end

  # Fonction permettant de retourner la carte cliquée si possible, renvoie nil sinon
  def return_card(mouse_x, mouse_y)
    @cards.each do |card|
      if card.contain?(mouse_x, mouse_y) && !card.is_revealed
        card.is_revealed = true
        return card
      end
    end
    nil
  end

  # Fonction permettant de tester si la paire retournée est bonne ou non
  def test_equality
    unless @current_pair[0].color_recto == @current_pair[1].color_recto
      @current_pair.each { |card| card.is_revealed = false }
      @lives -= 1
    end
  end

  # Fonction permettant de tester la fin de la partie
  def test_end
    count_max = (Constants::CARDS_COUNT%2 == 0) ? CARDS_COUNT : Constants::CARDS_COUNT-1
    counter = 0
    @cards.each { |card| counter += 1 if card.is_revealed }
    @state = :end if @lives == 0 || counter == count_max
  end

end

Fichier main.rb

1
2
3
4
require_relative 'window'

window = GameWindow.new(Gosu::screen_width, Gosu::screen_height, true)
window.show

Améliorations :

Bien que notre jeu soit déjà jouable en l’état, nous pourrions l’améliorer de plusieurs façons.

Pour commencer, nous pourrions ajouter différentes tailles de grille. Actuellement, nous avons une grille de dimension trois par trois. Nous pourrions ajouter quatre par quatre ou encore cinq par cinq par exemple. Bien entendu, il faudrait plus de types de carte ainsi qu’adapter le nombre de vies selon les dimensions.

Deuxièmement, nous pourrions faire en sorte de pouvoir choisir le thème des cartes. Bon, pour le moment, ce ne sont que des couleurs, donc il n’y aurait pas trop de choix (contrairement à des images), mais nous pouvons imaginer un thème « couleurs vives » ou un autre thème « couleurs sombres » par exemple.

Troisièmement, nous pourrions ajouter un menu permettant au joueur de choisir la taille ainsi que le thème de la partie. Avec des cliques, cela se fait très bien.

Enfin, nous pourrions rendre possible le mode non plein écran. Le joueur pourrait alors passer en plein écran avec un clique central ou en cliquant sur le carré dans la barre du programme. Et il pourrait repasser en mode plus petit avec le clique central de nouveau.

Si vous souhaitez vous exercer davantage ou tout simplement vous amuser, vous avez donc quelques possibilités. :)


Voilà, c’est déjà la fin de ce tutoriel ! Au cours de celui-ci vous avez pu vous familiariser avec Gosu et mettre en pratique vos connaissances.

Si vous souhaitez poursuivre l’aventure avec cette bibliothèque, je vous propose d’explorer :

Enfin, n’hésitez pas à partager le fruit de votre apprentissage sur les forums.

À bientôt ! :)

Merci à Karnaj pour ses retours.

2 commentaires

Merci pour ce chouette tuto/billet, ça va être l’occasion pour moi de faire du Ruby un peu plus avancé et de me lancer dans le jeu vidéo sans me casser la tête ! :)

Bat’

Je t’en prie. :)

Si tu connais Ruby, c’est sûr que Gosu est vraiment intéressant pour ça, comme Pygame est intéressant si on connaît Python ; après si tu souhaites commencer ou aller plus ou loin dans le développement de jeux vidéo, je trouve Love2D génial, mais ça implique d’apprendre Lua.

+0 -0
Vous devez être connecté pour pouvoir poster un message.
Connexion

Pas encore inscrit ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte