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.

Salvataggio di giochi

Introduzione

Salvare giochi può essere complicato. Ad esempio, potrebbe essere opportuno memorizzare informazioni da più oggetti distribuiti su più livelli. I sistemi avanzati di salvataggio dovrebbero consentire di ottenere informazioni aggiuntive su un numero arbitrario di oggetti. Ciò consentirà alla funzione di salvataggio di adattarsi man mano che il gioco diventa più complesso.

Nota

Se si sta cercando di salvare la configurazione utente, è possibile utilizzare la classe ConfigFile a questo proposito.

Vedi anche

È possibile osservare come funzionano il salvataggio e il caricamento in azione attraverso il progetto demo Salvataggio e caricamento (serializzazione).

Identificare gli oggetti persistenti

Innanzitutto, dobbiamo identificare quali oggetti vogliamo conservare tra una sessione di gioco e l'altra e quali informazioni vogliamo conservare da quegli oggetti. In questo tutorial, useremo i gruppi per segnare e gestire gli oggetti da salvare, ma sono certamente possibili anche altri metodi.

Inizieremo aggiungendo gli oggetti che desideriamo salvare al gruppo "Persist". Possiamo farlo tramite l'interfaccia grafica o tramite script. Aggiungiamo i nodi desiderati tramite l'interfaccia grafica:

../../_images/groups.webp

Una volta fatto ciò, quando dovremo salvare il gioco, potremo far sì che tutti gli oggetti li salvino e poi dire a tutti di salvare con questo 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.

Serializzazione

Il passo successivo è serializzare i dati. Questo semplifica notevolmente la lettura e l'archiviazione su disco. In questo caso, presumiamo che ogni membro del gruppo Persist sia un nodo istanziato e quindi abbia un percorso. GDScript ha la classe ausiliare JSON per convertire tra dizionario e stringa. Il nostro nodo deve contenere una funzione di salvataggio che restituisca questi dati. La funzione di salvataggio avrà questo aspetto:

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

Questo ci fornisce un dizionario con lo stile { "variable_name":value_of_variable }, che sarà utile per il caricamento.

Salvare e leggere i dati

Come spiegato nel tutorial File system, dovremo aprire un file per poterlo scrivere o leggere. Ora che abbiamo un modo per chiamare i nostri gruppi e ottenere i dati rilevanti, utilizziamo la classe JSON per convertirli in una stringa facilmente memorizzabile e salvarli in un file. Facendo così ci assicuriamo che ogni riga sia un oggetto a sé stante, quindi abbiamo anche un modo semplice per estrarre i dati dal file.

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

Partita salvata! Ora, per caricare, leggeremo ogni riga. Utilizziamo il metodo parse per convertire la stringa JSON in un dizionario, quindi iteriamo sul dizionario per leggere i nostri valori. Ma prima dobbiamo creare l'oggetto, e possiamo usare il nome del file e i valori padre per farlo. Ecco la nostra funzione di caricamento:

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

Ora possiamo salvare e caricare un numero arbitrario di oggetti disposti praticamente ovunque nell'albero di scene! Ogni oggetto può memorizzare dati diversi a seconda di ciò che necessita di salvare.

Alcune note

Abbiamo sorvolato sulla configurazione dello stato del gioco per il caricamento. Spetta ultimamente al creatore del progetto decidere dove indirizzare gran parte di questa logica. Questa è spesso complicata e dovrà essere ampiamente personalizzata in base alle esigenze del singolo progetto.

Inoltre, la nostra implementazione presuppone che nessun oggetto Persist sia figlio di altri oggetti Persist. Altrimenti, verrebbero creati percorsi non validi. Per gestire gli oggetti Persist annidati, si consiglia di salvare gli oggetti in più fasi. Caricare prima gli oggetti padre in modo che siano disponibili per la chiamata a add_child() quando vengono caricati gli oggetti figlio. Sarà inoltre necessario un modo per collegare i figli ai genitori, poiché il NodePath probabilmente non sarà valido.

Serializzazione JSON contro binaria

Per lo stato di un gioco semplice, JSON può funzionare e genera file leggibili in chiaro, facili da sottoporre a debug.

Tuttavia, JSON ha numerose limitazioni. Se è necessario memorizzare stati di gioco più complessi o in grandi quantità, la serializzazione binaria potrebbe essere un approccio migliore.

Limitazioni di JSON

Ecco alcuni grattacapi importanti da tenere a mente quando si utilizza JSON.

  • Dimensione di file: JSON memorizza i dati in formato testuale, che è molto più grande dei formati binari.

  • Tipi di dati: JSON offre solo un insieme limitato di tipi di dati. Se si dispone di tipi di dati non supportati da JSON, sarà necessario tradurre i dati da/a i tipi che JSON può gestire. Ad esempio, alcuni tipi importanti che JSON non può analizzare sono: Vector2, Vector3, Color, Rect2 e Quaternion.

  • Logica personalizzata necessaria per la codifica/decodifica: Se si dispone di classi personalizzate che si desidera memorizzare con JSON, sarà necessario scrivere la propria logica per codificare e decodificare tali classi.

Serializzazione binaria

La serializzazione binaria è un approccio alternativo per memorizzare lo stato del gioco e si può utilizzare con le funzioni get_var e store_var di FileAccess.

  • La serializzazione binaria dovrebbe produrre file più piccoli di JSON.

  • La serializzazione binaria può gestire la maggior parte dei tipi di dati più comuni.

  • La serializzazione binaria richiede meno logica personalizzata per la codifica e la decodifica delle classi personalizzate.

Si noti che non tutte le proprietà sono incluse. Saranno serializzate solo le proprietà configurate con il flag PROPERTY_USAGE_STORAGE impostato. È possibile aggiungere un nuovo flag di uso a una proprietà sovrascrivendo il metodo _get_property_list nella classe. È anche possibile verificare come è configurato l'uso della proprietà chiamando Object._get_property_list. Consultare PropertyUsageFlags per i possibili flag di uso.