L'enregistrement des parties

Introduction

Sauvegarder peut être compliqué. Par exemple, il est probable de vouloir stocker des informations de différents objets réparti dans différents niveaux. Les systèmes avancés de sauvegarde devraient permettre de sauvegarder des informations supplémentaires sur un nombre arbitraire d'objets. Cela permet à la fonction de sauvegarde de s'adapter à l'évolution en complexité du jeu.

Note

Si vous cherchez à sauvegarder la configuration utilisateur, vous pouvez utiliser la classe ConfigFile dans ce but.

Identifier des objets persistants

Tout d'abord, nous devons identifier les objets que nous voulons garder entre les sessions de jeu et les informations que nous voulons garder de ces objets. Pour ce tutoriel, nous utiliserons des groupes pour marquer et manipuler les objets à sauvegarder, mais d'autres méthodes sont certainement possibles.

Nous commencerons par ajouter les objets que nous souhaitons enregistrer au groupe "Persist". Comme dans le tutoriel Scripting (suite), nous pouvons le faire soit par l'interface graphique soit par le script. Ajoutons les nœuds pertinents en utilisant l'interface graphique :

../../_images/groups.png

Une fois que c'est fait, quand nous avons besoin de sauvegarder le jeu, nous pouvons obtenir tous les objets pour les sauvegarder et ensuite leur dire à tous de sauvegarder avec ce script :

var save_nodes = get_tree().get_nodes_in_group("Persist")
for i in save_nodes:
    # Now, we can call our save function on each node.
var saveNodes = GetTree().GetNodesInGroup("Persist");
foreach (Node saveNode in saveNodes)
{
    // Now, we can call our save function on each node.
}

Sérialisation

L'étape suivante consiste à sérialiser les données. Cela rend la lecture et le stockage sur disque beaucoup plus faciles. Dans ce cas, nous supposons que chaque membre du groupe Persist est un nœud instancié et a donc un chemin. GDScript a des fonctions d'aide pour cela, comme to_json() et parse_json(), donc nous allons utiliser un dictionnaire. Notre nœud doit contenir une fonction de sauvegarde qui renvoie ces données. La fonction de sauvegarde ressemblera à ceci :

func save():
    var save_dict = {
        "filename" : get_filename(),
        "parent" : get_parent().get_path(),
        "pos_x" : position.x, # Vector2 is not supported by JSON
        "pos_y" : position.y,
        "attack" : attack,
        "defense" : defense,
        "current_health" : current_health,
        "max_health" : max_health,
        "damage" : damage,
        "regen" : regen,
        "experience" : experience,
        "tnl" : tnl,
        "level" : level,
        "attack_growth" : attack_growth,
        "defense_growth" : defense_growth,
        "health_growth" : health_growth,
        "is_alive" : is_alive,
        "last_attack" : last_attack
    }
    return save_dict
public Godot.Collections.Dictionary<string, object> Save()
{
    return new Godot.Collections.Dictionary<string, object>()
    {
        { "Filename", GetFilename() },
        { "Parent", GetParent().GetPath() },
        { "PosX", Position.x }, // Vector2 is not supported by JSON
        { "PosY", Position.y },
        { "Attack", Attack },
        { "Defense", Defense },
        { "CurrentHealth", CurrentHealth },
        { "MaxHealth", MaxHealth },
        { "Damage", Damage },
        { "Regen", Regen },
        { "Experience", Experience },
        { "Tnl", Tnl },
        { "Level", Level },
        { "AttackGrowth", AttackGrowth },
        { "DefenseGrowth", DefenseGrowth },
        { "HealthGrowth", HealthGrowth },
        { "IsAlive", IsAlive },
        { "LastAttack", LastAttack }
    };
}

Cela nous donne un dictionnaire avec le style {"nom_variable":valeur_de_variable }, qui sera utile lors du chargement.

Sauvegarde et lecture des données

Comme expliqué dans le tutoriel Système de fichiers, nous devons ouvrir un fichier pour pouvoir y écrire ou y lire. Maintenant que nous avons un moyen d'appeler nos groupes et d'obtenir leurs données pertinentes, utilisons to_json() pour les convertir en une chaîne facilement stockable et les stocker dans un fichier. En procédant de cette façon, on s'assure que chaque ligne est son propre objet, de sorte que nous avons un moyen facile de sortir les données du fichier également.

# Note: This can be called from anywhere inside the tree. This function is
# path independent.
# Go through everything in the persist category and ask them to return a
# dict of relevant variables.
func save_game():
    var save_game = File.new()
    save_game.open("user://savegame.save", File.WRITE)
    var save_nodes = get_tree().get_nodes_in_group("Persist")
    for node in save_nodes:
        # Check the node is an instanced scene so it can be instanced again during load.
        if node.filename.empty():
            print("persistent node '%s' is not an instanced scene, skipped" % node.name)
            continue

        # Check the node has a save function.
        if !node.has_method("save"):
            print("persistent node '%s' is missing a save() function, skipped" % node.name)
            continue

        # Call the node's save function.
        var node_data = node.call("save")

        # Store the save dictionary as a new line in the save file.
        save_game.store_line(to_json(node_data))
    save_game.close()
// Note: This can be called from anywhere inside the tree. This function is
// path independent.
// Go through everything in the persist category and ask them to return a
// dict of relevant variables.
public void SaveGame()
{
    var saveGame = new File();
    saveGame.Open("user://savegame.save", (int)File.ModeFlags.Write);

    var saveNodes = GetTree().GetNodesInGroup("Persist");
    foreach (Node saveNode in saveNodes)
    {
        // Check the node is an instanced scene so it can be instanced again during load.
        if (saveNode.Filename.Empty())
        {
            GD.Print(String.Format("persistent node '{0}' is not an instanced scene, skipped", saveNode.Name));
            continue;
        }

        // Check the node has a save function.
        if (!saveNode.HasMethod("Save"))
        {
            GD.Print(String.Format("persistent node '{0}' is missing a Save() function, skipped", saveNode.Name));
            continue;
        }

        // Call the node's save function.
        var nodeData = saveNode.Call("Save");

        // Store the save dictionary as a new line in the save file.
        saveGame.StoreLine(JSON.Print(nodeData));
    }

    saveGame.Close();
}

Jeu sauvegardé ! Le chargement est également assez simple. Pour cela, nous allons lire chaque ligne, utiliser parse_json() pour la reconvertir dans un dict, et ensuite itérer dans le dict pour lire nos valeurs. Mais nous devons d'abord créer l'objet et nous pouvons utiliser le nom du fichier et les valeurs parentales pour y parvenir. Voici notre fonction de chargement :

# Note: This can be called from anywhere inside the tree. This function
# is path independent.
func load_game():
    var save_game = File.new()
    if not save_game.file_exists("user://savegame.save"):
        return # Error! We don't have a save to load.

    # We need to revert the game state so we're not cloning objects
    # during loading. This will vary wildly depending on the needs of a
    # project, so take care with this step.
    # For our example, we will accomplish this by deleting saveable objects.
    var save_nodes = get_tree().get_nodes_in_group("Persist")
    for i in save_nodes:
        i.queue_free()

    # Load the file line by line and process that dictionary to restore
    # the object it represents.
    save_game.open("user://savegame.save", File.READ)
    while save_game.get_position() < save_game.get_len():
        # Get the saved dictionary from the next line in the save file
        var node_data = parse_json(save_game.get_line())

        # Firstly, we need to create the object and add it to the tree and set its position.
        var new_object = load(node_data["filename"]).instance()
        get_node(node_data["parent"]).add_child(new_object)
        new_object.position = Vector2(node_data["pos_x"], node_data["pos_y"])

        # Now we set the remaining variables.
        for i in node_data.keys():
            if i == "filename" or i == "parent" or i == "pos_x" or i == "pos_y":
                continue
            new_object.set(i, node_data[i])

    save_game.close()
// Note: This can be called from anywhere inside the tree. This function is
// path independent.
public void LoadGame()
{
    var saveGame = new File();
    if (!saveGame.FileExists("user://savegame.save"))
        return; // Error!  We don't have a save to load.

    // We need to revert the game state so we're not cloning objects during loading.
    // This will vary wildly depending on the needs of a project, so take care with
    // this step.
    // For our example, we will accomplish this by deleting saveable objects.
    var saveNodes = GetTree().GetNodesInGroup("Persist");
    foreach (Node saveNode in saveNodes)
        saveNode.QueueFree();

    // Load the file line by line and process that dictionary to restore the object
    // it represents.
    saveGame.Open("user://savegame.save", (int)File.ModeFlags.Read);

    while (saveGame.GetPosition() < saveGame.GetLen())
    {
        // Get the saved dictionary from the next line in the save file
        var nodeData = new Godot.Collections.Dictionary<string, object>((Godot.Collections.Dictionary)JSON.Parse(saveGame.GetLine()).Result);

        // Firstly, we need to create the object and add it to the tree and set its position.
        var newObjectScene = (PackedScene)ResourceLoader.Load(nodeData["Filename"].ToString());
        var newObject = (Node)newObjectScene.Instance();
        GetNode(nodeData["Parent"].ToString()).AddChild(newObject);
        newObject.Set("Position", new Vector2((float)nodeData["PosX"], (float)nodeData["PosY"]));

        // Now we set the remaining variables.
        foreach (KeyValuePair<object, object> entry in nodeData)
        {
            string key = entry.Key.ToString();
            if (key == "Filename" || key == "Parent" || key == "PosX" || key == "PosY")
                continue;
            newObject.Set(key, entry.Value);
        }
    }

    saveGame.Close();
}

Nous pouvons maintenant sauvegarder et charger un nombre arbitraire d'objets disposés presque partout dans l'arbre de scène ! Chaque objet peut stocker des données différentes en fonction de ce qu'il doit sauvegarder.

Quelques notes

Nous avons passé sous silence la mise en place de l'état du jeu pour le chargement. C'est finalement au créateur du projet qu'il revient de décider où va une grande partie de cette logique. C'est souvent compliqué et devra être fortement personnalisé en fonction des besoins du projet.

De plus, notre implémentation suppose qu'aucun objet Persist n'est l'enfant d'un autre objet Persist. Sinon, des chemins invalides seraient créés. Pour prendre en compte les objets Persistants imbriqués, envisagez de sauvegarder les objets par étapes. Chargez d'abord les objets parents pour qu'ils soient disponibles pour l'appel add_child() quand les objets enfants sont chargés. Vous aurez aussi besoin d'un moyen de lier les enfants aux parents car le NodePath sera probablement invalide.