Spiele speichern

Einführung

Save games can be complicated. For example, it may be desirable to store information from multiple objects across multiple levels. Advanced save game systems should allow for additional information about an arbitrary number of objects. This will allow the save function to scale as the game grows more complex.

Bemerkung

If you're looking to save user configuration, you can use the ConfigFile class for this purpose.

Identifizieren beständiger Objekte

Erstens sollten wir ermitteln, welche Objekte wir zwischen den Spielsitzungen behalten möchten und welche Informationen wir von diesen Objekten behalten möchten. Für diese Anleitung verwenden wir Gruppen, um zu speichernde Objekte zu markieren und zu behandeln, aber andere Methoden sind sicherlich möglich.

Wir beginnen mit dem Hinzufügen von Objekten, die wir der Gruppe "Persist" speichern möchten. Wie in der Anleitung Skripten (Fortsetzung) können wir dies entweder über die GUI oder das Skript tun. Fügen wir die entsprechenden Knoten mithilfe der GUI hinzu:

../../_images/groups.png

Sobald dies erledigt ist und wir das Spiel speichern müssen, können wir alle Objekte dazu bringen sie zu speichern und ihnen dann sagen, dass sie mit diesem Skript speichern sollen:

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

Serialisierung

The next step is to serialize the data. This makes it much easier to read from and store to disk. In this case, we're assuming each member of group Persist is an instanced node and thus has a path. GDScript has helper functions for this, such as to_json() and parse_json(), so we will use a dictionary. Our node needs to contain a save function that returns this data. The save function will look like this:

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

This gives us a dictionary with the style { "variable_name":value_of_variable }, which will be useful when loading.

Daten speichern und einlesen

As covered in the Dateisystem tutorial, we'll need to open a file so we can write to it or read from it. Now that we have a way to call our groups and get their relevant data, let's use to_json() to convert it into an easily stored string and store them in a file. Doing it this way ensures that each line is its own object, so we have an easy way to pull the data out of the file as well.

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

Spiel gespeichert! Das Laden ist auch ziemlich einfach. Dazu verwenden wir parse_json() um jede Zeile in ein Wörterbuch zurückzulesen, und durchlaufen dann das Wörterbuch um unsere Werte zu lesen. Zuerst muss aber das Objekt erstellt werden und der Dateiname und die übergeordneten Werte können hierfür genutzt werden. Hier ist unsere Ladefunktion:

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

Jetzt können wir eine beliebige Anzahl von Objekten speichern und laden, die fast überall im Szenenbaum angeordnet sind! Jedes Objekt kann unterschiedliche Daten speichern, je nachdem was benötigt wird.

Einige Anmerkungen

We have glossed over setting up the game state for loading. It's ultimately up to the project creator where much of this logic goes. This is often complicated and will need to be heavily customized based on the needs of the individual project.

Additionally, our implementation assumes no Persist objects are children of other Persist objects. Otherwise, invalid paths would be created. To accommodate nested Persist objects, consider saving objects in stages. Load parent objects first so they are available for the add_child() call when child objects are loaded. You will also need a way to link children to parents as the NodePath will likely be invalid.