Concevoir un jeu d'exploration dans l'espace - Partie 01
Ce tutoriel a été réalisé avec la version 3.1.7 du moteur Dina.
Introduction
Nous allons concevoir un petit jeu d'exploration de l'espace.
Le jeu 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
Mise en place de l'architecture
Eléments graphiques
J'ai pris pour habitude de séparer le code et les éléments graphiques (images, maps et font) et sonores (sons et musiques).
Si vous ne l'aviez jamais remarqué, c'est une pratique très répandue dans le monde des jeux vidéos.
Pour cela, j'ai créé un répertoire "datas" qui va contenir les images, les sons, etc. Bref, tout ce qui ne sera pas du code.
Ce répertoire sera complété par d'autres sous-répertoires en cours de développement.
Code
Du côté du code, je sépare les différents éléments en plusieurs thèmes.
- "menus" pour tout ce qui touche aux différents menus du jeu (options, crédits et menu principal),
- "dialogues" pour l'introduction et les animations entre les phases de jeu,
- "game" pour le jeu en tant que tel.
Mais on pourrait tout aussi bien laisser à la racine du répertoire ! Ce n'est qu'une de mes habitudes.
C'est à vous de trouver votre façon de vous organiser.
Débutons la création du jeu !
La premiere phase est de créer le fichier principal du jeu : main.lua
Pour pouvoir visualiser en temps réel les messages dans la console, nous allons inscrire au début les lignes suivantes :
-- La ligne ci-dessous sert a afficher les messages dans la console en temps réel
io.stdout:setvbuf('no')
Nous allons charger le moteur Dina en rajoutant simplement la ligne suivante :
local Dina = require("Dina")
Enfin, nous allons rajouter les 3 fonctions de Love (nécessaires et obligatoires) :
function love.load()
end
function love.update(dt)
end
function love.draw()
end
Pour l'instant, si vous exécutez ce code, vous obtiendrez un écran noir.
Normalement, vous ne devriez jamais avoir de messages d'erreur.
Si toutefois, c'est le cas, mettez en commentaire la ligne pour charger le moteur Dina et essayer à nouveau.
Si vous n'avez plus de message d'erreur, veuillez m'envoyer un mail en inscrivant le message d'erreur obtenu à support@dina.lacombedominique.com
Logo
On va commencer par afficher notre logo.
Comme j'aime bien séparer mon code pour qu'on puisse réutiliser chaque composant un peu n'importe où, on va se créer un nouveau fichier logo.lua dans le répertoire menus qui contiendra les informations suivantes :
local Logo = {}
local Dina = require("Dina")
function Logo.load()
end
function Logo:update(dt)
Dina:update(dt, false)
end
function Logo:draw()
Dina:draw(false)
end
return Logo
Retenez-bien cette structure, elle sera utilisée pour tous les fichiers.
Si on doit procéder autrement, je vous le préciserais.
On va également créer un répertoire images dans le répertoire datas et un répertoire logo dans le répertoire images.
Dans le répertoire logo, j'y ai copié le fichier Dina_Logo.png (disponible ici : logo Dina) qui représente le logo du moteur Dina.
Vous pouvez le changer par votre propre logo si vous le souhaitez.
Tout d'abord, on va devoir charger l'image pour pouvoir l'afficher. Pour cela, vous devez indiquer au moteur où se trouve l'image.
On va donc rajouter cette ligne dans la fonction Logo.load :
local image = Dina("Image", "datas/images/logo/Dina_Logo.png")
Attention : le chemin et le nom du fichier doivent etre exactement identique en tenant compte des majuscules.
Dans le cas contraire, vous pourriez rencontrer des erreurs lors du chargement des fichiers.
On va ensuite centrer l'image à l'écran.
Pour cela, nous devons récupérer la taille de l'image :
local iw, ih = image:getDimensions()
Ensuite, il ne nous reste plus qu'à calculer la nouvelle position :
local x = (Dina.width - iw) / 2
local y = (Dina.height - ih) / 2
image:setPosition(x, y)
On aurait aussi pu le faire en une seule ligne :
image:setPosition((Dina.width - iw) / 2, (Dina.height - ih) / 2)
A certains moments, il m'arrivera d'utiliser une forme contractée (sur une seule ligne) et parfois non.
Mais au final, cela ne fait aucune différence : on obtient le même résultat.
Ensuite, on doit retourner dans le fichier main.lua pour y faire quelques ajustements.
Un premier ajustement est de charger les contrôleurs (clavier et gamepad) dans love.load.
Donc, tout au début de la fonction love.load, avant toute autre ligne, on va rajouter la ligne suivante :
Dina:loadController()
Cette ligne a pour but de charger la gestion du clavier et du gamepad (mais pas de la souris).
Ensuite, il faut charger le fichier logo que nous venons de créer.
Comme nous voulons avoir plusieurs écrans (logo, menus et jeux), nous allons utiliser une machine à états.
Donc, nous ajoutons un nouvel état logo contenu dans le fichier menus/logo (celui que l'on vient de créer).
Dina:addState("logo", "menus/logo")
Ensuite, on indique quel est l'état courant (ici, logo) :
Dina:setState("logo")
Puis, on affiche les composants chargés dans le moteur dans la fonction love.draw :
Dina:draw()
On va en profiter pour rajouter une nouvelle variable et faire un changement dans la fonction love.update :
local maxdt = 1/60
function love.update(dt)
dt = math.min(dt, maxdt)
Dina:update(dt)
end
Voilà, en lançant le jeu, vous devez voir le logo en plein milieu de l'écran.
Menu principal
Architecture du fichier et ajout du MenuManager
Maintenant que nous avons affiché notre logo, nous allons créer notre menu principal dans le fichier menus/mainmenu.lua.
On reprend le même principe que pour le Logo : on charge le moteur Dina et on crée les fonctions load, update et draw.
Si vous avez fait un copier/coller depuis le fichier logo.lua, n'oubliez pas de renommer la variable Logo en MainMenu.
Dans la fonction MainMenu.load, nous allons créer un MenuManager.
Ce MenuManager contient déjà toutes les fonctions nécessaires pour la création et la gestion du menu principal.
local menu = Dina("MenuManager", 40)
Ajout des éléments du menu
On va ensuite indiquer le titre du jeu. Pour cela, rien de plus facile :
menu:addTitle("Dina Space Explorer", 50, "datas/font/Exo2-Regular.ttf", 60)
IMPORTANT : les versions avant la version 3.2.0 utilisaient la fonction
setTitleau lieu deaddTitle.
Le second paramètre correspond à la position en Y du titre. Je l'ai positionné à 50 pixels du haut de l'écran.
Les troisième et quatrième paramètres correspondent à la police de caractères (téléchargée à partir de https://fonts.google.com) et sa taille.
Après le titre, nous allons nous attaquer aux éléments du menu à afficher.
Notre menu principal va comporter les éléments suivants :
- Jouer
- Crédits
- Quitter
Pour cela, on va utiliser la fonction addItem qui demande les 6 paramètres suivants :
- le texte à afficher
- la police de caractères
- la taille de la police de caractères
- une fonction pour la sélection d'un item du menu
- une fonction pour la désélection d'un item du menu
- une fonction pour valider l'item du menu
On va se concentrer sur les 3 derniers paramètres.
Fonction pour la sélection d'un item du menu
Cette fonction a pour but d'effectuer des actions lors de la sélection d'un item de menu. Elle reçoit un unique paramètre : l'item du menu.
L'item du menu étant un composant de type "Text", vous pouvez utiliser l'ensemble des fonctionnalités de ce componsant (voir la documentation).
Pour notre tutoriel, nous allons faire quelque chose de simple : on va simplement changer la couleur du texte.
local function OnSelection(Item)
Item:setTextColor(Colors.LIME)
end
Fonction pour la désélection d'un item du menu
Cette fonction a pour but d'effectuer des actions lors de la désélection d'un item de menu. Elle reçoit un unique paramètre : l'item du menu.
L'item du menu étant un composant de type "Text", vous pouvez utiliser l'ensemble des fonctionnalités de ce componsant (voir la documentation).
Pour notre tutoriel, nous allons faire quelque chose de simple : on va simplement remettre la couleur du texte en blanc (couleur par défaut du composant Text).
local function OnDeselection(Item)
Item:setTextColor(Colors.WHITE)
end
Fonction pour valider l'item du menu
Cette fonction a pour but de déclencher les actions lors de la validation de l'item. Elle n'attend aucun paramètre.
Notez bien que vous devrez avoir autant de fonctions de validation que vous avez d'items dans votre menu.
local function LaunchGame()
Dina:setState("game")
end
local function DisplayCredits()
Dina:setState("credits")
end
local function Quit()
love.event.quit(0)
end
Vous l'avez sûrement remarqué : nous avons indiqué les états game et credits dans les fonctions ci-dessus. Ils seront créés ultérieurement.
Maintenant que nous avons toutes nos fonctions, nous pouvons créer nos items du menu.
menu:addItem("Jouer", "datas/font/Righteous-Regular.ttf", 25, OnSelection, OnDeselection, LaunchGame)
menu:addItem("Crédits", "datas/font/Righteous-Regular.ttf", 25, OnSelection, OnDeselection, DisplayCredits)
menu:addItem("Quitter", "datas/font/Righteous-Regular.ttf", 25, OnSelection, OnDeselection, Quit)
Si vous lancez le jeu maintenant, vous obtiendrez toujours le logo. Pour aller au menu principal, il faut encore rajouter quelques lignes dans le fichier logo.lua et une ligne dans main.lua.
Maintenant, nous allons aborder une partie simple à coder mais plus complexe à comprendre : la définition des touches d'action.
Définition des touches d'action
Clarifions ce que j'entends par "touches d'action" : il s'agit d'associer une ou plusieurs touches (ou boutons) à une action donnée.
On peut définir une touche (ou bouton) à plusieurs actions. Mais c'est à vous de bien identifier le besoin.
Dans l'état actuel du jeu, notre objectif est de pouvoir passer au menu principal si le joueur a appuyé sur une touche du clavier ou un bouton du gamepad.
Pour cela, nous devons avoir une fonction qui va nous permettre de changer l'état pour menu comme ceci :
function Logo:toMenu()
Dina:setState("menu")
end
Il est obligatoire que la fonction d'action appartienne à un objet donné.
Ici, j'ai créé la fonctiontoMenudans l'objetLogo
Ensuite, nous devons rajouter dans la fonction Logo.load la ligne ci-dessous :
Dina:setActionKeys(Logo, "toMenu", "pressed", {"Keyboard", "all"}, {"Gamepad", "all"})
La fonction setActionKeys comprend 3 paramètres obligatoires et une suite de une ou plusieurs touches ou boutons :
- L'objet qui doit obligatoirement contenir la fonction à exécuter
- Le nom de la fonction à exécuter
- L'état de la touche parmi :
pressed(appuyé),released(relaché) oucontinuous(en continu)
- La suite de touches ou boutons
Dans la ligne de code ci-haut, nous avons indiqué que si le joueur appuie sur une des touches du clavier ({"Keyboard", "all"}) ou un des boutons/sticks du gamepad ({"Gamepad", "all"}) alors la fonction Logo:toMenu sera exécutée.
Bien que nous ayons mis en place tout ce qu'il faut pour aller au menu, on doit encore rajouter une simple ligne dans le fichier main.lua pour qu'il puisse être chargé.
Juste en dessous de l'ajout de l'état logo, on va rajouter la ligne suivante :
Dina:addState("menu", "menus/mainmenu")
En lançant le jeu, vous pourrez désormais passer au menu principal après avoir appuyé sur une touche du clavier ou un bouton du gamepad.
Maintenant, faisons la même chose pour le menu principal.
Le MenuManager utilise la fonction setActionKeys du moteur Dina mais des raccourcis ont été faits pour simplifier son utilisation. Nous allons les utiliser.
Pour sélectionner l'item du menu suivant, il existe la fonction setNextKeys. Pour l'item précédent, il y a la fonction setPreviousKeys. Et enfin, pour validation la sélection, il y a la fonction setValidateKeys.
Ces 3 fonctions sont également paramétrées avec des touches par défaut :
setNextKeysavec :- la flèche vers le bas du clavier
- l'axe gauche vers le bas du gamepad
setPreviousKeysavec- la flèche vers le haut du clavier
- l'axe gauche vers le haut du gamepad
setValidateKeysavec- la touche espace du clavier
- le bouton A du gamepad
Toutefois, si vous souhaitez ajouter une nouvelle touche, vous devez redéfinir la totalité des touches comme ci-dessous à la fin de la fonction load :
-- Touches pour aller au menu suivant
menu:setNextKeys( { "Gamepad", "lefty", 1 }, { "Gamepad", "dpdown" }, { "Keyboard", "down" } )
-- Touches pour aller au menu précédent
menu:setPreviousKeys( { "Gamepad", "lefty", -1 }, { "Gamepad", "dpup" }, { "Keyboard", "up" } )
-- Touches pour valider le menu sélectionné
menu:setValidateKeys( { "Gamepad", "a" }, { "Keyboard", "return" } )
Dans le code ci-dessus, j'ai rajouté la croix vers le bas (dpdown) pour aller à l'item suivant, la croix vers le haut (dpup) pour aller à l'item précédent et la touche Entrée (return) à la place de la barre d'espace.
Vous vous interrogez probablement sur le 1 et le -1 après lefty. Il s'agit simplement de la direction de l'axe :
- sur l'axe Y (
leftyourighty) :- 1 = vers le bas
- -1 = vers le haut
- sur l'axe X (
leftxourightx) :- 1 = vers la droite
- -1 = vers la gauche
Si vous lancez le jeu, vous allez voir le logo Dina (ou votre logo si vous l'avez changé). Puis, après avoir appuyé sur une touche, vous verrez le menu principal comme ceci :

En appuyant sur les flèches du haut ou du bas, vous verrez l'item du menu sélectionné devenir vert fluo (Colors.LIME).
Nous allons mettre de côté les menus Options et Crédits et concevoir (enfin) le jeu.
Conception du jeu
Pour rappel, notre objectif est de concevoir un jeu d'exploration spatial. Souhaitant garder un tutoriel assez simple, on va partir sur un jeu d'esquive d'astéroïdes.
On va créer le fichier game.lua dans le répertoire game.
En plus de déclarer la variable Dina pour stocker le moteur, on va ajouter plusieurs variables :
Shipqui servira pour afficher notre vaisseau; ce sera un composant ImageLimitsqui définira les limites min et max à ne pas dépasser (dans notre cas, les bords de l'écran)Movequi servira pour la gestion du déplacement du vaisseau
local Ship = {}
local Limits = {}
local Move = {}
On va stocker, dans la variable Move, les informations suivantes :
Move.speed = 2
Move.angle = 10
La variable Move.speed contient la vitesse de déplacement du vaisseau (en pixels) et Move.angle, la rotation maximum en degrés à appliquer au vaisseau lors du déplacement.
Comme pour tout le reste, j'ai décomposé les sous-répertoires de datas/image par thème.
Nous allons rajouter un dossier game où j'y ai mis toutes les images nécessaires pour le jeu.
Dans la fonction Game.load, nous allons créer notre vaisseau.
Ship = Dina("Image", "datas/images/game/ship.png")
Pour que ce soit plus simple à gérer plus tard, on va utiliser le centre de l'image comme point d'origine en rajoutant simplement la ligne suivante :
Ship:centerOrigin()
Maintenant, nous allons récupérer les dimensions de l'image :
local sw, sh = Ship:getDimensions()
Pour ensuite, pouvoir positionner au centre et en bas de l'écran notre vaisseau (on a retiré 5 pixels pour afficher entièrement l'image du vaisseau).
Ship:setPosition(Dina.width/2, Dina.height - sh/2 - 5)
Maintenant que notre vaisseau est bien positionné à l'écran, on va faire en sorte de pouvoir le déplacer.
Comme nous l'avons vu tout à l'heure avec le logo, on va indiquer les fonctions pour :
- aller à gauche
-- Fonction pour déplacer le joueur vers la gauche
function Move:left(Direction)
self:move(math.abs(Direction) * -1)
end
- aller à droite
-- Fonction pour déplacer le joueur vers la droite
function Move:right(Direction)
self:move(math.abs(Direction))
end
La fonction fournie en paramètre à la fonction
setActionKeysreçoit systématiquement deux paramètres qui correspondent à :
- la force appliquée :
- entre -1 et 1 pour les axes ou entre 0 et 1 pour les gachettes du gamepad
- la valeur 1 pour les autres boutons du gamepad et les touches du clavier
- le delta-time :
- temps écoulé entre l'affichage de 2 frames
La valeur ne peut dépasser 1/60 puisque nous l'avons limité dans le fichiermain.lua.De plus, cette fonction doit obligatoirement être présente au sein d'une table (dans notre cas, la table
Move).
Comme le code pour le déplacement est identique pour les 2 directions, je l'ai regroupé dans une seule et unique fonction move :
-- Fonction pour déplacer le joueur dans la direction donnée
function Move:move(Direction)
-- Calcul de la rotation du vaisseau
local rotation = self.angle * Direction
-- Déplacement du vaisseau
Ship.x = Ship.x + self.speed * Direction
-- Vérification des limites
if Ship.x < Limits.min then
Ship.x = Limits.min
rotation = 0
elseif Ship.x > Limits.max then
Ship.x = Limits.max
rotation = 0
end
-- Application de la rotation
Ship:setRotation(rotation)
end
Voulant donner une impression de mouvement, j'ai appliqué une rotation à l'image d"un angle de 20 degrés selon la direction du déplacement.
Cette rotation varie selon la "force" appliquée sur l'axe du gamepad (le clavier et la croix du gamepad étant soit 0%; soit 100%).
La même "force" est appliquée à la vitesse de déplacement.
Ensuite, on teste si le vaisseau a dépassé l'une ou l'autre des limites fixées. Si c'est le cas, on change la position du vaisseau pour le positionner sur la limite et on supprime la rotation (valeur 0).
Comme on ne sait pas si le joueur a fini d'appuyer sur une touche ou pas, on va systématiquement remettre la rotation du vaisseau à 0 au début de la fonction game:update :
Ship:setRotation(0)
Maintenant, nous allons définir les valeurs pour les limites de déplacements dans la fonction game.load.
Limits.min = sw/2
Limits.max = Dina.width - sw/2
Il ne nous reste plus qu'à rajouter les touches d'action :
Dina:setActionKeys(Move, "left", "continuous", {"Keyboard", "left"}, {"Gamepad", "leftx", -1}, {"Gamepad", "dpleft"})
Dina:setActionKeys(Move, "right", "continuous", {"Keyboard", "right"}, {"Gamepad", "leftx", 1}, {"Gamepad", "dpright"})
J'ai utilisé le mode continuous pour que le déplacement se fasse quand on reste appuyé sur une touche.
Vous devriez obtenir ceci :
local Game = {}
local Dina = require("Dina")
local Ship = {}
local Limits = {}
local Move = {}
Move.speed = 2
Move.angle = 10
-- Fonction pour déplacer le joueur dans la direction donnée
function Move:move(Direction)
-- Calcul de la rotation du vaisseau
local rotation = self.angle * Direction
-- Déplacement du vaisseau
Ship.x = Ship.x + self.speed * Direction
-- Vérification des limites
if Ship.x < Limits.min then
Ship.x = Limits.min
rotation = 0
elseif Ship.x > Limits.max then
Ship.x = Limits.max
rotation = 0
end
-- Application de la rotation
Ship:setRotation(rotation)
end
-- Fonction pour déplacer le joueur vers la gauche
function Move:left(Direction)
self:move(math.abs(Direction) * -1)
end
-- Fonction pour déplacer le joueur vers la droite
function Move:right(Direction)
self:move(math.abs(Direction))
end
function Game.load()
Ship = Dina("Image", "datas/images/game/ship.png")
Ship:centerOrigin()
local sw, sh = Ship:getDimensions()
Ship:setPosition(Dina.width/2, Dina.height - sh/2 - 5)
Limits.min = sw/2
Limits.max = Dina.width - sw/2
Dina:setActionKeys(Move, "left", "continuous", {"Keyboard", "left"}, {"Gamepad", "leftx", -1})
Dina:setActionKeys(Move, "right", "continuous", {"Keyboard", "right"}, {"Gamepad", "leftx", 1})
end
function Game:update(dt)
-- on remet la rotation à 0 du vaisseau
Ship:setRotation(0)
Dina:update(dt, false)
end
function Game:draw()
Dina:draw(false)
end
return Game
Un dernier ajout dans le fichier main.lua et on pourra lancer le jeu.
Dans la fonction love.load, on va rajouter un nouvel état pour pouvoir charger le jeu :
Dina:addState("game", "game/game")
Voici ce que vous devriez obtenir en lançant le jeu :

Le code de ce tutoriel se trouve ici.
Me contacter