Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
Збереження ігор
Вступ
Збереження гри може бути складним. Наприклад, може бути бажано зберігати інформацію з декількох об'єктів на декількох рівнях. Розширені системи збереження ігор повинні містити додаткову інформацію про довільну кількість об'єктів. Це дозволить функції збереження масштабуватися, оскільки гра стає все складнішою.
Примітка
Якщо ви хочете зберегти конфігурацію користувача, ви можете використовувати клас ConfigFile для цієї мети.
Дивись також
Ви можете побачити, як збереження та завантаження працює в дії, використовуючи демонстраційний проект «Збереження та завантаження (серіалізація) <https://github.com/godotengine/godot-demo-projects/blob/master/loading/serialization>`__.
Визначення постійних об'єктів
Спершу, ми повинні визначити, які об'єкти ми хочемо зберігати між ігровими сесіями та яку інформацію від цих об'єктів ми хочемо зберегти. Для цього підручника ми будемо використовувати групи для позначення та обробки об'єктів, які потрібно зберегти, але, безумовно, можливі інші методи.
Почнемо з додавання об'єктів, які ми хочемо зберегти, до групи "Persist". Ми можемо зробити це за допомогою графічного інтерфейсу, або скрипту. Додамо відповідні вузли за допомогою графічного інтерфейсу:
Як тільки це буде зроблено, при потребі зберегти гру, ми зможемо отримати всі об'єкти, які необхідно зберегти, і зберегти їх за допомогою цього скрипту:
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.
}
Серіалізація
Наступним кроком є серіалізація даних. Це значно полегшує читання та збереження на диску. У цьому випадку ми припускаємо, що кожен член групи Persist є примірником вузла і, отже, має шлях. GDScript має допоміжний клас JSON для перетворення між словником і рядком. Наш вузол повинен містити функцію збереження, яка повертає ці дані. Функція збереження буде виглядати так:
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 }
};
}
Це дає нам словник в стилі { "назва_змінної":значення_змінної }, який буде корисний при завантаженні.
Збереження та читання даних
Як описано в підручнику Файлова система, нам потрібно буде відкрити файл, щоб ми могли писати в нього або читати з нього. Тепер, коли ми маємо спосіб викликати наші групи та отримати відповідні дані, давайте використаємо клас 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_file = 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_file.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 saveFile = 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.
saveFile.StoreLine(jsonString);
}
}
Гра збережена! Тепер, щоб завантажити, ми прочитаємо кожен рядок. Використовуйте метод parse, щоб прочитати рядок JSON назад у словник, а потім повторіть dict, щоб прочитати наші значення. Але нам потрібно спочатку створити об’єкт, і ми можемо використовувати ім’я файлу та батьківські значення для досягнення цього. Ось наша функція завантаження:
# 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_file = FileAccess.open("user://savegame.save", FileAccess.READ)
while save_file.get_position() < save_file.get_length():
var json_string = save_file.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.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 saveFile = FileAccess.Open("user://savegame.save", FileAccess.ModeFlags.Read);
while (saveFile.GetPosition() < saveFile.GetLength())
{
var jsonString = saveFile.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);
}
}
}
Тепер ми можемо зберегти і завантажити довільну кількість об'єктів практично в будь-якому місці по всьому дереву сцени! Кожен об'єкт може зберігати різні дані в залежності від того, що йому потрібно зберегти.
Деякі примітки
Ми приховали налаштування стану гри для завантаження. Зрештою, це залежить від творця проекту, де піде більша частина цієї логіки. Це часто ускладнюється і потрібно буде багато попрацювати, виходячи з потреб окремого проекту.
Крім того, наша реалізація припускає, що жоден об'єкт Persist не є дочірнім для інших об'єктів Persist. В іншому випадку будуть створені недійсні шляхи. Щоб врахувати вкладені об'єкти Persist, розгляньте можливість збереження об'єктів поетапно. Спочатку завантажте батьківські об'єкти, щоб вони були доступні для виклику add_child() під час завантаження дочірніх об'єктів. Вам також знадобиться спосіб пов'язати дочірні об'єкти з батьківськими, оскільки NodePath, ймовірно, буде недійсним.
JSON проти бінарної серіалізації
Для простого ігрового стану JSON може працювати, і він генерує файли, зрозумілі людині, які легко налагодити.
Але JSON має багато обмежень. Якщо вам потрібно зберігати складніший стан гри або багато його, binary serialization може бути кращим підходом.
Обмеження JSON
Ось деякі важливі моменти, про які слід знати під час використання JSON.
Розмір файлу: JSON зберігає дані в текстовому форматі, який набагато більший за двійкові формати.
Типи даних: JSON пропонує лише обмежений набір типів даних. Якщо у вас є типи даних, яких немає в JSON, вам потрібно буде перевести дані в типи, які JSON може обробляти, і з них. Наприклад, деякі важливі типи, які JSON не може проаналізувати:
Vector2,Vector3,Color,Rect2іQuaternion.Власна логіка, необхідна для кодування/декодування: Якщо у вас є спеціальні класи, які ви хочете зберігати в JSON, вам потрібно буде написати власну логіку для кодування та декодування цих класів.
Бінарна серіалізація
Binary serialization — це альтернативний підхід для збереження стану гри, і ви можете використовувати його з функціями get_var і store_var FileAccess.
Двійкова серіалізація повинна створювати менші файли, ніж JSON.
Двійкова серіалізація може обробляти більшість поширених типів даних.
Двійкова серіалізація вимагає менше спеціальної логіки для кодування та декодування спеціальних класів.
Зауважте, що включено не всі властивості. Буде серіалізовано лише властивості, налаштовані за допомогою прапорця PROPERTY_USAGE_STORAGE. Ви можете додати новий прапор використання до властивості, перевизначивши метод _get_property_list у вашому класі. Ви також можете перевірити, як налаштовано використання властивості, викликавши Object._get_property_list. Перегляньте PropertyUsageFlags для можливих позначок використання.