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:
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.
var saveNodes = GetTree().GetNodesInGroup("Persist");
foreach (Node saveNode in saveNodes)
{
// 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
public Godot.Collections.Dictionary<string, Variant> Save()
{
return new Godot.Collections.Dictionary<string, Variant>()
{
{ "Filename", SceneFilePath },
{ "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 }
};
}
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)
// 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()
{
using var saveGame = FileAccess.Open("user://savegame.save", FileAccess.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 (string.IsNullOrEmpty(saveNode.SceneFilePath))
{
GD.Print($"persistent node '{saveNode.Name}' is not an instanced scene, skipped");
continue;
}
// Check the node has a save function.
if (!saveNode.HasMethod("Save"))
{
GD.Print($"persistent node '{saveNode.Name}' is missing a Save() function, skipped");
continue;
}
// Call the node's save function.
var nodeData = saveNode.Call("Save");
// Json provides a static method to serialized JSON string.
var jsonString = Json.Stringify(nodeData);
// Store the save dictionary as a new line in the save file.
saveGame.StoreLine(jsonString);
}
}
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])
// Note: This can be called from anywhere inside the tree. This function is
// path independent.
public void LoadGame()
{
if (!FileAccess.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.
using var saveGame = FileAccess.Open("user://savegame.save", FileAccess.ModeFlags.Read);
while (saveGame.GetPosition() < saveGame.GetLength())
{
var jsonString = saveGame.GetLine();
// Creates the helper class to interact with JSON
var json = new Json();
var parseResult = json.Parse(jsonString);
if (parseResult != Error.Ok)
{
GD.Print($"JSON Parse Error: {json.GetErrorMessage()} in {jsonString} at line {json.GetErrorLine()}");
continue;
}
// Get the data from the JSON object
var nodeData = new Godot.Collections.Dictionary<string, Variant>((Godot.Collections.Dictionary)json.Data);
// Firstly, we need to create the object and add it to the tree and set its position.
var newObjectScene = GD.Load<PackedScene>(nodeData["Filename"].ToString());
var newObject = newObjectScene.Instantiate<Node>();
GetNode(nodeData["Parent"].ToString()).AddChild(newObject);
newObject.Set(Node2D.PropertyName.Position, new Vector2((float)nodeData["PosX"], (float)nodeData["PosY"]));
// Now we set the remaining variables.
foreach (var (key, value) in nodeData)
{
if (key == "Filename" || key == "Parent" || key == "PosX" || key == "PosY")
{
continue;
}
newObject.Set(key, value);
}
}
}
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
, undQuaternion
.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.