Сохранение игр

Введение

Игры с сохранением могут быть сложными. Например, может быть желательно сохранить информацию о нескольких объектах на нескольких уровнях. Продвинутые системы сохранения игр должны позволять сохранять дополнительную информацию о произвольном количестве объектов. Это позволит масштабировать функцию сохранения по мере усложнения игры.

Примечание

Если вы хотите сохранить конфигурацию пользователя, вы можете использовать для этого класс ConfigFile.

Идентификация постоянных объектов

Во-первых, мы должны определить, какие объекты мы хотим сохранить между игровыми сессиями и какую информацию мы хотим сохранить от этих объектов. В этом учебнике мы будем использовать группы для маркировки и обработки объектов для сохранения, но, конечно, возможны и другие методы.

We will start by adding objects we wish to save to the "Persist" group. We can do this through either the GUI or script. Let's add the relevant nodes using the GUI:

../../_images/groups.png

Когда это будет сделано, когда нам понадобится сохранить игру, мы сможем получить все объекты для сохранения, а затем сказать им всем сохраниться с помощью этого скрипта:

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

Сериализация

Следующим шагом является сериализация данных. Это значительно упрощает считывание и сохранение данных на диске. В данном случае мы предполагаем, что каждый член группы Persist является экземпляром узла и, следовательно, имеет путь. В GDScript есть вспомогательные функции для этого, такие как to_json() и parse_json(), поэтому мы будем использовать словарь. Наш узел должен содержать функцию сохранения, которая возвращает эти данные. Функция сохранения будет выглядеть следующим образом:

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

Это дает нам словарь со стилем {"имя_переменной":value_of_variable }, который будет полезен при загрузке.

Сохранение и чтение данных

Как описано в руководстве Файловая система, нам нужно будет открыть файл, чтобы мы могли записывать в него или читать из него. Теперь, когда у нас есть способ вызвать наши группы и получить их соответствующие данные, давайте используем to_json(), чтобы преобразовать его в легко сохраняемую строку и сохранить их в файле. Выполнение этого таким образом гарантирует, что каждая строка является собственным объектом, поэтому у нас есть простой способ извлечь данные из файла.

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

Игра сохранена! Загрузка также довольно проста. Для этого мы будем читать каждую строку, использовать функцию parse_json(), чтобы считать ее обратно в dict, а затем выполнять итерации над dict, чтобы считать наши значения. Но сначала нам нужно будет создать объект, и для этого мы можем использовать значения имени файла и родителя. Вот наша функция load:

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

Теперь мы можем сохранять и загружать произвольное количество объектов, расположенных практически в любом месте дерева сцены! Каждый объект может хранить различные данные в зависимости от того, что ему нужно сохранить.

Некоторые примечания

Мы подробно остановились на настройке состояния игры для загрузки. В конечном итоге от создателя проекта зависит, куда будет направлена большая часть этой логики. Это часто бывает сложно и требует значительной настройки в зависимости от потребностей конкретного проекта.

Кроме того, наша реализация предполагает, что никакие объекты Persist не являются дочерними для других объектов Persist. В противном случае будут создаваться недопустимые пути. Чтобы учесть вложенные объекты Persist, рассмотрите возможность поэтапного сохранения объектов. Сначала загрузите родительские объекты, чтобы они были доступны для вызова add_child(), когда загружаются дочерние объекты. Вам также понадобится способ связать дочерние объекты с родительскими, поскольку NodePath, скорее всего, будет недействительным.