Up to date

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

ゲームの保存

はじめに

ゲームを保存するのは複雑です。例えば、 複数のレベルにまたがった複数のオブジェクトによる情報が保存される事が望ましい場合があります。 より高度なゲームシステムのセーブでは、任意の数のオブジェクトに関する追加情報の保存ができる必要があります。 これにより、ゲームがより複雑に成長するにつれて、保存機能を拡張できます。

注釈

ユーザー設定を保存する場合は、この目的で ConfigFile クラスを使用できます。

参考

You can see how saving and loading works in action using the Saving and Loading (Serialization) demo project.

永続オブジェクトの識別

まず、ゲームセッション間で保持するオブジェクトと、それらのオブジェクトから保持する情報を特定する必要があります。このチュートリアルでは、グループを使用して保存するオブジェクトをマークおよび処理しますが、他の方法ももちろん可能です。

We will start by adding objects we wish to save to the "Persist" group. We can do this through either the GUI or script. Let's add the relevant nodes using the GUI:

../../_images/groups.png

これが完了したら、ゲームを保存する必要があるときに、このスクリプトを使用して、保存するすべてのオブジェクトを取得し、それをすべて保存するように指示できます:

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.

シリアル化

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 class JSON to convert between dictionary and string, 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_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

これにより、スタイルが { "variable_name":value_of_variable } の辞書が得られます。これはロード時に役立ちます。

データの保存と読み取り

As covered in the ファイルシステム 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 the class 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 = 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)

Game saved! Now, to load, we'll read each line. Use the parse method to read the JSON string back to a dictionary, and then iterate over the dict to read our values. But we'll need to first create the object and we can use the filename and parent values to achieve that. Here is our load function:

# 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])

これで、シーン ツリーのほぼ任意の場所に配置された任意の数のオブジェクトを保存して読み込むことができます!各オブジェクトは、保存する必要があるデータに応じて異なるデータを格納できます。

いくつかの注意事項

ゲームの状態を読み込むための設定に焦点をあてて説明をおこないました。最終的には、このロジックの多くがプロジェクトの作成者に委ねられます。これはしばしば複雑であり、個々のプロジェクトのニーズに基づいて大幅にカスタマイズする必要があります。

さらに、この実装では、Persistオブジェクトは他のPersistオブジェクトの子ではないと想定しています。そうしないと、無効なパスが作成されます。ネストされた永続オブジェクトに対応するには、オブジェクトを段階的に保存することを検討してください。最初に親オブジェクトをロードして、子オブジェクトがロードされたときに add_child() 呼び出しで使用できるようにします。NodePath はおそらく無効になるため、子を親にリンクする方法も必要です。

JSON vs binary serialization

For simple game state, JSON may work and it generates human-readable files that are easy to debug.

But JSON has many limitations. If you need to store more complex game state or a lot of it, binary serialization may be a better approach.

JSON limitations

Here are some important gotchas to know about when using JSON.

  • Filesize: JSON stores data in text format, which is much larger than binary formats.

  • Data types: JSON only offers a limited set of data types. If you have data types that JSON doesn't have, you will need to translate your data to and from types that JSON can handle. For example, some important types that JSON can't parse are: Vector2, Vector3, Color, Rect2, and Quaternion.

  • Custom logic needed for encoding/decoding: If you have any custom classes that you want to store with JSON, you will need to write your own logic for encoding and decoding those classes.

Binary serialization

Binary serialization is an alternative approach for storing game state, and you can use it with the functions get_var and store_var of FileAccess.

  • Binary serialization should produce smaller files than JSON.

  • Binary serialization can handle most common data types.

  • Binary serialization requires less custom logic for encoding and decoding custom classes.

Note that not all properties are included. Only properties that are configured with the PROPERTY_USAGE_STORAGE flag set will be serialized. You can add a new usage flag to a property by overriding the _get_property_list method in your class. You can also check how property usage is configured by calling Object._get_property_list. See PropertyUsageFlags for the possible usage flags.