Concevoir un jeu d'exploration dans l'espace - Partie 05
Ce tutoriel a été réalisé avec la version 3.1.7 du moteur Dina.
Rappel des tutoriels précédents
Notre objectif est de concevoir un jeu qui se composera des éléments suivants :
- un écran pour afficher un logo (celui de votre futur studio, par exemple)
- un écran principal
- un écran pour les crédits (toujours sympa de citer les personnes qui nous ont aidées, par exemple les créateurs d'images ou de musique)
- le jeu en lui-même qui sera découpé comme suit :
- une introduction (pour essayer de mettre le joueur dans l'ambiance du jeu)
- la phase d'exploration qui se limitera à éviter des astéroïdes
- une animation lorsqu'on decouvrira une planète
- un mini-jeu pour se poser sur la planète (de type Lunar-landing)
- un écran de fin
Pour l'instant, voici ce que nous avons réalisé jusqu'à présent :
- un écran pour afficher un logo
- un écran principal
- un début de jeu qui contient pour le moment :
- une introduction
- la phase d'exploration où on doit éviter des astéroïdes
- une animation lorsqu'on découvre la planète
- un mini-jeu pour se poser sur la planète (de type Lunar-landing)
- un écran de fin
Interface graphique utilisateur (partie 2)
Lorsque nous atterrissons, nous avons défini une valeur fixe dans MAX_LANDING_SPEED.
Cependant, le joueur ne connait pas la vitesse du vaisseau.
Au lieu de rajouter la valeur de la vitesse directement sur l'écran, je vous propose de rajouter une barre de vitesse.
Selon sa couleur, le joueur aura une idée de sa vitesse.
- Rouge : le vaisseau va beaucoup trop vite
- Orange : le vaisseau va trop vite mais se rapproche de la bonne vitesse
- Vert : la vitesse est dans la norme
On fera également en sorte qu"elle s'affiche par-dessus le vaisseau pour qu'elle soit toujours visible.
On va rajouter 2 nouvelles variables :
local SpeedBar = {}
local SPEEDBAR_LENGTH = 150
Nous allons l'initialiser dans la fonction Landing.load comme suit :
SpeedBar = Dina("Progressbar", (Dina.width -SPEEDBAR_LENGTH)/2, 5, --positionSPEEDBAR_LENGTH, 20, --largeur et hauteur 0, MAX_SHIP_SPEED, --valeurs min et max Colors.GREEN, --couleur de la barre Colors.ORANGERED, --couleur de la bordure nil, --pas de couleur du fond 10, --ordre d'affichage (Z-Order) 2) --épaisseur de la bordure
Ensuite, on va changer sa valeur et sa couleur dans Landing:update, juste après le déplacement du vaisseau, avec le code suivant :
SpeedBar:setValue(Ship.vy)
if Ship.vy < MAX_LANDING_SPEED then
SpeedBar:setColor(Colors.GREEN)
elseif Ship.vy < (MAX_SHIP_SPEED - MAX_LANDING_SPEED) then
SpeedBar:setColor(Colors.YELLOW)
else
SpeedBar:setColor(Colors.RED)
end
Et pour finir, quand l'atterrissage est réussi, on masque la barre (on en profite pour masquer les vies) :
--On masque les vies et l'indicateur de vitesse
LifeGroup:setVisible(false)
SpeedBar:setVisible(false)
Décollage du vaisseau
Tout d'abord, nous allons nous créer une nouvelle fonction Move:takeOff qui sera exécuté lorsque le joueur appuiera sur une touche pour déclencher le décollage.
function Move:takeOff()
if Ship.landed then
Ship.takeOff = true
end
end
On vérifie si le vaisseau est bien posé pour ne pas déclencher le décollage par inadvertance.
Comme vous l'avez remarqué, on a rajouté un nouvel attribut au vaisseau que l'on va initialiser dans Landing.load.
Je vous laisse le faire.
L'étape suivante est de définir les touches d'action pour lancer le décollage dans Landing.load :
Dina:setActionKeys(Move, "takeOff", "released", {"Keyboard", "space"}, {"Gamepad", "a"})
Ensuite, on a une modification importante à faire dans Landing:update.
Au tout début de la fonction (1ère ligne), on va rajouter le test ci-dessous :
if not Ship.takeOff then
Puis, à la fin de la fonction, on va rajouter :
else -- if not Ship.takeOff
end --if not Ship.takeOff
Les commentaires sont là pour me rappeller quel est le test qui lui est associé.
Tout ce qu'il nous reste à faire, c'est de faire déplacer la fusée vers le haut.
Le code ci-dessous est à placer entre le else -- if not Ship.takeOff et le end --if not Ship.takeOff :
--Animation de décollage
Engine:setVisible(true)
Ship.vy = Ship.vy - (0.8 * dt)
--Déplacement du vaisseau
MoveShip()
local sx, sy = Ship:getPosition()
local ew, eh = Engine:getDimensions()
if sy < 0 - eh then
Dina:setState("game")
end
Si on ne laisait que ça, lorsqu'on lancerait de nouveau le mini-jeu, il y aurait de nombreux problèmes.
Juste avant le changement d'état, on va remettre à zéro certaines données.
Pour cela, nous allons créer la fonction ResetDatas comme suit :
local function ResetDatas()
Ship = {}
Engine = {}
LifeGroup = {}
Terrain = {}
SpeedBar = {}
end
Et enfin, nous allons l'ajouter avant le passage à l'état game :
if sy < 0 - eh then
ResetDatas()
Dina:setState("game")
end
On va également l'appliquer lors du passage vers le gameover que l'on avait déjà modifié :
if nbLifes < 1 then
ResetDatas()
Dina:setState("gameover")
end
Ajout du score
Pour le calcul du score, on va faire ça simple :
- On évite un astéroïde : + 100 points
- On atterrit sur la planète : + 200 points * nombre de vies restantes
Comme il y a 16 astéroïdes qui apparaissent, cela fera un total de 1600 points.
La première difficulté est de transférer (ou récupérer) le score du jeu vers le mini-jeu. Pour cela, nous sommes aidés par le moteur Dina.
Créons d'abord une variable qui va permettre de contenir (et afficher) le score :
local Score = {}
Nous allons utiliser un composant Text qui est initialisé comme suit :
Score = Dina("Text", Dina:getGlobalValue("score") or 0, Dina.width/2, 20)
Score:setAlignments("right", "up")
Score:setDimensions(Dina.width/2 - 10, 15)
Nous alignons le texte à droite de l'écran, juste en dessous de la barre de progression.
Un ajustement des dimensions du composant est fait lors de la première insertion de texte (c'est notre cas) ou après avoir effacé le texte.
C'est pour cela que nous devons redéfinir les dimensions après la création du composant.
Il ne nous reste plus qu'à comptabiliser le score :
-- Si l'astéroïde est en dessous de l'écran, on le supprime
if (ay - ah) > Dina.height then
Score:setContent(Score:getContent() + 100)
table.remove(Asteroids, i)
end
Ensuite, on doit calculer le score si on a atterri dans le mini-jeu.
Comme tout à l'heure; on doit d'abord créer une variable qui va permettre de contenir (et afficher) le score :
local Score = {}
Et on utilise un composant Text qui est initialisé comme suit :
Score = Dina("Text", Dina:getGlobalValue("score") or 0, Dina.width/2, 5) --ici, on le positionne tout en haut
Score:setAlignments("right", "up")
Score:setDimensions(Dina.width/2 - 10, 15)
On comptabilise le score si on a atterrit et on met à jour la donnée globale :
if Ship.vy < MAX_LANDING_SPEED then -- Atterrissage réussi Ship.landed = true Ship.vx = 0 Ship.vy = 0 local y = GroundY - sh / 2 Ship:setPosition(sx, y) Ship:setRotation(0) Engine:setVisible(false) Engine:setPosition(sx, y) Engine:setRotation(0) SpeedBar:setVisible(false) --Comptabilisation du score Score:setContent(Score:getContent() + (200 * LifeGroup:nbComponents())) --Mise à jour de la donnée globale Dina:setGlobalValue("score", Score:getContent())else -- if Ship.vy < MAX_LANDING_SPEED
On va en profiter pour changer la condition de départ de la fonction Landing:update :
function Landing:update(dt)
if not Ship.landed then --Remplace if not Ship.takeOff then
-- On masque le moteur
Engine:setVisible(false)
Et on remplace également le else correspondant :
end -- if Ship.vy < MAX_LANDING_SPEED
end -- if (GroundY - (sy + sh/2)) < MIN_GROUND_LENGTH
elseif Ship.takeOff then --remplace else --if not Ship.takeOff
-- Animation de décollage
Engine:setVisible(true)
Sans ces changements le score s'accumulerait tant que le joueur n'a pas décollé.
La dernière chose à faire est d'afficher le score lors du GameOver.
Pour cela, rien de plus simple : on ajoute un nouvel élément dans le menu qui contiendra le score.
function GameOver.load()
local menu = Dina("MenuManager", 50) --on réduit l'écart entre les items du menu passant de 75 à 50
menu:setTitle("Jeu terminé", Dina.height/4, "datas/font/Righteous-Regular.ttf", 60, Colors.WHITE, true, Colors.SILVER, 5, 5)
-- Ajout de l'image de fond
local rnd = love.math.random(2)
menu:addImage("datas/images/gameover/blackhole_"..rnd..".jpg", Dina.width/2, Dina.height/2, true)
-- Ajout du texte à afficher à l'écran
menu:addItem("Score final : "..Dina:getGlobalValue("score"), "datas/font/Righteous-Regular.ttf", 40, ReturnToMenu)
menu:addItem("La civilisation s'est éteinte avec vous...", "datas/font/Exo2-Regular.ttf", 40)
menu:addItem("Appuyer sur 'A' pour revenir au menu principal", "datas/font/Exo2-Regular.ttf", 16)
-- Définition des touches
menu:setNextKeys({"Keyboard", "all"}, {"Gamepad", "a"})
end
Puis, nous effaçons le score quand on quitte le GameOver :
local function ReturnToMenu()
Dina:setGlobalValue("score", nil)
Dina:setState("logo")
end
Voilà, nous avons un jeu avec un gameplay complet.
Ecran des crédits
Finissons le dernier écran du menu : les crédits.
On crée un fichier credits.lua dans le répertoire menus et on reprend la structure du Logo pour la coller dans le nouveau fichier en renommant Logo par Credits.
Comme toujours, on va commencer par créer les variables dont nous avons besoin :
- CreditGroup pour stocker les crédits
- delay
- timer
- activeKey
- Speed
local CreditGroup = {}
local delay
local timer
local activeKey
local Speed = 75
On a besoin en plus d'une fonction qui permet de lire un fichier. Je vous fournie le code complet ci-dessous :
local function ReadFile(File, SkipLineChar)
local toSkip = false
local str = {}
for line in love.filesystem.lines(File) do
toSkip = IsStringValid(SkipLineChar) and string.sub(line, 1, 1) == SkipLineChar
if not toSkip then
table.insert(str, line)
end
end
return str
end
Passons à l'initialisation :
function Credits.load()
--Création du groupe
CreditGroup = Dina("Group")
--Lecture du fichier des crédits sans les liens
local Lignes = ReadFile("datas/credits.txt", "[")
--Parcours du contenu du fichier
for _,ligne in pairs(Lignes) do
local gw, gh = CreditGroup:getDimensions()
--Création d'un composant Text par ligne
local text = Dina("Text", ligne,0,gh + 5)
--On l'affiche centré sur toute la largeur de l'écran
text:setDimensions(Dina.width, text:getHeight() + 3)
text:setAlignments("center", "center")
CreditGroup:add(text)
end
--Positionnement du groupe en dessous de l'écran
CreditGroup:setPosition(0, Dina.height + 1)
--Définition les touches d'actions
Dina:setActionKeys(Credits, "quit", "pressed", {"Keyboard", "all"}, {"Gamepad", "all"})
--Initialisation des variables
delay = 2
timer = 0
activeKey = false
end
Puis, lors de l'exécution de Credits:update :
function Credits:update(dt)
if not activeKey then
timer = timer + dt
if timer > delay then
activeKey = true
end
end
local gx, gy = CreditGroup:getPosition()
gy = gy - Speed * dt
CreditGroup:setPosition(gx, gy)
local gw, gh = CreditGroup:getDimensions()
if gy + gh < Dina.height/2 then
Credits:quit()
end
Dina:update(dt, false)
end
Et pour finir, on crée la fonction qui permet de revenir au menu principal :
function Credits:quit()
if activeKey then
Dina:setState("menu")
end
end
Voilà, notre série de tutoriels pour la création d'un jeu d'exploration spatiale est terminée.
J'espère que vous l'avez apprécié.
N'hésitez pas à me transmettre vos remarques et/ou suggestions.
A bientôt pour de nouvelles aventures !
Le code de ce tutoriel se trouve ici.
Me contacter