Python – Save e Load

Introduzione

In questo tutorial andrò a toccare uno degli argomenti più discussi e richiesti, il salvataggio e il caricamento dei dati di gioco. Il tutorial richiede una minima conoscenza di programmazione, non specificatamente del Python anche se sarebbe logico, a tale proposito puoi trovare diverse risorse in rete da dove attingere materiale, ti propongo alcune pagine che ti serviranno per capire di cosa sto parlando, perché questo tutorial non è un corso di Python.

Da queste pagine puoi attingere le informazioni che ti servono per capire i concetti di questo tutorial.

Prefazione

Premetto che non sono un programmatore, ho letto qualcosa e ho un’infarinatura, ma grazie agli input dei vari tutorial e di Stefano Romano sono riuscito ad arrivare a fare uno script per il salvataggio dei dati, sono ancora in fase di studio di questo argomento ma ho voluto comunque condividerlo con gli utenti, perché lo trovo un’argomento vitale per la creazione di giochi. 

Impostare il lavoro

Come prima cosa per questo tutorial serve che tu crei una cartella e la rinomini appropriatamente (tipo save_load), non ha importanza dove, ti serve per salvare il file .blend di questo tutorial, il motivo lo capirai dopo. Quindi apri Blender e imposta le finestre di lavoro comprese di Text Editor, se non hai mai cambiato l’impostazione di default del layout Game Logic non dovrai fare niente. Adesso salva il file nella cartella appena creata e iniziamo.

Teoria

Cercando in rete un tutorial che mi aiuti a salvare i dati del mio gioco ho sempre trovato video e pagine che salvavano i dati su un file esterno di tipo .txt, questa è una possibilità, ma non è la migliore per il fatto che il file è di facile lettura e scrittura (una volta esportato il gioco), quindi il giocatore può barare (cheattare come si dice ora) cambiando i valori nel file, questo non è il massimo. Molti però usano questo sistema. 

In questo tutorial invece propongo di usare il dizionario che Blender mette a disposizione e ha anche una sua opzione nell’Actuator Game save e load globalDict. Questa opzione produce un file nomeDelFile.bgeconfig, diciamo che è un file di tipo .dat, quindi di difficile lettura con il blocco note. In questo file possiamo inserire tutto quello che vogliamo dalla posizione degli oggetti alle Properties, valori di default ecc ecc. Il codice per farlo è semplice, poi dipende da cosa vuoi salvare, iniziamo.

Codice base

Nel Text Editor premi il pulsante New e dai un nome al file, io ho usato saveload.py. Se hai mai visionato uno script Python per Blender avrai notato che le prime righe sono ricorrenti e sono semplicemente l’importazione del modulo del game engine e il collegamento del Controller e la dichiarazione del proprietario, quindi scrivi questo codice 

import bge

### VARIABILI GLOBALI ###
cont = bge.logic.getCurrentController()
own = cont.owner

Ho inserito il commento VARIABILI GLOBALI (i commenti vengono ignorati dal programma e li puoi inserire usando il carattere #) perché queste variabili servono in tutto lo script e così facendo non devo riscriverle.

Organizzare il lavoro in modo efficiente è una buona abitudine e facilita il flusso lavorativo e semplifica la lettura dello script anche a distanza di tempo; andrò a creare due definizioni (funzioni) separate, una per il save e una per il load. Ecco il codice

def save():


def load():
    pass

Ho usato la parola pass per saltare la funzione load, perché per ora scriverò sul save.

Struttura del save

Ho trovato utile la funzione interna bge.logic.globalDict perché evita di usare gli Actuator che allungherebbero il codice. Scrivi all’interno della def save (indentate) queste due righe di codice

bge.logic.globalDict
   
bge.logic.saveGlobalDict()

In queste due righe di codice c’è il nucleo di questo tutorial, la prima crea un’oggetto all’interno del dizionario e la seconda salva il dizionario. Ma queste due righe così permettono a Blender soltanto di creare il file necessario che tiene i nostri dati. Per fare eseguire il codice abbiamo comunque bisogno di un paio di Mattoni, inserisci un Sensor Keyboard, rinominalo keyS e assegnagli la lettera S, poi inserisci un Controller Python imposta l’opzione module dal menù a tendina, in questo modo userai le due definizioni (funzioni) invece di due script separati, collega i Mattoni tra loro. E’ una buona abitudine perché in questo modo puoi creare più funzioni e farle eseguire separatamente. Adesso scrivi nello spazio accanto al menù a tendina saveload.save, la parte a sinistra del punto è il nome del file (nel mio caso saveload) la parte a destra è la definizione (funzione), se hai dato un nome diverso allo script usa quello.

Adesso scrivi prima delle due righe del globaldict il seguente codice

sens_keyS = cont.sensors['keyS']

Con questo codice assegno alla variabile sens_keyS il Sensore collegato al Controller che si chiama keyS. Puoi trovare una spiegazione dettagliata su come collegare i Mattoni via script QUA. Adesso salva, fai partire il motore e premi il tasto S. Apparentemente non è successo niente, ma se apri la cartella dove hai salvato il file troverai un uovo file con estenzione .bgeconfig, se il riscontro è negativo, il file non è creato, rifai i passaggi sopra elencati.

Cosa salvare ?

A questo punto devo decidere cosa voglio salvare nel mio file appena creato. Si può salvare di tutto, ma per questo esempio voglio salvare la posizione del cubo, per farlo però bisogna scrivere alcune righe di codice per sapere dove si trova il cubo. La funzione interna da usare è worldPosition, ma questa funzione genera un’array (il nome in Python è lista) di tre elementi e il globalDict non supporta le liste (array), quindi bisogna suddividere in parti il risultato di questa funzione worldPosition. 

Sai benissimo che un’oggetto ha tre coordinate nel mondo 3D, e sono i tre assi XYZ, a questo punto è semplice capire che bisogna inserire tre variabili una per ogni asse, il codice viene così

posX = own.worldPosition.x
posY = own.worldPosition.y
posZ = own.worldPosition.z

Il codice va inserito sotto la dichiarazione del sensore (keyS), queste righe assegnano alle tre variabili (posX, posY, posZ) la posizione dell’oggetto proprietario dello script (own dichiarato nelle variabili globali) nelle coordinate singole .x .y .z che completano le linee.

Il prossimo passaggio è assegnare queste tre variabili ad un’oggetto (del codice, non un modello 3D). Nella riga bge.logic.globalDict aggiungi il seguente codice dopo la parola Dict

["posCubo"] = [ posX, posY, posZ ]

Il nome posCubo lo diamo noi, ma in questo esempio è palese il suo riferimento, si possono salvare più linee di codice per lo stesso oggetto. Ho aggiunto poi le tre variabili create in precedenza.

Adesso però voglio sistemare il codice con un’istruzione ben precisa in modo che solo e soltanto quando quella istruzione viene eseguita il Sensor invia l’impulso positivo. Il codice quindi viene così

if keyS.positive:
        bge.logic.globalDict['posCubo'] = [ posX, posY, posZ]
        
        bge.logic.saveGlobalDict()

Per chi ha dimistichezza con la programmazione la struttura la conosce, ma è semplice e dice :

Se sens_keyS è positivo (cioè viene premuto) esegui il codice sotto, cioè inserisci le tre variabili nel globalDict e salvalo. Il codice per il salvataggio è terminato.

Caricare le informazioni

Adesso occupiamoci della def load, uso altri passi per creare la procedura, in realtà non esiste un metodo sequenziale, cioè dei passaggi obbligatori, si può prima fare l’intero script e poi aggiungere i mattoni, si possono aggiungere i mattoni e poi fare lo script, dipende da come si desidera impostare il proprio flusso lavorativo. Nella def save ho prima scritto parte del codice, poi inserito i mattoni, e poi completare i codice a seconda dei mattoni inseriti, adesso agirò in maniera diversa. Ho scritto queste poche righe, che non sono attinenti col tutorial, per farti capire che bisogna avere un modus operandi quando si creano video giochi. 

Inserisco un Sensor Keyboard e lo chiamo keyL assegnandogli la lettera L, poi inserisco un Controller Python e scelgo l’opzione module dove scrivo saveload.load. Come prima cosa nella definizione load inserisco la riga di codice che richiama il sensore, puoi benissimo copiarla e incollarla dalla def save, ricorda di cambiare il nome e sopratutto il keyF tra le parentesi quadre, che richiama nello specifico il sensore. La procedura di caricamento dei dati è all’inverso, mentre prima inserivo nel globalDict le tre variabili della posizione e poi salvavo, adesso prima carico il globalDict e poi estrapolo le variabili e le assegno al cubo. Quindi uso l’istruzione if come precedentemente, se il tasto viene premuto carico il globalDict e assegno ad una variabile il risultato che poi assegno al cubo, alla fine di questa procedura il cubo tornerà nella posizione di salvataggio. Ecco il codice, spiegato riga per riga.

# se il tasto viene premuto
if sens_keyL.positive:
        # carico le informazioni dal globalDict
        bge.logic.loadGlobalDict()
        # assegno alla variabile cuboPos i dati salvati
        cuboPos = bge.logic.globalDict["posCubo"]
        
        # assegno a ogni variabile l'indice dei dati salvati
        posX = cuboPos[0]      
        posY = cuboPos[1]
        posZ = cuboPos[2]
        # assegno le tre variabili alla posizione attuale del cubo
        own.worldPosition = [ posX, posY, posZ ]

Una volta scritto il codice, puoi salvare, spostare il cubo e caricare la posizione. Se hai scritto tutto correttamente il cubo dovrebbe tornare al punto di origine, se non è così rivedi il codice, che posto per intero qua sotto con i commenti

import bge

### VARIABILI GLOBALI ###
cont = bge.logic.getCurrentController()
own = cont.owner

def save():
    # dichiarazione del sensor 
    sens_keyS = cont.sensors['keyS']
    # dichiarazione di tre variabili per la posizione
    posX = own.worldPosition.x
    posY = own.worldPosition.y
    posZ = own.worldPosition.z
    # struttura di controllo e salvataggio
    if sens_keyS.positive:
        bge.logic.globalDict['posCubo'] = [ posX, posY, posZ]
        bge.logic.saveGlobalDict()
    
def load():
    # dichiarazione del sensor
    sens_keyL = cont.sensors['keyL']
    
    # se il tasto viene premuto
    if sens_keyL.positive:
        # carico le informazioni dal globalDict
        bge.logic.loadGlobalDict()
        # assegno alla variabile cuboPos i dati salvati
        cuboPos = bge.logic.globalDict["posCubo"]
        
        # assegno a ogni variabile l'indice dei dati salvati
        posX = cuboPos[0]      
        posY = cuboPos[1]
        posZ = cuboPos[2]
        # assegno le tre variabili alla posizione del cubo
        own.worldPosition = [ posX, posY, posZ ]

Per questo tutorial è tutto, da non programmatore spero di aver spiegato bene uno dei punti focali del game engine, nei prossimi articoli cercherò di spiegare come salvare una Property e come poter salvare i dati di un’altro oggetto che non sia il proprietario dello script. Ringrazio Stefano Romano per avermi indirizzato e a tratti guidato nel capire questo processo che una volta appreso è semplice ma potentissimo. Buon Blender a tutti.