Créer un petit jeu de plateforme avec Löve
Posté le 19/03/2021 dans Lua
On fait suite au précédent article du blog qui introduisait le développement de jeux vidéo en Lua avec Löve. Tu vas voir comment créer un petit jeu de plateforme inspiré de Super Meat Boy et de Disc Room uniquement avec Löve.
Disc Room Escape
Le jeu s'appelle Disc Room Escape et est jouable sur itch.io. Le but est simplement de traverser les différents niveaux, sans mourir, dans la limite de temps disponible. Il faudra sauter sur des plateformes et éviter des scies mortelles.
Ici, pas besoin de librairies et outils annexes. Tout sera fait uniquement avec Löve.
Code source
Le code source est intégralement disponible sur Github. N'hésite pas à le récupérer, à jouer avec, à le modifier et à te l'approprier.
Je vais t'expliquer dans les grandes lignes son fonctionnement. Suis les liens vers les fichiers sources pour les comprendre. Sur cet article, je ne mettrais en exemple que certains bouts de code intéressants pour éviter de trop le surcharger d'information.
Etat du jeu
On va commencer par créer un système qui gère l'état de notre jeu. Dans les prochains fichiers, on va beaucoup utiliser setmetatable, qui permet de gérer des sortes de classes en Lua.
Tu voudras avoir un écran d'accueil où tu lances le jeu, un écran avec le jeu en cours, un écran de fin lors de la victoire et un écran de fin lors de la défaite. Ces écrans vont utiliser les fonctions update et draw de Löve pour se mettre à jour, afficher les éléments et gérer les touches.
function mt:update(dt)
if love.keyboard.isDown('return') or (Joystick and Joystick:isGamepadDown('start')) then
GameState.setCurrent('Play', GAME_LEVEL_START)
local doorSound = love.audio.newSource(SOUND_DOOR, "static")
doorSound:play()
end
end
function mt:draw()
love.graphics.setNewFont(12)
love.graphics.setColor(1, 1, 1, 1)
love.graphics.setBackgroundColor( 104/255, 124/255, 133/255 )
love.graphics.draw(self.img, 70, 0, 0, 0.4, 0.4)
love.graphics.print({{0,0,0,1}, 'Press [enter] or [start] to start the game !'}, 75, 220)
end
Tous ces écrans vont être manipulables grâce à une classe GameState qui va se charger de gérer la transition des états. Il suffit par exemple, d'utiliser la commande suivante pour changer de niveau :
GameState.setCurrent('Play', self.level_num + 1)
Le fichier principal de Löve va juste se charger de démarrer le jeu sur l'écran d'accueil et d'initier le joystick, le fichier de configuration et les constantes du jeu.
Animation et assets
Tous les assets du jeu, sons et images, sont disponibles ici. Tu peux les modifier selon tes propres envies. Tu auras besoin de deux petits helpers, un pour gérer les animations et un autre pour gérer les assets.
Le monde
Chaque élément du jeu sera ajouté à un monde, qui va se charger de vérifier en continue la position des éléments et leurs collisions.
On vérifie la collision de nos éléments à l'aide d'une fonction toute simple :
local function checkCollision(a, b)
return a.x < b.x + b.w and
a.x + a.w > b.x and
a.y < b.y + b.h and
a.h + a.y > b.y
end
Les niveaux
Les niveaux seront représentés par des tableaux de la manière suivante :
return {
6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,
10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,
10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,
10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,
10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,
10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,
10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,
10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,
10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,
10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,
10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,
10,0,0,0,0,0,0,0,0,3,3,0,0,0,3,3,0,0,0,0,0,0,0,0,10,
10,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,10,
2,2,2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,2,2,2,2,2,2,2,
1,1,1,1,1,1,1,5,5,5,5,5,5,5,5,5,5,5,1,1,1,1,1,1,1,
}
Chaque chiffre va représenter un élément sur notre écran.
- le 0 représente le vide.
- le 1 sera des murs.
- le 2 sera le sol.
- le 3 sera des plateformes amovibles.
- le 4 sera les portes de sorties.
- le 5 sera des scies fixes.
- le 6 sera le toît.
- le 7 sera le boss du jeu.
- le 8 sera les ennemies du jeu, des scies qui se déplacent.
- le 9 sera notre héro.
- le 10 sera un élement du décor, l'échafaudage.
Chacun de ces éléments utilise le système de classes de Lua via setmetatable pour sa représentation. Une classe Level va se charger de l'affichage des éléments du niveau en fonction de ces chiffres.
Il sera alors possible de déclencher des évenements dans le monde via une fonction trigger, par exemple lorsque le joueur franchit une porte.
Pour gérer cet évenement, dans notre classe Door on a :
function mt:update(dt)
self.touches_hero = GameState.getCurrent().world:check(self, 'is_hero')
end
function mt:draw()
assets.qdraw(7, self.x, self.y)
if self.touches_hero then
GameState.getCurrent():trigger('door:open')
end
end
Et dans l'état du jeu en cours PlayState on a :
function mt:trigger(event, actor, data)
if event == 'door:open' then
local doorSound = love.audio.newSource(SOUND_DOOR, "static")
doorSound:play()
if self.level_num < GAME_LEVEL_MAX then
GameState.setCurrent('Play', self.level_num + 1)
else
GameState.setCurrent('Win')
end
end
end
Le hero
Notre hero va devoir se déplacer si on utilise le joystick ou le clavier, en utilisant un système d'accélération et de décélération dans son update :
local dx, dy = 0, 0
if love.keyboard.isDown('left') or (Joystick and (Joystick:isGamepadDown('dpleft') or Joystick:getGamepadAxis('leftx') <= -0.25)) then
self:setAnim('run')
self.last_direction = -1
-- acceleration system
self.vx = self.vx + (-self.speed * self.acceleration * dt)
if self.vx < -self.speed then self.vx = -self.speed end
elseif love.keyboard.isDown('right') or (Joystick and (Joystick:isGamepadDown('dpright') or Joystick:getGamepadAxis('leftx') >= 0.25)) then
self:setAnim('run')
self.last_direction = 1
-- acceleration system
self.vx = self.vx + (self.speed * self.acceleration * dt)
if self.vx > self.speed then self.vx = self.speed end
else
-- deceleration system
if self.vx < 0 then
self.vx = self.vx + (self.speed * self.deceleration * dt)
if self.vx > 0 then self.vx = 0 end
elseif self.vx > 0 then
self.vx = self.vx + (-self.speed * self.deceleration * dt)
if self.vx < 0 then self.vx = 0 end
end
end
dx = dx + self.vx * dt
Tu va devoir gérer la gravité lors du saut dans le update également :
if (love.keyboard.isDown('up') or (Joystick and (Joystick:isGamepadDown('a')))) then
-- init jump
if self:canJump() then
self.vy = HERO_JUMP_SPEED
self.is_jumping = true
local jumpSound = love.audio.newSource(SOUND_JUMP, "static")
jumpSound:play()
-- during the jump
elseif self.is_jumping == true then
-- reduce the gravity for smooth jump
if self.vy < 0 then
self.vy = self.vy - HERO_JUMP_GRAVITY * dt
end
end
end
-- gravity
if self:isGrounded() then
self.vy = 0
self.is_jumping = false
self.ungroundedTime = 0
else
self:setAnim('jump')
self.vy = math.min(self.vy + HERO_GRAVITY * dt, HERO_MAX_VELOCITY)
self.ungroundedTime = self.ungroundedTime + dt
end
Et bien évidemment, il faudra l'animer à l'aide de notre helper :
self:setAnim('run')
et enfin le déplacer dans notre monde via :
GameState.getCurrent().world:move(self, self.x + dx, self.y + self.vy, 'is_solid')
Les particules de sang
Enfin, à la mort, on va utiliser un système de particules pour gérer le sang. On va pouvoir utiliser différents paramètres pour styliser nos particules :
p.psystem:setParticleLifetime(0.5, 3)
p.psystem:setEmissionRate(128)
p.psystem:setEmitterLifetime(0.5)
p.psystem:setSizeVariation(1)
p.psystem:setLinearAcceleration(-100, -100, 100, 100)
p.psystem:setColors(1, 1, 1, 1, 1, 1, 1, 0)
Conclusion
J'espère que cet aperçu va te donner envie d'essayer Löve plus en profondeur !
Ici, on a tout fait à la main, sans librairie. C'est la meilleur manière de procéder je pense pour avoir le contrôle complet de ton code.
Mais si tu veux voir ce que ça donne en utilisant des librairies de gestion de collisions comme Bump, des utilitaires pour gérer l'état du jeu ou la caméra comme Hump, l'outil de gestion des animations Anim8, ou encore l'utilitaire STI pour manipuler des niveaux créés avec Tiled, tu peux jeter un oeil à mon deuxième projet Löve, The Legend Of Shifu, dont l'intégralité du code source est disponible sur Github. C'est un petit jeu inspiré de The Binding Of Isaac.
Have fun !