Jeux: tirer sur les ennemis avec love2d

Utilisons tout ce que nous avons appris jusqu’ici pour créer un jeu simple. Vous pouvez lire tout ce que vous voulez sur la programmation, mais pour l’apprendre réellement, vous devez pratiquer.

Un jeu est essentiellement un ensemble de problèmes que vous devez résoudre. Lorsque vous demandez à un programmeur expérimenté de faire un PONG, il ne cherchera pas comment le faire. Il sait déjà comment le diviser en plusieurs problèmes séparés et comment les résoudre.
Ce chapitre est là pour vous montrer comment diviser un jeu en plusieurs tâches.

Le jeu que nous allons faire est simple :
un ennemi rebondit contre les murs et nous devons l’abattre. Chaque fois que nous le toucherons, il se déplacera un peu plus vite jusqu’à ce que vous le manquiez. Le jeu sera alors fini et vous redémarrerez une autre partie.

Le jeu que l’on va coder
Le jeu que l’on va coder

Nous allons utiliser des images. Vous êtes libre d’utiliser les vôtres :

Ces images sont créées par Kenney, qui crée beaucoup d’éléments « gratuits » que tout le monde peut utiliser pour ses jeux.

Commençons avec les 3 fonctions de rappel et chargeons la bibliothèque classic, celle simulant les classes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function love.load()
  Object = require "classic"
end

function love.update(dt)

end

function love.draw()

end

Commençons avec le joueur. Créez un nouveau fichier et appelez-le player.lua.

Nous pourrions faire une superclasse pour tous nos objets, mais comme c’est un jeu simple, faisons sans.

Tâche: Créer un joueur mobile

Créez une classe Player :

1
2
3
4
5
6
--! file: player.lua
Player = Object:extend()

function Player:new()

end

Je vais assigner l’image du panda à mon joueur :

1
2
3
4
5
6
7
function Player:new()
  self.image = love.graphics.newImage("panda.png")
end

function Player:draw()
  love.graphics.draw(self.image)
end

Ensuite, donnons la possibilité à notre joueur de bouger avec les flèches :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function Player:new()
  self.image = love.graphics.newImage("panda.png")
  self.x = 300
  self.y = 20
  self.speed = 500
end

function Player:update(dt)
  if love.keyboard.isDown("left") then
    self.x = self.x - self.speed * dt
  elseif love.keyboard.isDown("right") then
    self.x = self.x + self.speed * dt
  end
end

function Player:draw()
  love.graphics.draw(self.image, self.x, self.y)
end

Maintenant, nous devrions être capables de bouger notre joueur. Revenons à notre main.lua et chargeons notre joueur :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
--! file: main.lua
function love.load()
  Object = require "classic"
  require "player"

  player = Player()
end

function love.update(dt)
  player:update(dt)
end

function love.draw()
  player:draw()
end

Comme vous pouvez le voir, nous pouvons bouger notre joueur, mais il peut sortir de la fenêtre. Fixons ce problème avec des conditions :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
--! file: player.lua

function Player:update(dt)
  if love.keyboard.isDown("left") then
    self.x = self.x - self.speed * dt
  elseif love.keyboard.isDown("right") then
    self.x = self.x + self.speed * dt
  end

  --Get the width of the window
  local window_width = love.graphics.getWidth()

  --If the x is too far to the left then..
  if self.x < 0 then
    --Set x to 0
    self.x = 0

  --Else, if the x is too far to the right then..
  elseif self.x > window_width then
    --Set the x to the window's width.
    self.x = window_width
  end
end

Oups, notre joueur peut encore sortir sur la droite. Nous avons besoin d’inclure sa largeur quand nous vérifions s’il heurte le mur de droite :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function Player:new()
  self.image = love.graphics.newImage("panda.png")
  self.x = 300
  self.y = 20
  self.speed = 500
  self.width = self.image:getWidth()
end

function Player:update(dt)
  if love.keyboard.isDown("left") then
    self.x = self.x - self.speed * dt
  elseif love.keyboard.isDown("right") then
    self.x = self.x + self.speed * dt
  end

  --Get the width of the window
  local window_width = love.graphics.getWidth()

  --If the left side is too far to the left then..
  if self.x < 0 then
    --Set x to 0
    self.x = 0

  --Else, if the right side is too far to the right then..
  elseif self.x + self.width > window_width then
    --Set the right side to the window's width.
    self.x = window_width - self.width
  end
end

Maintenant que le problème est résolu, notre joueur ne peut plus sortir de la fenêtre.

Tâche: Créer un ennemi mobile

Créons la classe Enemy en créant un nouveau fichier appelé enemy.lua et entrez le code suivant :

1
2
3
4
5
6
--! file: enemy.lua
Enemy = Object:extend()

function Enemy:new()

end

Je vais associer l’image du serpent à l’ennemi et le faire bouger :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function Enemy:new()
  self.image = love.graphics.newImage("snake.png")
  self.x = 325
  self.y = 450
  self.speed = 100
end

function Enemy:update(dt)
  self.x = self.x + self.speed * dt
end

function Enemy:draw()
  love.graphics.draw(self.image, self.x, self.y)
end

Nous avons besoin de faire rebondir l’ennemi sur les murs, mais chargeons-le d’abord :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
--! file: main.lua
function love.load()
  Object = require "classic"
  require "player"
  require "enemy"

  player = Player()
  enemy = Enemy()
end

function love.update(dt)
  player:update(dt)
  enemy:update(dt)
end

function love.draw()
  player:draw()
  enemy:draw()
end

Bon, nous pouvons voir l’ennemi bouger et nous constatons qu’il peut sortir de notre fenêtre. Réglons le problème comme avec le joueur :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function Enemy:new()
  self.image = love.graphics.newImage("snake.png")
  self.x = 325
  self.y = 450
  self.speed = 100
  self.width = self.image:getWidth()
  self.height = self.image:getHeight()
end

function Enemy:update(dt)
  self.x = self.x + self.speed * dt

  local window_width = love.graphics.getWidth()

  if self.x < 0 then
    self.x = 0
  elseif self.x + self.width > window_width then
    self.x = window_width - self.width
  end
end
note

« Cela devient très similaire à notre classe joueur. Êtes-vous sûr de ne pas vouloir faire une superclasse ? »

Ok, laissez-moi vous expliquer ça très rapidement. Nous créons ce jeu de façon « rapide et simple ». Ce chapitre porte sur un jeu fonctionnel, pas sur l’efficacité du code. Ça sera pour un autre chapitre.
Oui, avoir un code propre est important, mais ce n’est pas aussi important que de savoir comment faire un jeu. Je souhaite que ces tutoriels soient aussi courts et compacts que possible. En vous faisant créer une superclasse, nous allons rendre ce chapitre encore plus long. Cela dit, je vous encourage à améliorer le code à la fin de ce chapitre.

Revenons maintenant à notre code. Notre ennemi s’arrête contre le mur, mais nous voulons qu’il rebondisse. Comment allons-nous faire cela ? Il heurte le mur de droite et ensuite ? Il devrait se déplacer dans une autre direction. Comment faisons-nous pour qu’il aille dans une autre direction ? En changeant la valeur de la vitesse. Et quelle sera la valeur de la vitesse ? Elle ne devrait plus être 100 mais -100.

Donc devrions-nous mettre self.speed = -100 ? Bien sûr que non, parce que comme je l’ai dit auparavant, nous rendrons l’ennemi de plus en plus rapide quand nous le toucherons. À la place, nous devrions inverser la valeur de la vitesse pour qu’elle devienne -speed.

Et s’il heurte le mur gauche ? À ce point, la vitesse est un nombre négatif et nous devons la transformer en un nombre positif. Comment pouvons-nous faire ça ? Eh bien, multiplier 2 nombres négatifs entre eux donne un nombre positif. Donc si nous disons que la vitesse, laquelle est actuellement négative, devient -speed, elle deviendra un nombre positif :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function Enemy:update(dt)
  self.x = self.x + self.speed * dt

  local window_width = love.graphics.getWidth()

  if self.x < 0 then
    self.x = 0
    self.speed = -self.speed
  elseif self.x + self.width > window_width then
    self.x = window_width - self.width
    self.speed = -self.speed
  end
end

Très bien, nous avons un joueur et un ennemi qui se déplacent. Maintenant, tout ce qui reste est la balle.

Tâche: Être capable de tirer

Créez un nouveau fichier appelé bullet.lua et mettez-y le code suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
--! file: bullet.lua

Bullet = Object:extend()

function Bullet:new()
  self.image = love.graphics.newImage("bullet.png")
end

function Bullet:draw()
  love.graphics.draw(self.image)
end

La balle devrait se déplacer verticalement au lieu d’horizontalement :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
--We pass the x and y of the player.
function Bullet:new(x, y)
  self.image = love.graphics.newImage("bullet.png")
  self.x = x
  self.y = y
  self.speed = 700
  --We'll need these for collision checking
  self.width = self.image:getWidth()
  self.height = self.image:getHeight()
end

function Bullet:update(dt)
  self.y = self.y + self.speed * dt
end

function Bullet:draw()
  love.graphics.draw(self.image, self.x, self.y)
end

Maintenant, nous avons besoin de tirer des balles. Dans le main.lua, chargeons le fichier et créons une table :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
--! file: main.lua
function love.load()
  Object = require "classic"
  require "player"
  require "enemy"
  require "bullet"

  player = Player()
  enemy = Enemy()
  listOfBullets = {}
end

Nous donnons au joueur une fonction qui crée des balles quand la barre d’espace est enfoncée :

1
2
3
4
5
6
7
8
--! file: player.lua
function Player:keyPressed(key)
  --If the spacebar is pressed
  if key == "space" then
    --Put a new instance of Bullet inside listOfBullets.
    table.insert(listOfBullets, Bullet(self.x, self.y))
  end
end

Et nous avons besoin d’appeler cette fonction dans la fonction de rappel love.keypressed :

1
2
3
4
--! file: main.lua
function love.keypressed(key)
  player:keyPressed(key)
end

Et maintenant, nous avons besoin de parcourir la table et mettre à jour/dessiner les balles :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function love.load()
  Object = require "classic"
  require "player"
  require "enemy"
  require "bullet"

  player = Player()
  enemy = Enemy()
  listOfBullets = {}
end

function love.update(dt)
  player:update(dt)
  enemy:update(dt)

  for i,v in ipairs(listOfBullets) do
    v:update(dt)
  end
end

function love.draw()
  player:draw()
  enemy:draw()

  for i,v in ipairs(listOfBullets) do
    v:draw()
  end
end

Incroyable, notre joueur peut maintenant tirer des balles.

Tâche: L’impact des balles sur la vitesse de l’ennemi

Maintenant, nous devons faire en sorte que le serpent puisse être touché par une balle. Nous donnons à Bullet une fonction de détection de collision :

1
2
3
4
--! file: bullet.lua
function Bullet:checkCollision(obj)

end

Vous souvenez-vous toujours comment faire ?
Vous souvenez-vous des 4 conditions qui doivent être vraies pour s’assurer qu’une collision a eu lieu ?
Au lieu de retourner vrai ou faux, nous incrémentons la vitesse de l’ennemi. Nous ajoutons la propriété dead à la balle que nous utiliserons pour la supprimer de la liste :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function Bullet:checkCollision(obj)
  local self_left = self.x
  local self_right = self.x + self.width
  local self_top = self.y
  local self_bottom = self.y + self.height

  local obj_left = obj.x
  local obj_right = obj.x + obj.width
  local obj_top = obj.y
  local obj_bottom = obj.y + obj.height

  if self_right > obj_left and
    self_left < obj_right and
    self_bottom > obj_top and
    self_top < obj_bottom then
    self.dead = true

    --Increase enemy speed
    obj.speed = obj.speed + 50
  end
end

Maintenant, nous avons besoin d’appeler checkCollision dans main.lua :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function love.update(dt)
  player:update(dt)
  enemy:update(dt)

  for i,v in ipairs(listOfBullets) do
    v:update(dt)

    --Each bullet checks if there is collision with the enemy
    v:checkCollision(enemy)
  end
end

Maintenant, nous avons besoin de détruire les balles qui sont mortes :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function love.update(dt)
  player:update(dt)
  enemy:update(dt)

  for i,v in ipairs(listOfBullets) do
    v:update(dt)
    v:checkCollision(enemy)

    --If the bullet has the property dead and it's true then..
    if v.dead then
      --Remove it from the list
      table.remove(listOfBullets, i)
    end
  end
end

La dernière chose à faire est de redémarrer le jeu quand nous manquons l’ennemi. Nous avons besoin de savoir si la balle est en dehors de l’écran :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
--! file: bullet.lua
function Bullet:update(dt)
  self.y = self.y + self.speed * dt

  --If the bullet is out of the screen
  if self.y > love.graphics.getHeight() then
    --Restart the game
    love.load()
  end
end

Testons tout ça.
Vous remarquerez peut-être que lorsque vous frappez l’ennemi alors qu’il se déplace vers la gauche, le jeu ralentit. C’est parce que la vitesse de l’ennemi est à ce moment un chiffre négatif, donc en incrémentant le nombre, l’ennemi ira moins vite.
Pour corriger ça, nous avons besoin de savoir si la vitesse de l’ennemi est négative ou non :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function Bullet:checkCollision(obj)
  local self_left = self.x
  local self_right = self.x + self.width
  local self_top = self.y
  local self_bottom = self.y + self.height

  local obj_left = obj.x
  local obj_right = obj.x + obj.width
  local obj_top = obj.y
  local obj_bottom = obj.y + obj.height

  if self_right > obj_left and
    self_left < obj_right and
    self_bottom > obj_top and
    self_top < obj_bottom then
    self.dead = true

    --Increase enemy speed
    if obj.speed > 0 then
      obj.speed = obj.speed + 50
    else
      obj.speed = obj.speed - 50
    end
  end
end

Avec ça, notre jeu est terminé, ou pas encore ?
Vous devriez essayer d’ajouter des fonctionnalités au jeu vous-même, ou créer un tout nouveau jeu.

Résumé

Un jeu est essentiellement un ensemble de problèmes qui doivent être résolus.