Sauvegarde

Sauvegarder et charger

Imaginez un jeu sans sauvegarde...

Sauvegarder

Pour une raison ou une autre, je ne voulais pas m’y mettre tout de suite. En fait, j’avais peur que ce soit compliqué. Mais comme j’avais déjà créé quelques scènes de test, c’était plus pratique de pouvoir sauvegarder le jeu. Et bien sûr de le charger.

Pour ça, j’ai créé dans engine3 une classe, serialization.lua, qui est un wrapper autour de binser.

Cela permet de sauvegarder correctement les données des objets.

Dans le jeu, j’ai ensuite créé une classe state.lua qui ressemble à peu près à ceci.

function State:saveEntities()
    for _, entity in pairs(entityManager.entities) do
        if entity.saveEntity then
            entity:saveEntity()
        end
    end
end

---@param file string Where to save the data
function State:save(file, force)
    -- Check if it is possible to save right now
    if action:isProcessWorking() and force ~= true then
        print("saving is not possible now, a action is still working")
        return false
    end

    local data = {}

    -- Create a save state for all entities that are needed
    self:saveEntities()

    -- We don't need all session data.
    -- @warning keep this up to date if you want to add something
    local sessionData = session:getData()
    local validSessionKeyNamePatterns = {
        "^actor-.*",
        "^item-.*",
        "^animation-.*",
        "^scene-.*",
        "action",
        "^entity_delete",
        "^entity_properties",
        "^currentSceneUkey"
    }
    for sessionKeyName, sessionValue in pairs(sessionData) do
        -- I don't know how to create multi patterns ... [x|y] don't work
        local check = false
        for _, pattern in pairs(validSessionKeyNamePatterns) do
            if string.match(sessionKeyName, pattern) ~= nil then
                if self.config.log.state then
                    print("[SESSION.SET." .. sessionKeyName .. "]")
                end
                data[sessionKeyName] = sessionValue
                check = true
                break
            end
        end

        if check == false and self.config.log.state then
            print("[SESSION.INFO." .. sessionKeyName .. "] don't match any save pattern")
        end
    end

    local success, message = love.filesystem.write(file, Serialization.serialize(data))
    if success == false then
        error("Cannot save data, " .. message)
    end
end

---@param file string What file data to load
function State:load(file)
    local data = love.filesystem.read(file)
    if data then
        local results, len = Serialization.deserialize(data)
        -- at the moment we only use one object in each file
        if len == 1 then
            -- set the session data
            session:setData(results[1])
        end
    end
end

Utilisation

Dans chaque classe entity, on peut maintenant créer (en option) deux fonctions utilisées lors de la sauvegarde et du chargement du jeu. Voici un exemple résumé de l’entity des personnages. L’avantage de cette approche, c’est qu’on peut aller dans toutes les directions à l’avenir, et aussi que les fichiers restent très petits.

-- special function that will execute when saving the game
function Actor:saveEntity()
    -- maybe a extend class did create some data
    local data = session:get("actor-" .. self.ukey) or {}

    data.sceneUkey = self.sceneUkey
    data.lastMove = self.lastMove
    data.movableByUserInput = self.movableByUserInput

    if self.activeFrameset then
        data.startPositionX = self.activeFrameset:getX()
        data.startPositionY = self.activeFrameset:getY()
    end

    -- maybe the actor did change the frameset (using a hat)
    data.frameset = {}
    for index, frameset in pairs(self.frameset) do
        data.frameset[index] = frameset.name
    end

    session:set("actor-" .. self.ukey, data)
end

function Actor:loadEntity()
    local restoreData = session:get("actor-" .. self.ukey)
    if restoreData == nil then
        return
    end

    if restoreData.sceneUkey ~= nil then
        self:setSceneUkey(restoreData.sceneUkey)
    end

    if restoreData.movableByUserInput ~= nil then
        self:setMovableByUserInput(restoreData.movableByUserInput)
    end

    -- maybe the actor did change the frameset (using a hat)
    if restoreData.frameset then
        for index, framesetName in pairs(restoreData.frameset) do
            self:setFrameset(index, framesetName)
        end
    end

    if restoreData.startPositionX and restoreData.startPositionY then
        self.movable.vPosition.x = restoreData.startPositionX
        self.movable.vPosition.y = restoreData.startPositionY
        self:setXY(self.movable.vPosition.x, self.movable.vPosition.y)
    end

    if restoreData.lastMove then
        -- what will be the new activeFrameset (index)
        self:setActiveFrameset(restoreData.lastMove)
    end

    -- when load scene is used, we don't set the player position
    -- @warning we do this after setXY, because that function will show the player
    local currentSceneUkey = session:get("currentSceneUkey")
    if currentSceneUkey and self.sceneUkey ~= currentSceneUkey then
        self:hide()
    end
end

Pour finir

Comme vous le voyez, il suffit de veiller à ne sauvegarder que les données dont on a réellement besoin. Toutes les autres données sont inutiles et rendent le débogage presque impossible. N’attendez pas trop pour mettre en place un tel système, car vous devrez de toute façon le faire.

Pour l’instant, le code ci-dessus est déclenché par un bouton, avec une capture d’écran du jeu, et le personnage choisi annonce que la sauvegarde a réussi.

self.buttonSave.callbackAfterMousePressedEvent["btnSave"] = function(button)
        local currentTime = os.time()
        local saveGameName = "savegame" .. currentTime .. ".data"
        local saveGameScreenshot = "screenshot" .. currentTime .. ".png"

        -- create a screenshot without the state menu (write it to the save directory (love default))
        love.graphics.captureScreenshot(saveGameScreenshot)

        -- save the game
        State():save(saveGameName)

        -- say that the save the game was correct
        local activeActor = entityManager:find({ movableByUserInput = true }, "Player")
        if #activeActor == 1 then
            local actionSay = ActionSay()
            actionSay:execute(
                {
                    action = action.SAY,
                    initiator = activeActor[1]
                },
                {
                    text = i18n:s("Save game done!")
                },
                nil,
                {},
                {},
                nil
            )
        end

        self.buttonLoad:setIsVisible(true)

        return { preventDefault = true }
    end