Guardar partidas

Introducción

Guardar partidas puede ser complicado. Por ejemplo, puede ser necesario almacenar información de varios objetos en varios niveles. Los sistemas avanzados de guardado de partidas deberían permitir la obtención de información adicional sobre un número arbitrario de objetos. Esto permitirá que la función de guardado se amplíe a medida que el juego se vuelva más complejo.

Nota

Si quieres guardar la configuración del usuario, puedes usar la clase ConfigFile para este propósito.

Identificar objetos persistentes

En primer lugar, debemos identificar qué objetos queremos conservar entre las sesiones de juego y qué información queremos conservar de esos objetos. Para este tutorial, usaremos grupos para marcar y manejar los objetos a guardar, pero desde luego existen otros métodos posibles.

Empezaremos añadiendo los objetos que deseamos guardar al grupo "Persist". Como en el tutorial Scripting (continuación), podemos hacerlo a través de la GUI o el script. Añadiremos los nodos relevantes usando la GUI:

../../_images/groups.png

Una vez hecho esto, cuando necesitemos guardar la partida, podemos hacer que todos los objetos se salven y luego decirles a todos que se guarden con este 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.
}

Serialización

El siguiente paso es serializar los datos. Esto hace que sea mucho más fácil de leer y almacenar en el disco. En este caso, asumiremos que cada miembro del grupo Persist es un nodo instanciado y por lo tanto tiene una ruta. GDScript tiene funciones de ayuda para esto, como to_json() y parse_json(), así que usaremos un diccionario. Nuestro nodo necesita contener una función de guardado que devuelva estos datos. La función de guardar tendrá este aspecto:

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 }
    };
}

Esto nos da un diccionario con el estilo { "variable_name":value_of_variable }, que nos será útil al cargar.

Guardar y leer datos

Como se explica en el tutorial de Sistema de archivos, necesitaremos abrir un archivo para poder escribir en él o leer de él. Ahora que tenemos una forma de llamar a nuestros grupos y obtener sus datos relevantes, usemos to_json() para convertirlo en una cadena que se pueda guardar fácilmente y almacenarla en un archivo. Haciéndolo de esta manera nos aseguramos de que cada línea sea su propio objeto, así que también tenemos una manera fácil de recuperar los datos del archivo.

# 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();
}

¡Juego guardado! La carga es bastante simple también. Para ello, leeremos cada línea; usaremos parse_json() para volver a leerla hasta un diccionario, y luego iteraremos sobre el diccionario para leer nuestros valores. Pero primero tendremos que crear el objeto y podemos usar el nombre de archivo y los valores del padre para lograrlo. Aquí está nuestra función de carga:

# 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();
}

Ahora podemos guardar y cargar un número arbitrario de objetos dispuestos casi en cualquier lugar del árbol de la escena. Cada objeto puede almacenar diferentes datos dependiendo de lo que necesite guardar.

Algunas notas

Hemos pasado por alto la configuración del estado del juego para la carga. En última instancia, depende del creador del proyecto donde va gran parte de esta lógica. Esto suele ser complicado y tendrá que ser muy personalizado en función de las necesidades del proyecto en particular.

Además, nuestra implementación asume que ningún objeto persistente es hijo de otro objeto persistente. De lo contrario, se crearían rutas inválidas. Para acomodar los objetos persistentes anidados, considera guardar los objetos en etapas. Cargar primero los objetos padre para que estén disponibles para la llamada add_child() cuando se carguen los objetos hijo. También necesitarás una forma de enlazar a los hijos con los padres ya que la llamada NodePath probablemente será inválida.