保存游戏

介绍

保存游戏可能很复杂。可能需要存储比当前级别或级别上获得的星数更多的信息。更高级的“保存”游戏可能需要存储有关任意数量对象的附加信息。这将允许保存函数在游戏变得更复杂时进行缩放。

识别持久对象

首先,我们应该确定我们想在游戏期间保留哪些对象,以及我们想从这些对象中保留哪些信息。对于本教程,我们将使用组来标记和处理要保存的对象,但其他方法当然是可能的。

我们将从添加希望保存到“持久”组的对象开始。就像在 脚本(续) 教程,我们可以通过图形用户界面或脚本来实现这一点。让我们使用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 Dictionary<object, object> Save()
{
    return new Dictionary<object, 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 }
    };
}

这给了我们一本风格各异的字典 {{ "variable_name":value_of_variable }} ,这在加载时很有用。

保存和读取数据

文件系统 教程中,我们需要打开一个文件并对其进行写入,然后再从中进行读取。既然我们有了一种方法来调用我们的组并获取它们的相关数据,那么让我们使用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 i in save_nodes:
        var node_data = i.call("save");
        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)
    {
        var nodeData = saveNode.Call("Save");
        saveGame.StoreLine(JSON.Print(nodeData));
    }

    saveGame.Close();
}

游戏已保存!装载也相当简单。为此,我们将读取每一行,使用parse_json()将其读回到dict,然后迭代dict以读取我们的值。但我们需要首先创建对象,并且可以使用文件名和父值来实现这一点。这是我们的负载函数:

# 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 not save_game.eof_reached():
        var current_line = 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(current_line["filename"]).instance()
        get_node(current_line["parent"]).add_child(new_object)
        new_object.position = Vector2(current_line["pos_x"], current_line["pos_y"])
        # Now we set the remaining variables.
        for i in current_line.keys():
            if i == "filename" or i == "parent" or i == "pos_x" or i == "pos_y":
                continue
            new_object.set(i, current_line[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.EofReached())
    {
        var currentLine = (Dictionary<object, object>)JSON.Parse(saveGame.GetLine()).Result;
        if (currentLine == null)
            continue;

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

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

    saveGame.Close();
}

现在,我们可以保存和加载任意数量的对象,这些对象几乎分布在场景树的任何位置!根据需要保存的内容,每个对象可以存储不同的数据。

一些注释

我们可能忽略了一个步骤,但是将游戏状态设置为一个适合开始加载数据的状态可能很复杂。这个步骤需要根据单个项目的需要进行大量定制。

此实现假定没有持久对象是其他持久对象的子对象。否则,将创建无效路径。要容纳嵌套的持久对象,请考虑分阶段保存对象。首先加载父对象,以便在加载子对象时,它们可用于add_child()调用。您还需要一种将孩子链接到家长的方法,因为节点路径可能无效。