Jeu de plateforme avec love2d

Info
Nous utilisons le code du chapitre précédent

Chute

Maintenant que nous pouvons résoudre la collision, nous pouvons créer un jeu de plateforme. Un jeu où vous tombez et pouvez sauter. Créons d’abord une carte pour nous promener. Nous pouvons supprimer le seul mur que nous avons ajouté.

 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
30
31
32
33
34
35
36
37
function love.load()
  Object = require "classic"
  require "entity"
  require "player"
  require "wall"
  require "box"

  player = Player(100, 100)
  box = Box(400, 150)

  objects = {}
  table.insert(objects, player)
  table.insert(objects, box)

  map = {
    {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
    {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
    {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
    {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
    {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
    {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
    {1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1},
    {1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1},
    {1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1},
    {1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1},
    {1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1},
    {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}
  }

  for i,v in ipairs(map) do
    for j,w in ipairs(v) do
      if w == 1 then
        table.insert(objects, Wall((j-1)*50, (i-1)*50))
      end
    end
  end
end
Image représentant la map que l’on doit obtenir avec le code précédent
Image représentant la map que l’on doit obtenir avec le code précédent

Selon la qualité de votre ordinateur, vous remarquerez peut-être que le jeu est devenu très lent. En effet, tous les murs que nous avons ajoutés vérifient les collisions entre eux. C’est très inefficace, car il n’est pas nécessaire de vérifier cela. Les murs ne bougent jamais, ils ne se chevaucheront donc jamais. Au lieu de cela, nous devons créer une table séparée pour tous les murs. La table des objets vérifie la collision avec elle-même et la table des murs, mais la table des murs ne vérifie pas la collision avec elle-même.

  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
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
function love.load()
  Object = require "classic"
  require "entity"
  require "player"
  require "wall"
  require "box"

  player = Player(100, 100)
  box = Box(400, 150)

  objects = {}
  table.insert(objects, player)
  table.insert(objects, box)

  --Create the walls table
  ---- ADD THIS
  walls = {}
  -------------

  map = {
    {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
    {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
    {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
    {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
    {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
    {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
    {1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1},
    {1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1},
    {1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1},
    {1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1},
    {1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1},
    {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}
  }

  for i,v in ipairs(map) do
    for j,w in ipairs(v) do
      if w == 1 then
        -- Add all the walls to the walls table instead.
        ---- CHANGE THIS
        table.insert(walls, Wall((j-1)*50, (i-1)*50))
        -------------
      end
    end
  end
end

function love.update(dt)
  for i,v in ipairs(objects) do
      v:update(dt)
  end

  -- Update the walls
  ---- ADD THIS
  for i,v in ipairs(walls) do
    v:update(dt)
  end
  -------------

  local loop = true
  local limit = 0

  while loop do
    loop = false

    limit = limit + 1
    if limit > 100 then
      break
    end

    for i=1,#objects-1 do
      for j=i+1,#objects do
        local collision = objects[i]:resolveCollision(objects[j])
        if collision then
          loop = true
        end
      end
    end

    -- For each object check collision with every wall.
    ---- ADD THIS
    for i,wall in ipairs(walls) do
      for j,object in ipairs(objects) do
        local collision = object:resolveCollision(wall)
        if collision then
          loop = true
        end
      end
    end
    -------------
  end
end

function love.draw()
  for i,v in ipairs(objects) do
    v:draw()
  end

  -- Draw the walls
  ---- ADD THIS
  for i,v in ipairs(walls) do
    v:draw()
  end
  -------------
end

D’accord, maintenant nous pouvons commencer à ajouter de la physique de plateforme. Commençons par tomber. Dans player.lua, nous faisons déjà descendre le joueur lorsque nous appuyons sur la touche bas. En supprimant cette instruction if, le joueur tombera automatiquement.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
--! file: player.lua
function Player:update(dt)
  Player.super.update(self, dt)

  if love.keyboard.isDown("left") then
    self.x = self.x - 200 * dt
  elseif love.keyboard.isDown("right") then
    self.x = self.x + 200 * dt
  end

  if love.keyboard.isDown("up") then
    self.y = self.y - 200 * dt
  end

  -- Remove the if-statement
  self.y = self.y + 200 * dt
end
Animation représentant la chute du joueur
Animation représentant la chute du joueur

Cela fonctionne, l’objet tombe, mais ce n’est pas ainsi que fonctionne la gravité. Un objet devrait lentement tomber et, à mesure qu’il tombe, il devrait gagner en vitesse. Créons quelque chose qui ressemble plus à de la gravité réelle dans la classe Entity. Nous avons besoin d’une propriété de gravité et de poids. Nous utilisons la propriété de gravité pour augmenter la position y de l’entité, et nous utilisons la propriété de poids pour augmenter la gravité. Nous augmentons donc la vitesse à laquelle nous chutons.

 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
30
31
32
--! file: entity.lua
function Entity:new(x, y, image_path)
  self.x = x
  self.y = y
  self.image = love.graphics.newImage(image_path)
  self.width = self.image:getWidth()
  self.height = self.image:getHeight()

  self.last = {}
  self.last.x = self.x
  self.last.y = self.y

  self.strength = 0
  self.tempStrength = 0

  -- Add the gravity and weight properties
  self.gravity = 0
  self.weight = 400
end

function Entity:update(dt)
  self.last.x = self.x
  self.last.y = self.y

  self.tempStrength = self.strength

  -- Increase the gravity using the weight
  self.gravity = self.gravity + self.weight * dt

  -- Increase the y-position
  self.y = self.y + self.gravity * dt
end

Comme les murs n’ont pas besoin de tomber, nous pouvons lui donner un poids de 0.

1
2
3
4
5
6
7
8
--! file: wall.lua
Wall = Entity:extend()

function Wall:new(x, y)
  Wall.super.new(self, x, y, "wall.png", 1)
  self.strength = 100
  self.weight = 0
end

Et nous pouvons supprimer la partie du lecteur qui le fait tomber automatiquement, ainsi que remonter en appuyant sur la touche Haut.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
--! file: player.lua
function Player:update(dt)
  -- It's important that we do this before changing the position
  Player.super.update(self, dt)

  if love.keyboard.isDown("left") then
    self.x = self.x - 200 * dt
  elseif love.keyboard.isDown("right") then
    self.x = self.x + 200 * dt
  end

  -- Remove the vertical movement
end
Animation du code précédent simulant la gravité
Animation du code précédent simulant la gravité

Mais lorsque vous exécutez le jeu assez longtemps, vous remarquerez peut-être que le joueur et la boîte tombent à travers le sol. En effet, la gravité continue d’augmenter même s’ils sont debout sur le sol. Nous devons réinitialiser la gravité lorsqu’ils se tiennent sur le sol. Nous pouvons le faire dans Entity: resolverCollision (e).

 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
30
31
32
33
34
35
--! file: entity.lua
function Entity:resolveCollision(e)
  if self.tempStrength > e.tempStrength then
    return e:resolveCollision(self)
  end

  if self:checkCollision(e) then
    self.tempStrength = e.tempStrength
    if self:wasVerticallyAligned(e) then
      if self.x + self.width/2 < e.x + e.width/2 then
        local pushback = self.x + self.width - e.x
        self.x = self.x - pushback
      else
        local pushback = e.x + e.width - self.x
        self.x = self.x + pushback
      end
    elseif self:wasHorizontallyAligned(e) then
      if self.y + self.height/2 < e.y + e.height/2 then
        local pushback = self.y + self.height - e.y
        self.y = self.y - pushback
        -- We're touching a wall from the bottom
        -- This means we're standing on the ground.
        -- Reset the gravity
        ---- ADD THIS
        self.gravity = 0
        -------------
      else
        local pushback = e.y + e.height - self.y
        self.y = self.y + pushback
      end
    end
    return true
  end
  return false
end

Et maintenant, ils ne tombent plus à travers le mur.

Sauter

Il est maintenant temps de rendre le joueur capable de sauter. Nous le faisons sauter lorsque la touche haut est enfoncée. Commençons donc par le rappel love.keypressed (key) vers main.lua, et faisons-le appeler la fonction jump () du joueur, que nous allons faire dans un instant.

1
2
3
4
5
6
7
--! file: main.lua
function love.keypressed(key)
  -- Let the player jump when the up-key is pressed
  if key == "up" then
    player:jump()
  end
end

Alors, que doit-il se passer pour faire sauter le joueur? C’est en fait très simple. Nous donnons simplement à la gravité une valeur négative. Plus la valeur est basse (ou en d’autres termes, plus la valeur est négative), plus le joueur saute haut.

1
2
3
4
--! file: player.lua
function Player:jump()
  self.gravity = -300
end

Au fur et à mesure que la gravité du joueur change, il saute dans les airs et tombe lentement à mesure que la gravité continue d’augmenter.

Animation du code précédent
Animation du code précédent

Mais lorsque vous essayez, vous remarquerez que nous pouvons sauter plusieurs fois. Nous n’en voulons pas. Vous ne devriez pouvoir sauter que lorsque vous vous tenez au sol. Nous pouvons le faire en ajoutant une propriété canJump au lecteur. Lorsque vous atterrissez au sol, la propriété devient vraie et lorsque vous sautez, ce que vous ne pouvez faire que lorsque canJump est vrai, cela devient faux.

 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
function Player:new(x, y)
  Player.super.new(self, x, y, "player.png", 5)
  self.strength = 10

  self.canJump = false
end

function Player:update(dt)
  Player.super.update(self, dt)

  if love.keyboard.isDown("left") then
    self.x = self.x - 200 * dt
  elseif love.keyboard.isDown("right") then
    self.x = self.x + 200 * dt
  end

  -- Remove the if-statement
  -- self.y = self.y + 200 * dt
end

function Player:jump()
  if self.canJump then
    self.gravity = -300
    self.canJump = false
  end
end

Mais pour le moment, si nous voulons effectuer une action à l’atterrissage, dans ce cas, définir canJump sur true, nous devons le faire dans la classe Entity. Ajoutons une fonction qui est appelée lors de la résolution d’une collision, afin de pouvoir remplacer ces fonctions dans la classe Player.

 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
--! file: entity.lua
function Entity:resolveCollision(e)
  if self.tempStrength > e.tempStrength then
    return e:resolveCollision(self)
  end

  if self:checkCollision(e) then
    self.tempStrength = e.tempStrength
    if self:wasVerticallyAligned(e) then
      if self.x + self.width/2 < e.x + e.width/2 then
        -- Replace these with the functions
        self:collide(e, "right")
      else
        self:collide(e, "left")
      end
    elseif self:wasHorizontallyAligned(e) then
      if self.y + self.height/2 < e.y + e.height/2 then
        self:collide(e, "bottom")
      else
        self:collide(e, "top")
      end
    end
    return true
  end
  return false
end

-- When the entity collides with something with his right side
function Entity:collide(e, direction)
  if direction == "right" then
    local pushback = self.x + self.width - e.x
    self.x = self.x - pushback
  elseif direction == "left" then
    local pushback = e.x + e.width - self.x
    self.x = self.x + pushback
  elseif direction == "bottom" then
    local pushback = self.y + self.height - e.y
    self.y = self.y - pushback
    self.gravity = 0
  elseif direction == "top" then
    local pushback = e.y + e.height - self.y
    self.y = self.y + pushback
  end
end

Et maintenant, nous pouvons remplacer la fonction collision (e) et définir canJump sur false lorsque la direction est en bas.

1
2
3
4
5
6
7
--! file: player.lua
function Player:collide(e, direction)
  Player.super.collide(self, e, direction)
  if direction == "bottom" then
    self.canJump = true
  end
end

Et maintenant, nous ne pouvons sauter qu’une seule fois. Mais nous pouvons toujours sauter en l’air lorsque vous sortez d’une plate-forme et sautez.

Animation représentant le code précédent avec 1 seul saut possible
Animation représentant le code précédent avec 1 seul saut possible

Nous pouvons résoudre ce problème en vérifiant si la position y précédente n’est pas égale à la position y actuelle. Lorsque vous vous tenez au sol, vous ne devez pas vous déplacer verticalement. Donc, si vous l’êtes, cela signifie que vous n’êtes pas debout sur le sol.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function Player:update(dt)
  Player.super.update(self, dt)

  if love.keyboard.isDown("left") then
    self.x = self.x - 200 * dt
  elseif love.keyboard.isDown("right") then
    self.x = self.x + 200 * dt
  end

  if self.last.y ~= self.y then
    self.canJump = false
  end
end

Agréable! Il est très courant dans les plateformes que vous devez frapper quelque chose dans une certaine direction. Pensez à Mario par exemple. Vous obtenez seulement quelque chose du point d’interrogation lorsque vous le frappez par le bas, et vous ne pouvez tuer un ennemi qu’en sautant dessus. Un exemple plus avancé est peut-être les plateformes sur lesquelles vous pouvez sauter. Lorsque vous le frappez par le bas, vous le sautez, mais le toucher par le haut vous fait vous tenir dessus.

Exemple d’un jeu de plateforme
Exemple d’un jeu de plateforme

Essayons de créer une telle plateforme.

Plateformeur

Nous allons utiliser notre boîte pour cela. Nous voulons faire en sorte que le joueur ne pousse pas la boîte mais au contraire la traverse. Quand il saute sur la boîte, il se tient au-dessus. Pour que cela se produise, nous devons à nouveau apporter des modifications. La fonction collision (e, direction) correspond à ce qui devrait se produire lorsque la collision est résolue. Mais nous ne voulons pas que la collision soit résolue. Nous voulons pouvoir parcourir la boîte.

Et si nous vérifions d’abord si les deux parties à la collision veulent que la collision soit résolue. Nous créons une fonction appelée checkResolved. Si self et e renvoient vrai, alors nous continuons à résoudre la collision.

 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
--! file: player.lua
function Entity:resolveCollision(e)
  if self.tempStrength > e.tempStrength then
    return e:resolveCollision(self)
  end

  if self:checkCollision(e) then
    self.tempStrength = e.tempStrength
    if self:wasVerticallyAligned(e) then
      if self.x + self.width/2 < e.x + e.width/2 then
        -- Call checkResolve for both parties.
        local a = self:checkResolve(e, "right")
        local b = e:checkResolve(self, "left")
        -- If both a and b are true then resolve the collision.
        if a and b then
          self:collide(e, "right")
        end
      else
        local a = self:checkResolve(e, "left")
        local b = e:checkResolve(self, "right")
        if a and b then
          self:collide(e, "left")
        end
      end
    elseif self:wasHorizontallyAligned(e) then
      if self.y + self.height/2 < e.y + e.height/2 then
        local a = self:checkResolve(e, "bottom")
        local b = e:checkResolve(self, "top")
        if a and b then
          self:collide(e, "bottom")
        end
      else
        local a = self:checkResolve(e, "bottom")
        local b = e:checkResolve(self, "top")
        if a and b then
          self:collide(e, "top")
        end
      end
    end
    return true
  end
  return false
end

function Entity:checkResolve(e, direction)
  return true
end

Maintenant, dans player.lua, nous pouvons remplacer la fonction checkResolve (e, direction). Avec classique, la bibliothèque de classes que nous utilisons, chaque classe a la fonction: (classe) qui peut être utilisée pour vérifier si une instance d’une classe appartient à un certain type de classe. Nous pouvons donc utiliser e: is (Box) pour vérifier si e est du type Box. Cela fonctionne également avec les baseclasses. Donc, si e devait être une boîte, e: is (Entity) retournerait vrai, car Box est une extension de l’entité de la classe de base.

Nous vérifions d’abord si nous entrons en collision avec une boîte, et si oui, nous vérifions si la direction est « en bas ». Si c’est le cas, retournez true (ce qui signifie que nous voulons résoudre la collision), sinon retournez false.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
--! file: player.lua 
function Player:checkResolve(e, direction)
  if e:is(Box) then
    if direction == "bottom" then
      return true
    else
      return false
    end
  end
  return true
end
Animation représentant un bug avec les collisions
Animation représentant un bug avec les collisions

Résumé

En augmentant la valeur que nous utilisons pour augmenter notre position y, nous pouvons simuler la gravité. Nous pouvons sauter en réglant la gravité sur une valeur négative. En ajoutant des fonctions et en remplaçant ces fonctions, nous pouvons appliquer des actions lors d’une collision, ou empêcher une collision de se produire.

La fin ?

Et avec cela, nous sommes à la fin de ce chapitre, et à la fin de ce livre (pour l’instant!). J’espère que vous avez apprécié la lecture de tous les chapitres et bien sûr beaucoup appris en les lisant. J’ai encore des plans pour de nouveaux chapitres, mais c’est pour une autre fois. D’ici là, je vous souhaite bonne chance dans votre cheminement pour devenir un programmeur de jeux encore plus grand que vous ne l’êtes déjà. Comme je l’ai dit dans l’introduction, vous pouvez lire sur la peinture tout ce que vous voulez, mais pour l’apprendre, vous devez le faire. Il en va de même pour la programmation. À votre santé !