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 클래스를 사용할 수 있습니다.
더 보기
이 문서 외에 여러가지 Godot 데모 프로젝트들도 살펴보면 좋습니다.
영구 객체 식별
먼저, 게임 세션 사이에 어떤 개체를 유지하고 싶은지, 그리고 해당 개체에서 어떤 정보를 유지하고 싶은지 식별해야 합니다. 이 튜토리얼에서는 그룹을 사용하여 저장할 객체를 표시하고 처리하지만 다른 방법도 가능합니다.
"Persist" 그룹에 저장하려는 개체를 추가하는 것부터 시작하겠습니다. GUI 또는 스크립트를 통해 이 작업을 수행할 수 있습니다. GUI를 사용하여 관련 노드를 추가해 보겠습니다.
이 작업이 완료되면 게임을 저장해야 할 때 모든 객체를 가져와 저장한 다음 이 스크립트를 사용하여 저장하도록 모든 객체에 지시할 수 있습니다.
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에는 사전과 문자열 사이를 변환하는 도우미 클래스 :ref:`JSON<class_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 }
};
}
이는 로드할 때 유용할 { "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_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 문자열을 다시 사전으로 읽은 다음 사전을 반복하여 값을 읽습니다. 하지만 먼저 개체를 만들어야 하며 이를 달성하기 위해 파일 이름과 부모 값을 사용할 수 있습니다. 로드 기능은 다음과 같습니다.
# 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() 호출에 사용할 수 있도록 상위 개체를 먼저 로드합니다. :ref:`NodePath <class_nodepath>`이 유효하지 않을 수 있으므로 자식 노드를 상위 항목에 연결하는 방법도 필요합니다.
JSON과 바이너리 직렬화
간단한 게임 상태의 경우 JSON이 작동할 수 있으며 디버깅하기 쉬운 사람이 읽을 수 있는 파일을 생성합니다.
그러나 JSON에는 많은 제한이 있습니다. 더 복잡한 게임 상태 또는 많은 양을 저장해야 하는 경우 :ref:`binary serialization<doc_binary_serialization_api>`가 더 나은 접근 방식일 수 있습니다.
JSON 제한 사항
JSON을 사용할 때 알아야 할 몇 가지 중요한 사항은 다음과 같습니다.
파일 크기: JSON은 데이터를 바이너리 형식보다 훨씬 큰 텍스트 형식으로 저장합니다.
데이터 유형: JSON은 제한된 데이터 유형 세트만 제공합니다. JSON에 없는 데이터 유형이 있는 경우 JSON이 처리할 수 있는 유형 간에 데이터를 변환해야 합니다. 예를 들어 JSON이 구문 분석할 수 없는 몇 가지 중요한 유형은
Vector2,Vector3,Color,Rect2및 ``Quaternion``입니다.인코딩/디코딩에 필요한 사용자 정의 로직: JSON으로 저장하려는 사용자 정의 클래스가 있는 경우 해당 클래스를 인코딩 및 디코딩하기 위한 자체 로직을 작성해야 합니다.
국제화
Binary serialization<doc_binary_serialization_api>`는 게임 상태를 저장하는 대체 접근 방식이며 :ref:`class_FileAccess`의 ``get_var` 및 store_var 함수와 함께 사용할 수 있습니다.
바이너리 직렬화는 JSON보다 작은 파일을 생성해야 합니다.
이진 직렬화는 가장 일반적인 데이터 유형을 처리할 수 있습니다.
이진 직렬화에는 사용자 정의 클래스를 인코딩하고 디코딩하는 데 필요한 사용자 정의 논리가 더 적습니다.
모든 속성이 포함되는 것은 아닙니다. PROPERTY_USAGE_STORAGE 플래그 세트로 구성된 속성만 직렬화됩니다. 클래스에서 _get_property_list 메서드를 재정의하여 속성에 새 사용 플래그를 추가할 수 있습니다. ``Object._get_property_list``를 호출하여 속성 사용이 어떻게 구성되어 있는지 확인할 수도 있습니다. 가능한 사용 플래그는 :ref:`PropertyUsageFlags<enum_@GlobalScope_PropertyUsageFlags>`를 참조하세요.