Les tuiles avec love2d

Info
Assurez-vous de lire les commentaires dans le code, j’y mets beaucoup d’informations importantes

Dans de nombreux jeux 2D, les niveaux sont constitués de tuiles. Nous allons créer notre propre niveau en mosaïque.

Commençons par créer une ligne. Créez un tableau et remplissez-le de uns et de zéros.

1
2
3
function love.load()
  tilemap = {1, 0, 0, 1, 1, 0, 1, 1, 1, 0}
end

C’est notre niveau. Un 1 est une tuile et un 0 est vide. Maintenant, nous devons le dessiner. Nous parcourons la table, et chaque fois que nous rencontrons un 1, nous dessinons un rectangle à sa position.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function love.draw()
  --ipairs recap
  --ipairs est une fonction spéciale qui permet de parcourir une table
  --À chaque itération, i devient le numéro de l'itération, donc 1, 2, 3, 4, etc.
  --À chaque itération, v devient la valeur à la position i, donc dans notre cas 1, 0, 0, 1, 1, 0, etc.
  for i,v in ipairs(tilemap) do
    if v == 1 then
      love.graphics.rectangle("line", i * 25, 100, 25, 25)
    end
  end
end
Représentation des tuiles dessinées
Représentation des tuiles dessinées

D’accord, cela fonctionne, mais maintenant nous voulons aller verticalement. Pour ce faire, nous mettons des tables à l’intérieur d’une table, également connue sous le nom de table 2D.

1
2
3
4
5
6
7
8
9
function love.load()
  tilemap = {
    {1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 1, 1, 1, 1, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
  }
end

Alors maintenant, nous avons un tableau rempli de tableaux. Voyez-le comme un tableau Excel.

Représentation du tableau sous Excel
Représentation du tableau sous Excel

1, 2, 3, etc. sont ce que nous appelons des lignes et A, B, C, etc. sont appelés des colonnes.

Une autre façon de voir les choses est comme une petite ville :

Une autre représentation mais avec des maisons
Une autre représentation mais avec des maisons

Chaque rangée de maisons est une table, et plusieurs rangées forment toute la ville, ou dans notre cas, notre niveau.

La maison verte est au 2ème rang sur la 5ème colonne.

La maison rouge est au 3ème rang de la 2ème colonne.

Avec les tableaux 2D, nous accédons aux valeurs comme ceci :

1
tilemap[4][3]

Cela signifie : La 3ème valeur de la 4ème table, ou la 3ème colonne de la 4ème ligne.

Comment le programme lit notre code
Comment le programme lit notre code

Dessinons notre niveau. Parce que nous avons une table 2D, nous devons utiliser une boucle for à l’intérieur d’une boucle for. Plus communément, on appelle ceci une boucle for imbriquée.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function love.draw()
  --Faisons-le d'abord sans ipairs.

  --Pour i=1 jusqu'au nombre de valeurs dans tilemap
  for i=1,#tilemap do
    --Pour j jusqu'au nombre de valeurs dans cette ligne
    for j=1,#tilemap[i] do
      --Si la valeur à la ligne i, colonne j est égale à 1
      if tilemap[i][j] == 1 then
        --Dessiner le rectangle.
        --Utiliser i et j pour positionner le rectangle.
        --j pour x, i pour y.
        love.graphics.rectangle("line", j * 25, i * 25, 25, 25)
      end 
    end
  end
end

Nous parcourons donc nos lignes et pour chaque ligne, nous parcourons nos colonnes.

Nous utilisons j, de notre boucle interne pour notre positionnement horizontal et i, de notre boucle externe pour le positionnement vertical. N’oubliez pas que ce ne sont que des noms de variables et qu’ils peuvent être nommés autrement, mais utiliser i et j comme ceci est courant.

Transformons les boucles for en une boucle ipairs.

1
2
3
4
5
6
7
8
9
function love.draw()
  for i,row in ipairs(tilemap) do
    for j,tile in ipairs(row) do
      if tile == 1 then
        love.graphics.rectangle("line", j * 25, i * 25, 25, 25)
      end
    end
  end
end

Nous utilisons deux variables, row et tile, pour rendre plus clair ce qui se passe. Nous parcourons la table tilemap et chaque valeur est une ligne. Nous parcourons la ligne et chaque valeur est une tuile.

Nous pouvons également utiliser des numéros différents pour nos tuiles, et utiliser ces numéros pour donner aux tuiles des couleurs différentes.

 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()
    tilemap = {
        {1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
        {1, 2, 2, 2, 2, 2, 2, 2, 2, 1},
        {1, 2, 3, 4, 5, 5, 4, 3, 2, 1},
        {1, 2, 2, 2, 2, 2, 2, 2, 2, 1},
        {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
    }
end

function love.draw()
  for i,row in ipairs(tilemap) do
    for j,tile in ipairs(row) do
        --Vérifions d'abord si la tuile n'est pas zéro
      if tile ~= 0 then

        --Définir la couleur en fonction du numéro de tuile
        if tile == 1 then
          --setColor utilise RGB, A est optionnel
          --Rouge, Vert, Bleu, Alpha
          love.graphics.setColor(1, 1, 1)
        elseif tile == 2 then
          love.graphics.setColor(1, 0, 0)
        elseif tile == 3 then
          love.graphics.setColor(1, 0, 1)
        elseif tile == 4 then
          love.graphics.setColor(0, 0, 1)
        elseif tile == 5 then
          love.graphics.setColor(0, 1, 1)
        end

        --Dessiner la tuile
        love.graphics.rectangle("fill", j * 25, i * 25, 25, 25)
      end 
    end
  end
end

Ou une meilleure façon de procéder :

 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
function love.load()
  tilemap = {
    {1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
    {1, 2, 2, 2, 2, 2, 2, 2, 2, 1},
    {1, 2, 3, 4, 5, 5, 4, 3, 2, 1},
    {1, 2, 2, 2, 2, 2, 2, 2, 2, 1},
    {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
  }

  --Créer une table nommée colors
  colors = {
    --La remplir avec des tables contenant des nombres RGB
    {1, 1, 1},
    {1, 0, 0},
    {1, 0, 1},
    {0, 0, 1},
    {0, 1, 1}
  }
end

function love.draw()
  for i,row in ipairs(tilemap) do
    for j,tile in ipairs(row) do
        --Vérifions d'abord si la tuile n'est pas zéro
      if tile ~= 0 then
        --Définir la couleur. .setColor() accepte aussi une table avec 3 nombres.
        --Nous passons la table à la position correspondant à la valeur de tile.
        --Donc si tile vaut 3, alors nous passons colors[3] qui est {1, 0, 1}
        love.graphics.setColor(colors[tile])
        --Dessiner la tuile
        love.graphics.rectangle("fill", j * 25, i * 25, 25, 25)
      end
    end
  end
end

Images

Nous pouvons donc créer un niveau coloré, mais maintenant nous voulons utiliser des images. Eh bien, c’est facile, il suffit d’ajouter une image, d’obtenir la largeur et la hauteur et de dessiner l’image au lieu d’un rectangle.

Je vais utiliser cette image :

Image qui servira de tuile pour les murs
Image qui servira de tuile pour les murs
 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()
  --Charger l'image
  image = love.graphics.newImage("tile.png")

  --Obtenir la largeur et la hauteur
  width = image:getWidth()
  height = image:getHeight()

  tilemap = {
    {1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
    {1, 2, 2, 2, 2, 2, 2, 2, 2, 1},
    {1, 2, 3, 4, 5, 5, 4, 3, 2, 1},
    {1, 2, 2, 2, 2, 2, 2, 2, 2, 1},
    {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
  }

  colors = {
    --Remplir avec des tables contenant des nombres RGB
    {1, 1, 1},
    {1, 0, 0},
    {1, 0, 1},
    {0, 0, 1},
    {0, 1, 1}
  }
end

function love.draw()
  for i,row in ipairs(tilemap) do
    for j,tile in ipairs(row) do
      if tile ~= 0 then
        love.graphics.setColor(colors[tile])
        --Dessiner l'image
        love.graphics.draw(image, j * width, i * height)
      end
    end
  end
end

C’est donc simple. Mais que se passe-t-il si nous voulons dessiner des images différentes ? Eh bien, nous pourrions utiliser plusieurs images, mais dans le chapitre précédent, nous avons appris comment dessiner une partie d’une image avec des quads. Nous pouvons également les utiliser pour les tuiles.

Utilisons ce jeu de tuiles :

Jeu de tuiles
Jeu de tuiles

Nous devons d’abord créer les quads.

 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
function love.load()
  --Charger l'image
  image = love.graphics.newImage("tileset.png")

  --Nous avons besoin de la largeur et hauteur complètes de l'image pour créer les quads
  local image_width = image:getWidth()
  local image_height = image:getHeight()

  --La largeur et hauteur de chaque tuile est 32, 32
  --Nous pourrions donc faire :
  width = 32
  height = 32
  --Mais disons que nous ne connaissons pas la largeur et la hauteur d'une tuile
  --Nous pouvons aussi utiliser le nombre de lignes et colonnes dans le tileset
  --Notre tileset a 2 lignes et 3 colonnes
  --Mais nous devons soustraire 2 pour compenser les pixels vides inclus pour éviter le bleeding
  width = (image_width / 3) - 2
  height = (image_height / 2) - 2

  --Créer les quads
  quads = {}

  for i=0,1 do
    for j=0,2 do
      --La seule raison pour laquelle ce code est divisé en plusieurs lignes
      --est pour qu'il tienne sur la page
      table.insert(quads,
          love.graphics.newQuad(
              1 + j * (width + 2),
              1 + i * (height + 2),
              width, height,
              image_width, image_height))
    end
  end

  tilemap = {
    {1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
    {1, 2, 2, 2, 2, 2, 2, 2, 2, 1},
    {1, 2, 3, 4, 5, 5, 4, 3, 2, 1},
    {1, 2, 2, 2, 2, 2, 2, 2, 2, 1},
    {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
  }
end

Maintenant que nous avons une table avec des quads, nous mettons le numéro correspondant dans notre tilemap en fonction du quad que nous voulons. En fonction de l’ordre dans lequel nous avons créé nos quads, ils se trouvent à cette position dans notre tableau :

Jeu de tuiles numéroté pour se faciliter la vie
Jeu de tuiles numéroté pour se faciliter la vie

Donc si nous voulons créer ceci :

Ce à quoi devra ressembler notre niveau
Ce à quoi devra ressembler notre niveau

Nous ferions ressembler notre tilemap à ceci :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
tilemap = {
  {1, 6, 6, 2, 1, 6, 6, 2},
  {3, 0, 0, 4, 5, 0, 0, 3},
  {3, 0, 0, 0, 0, 0, 0, 3},
  {4, 2, 0, 0, 0, 0, 1, 5},
  {1, 5, 0, 0, 0, 0, 4, 2},
  {3, 0, 0, 0, 0, 0, 0, 3},
  {3, 0, 0, 1, 2, 0, 0, 3},
  {4, 6, 6, 5, 4, 6, 6, 5}
}

Si vous comparez le tilemap avec l’image et les nombres, vous pouvez voir comment chaque tuile est utilisée.

Maintenant, nous devons dessiner le bon quad.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function love.draw()
  for i,row in ipairs(tilemap) do
    for j,tile in ipairs(row) do
      if tile ~= 0 then
        --Dessiner l'image avec le quad correct
        love.graphics.draw(image, quads[tile], j * width, i * height)
      end
    end
  end
end

Donc à la position (1,1) nous dessinons le quad en position 1. À la position (1,2) nous dessinons le quad à la position 6, etc.

Si vous lancez le jeu, vous verrez que notre niveau ressemble maintenant à l’image ci-dessus.

Joueur

Maintenant que nous avons un niveau, créons un joueur qui peut se promener, mais qui ne peut pas traverser les murs.

Je vais utiliser cette image pour le joueur :

Image représentant le joueur
Image représentant le joueur
 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
function love.load()
  image = love.graphics.newImage("tileset.png")

  local image_width = image:getWidth()
  local image_height = image:getHeight()
  width = (image_width / 3) - 2
  height = (image_height / 2) - 2

  quads = {}

  for i=0,1 do
    for j=0,2 do
      table.insert(quads,
        love.graphics.newQuad(
          1 + j * (width + 2),
          1 + i * (height + 2),
          width, height,
          image_width, image_height))
    end
  end

  tilemap = {
    {1, 6, 6, 2, 1, 6, 6, 2},
    {3, 0, 0, 4, 5, 0, 0, 3},
    {3, 0, 0, 0, 0, 0, 0, 3},
    {4, 2, 0, 0, 0, 0, 1, 5},
    {1, 5, 0, 0, 0, 0, 4, 2},
    {3, 0, 0, 0, 0, 0, 0, 3},
    {3, 0, 0, 1, 2, 0, 0, 3},
    {4, 6, 6, 5, 4, 6, 6, 5}
  }

  --Créer notre joueur
  player = {
    image = love.graphics.newImage("player.png"),
    tile_x = 2,
    tile_y = 2
  }
end

Les propriétés tile_x et tile_y correspondent à la position du joueur sur notre tilemap. Ces nombres seront multipliés par la largeur et la hauteur des tuiles lorsqu’ils seront dessinés. Mais d’abord, faisons bouger le joueur. Au lieu d’un mouvement fluide, nous le ferons sauter à sa position suivante ; nous n’aurons donc pas besoin de dt pour le mouvement. Cela signifie également que nous ne voulons pas savoir si les touches de mouvement sont enfoncées, mais si elles viennent d’être pressées. Pour cela, nous utilisons l’événement love.keypressed.

Nous créons d’abord les variables locales x et y. Ensuite, nous ajoutons ou soustrayons 1 à cette variable en fonction de la touche qui a été enfoncée, et finalement nous attribuons cette valeur à la position du joueur.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function love.keypressed(key)
  local x = player.tile_x
  local y = player.tile_y

  if key == "left" then
      x = x - 1
  elseif key == "right" then
      x = x + 1
  elseif key == "up" then
      y = y - 1
  elseif key == "down" then
      y = y + 1
  end

  player.tile_x = x
  player.tile_y = y
end

Maintenant qu’il peut bouger, dessinons-le.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function love.draw()
  for i,row in ipairs(tilemap) do
    for j,tile in ipairs(row) do
      if tile ~= 0 then
        --Dessiner l'image avec le quad correct
        love.graphics.draw(image, quads[tile], j * width, i * height)
      end
    end
  end

  --Dessiner le joueur et multiplier sa position de tuile par la largeur et la hauteur des tuiles
  love.graphics.draw(player.image, player.tile_x * width, player.tile_y * height)
end
Ce que permet de faire le code actuel
Ce que permet de faire le code actuel

Lorsque vous exécutez le jeu, vous devriez pouvoir vous déplacer avec votre joueur. Mais le problème est qu’il peut traverser les murs. Corrigeons cela en vérifiant si la position où il veut aller est un mur.

Créez d’abord une fonction appelée isEmpty. À l’intérieur, nous retournons si la valeur aux coordonnées données est égale à 0.

1
2
3
function isEmpty(x, y)
  return tilemap[y][x] == 0
end

Il peut sembler étrange que x et y soient inversés, mais c’est correct. Parce que la position y est la ligne et la position x est la colonne.

Maintenant que nous avons notre fonction, nous pouvons vérifier si l’endroit où nous voulons aller est un emplacement vide, et si c’est le cas, cela signifie que nous pouvons y aller.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function love.keypressed(key)
  local x = player.tile_x
  local y = player.tile_y

  if key == "left" then
    x = x - 1
  elseif key == "right" then
    x = x + 1
  elseif key == "up" then
    y = y - 1
  elseif key == "down" then
    y = y + 1
  end

  if isEmpty(x, y) then
    player.tile_x = x
    player.tile_y = y
  end
end
Ce que permet de faire le code définitif
Ce que permet de faire le code définitif

Super, maintenant notre joueur est piégé à l’intérieur de nos murs. Essayez de voir si vous pouvez lui faire ramasser des objets ou lui faire ouvrir une porte lorsque vous appuyez sur une touche. Cherchez des idées et essayez de les appliquer. C’est comme ça que vous apprendrez.

Résumé

Nous pouvons utiliser des tuiles pour faire des niveaux. Un tilemap est constitué de lignes et de colonnes. Chaque ligne contient un certain nombre de colonnes. Les lignes sont alignées verticalement et les colonnes horizontalement. Nous pouvons utiliser un jeu de tuiles et des quads pour dessiner notre niveau.