Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

Spielestände speichern

Einführung

Das Behandeln von Spielständen kann komplex sein. So kann es zum Beispiel wünschenswert sein, Informationen von mehreren Objekten über mehrere Levels hinweg zu speichern. Fortgeschrittene Spielespeichersysteme sollten zusätzliche Informationen über eine beliebige Anzahl von Objekten ermöglichen. Dadurch kann die Speicherfunktion skaliert werden, wenn das Spiel komplexer wird.

Bemerkung

Wenn Sie die Benutzerkonfiguration speichern möchten, können Sie zu diesem Zweck die Klasse ConfigFile verwenden.

Siehe auch

Sie können sehen, wie das Speichern und Laden in Aktion funktioniert, indem Sie das Speichern und Laden (Serialisierung) Demo-Projekt verwenden.

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 dieses Tutorial verwenden wir Gruppen, um zu speichernde Objekte zu markieren und zu behandeln, aber andere Methoden sind genauso möglich.

Wir beginnen mit dem Hinzufügen von Objekten, die wir der Gruppe "Persist" speichern möchten. Dies können wir entweder über die GUI oder das Skript tun. Fügen wir die entsprechenden Nodes 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 node in save_nodes:
    # Now, we can call our save function on each node.

Serialisierung

Der nächste Schritt ist die Serialisierung der Daten. Dies erleichtert das Auslesen und Speichern auf der Festplatte. In diesem Fall gehen wir davon aus, dass jedes Mitglied der Gruppe Persist ein instanziierter Node ist und somit einen Pfad hat. GDScript verfügt über die Hilfsklasse JSON, um zwischen Dictionary und String zu konvertieren. Unser Node muss eine Speicherfunktion enthalten, die diese Daten zurückgibt. Die Speicherfunktion wird wie folgt aussehen:

func save():
    var save_dict = {
        "filename" : get_scene_file_path(),
        "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

So erhalten wir ein Dictionary in der Form { "variable_name":value_of_variable }, das beim Laden nützlich sein wird.

Daten speichern und einlesen

Wie im Dateisystem-Tutorial behandelt, müssen wir eine Datei öffnen, damit wir in sie schreiben oder aus ihr lesen können. Da wir nun eine Möglichkeit haben, unsere Gruppen aufzurufen und ihre relevanten Daten zu erhalten, verwenden wir die Klasse JSON, um sie in einen einfach zu speichernden String umzuwandeln und ihn in einer Datei zu speichern. Auf diese Weise wird sichergestellt, dass jede Zeile ein eigenes Objekt ist, so dass wir die Daten auch leicht aus der Datei herausholen können.

# 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 = FileAccess.open("user://savegame.save", FileAccess.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.scene_file_path.is_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")

        # JSON provides a static method to serialized JSON string.
        var json_string = JSON.stringify(node_data)

        # Store the save dictionary as a new line in the save file.
        save_game.store_line(json_string)

Spiel gespeichert! Um das Spiel zu laden, lesen wir nun jede Zeile. Verwenden Sie die Methode parse, um den JSON-String zurück in ein Dictionary zu lesen, und iterieren Sie dann über das Dictionary, um unsere Werte zu lesen. Aber wir müssen zuerst das Objekt erstellen, und wir können den Dateinamen und die Parent-Werte verwenden, um das zu erreichen. Hier ist unsere Ladefunktion:

# Note: This can be called from anywhere inside the tree. This function
# is path independent.
func load_game():
    if not FileAccess.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.
    var save_game = FileAccess.open("user://savegame.save", FileAccess.READ)
    while save_game.get_position() < save_game.get_length():
        var json_string = save_game.get_line()

        # Creates the helper class to interact with JSON
        var json = JSON.new()

        # Check if there is any error while parsing the JSON string, skip in case of failure
        var parse_result = json.parse(json_string)
        if not parse_result == OK:
            print("JSON Parse Error: ", json.get_error_message(), " in ", json_string, " at line ", json.get_error_line())
            continue

        # Get the data from the JSON object
        var node_data = json.get_data()

        # Firstly, we need to create the object and add it to the tree and set its position.
        var new_object = load(node_data["filename"]).instantiate()
        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])

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

Wir haben die Einrichtung des Spielzustands für das Laden nur am Rande erwähnt. Es ist letztendlich Sache des Projektentwicklers, wo ein Großteil dieser Logik hingeht. Dies ist oft kompliziert und muss je nach den Bedürfnissen des einzelnen Projekts stark angepasst werden.

Außerdem geht unsere Implementierung davon aus, dass keine Persist-Objekte Child-Objekte von anderen Persist-Objekten sind. Andernfalls würden ungültige Pfade entstehen. Um verschachtelte Persist-Objekte unterzubringen, sollten Sie das Speichern von Objekten in Stufen erwägen. Laden Sie zuerst die Parent-Objekte, damit sie für den Aufruf add_child() zur Verfügung stehen, wenn die Child-Objekte geladen werden. Sie brauchen auch eine Möglichkeit, Child-Objekte mit Parent-Objekten zu verknüpfen, da der NodePath wahrscheinlich ungültig sein wird.

JSON vs. binäre Serialisierung

Für einfache Spielzustände kann JSON funktionieren und erzeugt menschenlesbare Dateien, die leicht zu debuggen sind.

Aber JSON hat viele Einschränkungen. Wenn Sie komplexere Spielzustände oder viele davon speichern müssen, ist binary serialization vielleicht ein besserer Ansatz.

JSON-Beschränkungen

Hier sind einige wichtige Stolpersteine, die bei der Verwendung von JSON zu beachten sind.

  • Dateigröße: JSON speichert Daten im Textformat, das viel größer ist als Binärformate.

  • Datentypen: JSON bietet nur eine begrenzte Anzahl von Datentypen. Wenn Sie Datentypen haben, die JSON nicht kennt, müssen Sie Ihre Daten in und aus Typen übersetzen, die JSON verarbeiten kann. Einige wichtige Typen, die JSON nicht parsen kann, sind zum Beispiel: Vector2, Vector3, Color, Rect2, und Quaternion.

  • Benutzerdefinierte Logik für Kodierung/Dekodierung erforderlich: Wenn Sie benutzerdefinierte Klassen haben, die Sie mit JSON speichern möchten, müssen Sie Ihre eigene Logik für die Kodierung und Dekodierung dieser Klassen schreiben.

Binäre Serialisierung

Binär-Serialisierung ist ein alternativer Ansatz für die Speicherung des Spielstatus und kann mit den Funktionen get_var und store_var von FileAccess verwendet werden.

  • Die binäre Serialisierung sollte kleinere Dateien als JSON erzeugen.

  • Binäre Serialisierung kann die meisten gängigen Datentypen verarbeiten.

  • Binäre Serialisierung erfordert weniger benutzerdefinierte Logik für die Kodierung und Dekodierung benutzerdefinierter Klassen.

Beachten Sie, dass nicht alle Propertys einbezogen werden. Nur Propertys, die mit dem gesetzten PROPERTY_USAGE_STORAGE-Flag konfiguriert sind, werden serialisiert. Sie können ein neues Usage-Flag zu einer Property hinzufügen, indem Sie die Methode _get_property_list in Ihrer Klasse überschreiben. Sie können auch überprüfen, wie die Verwendung von Propertys konfiguriert ist, indem Sie Object._get_property_list aufrufen. Siehe PropertyUsageFlags für die möglichen Usage-Flags.