Jeux: tirer sur les enemis 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 tas 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 montre 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 l’aurons, il se déplacera un peu plus vite jusqu’à ce que vous le manquiez. Le jeu sera 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 votre :

Ces images sont faites 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 rappels et chargeons la lib 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 simple jeu, 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 capable 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 if :

 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 too 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 notre largeur quand nous vérifions si nous heurtons 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 too 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 à enemy et la 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 pouvons voir 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 sur de ne pas vouloir faire une superclasse ? »

Ok, laissez moi vous expliquer ça très rapidement. Nous rendons ce jeu « rapide et sale ». 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 tuto soient aussi courts et compacts que possible. En vous faisant faire une superclasse nous allons rendre ce chapitre encore plus long. Tout cela a été 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 qu’elle sera la valeur de la vitesse ? Elle ne devrait plus être 100 mais -100.

Donc devrions nous mettre self.speed = -100 ? Bien sur 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 donc elle deviendra -speed.

Et si elle heurte le mur gauche ? À ce point, la vitesse est un nombre négatif et et nous devons la transformer en un nombre positif. Comment pouvons-nous faire ça ? Hé bien multiplier 2 nombres négatifs entre eux donne un nombre positif donc si nous disons que la vitesse, laquelle est négatif actuellement, 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éplace. 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 chargons 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 passer en revue 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’impacte 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 ont besoin d’être vrai pour s’assurer qu’une collision a eu lieu ?
À la place 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 bullets 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 frapper l’ennemi alors qu’il se déplace sur la gauche, le jeu ralenti. C’est parce que la vitesse de l’ennemi est à ce moment un chiffre négatif donc en incémentant le nombre, l’ennemi ira moins vite.
Pour fixer ç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 tas de problèmes qui doivent être résolus.