Sauvegarde et chargement avec love2d

Faisons en sorte que nous puissions enregistrer et charger nos progrès dans notre jeu. Nous le faisons en écrivant et en lisant un fichier. Mais nous devons d’abord avoir un jeu, alors faisons un petit jeu où vous pouvez ramasser des pièces.

On commence avec un joueur qui peut bouger

 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
function love.load()
  -- Create a player object with an x, y and size
  player = {
    x = 100,
    y = 100,
    size = 25
  }
end

function love.update(dt)
  -- Make it moveable with keyboard 
  if love.keyboard.isDown("left") then
    player.x = player.x - 200 * dt
  elseif love.keyboard.isDown("right") then
    player.x = player.x + 200 * dt
  end

  -- Note how I start a new if-statement instead of contuing the elseif
  -- I do this because else you wouldn't be able to move diagonally.
  if love.keyboard.isDown("up") then
    player.y = player.y - 200 * dt
  elseif love.keyboard.isDown("down") then
    player.y = player.y + 200 * dt
  end
end

function love.draw()
  -- The players and coins are going to be circles
  love.graphics.circle("line", player.x, player.y, player.size)
end

Et pour le plaisir, donnons un visage au joueur.

Visage pour le joueur que nous allons utiliser
Visage pour le joueur que nous allons utiliser
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function love.load()
  -- Create a player object with an x, y and size
  player = {
    x = 100,
    y = 100,
    size = 25,
    image = love.graphics.newImage("face.png")
  }
end

function love.draw()
  love.graphics.circle("line", player.x, player.y, player.size)
  -- Set the origin of the face to the center of the image
  love.graphics.draw(player.image, player.x, player.y,
    0, 1, 1, player.image:getWidth()/2, player.image:getHeight()/2)
end

Ensuite, nous voulons ajouter des pièces. Nous les placerons au hasard sur l’écran. Et donnons-leur un petit signe dollar.

Signe dollar placé aléatoirement dans le jeu
Signe dollar placé aléatoirement dans le jeu
 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
function love.load()
  player = {
    x = 100,
    y = 100,
    size = 25,
    image = love.graphics.newImage("face.png")
  }

  coins = {}

  for i=1,25 do
    table.insert(coins,
      {
        -- Give it a random position with math.random
        x = math.random(50, 650),
        y = math.random(50, 450),
        size = 10,
        image = love.graphics.newImage("dollar.png")
      }
    )
  end
end

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

  if love.keyboard.isDown("up") then
    player.y = player.y - 200 * dt
  elseif love.keyboard.isDown("down") then
    player.y = player.y + 200 * dt
  end
end

function love.draw()
  love.graphics.circle("line", player.x, player.y, player.size)
  love.graphics.draw(player.image, player.x, player.y,
    0, 1, 1, player.image:getWidth()/2, player.image:getHeight()/2)

  for i,v in ipairs(coins) do
    love.graphics.circle("line", v.x, v.y, v.size)
    love.graphics.draw(v.image, v.x, v.y,
      0, 1, 1, v.image:getWidth()/2, v.image:getHeight()/2)
  end
end

Maintenant, nous voulons pouvoir les récupérer. Pour cela, nous devons vérifier s’il y a collision. Pour ce faire, nous calculons la distance, puis vérifions si la distance est inférieure au rayon des deux cercles résumé.

Comment gérer la collision avec des cercles
Comment gérer la collision avec des cercles
1
2
3
4
5
6
7
8
function checkCollision(p1, p2) 
  -- Calculating distance in 1 line
  -- Subtract the x's and y's, square the difference
  -- Sum the squares and find the root of the sum.
  local distance = math.sqrt((p1.x - p2.x)^2 + (p1.y - p2.y)^2)
  -- Return whether the distance is lower than the sum of the sizes.
  return distance < p1.size + p2.size
end

Et maintenant, nous parcourons toutes les pièces et vérifions si cela touche le joueur. Faisons en sorte que le joueur grandisse en ramassant une pièce.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function love.update(dt)
  if love.keyboard.isDown("left") then
    player.x = player.x - 200 * dt
  elseif love.keyboard.isDown("right") then
    player.x = player.x + 200 * dt
  end

  if love.keyboard.isDown("up") then
    player.y = player.y - 200 * dt
  elseif love.keyboard.isDown("down") then
    player.y = player.y + 200 * dt
  end

  for i,v in ipairs(coins) do
    if checkCollision(player, v) then
      table.remove(coins, i)
      player.size = player.size + 1
    end
  end
end
Animation de ce que l’on a codé jusqu’à maintenant
Animation de ce que l’on a codé jusqu’à maintenant

Maintenant, nous pouvons nous déplacer et ramasser des pièces. Agréable! Mais avant de passer à l’enregistrement et au chargement, apportons quelques modifications supplémentaires.

Si vous redémarrez le jeu plusieurs fois, vous remarquerez peut-être que même si les cercles sont positionnés de manière aléatoire, ils sont toujours au même endroit.

Pour résoudre ce problème, nous pouvons utiliser math.randomseed(). Les nombres aléatoires que vous générez sont basés sur un nombre, que nous appelons la graine. Et parce que nous ne changeons pas la graine, vous obtenez toujours les mêmes positions « aléatoires ». L’avantage des graines est qu’elles sont la clé d’un certain caractère aléatoire, que vous pouvez enregistrer et partager. Avec Minecraft par exemple, vous pouvez partager la graine qui a été utilisée pour générer un monde, et d’autres personnes peuvent l’utiliser pour obtenir le même monde généré.

Alors, quel nombre utilisons-nous comme semence ? Parce que si nous devions faire des maths au hasard (123), le jeu aurait toujours le même numéro. Nous avons besoin d’un numéro unique à chaque fois que nous commençons le jeu. Et pour cela, nous pouvons utiliser os.time(). Il s’agit d’une fonction Lua qui vous donne l’heure de votre système d’exploitation à la seconde. C’est 86400 numéros uniques par jour !

Mais le mieux pourrait être d’utiliser la bibliothèque mathématique de LÖVE. Le nombre aléatoire généré par LÖVE (rng) est automatiquement amorcé (avec os.time()) et est globalement meilleur / plus aléatoire que le rng de Lua.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function love.load()
  player = {
    x = 100,
    y = 100,
    size = 25,
    image = love.graphics.newImage("face.png")
  }

  coins = {}

  for i=1,25 do
    table.insert(coins,
      {
        x = love.math.random(50, 650),
        y = love.math.random(50, 450),
        size = 10,
        image = love.graphics.newImage("dollar.png")
      }
    )
  end
end

Une autre chose qui me dérange est la boucle for dans laquelle nous retirons des pièces. Maintenant, ramasser des pièces fonctionne bien, mais en général lorsque vous parcourez une liste et que vous supprimez des éléments de cette liste, vous souhaitez utiliser une approche plus sûre. Parce qu’en supprimant un élément, la liste se raccourcit et gâche l’utilité de la boucle for.

Essayez ce code par exemple :

1
2
3
4
5
6
local test = {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}
for i,v in ipairs(test) do
  table.remove(test, i)
end
print(#test)
-- Output: 5

Il affiche 5, ce qui signifie qu’il n’a vidé que la moitié de la liste. Cela se produit car lorsqu’il supprime un élément de la liste, tout est déplacé sur le côté et les éléments sont ignorés.

Explications de comment supprimer des éléments dans une liste
Explications de comment supprimer des éléments dans une liste

Pour résoudre ce problème, nous devons parcourir le tableau en sens inverse. De cette façon, vous ne sauterez jamais aucun élément lorsque vous aurez supprimé quelque chose.

Explications de la meilleure stratégie à adopter
Explications de la meilleure stratégie à adopter

Cela signifie que nous n’utiliserons pas ipairs mais plutôt une boucle for normale, mais à l’envers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function love.update(dt)
  if love.keyboard.isDown("left") then
    player.x = player.x - 200 * dt
  elseif love.keyboard.isDown("right") then
    player.x = player.x + 200 * dt
  end

  if love.keyboard.isDown("up") then
    player.y = player.y - 200 * dt
  elseif love.keyboard.isDown("down") then
    player.y = player.y + 200 * dt
  end

  -- Start at the end, until 1, with steps of -1 
  for i=#coins,1,-1 do
    -- Use coins[i] instead of v
    if checkCollision(player, coins[i]) then
        table.remove(coins, i)
    end
  end
end

Très bien, il est temps de commencer à sauvegarder le jeu.

Sauvegarde

Donc, ce que nous faisons pour sauvegarder le jeu, c’est que nous créons un tableau, et dans ce tableau, nous mettons toutes les informations que nous voulons enregistrer. Alors, que voulons-nous économiser? Que diriez-vous de la position du joueur, de sa taille et de la même chose pour les pièces qui n’ont pas encore été ramassées. Créons donc la fonction saveGame () où nous stockons les données importantes dans une table.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function saveGame()
  data = {}
  data.player = {
    x = player.x,
    y = player.y,
    size = player.size
  }

  data.coins = {}
  for i,v in ipairs(coins) do
    -- In this case data.coins[i] = value is the same as table.insert(data.coins, value )
    data.coins[i] = {x = v.x, y = v.y}
  end
end

Alors pourquoi sauvegarder ces parties spécifiques et pas seulement la table entière? Eh bien, en général, vous ne voulez pas utiliser plus de données que nécessaire. Nous n’avons pas besoin d’enregistrer la largeur et la hauteur de l’image car elles seront toujours les mêmes. De plus, notre objet joueur a un objet image et nous ne pouvons pas enregistrer les objets LÖVE.

Alors maintenant nous avons toutes les informations dont nous avons besoin pour le sérialiser. Cela signifie que nous devons transformer le tableau en quelque chose que nous pouvons lire parce qu’en ce moment, lorsque vous imprimez un tableau, vous pourriez obtenir quelque chose comme un tableau: 0x00e4ca20, et ce ne sont pas les informations que nous voulons enregistrer.

Pour sérialiser la table, nous allons utiliser lume qui est une bibliothèque utilitaire de rxi. Vous pouvez trouver la bibliothèque sur GitHub.

Cliquez sur lume.lua puis sur Raw et copiez le code.

Accédez à votre éditeur de texte, créez un nouveau fichier appelé lume.lua et collez le code. Chargez-le avec require dans main.lua en haut de love.load().

Lume a toutes sortes de fonctions intéressantes, mais les plus importantes pour notre cas sont la sérialisation et la désérialisation. Essayons-le. Sérialisez le tableau de données, puis imprimez sa valeur.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function saveGame()
  data = {}
  data.player = {
    x = player.x,
    y = player.y,
    size = player.size
  }

  data.coins = {}
  for i,v in ipairs(coins) do
    -- In this case data.coins[i] = value is the same as table.insert(data.coins, value )
    data.coins[i] = {x = v.x, y = v.y}
  end

  serialized = lume.serialize(data)
  print(serialized)
end

Dans votre sortie, vous verrez le tableau imprimé de manière à ce que vous puissiez le lire. C’est ce que nous allons enregistrer dans notre fichier, ce qui est la prochaine étape.

Nous pouvons créer, éditer et lire des fichiers avec le module love.filesystem (wiki). Pour créer / écrire un fichier, nous utilisons love.filesystem.write (nom de fichier, données) (wiki). Le premier argument est le nom du fichier, le deuxième argument sont les données que nous voulons écrire dans le fichier.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function saveGame()
  data = {}
  data.player = {
    x = player.x,
    y = player.y,
    size = player.size
  }

  data.coins = {}
  for i,v in ipairs(coins) do
    -- In this case data.coins[i] = value is the same as table.insert(data.coins, value )
    data.coins[i] = {x = v.x, y = v.y}
  end

  serialized = lume.serialize(data)
  -- The filetype actually doesn't matter, and can even be omitted.
  love.filesystem.write("savedata.txt", serialized)
end

Maintenant, nous devons faire en sorte que vous puissiez enregistrer la partie lorsque vous appuyez sur F1.

1
2
3
4
5
function love.keypressed(key)
  if key == "f1" then
    saveGame()
  end
end

Exécutez le jeu, prenez des pièces et appuyez sur F1. Juste comme ça, nous avons fait notre premier fichier de sauvegarde ! Alors où est-il ? Si vous êtes sous Windows, il est enregistré dans AppData\Roaming\LOVE . Vous pouvez accéder au dossier AppData caché en appuyant sur Ctrl + R, puis en tapant « appdata » et en cliquant sur OK. Il devrait y avoir un dossier qui porte le même nom que le dossier dans lequel se trouve votre projet LÖVE. Et dans ce dossier, vous trouverez un fichier nommé savedata.txt. Si vous ouvrez le fichier, vous verrez que votre table est à l’intérieur.

Maintenant, faisons en sorte que nous puissions charger nos données.

Chargement

Pour charger nos données, nous devons :

  • Vérifier s’il existe un fichier de sauvegarde
  • Lire le dossier
  • Transformez les données en tableau
  • Appliquez les données à nos joueurs et à nos pièces

Commençons donc par vérifier si notre fichier existe, et si c’est le cas, nous lisons le fichier. Nous pouvons le faire avec love.filesystem.getInfo(nom de fichier) et love.filesystem.read(nom de fichier). Si un fichier existe, love.filesystem.getInfo(filename) renverra une table avec des informations, sinon il renverra nil. Puisque nous voulons seulement savoir si le fichier existe, nous pouvons mettre la fonction dans une instruction if, car nous n’avons pas besoin des informations fournies par le tableau.

 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()
  lume = require "lume"

  player = {
    x = 100,
    y = 100,
    size = 25,
    image = love.graphics.newImage("face.png")
  }

  coins = {}

  if love.filesystem.getInfo("savedata.txt") then
    file = love.filesystem.read("savedata.txt")
    print(file)
  end

  for i=1,25 do
    table.insert(coins,
      {
        x = love.math.random(50, 650),
        y = love.math.random(50, 450),
        size = 10,
        image = love.graphics.newImage("dollar.png")
      }
    )
  end
end

Exécutez le jeu et il devrait imprimer nos données de sauvegarde. Maintenant, nous devons transformer cette chaîne de table en une vraie table. Nous pouvons le faire avec lume.deserialize.

1
2
3
4
if love.filesystem.getInfo("savedata.txt") then
  file = love.filesystem.read("savedata.txt")
  data = lume.deserialize(file)
end

Et maintenant, nous pouvons appliquer les données à notre joueur et à nos pièces. Comment nous avons mis ce code avant de remplir le tableau des pièces. C’est parce que nous ne voulons pas générer les pièces que nous avons déjà récupérées dans notre fichier de sauvegarde. Les pièces que nous ajoutons sont désormais basées sur les données.

 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()
  lume = require "lume"

  player = {
    x = 100,
    y = 100,
    size = 25,
    image = love.graphics.newImage("face.png")
  }

  coins = {}

  if love.filesystem.getInfo("savedata.txt") then
    file = love.filesystem.read("savedata.txt")
    data = lume.deserialize(file)

    --Apply the player info
    player.x = data.player.x
    player.y = data.player.y
    player.size = data.player.size

    for i,v in ipairs(data.coins) do
      coins[i] = {
        x = v.x,
        y = v.y,
        size = 10,
        image = love.graphics.newImage("dollar.png")
      }
    end
  else
    -- Only execute this if you don't have a save file
    for i=1,25 do
      table.insert(coins,
        {
          x = love.math.random(50, 650),
          y = love.math.random(50, 450),
          size = 10,
          image = love.graphics.newImage("dollar.png")
        }
      )
    end
  end
end

Maintenant, lorsque vous exécutez le jeu, vous verrez qu’il charge votre fichier de sauvegarde. Vous pouvez récupérer des pièces, appuyer sur F1 pour enregistrer, et lorsque vous redémarrez le jeu, vous verrez qu’il a à nouveau enregistré et chargé votre jeu. Impressionnant! Mais que se passe-t-il si nous voulons redémarrer? Ajoutons un bouton qui supprime notre fichier de sauvegarde afin que nous puissions commencer une nouvelle partie.

Réinitialisation

Pour supprimer notre fichier de sauvegarde, nous pouvons utiliser love.filesystem.remove(nom de fichier). Faisons en sorte que lorsque nous appuyons sur F2, le fichier soit supprimé et redémarre le jeu. Nous pouvons quitter le jeu avec love.event.quit(), mais si nous passons « restart » comme premier argument, le jeu redémarrera à la place.

1
2
3
4
5
6
7
8
function love.keypressed(key)
  if key == "f1" then
    saveGame()
  elseif key == "f2" then
  love.filesystem.remove("savedata.txt")
    love.event.quit("restart")
  end
end

Et voilà, nous pouvons maintenant réinitialiser notre jeu !

Résumé

Les graines décident quelles valeurs aléatoires que vous générez, et cela peut être utilisé pour partager un niveau généré aléatoirement par exemple. Nous pouvons également utiliser le module mathématique de LÖVE pour le faire. Lorsque vous supprimez des éléments d’une liste, nous devons parcourir le tableau en sens inverse pour éviter que des éléments soient ignorés. Nous pouvons créer un fichier de sauvegarde en ajoutant des données importantes à une table, puis transformer cette table en chaîne et écrire cette chaîne dans un fichier avec love.filesystem.write(nom de fichier). Nous pouvons charger un fichier de sauvegarde en lisant le fichier avec love.filesystem.read(nom de fichier), en désérialisant les données et en appliquant les données à nos objets. Nous pouvons supprimer un fichier avec love.filesystem.remove(nom de fichier) et redémarrer le jeu avec love.event.quit(« redémarrer »).