Au début, il n’y avait rien. Puis le dev créa la map, et il vit que c’était bien. Mais le dev la trouva vite vide, alors il y mit un joueur, qu’il fit à son image. Et le dev se dit que c’était bien. Mais comme le joueur était seul, il ajouta des obstacles et des collisions. Et cela le combla. Car Down the Mines prenait vie.
Maintenant, il nous faut commencer à parler à l’utilisateur. Aussi, j’ai décidé de gérer dès à présent des éléments comme l’UI, le système de pause, la musique et même le game over dans mon jeu vidéo.
Une UI de jeu vidéo basique pour communiquer avec le joueur
Notre jeu est un rectangle, représentant la carte sur laquelle se déplace le joueur, entouré de noir. J’avais volontairement laissé cet espace en sachant que j’y intègrerai les scores à afficher. Et l’heure est venue !
Les éléments importants pour le joueur sont : son score (représenté par l’étage auquel il se trouve, le but du jeu étant d’avancer le plus profondément possible) ; ses points de vie ; ses points de fatigue ; son argent.
J’ai fait le plus simple possible. J’ai affiché les 4 nombres avec une icône à chaque fois. En changeant un peu la couleur de l’arrière-plan pour évoquer la roche.
Pour l’instant l’espacement n’est pas idéal, mais ça se règlera plus tard.
Des écrans de pause et de game over
Connaître ses stats, c’est bien, mais connaître l’état du jeu, c’est mieux. Et le jeu n’a que 3 états possibles : en cours, en pause, terminé. Aussi, il était important de permettre, déjà, de mettre le jeu en pause, ce que j’ai codé simplement en permettant au joueur d’y accéder en appuyant sur la touche Echap, et ensuite de l’afficher. Aussi, je gère cela au niveau du jeu.
Je trouve la nouvelle structure du jeu très parlante, et sa nouvelle méthode Update()
également.
type Game struct {
Level int
Mapp *Map
Player *Player
UI *UI
dialogs []Dialog
isPaused bool
isOver bool
isJustOver bool
sound *SoundManager
}
func (game *Game) Update() error {
if !game.isOver {
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
if game.isPaused {
game.isPaused = false
game.sound.Resume()
} else {
game.isPaused = true
game.sound.Pause()
}
} else if !game.isPaused {
game.UI.Update(game)
if !game.hasDialog() {
game.Player.Update(game)
} else {
game.dialogs[0].Update(game)
}
if game.Player.isDead() {
game.isOver = true
game.isJustOver = true
}
}
} else {
if game.isJustOver {
game.UI.Update(game)
game.dialogs = make([]Dialog, 0)
game.isJustOver = false
game.sound.Pause()
game.PlaySingle("game_over")
}
if inpututil.IsKeyJustPressed(ebiten.KeyEnter) {
game.reset()
}
}
return nil
}
Le dialogue est une étape importante de la vie, et du jeu aussi
L’autre moyen d’informer le joueur sur ce qu’il se passe, c’est d’afficher des dialogues, autrement dit des boîtes de texte qui apparaissent à l’écran. Pour ça, rien de plus simple, on les ajoute à la structure du jeu, comme certains d’entre vous l’ont peut-être remarqué dans l’extrait de code précédent.
Ma structure Dialog
est on ne peut plus simple : un texte initial, découpé en plusieurs lignes de X caractères, et on compte la ligne à laquelle on se trouve actuellement. On ajoute un Timer
qui suit l’avancée de l’affichage du Dialog
.
Un Timer
est une structure qui sert à compter le temps. « Oui, c’est évident ! » me direz-vous. Eh bien pas tant que ça.
En fait, le temps est une notion toute relative dans un jeu, et pas seulement parce qu’Einstein l’a dit au siècle dernier. En fait, dans un jeu on a tendance à compter non pas le temps qui passe mais les frames ou les ticks. Sans rentrer dans le détail des différences entre ces deux termes, disons simplement que notre jeu est, à l’instar d’une vidéo, une suite d’images qui s’enchainent assez vite pour sembler animées.
Sauf que, pendant notre jeu, les ticks peuvent survenir plus ou moins rapidement selon s’il y a beaucoup de calculs ou non. Aussi, il est important pour nous de compter les ticks et, pour suivre le temps qui passe, de se référer à la vitesse actuelle de rafraichissement, donnée en FPS (frames per second) ou TPS (ticks per second). Et c’est à ça que sert un Timer
.
type Timer struct {
currentTicks int
targetTicks int
hasJustFinished bool
}
func (timer *Timer) Update(l string) {
if timer.hasJustFinished {
timer.hasJustFinished = false
}
if timer.currentTicks < timer.targetTicks {
timer.currentTicks++
if timer.IsReady() {
timer.hasJustFinished = true
}
}
}
Je disais donc, notre Dialog
avance grâce à un Timer
. Mais vous me demanderez peut-être pourquoi. Eh bien pour donner un effet « machine à écrire ». Je ne souhaite pas que le texte apparaissent d’un coup à l’écran, mais semble plutôt s’écrire en temps réel. Aussi, je mets un Timer
qui fait en sorte que le texte s’affiche progressivement, en 1,5 seconde.
Ne reste plus qu’à afficher le texte dans un grand rectangle blanc à bordure noire, ce qui n’est pas aussi facile que ce que ça peut sembler.
En effet, la bordure noire est un rectangle, le rectangle blanc également, mais ensuite… où écrire le texte ? Il me faut calculer la taille du texte pour pouvoir le centrer dans le rectangle.
Mais une fois tout ceci fait, le résultat me plait bien. Et vous, qu’en pensez-vous ?
A défaut d’ambiance visuelle dans le jeu, Down the Mines profite d’une ambiance sonore
Je sais que vous vous demandiez ce qu’était cette dernière propriété de la structure Game
mise précédemment :
type Game struct {
Level int
Mapp *Map
Player *Player
UI *UI
dialogs []Dialog
isPaused bool
isOver bool
isJustOver bool
sound *SoundManager
}
Eh bien ma foi, comme son nom l’indique, il s’agit de ce qui me permet de joueur un son pendant le jeu ! Pour être exact, il s’agit d’une structure qui a pour but de me faciliter l’accès à tous les effets sonores, ainsi que de jouer les sons, qu’ils soient à jouer en boucle ou à l’unité.
Je dois avouer que la partie « son » du jeu ne m’était aucunement familière. Je vais même carrément dire que je n’avais aucune idée de ce que cela impliquait. J’avais déjà pu faire jouer du son à un navigateur en JavaScript, mais rien à voir avec Golang ou Ebiten, et j’avais donc peur de devoir y aller à l’aveugle.
Aussi, je dois remercier l’auteur (les auteurs ?) d’un article, qui m’a vraiment permis de comprendre ce que j’allais devoir faire et comment : Making Your Games More Immersive Adding Sound Effects.
Pour résumer ce qu’il s’y dit, nous devons créer un contexte de diffusion sonore, et ensuite 2 logiques : une pour jouer un son une fois (comme pour un effet), une pour jouer un son en boucle (pour une ambiance sonore par exemple).
Je crois qu’ici un peu de code peut parler mieux que mes mots, alors voici un extrait de mon SoundManager :
type SoundManager struct {
audioContext *audio.Context
loops map[string]*audio.Player
singles map[string]*audio.Player
actualLoop string
}
func (sm *SoundManager) PlayLoop(name string) {
if sm.actualLoop != "" {
sm.loops[sm.actualLoop].Pause()
}
sm.loops[name].Play()
sm.actualLoop = name
}
func (sm *SoundManager) Pause() {
sm.loops[sm.actualLoop].Pause()
}
func (sm *SoundManager) Resume() {
sm.loops[sm.actualLoop].Play()
}
func (sm *SoundManager) PlaySingle(name string) {
var err = sm.singles[name].Rewind()
utils.HandleErr(err)
sm.singles[name].Play()
}
Il ne me restait dès lors plus qu’à trouver des sons. Je suis donc allé sur des librairies de sons en ligne pour les effets sonores, et j’ai également utilisé une IA générative, Beatoven.ai, pour me créer un son d’ambiance. Vous me direz ce que vous en pensez ? Je lui ai demandé, je cite :
Une musique de plusieurs minutes, qui peut se répéter en boucle, et qui rend une ambiance calme, concentrée sur l’exploration de souterrains un peu sombres
Le résultat est, je trouve, pas mal du tout. On peut décider d’entendre ou non la musique, elle boucle bien, n’est ni trop intrusive ni trop effacée, … Bref j’aime bien ! Je vous mets ici la musique que vous pourrez écouter pour vous endormir en pensant au jeu. 😄
Résumé
Si on résume, on a donc ajouté, dans notre jeu vidéo :
- L’affichage des scores
- Des écrans de pause et de game over
- Des dialogues
- De la musique
Continuer sa lecture
Cet article s’inscrit dans une série d’articles qui traitent de la création de mon jeu, Down the Mines, que vous pouvez par ailleurs retrouver en ligne sur son site dédié.
Vous pouvez retrouver l’article précédent de la série, qui parlait de la mise en place de la structure du jeu vidéo, ainsi que du système de collisions et s’intitulait Down the Mines : gentille structure et méchantes collisions de notre jeu !.
Le prochain article parlera de la gestion des outils du joueur, et j’en mettrai ici le lien dès qu’il sera paru.