Écrire un jeu en 2d avec Ruby et Gosu - partie 2
Aujourd’hui on voit comment déplacer et contrôler le joueur, ainsi que comment faire tomber une pluie de smileys.
La totalité des articles:
- Installation de Gosu, affichage d’images statiques
- Déplacer le joueur et pluie de smileys
- Beep, fonte et collecte des smileys
- On s’occupe des vies
- Musique et game over
- Affichage selon un angle
- Plusieurs musiques et reset de la partie
Déplacer le joueur
On ajoute la méthode update
à la classe Window
. À l’instar de draw
cette méthode vient aussi de Gosu::Window
et est appelée régulièrement.
Dans draw
on dessine, on affiche. Dans update
on calcule, on met à jour
les éléments du jeu.
update
sera appelée 60 fois par seconde quoiqu’il arrive, par contredraw
pourra être appelée plus ou moins souvent suivant les besoins de l’OS. C’est pour cela qu’il est important de séparer le calcul de l’affichage. Cette division entre calcul et affichage est d’ailleurs typique d’un framework de jeu, ou même GUI, quelque soit le langage utilisé.
Le nouveau code de Window
est le suivant:
class Window < Gosu::Window
def initialize(width, height)
super
self.caption = "Collect The Smile!"
@background_image = Gosu::Image.new("assets/images/background.png")
@player = Player.new
end
def update
@player.go_left if Gosu::button_down?(Gosu::KbLeft)
@player.go_right if Gosu::button_down?(Gosu::KbRight)
@player.move
end
def draw
@background_image.draw(0, 0, ZOrder::Background)
@player.draw
end
end
Dans update
on utilise la méthode button_down?
de Gosu pour tester si le
joueur appuit sur la touche gauche et/ou la touche droite. L’appui simultané sur
les deux touches annulera en quelque sorte le déplacement. Et dans tout les cas
on déplace le joueur avec @player.move
.
Voyons maintenant la classe Player
, qui a gagné plusieurs nouvelles méthodes
par rapport au dernier article:
class Player
Y = 390
def initialize
@x = WindowHeight / 2
@velocity = 0.0
@image = Gosu::Image.new("assets/images/player.png")
end
def draw
@image.draw(@x, Y, ZOrder::Player)
end
def go_left
@velocity -= 0.5
end
def go_right
@velocity += 0.5
end
def move
@x += @velocity
@x %= WindowWidth
@velocity *= 0.96
end
end
Le joueur se déplaçant toujours sur la même ligne, sa coordonnée y ne change pas, j’en ai donc fait une constante.
L’idée générale pour le déplacement, c’est qu’il soit smooth, avec une accélération progressive et une glisse finale. On a donc un facteur d’accélération de 0.5 et un facteur de glisse de 0.96. Prenez le temps de jouer avec ces valeurs pour les comprendre, et peut-être pour en trouver d’autres qui vous conviennent mieux.
Comme toujours, nous convertirons ces nombres magiques en constantes:
class Player
Y = 390
AccelerationFactor = 0.5
SkidingFactor = 0.96
# ...
def go_left
@velocity -= AccelerationFactor
end
def go_right
@velocity += AccelerationFactor
end
def move
@x += @velocity
@x %= WindowWidth
@velocity *= SkidingFactor
end
end
Une pluie de smileys
Il est temps de faire pleuvoir des smileys !
Ajoutez un fichier smiley.rb
:
require 'gosu'
require_relative 'z_order'
require_relative 'player'
require_relative 'smiley'
require_relative 'window'
# ...
Et remplissez le avec le code suivant:
class Smiley
attr_reader :y
def initialize(type)
@image = if type == :smiley_up
Gosu::Image.new("assets/images/smiley-yellow.png")
elsif type == :smiley_down
Gosu::Image.new("assets/images/smiley-green.png")
end
@velocity = Gosu::random(0.8, 3.3)
@x = rand * (WindowWidth - @image.width)
@y = 0
end
def update
@y += @velocity
end
def draw
@image.draw(@x, @y, ZOrder::Items)
end
end
Il n’y a rien de vraiment nouveau par rapport à ce qu’on a vu jusqu’ici.
Je peux toutefois signaler la méthode Gosu::random(0.8, 3.3)
, qui retourne
un Float entre 0.8 inclus et 3.3 exclus. Et aussi le calcul de @x
:
@x = rand * (WindowWidth - @image.width)
qui soustrait la largeur du smiley de la largeur de la fenêtre pour s’assurer qu’un smiley sera toujours dans les limites de la fenêtre (visible, donc).
Pour intégrer les smileys au jeu, vous devez modifier les méthodes
initialize
, update
et draw
de la classe Window:
class Window < Gosu::Window
def initialize(width, height)
# ...
@items = []
end
def update
unless @items.size >= 15
r = rand
if r < 0.035
@items.push(Smiley.new(:smiley_up))
elsif r < 0.040
@items.push(Smiley.new(:smiley_down))
end
end
@items.each(&:update)
@items.reject! {|item| item.y > WindowHeight }
# Les 3 lignes suivantes sont décrites dans l'article précédent.
@player.go_left if Gosu::button_down?(Gosu::KbLeft)
@player.go_right if Gosu::button_down?(Gosu::KbRight)
@player.move
end
def draw
@background_image.draw(0, 0, ZOrder::Background)
# Ça c'est la nouvelle ligne:
@items.each(&:draw)
@player.draw
end
end
La nouvelle méthode update
mérite bien quelques explications !
Tout d’abord je veux afficher un maximum de 15 smileys à l’écran:
def update
if @items.size < 15
S’il y en a moins de 15, il y a à chaque update 3.5% de chances (0.035) de créer un sourire à l’endroit, et 0.5% de chances (0.04 - 0.035) de créer un sourire à l’envers:
r = rand
if r < 0.035
@items.push(Smiley.new(:smiley_up))
elsif r < 0.040
@items.push(Smiley.new(:smiley_down))
end
end
Dans tous les cas on met à jour les smileys existants (c’est à dire qu’on change leur coordonnée y):
@items.each(&:update)
Et on supprime les smileys qui sont sortit de la fenêtre de jeu (par le bas):
@items.reject! {|item| item.y > WindowHeight }
Ce code est hideux et il faut faire quelque chose pour lui ;)
On commence le refactoring de la classe Window en extrayant les deux méthodes
update_items
et update_player
:
class Window
# ...
def update
update_items
update_player
end
private
def update_items
unless @items.size >= 15
r = rand
if r < 0.035
@items.push(Smiley.new(:smiley_up))
elsif r < 0.040
@items.push(Smiley.new(:smiley_down))
end
end
@items.each(&:update)
@items.reject! {|item| item.y > WindowHeight }
end
def update_player
@player.go_left if Gosu::button_down?(Gosu::KbLeft)
@player.go_right if Gosu::button_down?(Gosu::KbRight)
@player.move
end
end
La nouvelle méthode update_items
mérite aussi sa petite extraction:
def update_items
populate_items
@items.each(&:update)
@items.reject! {|item| item.y > WindowHeight }
end
def populate_items
return if @items.size >= 15
type = rand
if type < 0.035
@items.push(Smiley.new(:smiley_up))
elsif type < 0.040
@items.push(Smiley.new(:smiley_down))
end
end
Maintenant populate_items
est toujours moche, et on sent que cette méthode
risque d’enfler dans l’avenir. Mais je pense que c’est le moment d’arrêter le refactoring
pour éviter de tomber dans l’overengineering
et aussi parce que
YAGNI !.
Pour finir, voici le contenu du jeu pour l’instant:
$ tree
.
├── assets
│ └── images
│ ├── background.png
│ ├── player.png
│ ├── smiley-green.png
│ └── smiley-yellow.png
├── main.rb
├── player.rb
├── smiley.rb
├── window.rb
└── z_order.rb
Le code et les assets se trouvent sur Github. La version précise pour cet article est la version 0.2.0.