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.

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.png

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 i in save_nodes:
    # Now, we can call our save function on each node.

Serializando

O próximo passo é serializar os dados. Isso torna muito mais fácil ler e armazenar em disco. Nesse caso, estamos assumindo que cada membro do grupo Persist é um nó instanciado e, portanto, tem um caminho. O GDScript tem funções auxiliares para isso, como to_json() e parse_json(), então usaremos um dicionário. Nosso nó precisa conter uma função save que retorne esses dados. A função salvar ficará assim:

func save():
    var save_dict = {
        "filename" : get_filename(),
        "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

Conforme abordado no tutorial Sistema de arquivos, precisaremos abrir um arquivo para que possamos escrever ou ler a partir dele. Agora que temos uma maneira de chamar nossos grupos e obter seus dados relevantes, vamos usar to_json() para convertê-los em uma string facilmente armazenada e armazená-los em um arquivo. Fazer isso dessa maneira garante que cada linha seja seu próprio objeto, portanto, também temos uma maneira fácil de extrair 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_game = File.new()
    save_game.open("user://savegame.save", File.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.filename.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")

        # Store the save dictionary as a new line in the save file.
        save_game.store_line(to_json(node_data))
    save_game.close()

Jogo salvo! O carregamento também é bastante simples. Para isso, vamos ler cada linha, usar parse_json() para lê-lo de volta para um dicionário e, em seguida, iterar sobre o dicionário para ler nossos valores. Mas precisamos primeiro criar o objeto e podemos usar o nome do arquivo e os valores pai para conseguir isso. Aqui está a nossa função de carregamento:

# Note: This can be called from anywhere inside the tree. This function
# is path independent.
func load_game():
    var save_game = File.new()
    if not save_game.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.
    save_game.open("user://savegame.save", File.READ)
    while save_game.get_position() < save_game.get_len():
        # Get the saved dictionary from the next line in the save file
        var node_data = parse_json(save_game.get_line())

        # Firstly, we need to create the object and add it to the tree and set its position.
        var new_object = load(node_data["filename"]).instance()
        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])

    save_game.close()

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.