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

Введение

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

Примечание

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

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

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

Мы начнем с добавления объектов, которые мы хотим сохранить, в группу "Persist". Как и в учебнике Написание скриптов (продолжение), мы можем сделать это с помощью графического интерфейса или сценария. Давайте добавим соответствующие узлы с помощью графического интерфейса:

../../_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, скорее всего, будет недействительным.