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.

Salvando jogos (save)

Introdução

Salvar jogos pode ser complicado. Por exemplo, pode ser desejável armazenar informação de vários objetos em vários níveis. Sistemas avançados de salvamento de jogos devem permitir informações adicionais sobre um número arbitrário de objetos. Isto permitirá que a função de salvamento seja escalonada à medida que o jogo se torna mais complexo.

Nota

Se você procura salvar a configuração de usuário, você pode utilizar a classe ConfigFile para este propósito.

Ver também

Você pode ver como o salvamento e carregamento funcionam na prática usando o projeto de demonstração Saving and Loading (Serialization).

Identificando objetos persistentes

Em primeiro lugar, devemos identificar quais objetos queremos manter entre as sessões de jogo e quais informações queremos manter desses objetos. Para este tutorial, usaremos grupos para marcar e manipular objetos para serem salvos, mas outros métodos certamente são possíveis.

Começaremos adicionando objetos que desejamos salvar ao grupo "Persist". Podemos fazer isto através da GUI ou do script. Vamos adicionar os nós relevantes usando a GUI:

../../_images/groups.webp

Uma vez feito isto, quando precisarmos salvar o jogo, podemos obter todos os objetos para salvá-los e, em seguida, dizer a todos para salvar com este script:

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.

Serializando

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 instantiated node and thus has a path. GDScript has the 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

Isso nos dá um dicionário com o estilo { "nome_da_variável":valor_da_variável }, que será útil ao carregar.

Salvando e lendo dados

Como abordado no tutorial Sistema de arquivos, precisaremos abrir um arquivo para poder gravar ou ler dados. Agora que temos uma forma de chamar nossos grupos e obter seus dados relevantes, vamos usar a classe JSON para convertê-los em uma string facilmente armazenável e salvá-los em um arquivo. Fazer isso dessa forma garante que cada linha seja seu próprio objeto, proporcionando uma maneira simples de recuperar os dados do arquivo.

# 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 instantiated scene so it can be instantiated again during load.
        if node.scene_file_path.is_empty():
            print("persistent node '%s' is not an instantiated 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)

Jogo salvo! Agora, para carregar, leremos cada linha. Use o método parse para converter a string JSON novamente em um dicionário e então iterar pelo dicionário para ler nossos valores. Mas primeiro precisaremos criar o objeto, e podemos usar os valores de nome de arquivo e pai para isso. Aqui está nossa função de carregamento:

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

Agora podemos salvar e carregar um número arbitrário de objetos dispostos em quase qualquer lugar na árvore de cena! Cada objeto pode armazenar dados diferentes dependendo do que precisa salvar.

Algumas observações

Nós ignoramos a configuração do estado do jogo para carregamento. Cabe fundamentalmente ao criador do projeto onde grande parte dessa lógica é aplicada. Isso geralmente é complicado e precisará ser fortemente personalizado com base nas necessidades do projeto individual.

Além disso, nossa implementação assume que nenhum objeto Persist é filho de outros objetos Persist. Caso contrário, seriam criados caminhos inválidos. Para acomodar objetos Persist aninhados, considere salvar objetos em etapas. Carregue os objetos pai primeiro para que estejam disponíveis para a chamada add_child() quando os objetos filho forem carregados. Você também precisará de uma maneira de vincular filhos aos pais, pois o NodePath provavelmente será inválido.

JSON versus serialização binária

Para estados simples de jogo, o JSON pode funcionar e gera arquivos legíveis por humanos que são fáceis de depurar.

Mas JSON tem muitas limitações. Se você precisar armazenar um estado de jogo mais complexo ou uma grande quantidade dele, a serialização binária pode ser uma abordagem melhor.

Limitações do JSON

Aqui estão alguns pontos importantes a saber ao usar JSON.

  • Tamanho do arquivo: JSON armazena dados em formato de texto, que é muito maior do que formatos binários.

  • Tipos de dados: JSON oferece apenas um conjunto limitado de tipos de dados. Se você tiver tipos de dados que o JSON não possui, precisará traduzir seus dados para e de tipos que o JSON possa manipular. Por exemplo, alguns tipos importantes que o JSON não consegue analisar são: Vector2, Vector3, Color, Rect2 e Quaternion.

  • Lógica personalizada necessária para codificação/decodificação: Se você tiver classes personalizadas que deseja armazenar com JSON, precisará escrever sua própria lógica para codificar e decodificar essas classes.

Serialização binária

A serialização binária é uma abordagem alternativa para armazenar o estado do jogo, e você pode utilizá-la com as funções get_var e store_var de FileAccess.

  • A serialização binária pode lidar com a maioria dos tipos de dados comuns.

  • A serialização binária exige menos lógica customizada para codificar e decodificar classes customizadas.

  • A serialização binária requer menos lógica personalizada para codificar e decodificar classes personalizadas.

Note que nem todas as propriedades são incluídas. Apenas as propriedades configuradas com a flag PROPERTY_USAGE_STORAGE definida serão serializadas. Você pode adicionar uma nova flag de uso a uma propriedade sobrescrevendo o método _get_property_list na sua classe. Você também pode verificar como o uso da propriedade está configurado chamando Object._get_property_list. Veja PropertyUsageFlags para as possíveis flags de uso.