Come posso mantenere la compatibilità con le versioni precedenti salvate del gioco?


8

Ho un gioco sim complesso a cui voglio aggiungere funzionalità di salvataggio. Lo aggiornerò continuamente con nuove funzionalità dopo il rilascio.

Come posso assicurarmi che i miei aggiornamenti non rompano i salvataggi esistenti? Che tipo di architettura dovrei seguire per renderlo possibile?


Non sono a conoscenza di un'architettura generica per questo obiettivo, ma farei in modo che il processo di patch aggiorni / converta i giochi di salvataggio per garantire la compatibilità con le nuove funzionalità.
Loodakrawa,

Risposte:


9

Un approccio semplice è mantenere le vecchie funzioni di caricamento. È necessaria una sola funzione di salvataggio che scrive solo l'ultima versione. La funzione di caricamento rileva la funzione di caricamento con versione corretta da invocare (in genere scrivendo un numero di versione da qualche parte all'inizio del formato del file di salvataggio). Qualcosa di simile a:

class GameState:
  loadV1(stream):
    // do stuff

  loadV2(stream):
    // do different stuff

  loadV3(stream):
    // yet other stuff

  save(stream):
    // note this is version 3
    stream.write(3)
    // write V3 data

  load(stream):
    version = stream.read()
    if version == 1: loadV1(stream)
    else if version == 2: loadV2(stream)
    else if version == 3: loadV3(stream)

Puoi farlo per l'intero file, per singole sezioni del file, per singoli oggetti / componenti di gioco, ecc. Esattamente quale divisione migliore dipenderà dal tuo gioco e dalla quantità di stato che stai serializzando.

Nota che questo ti porta solo finora. Ad un certo punto potresti cambiare il tuo gioco abbastanza che i dati di salvataggio delle versioni precedenti semplicemente non hanno senso. Ad esempio, un gioco di ruolo potrebbe avere diverse classi di personaggi che il giocatore può scegliere. Se rimuovi una classe di personaggi non c'è molto che puoi fare con i salvataggi di personaggi che hanno quella classe. Forse potresti convertirlo in una classe simile che esiste ancora ... forse. Lo stesso vale se cambi abbastanza altre parti del gioco da non assomigliare molto alle vecchie versioni.

Ricorda che una volta spedito il gioco è "fatto". Potresti rilasciare DLC o altri aggiornamenti nel tempo, ma non saranno cambiamenti particolarmente importanti nel gioco stesso. Prendiamo ad esempio la maggior parte degli MMO: WoW è stato mantenuto per molti anni con nuovi aggiornamenti e modifiche, ma è ancora più o meno lo stesso gioco che era quando è uscito per la prima volta.

Per lo sviluppo iniziale, semplicemente non me ne preoccuperei. I risparmi sono effimeri nei primi test. È un'altra storia una volta che arrivi alla beta pubblica, però.


1
Questo. Sfortunatamente, questo funziona raramente come pubblicizzato. Di solito queste funzioni di caricamento si basano su funzioni di supporto ( ReadCharacterpossono chiamare ReadStat, che possono cambiare o meno da una versione alla successiva), quindi è necessario mantenere le versioni per ognuna di esse, rendendo sempre più difficile tenere il passo. Come sempre, non esiste un proiettile d'argento e mantenere le vecchie funzioni di caricamento è un buon punto di partenza.
Panda Pajama,

5

Un modo semplice per ottenere una parvenza di versioning è dare un senso ai membri degli oggetti che si stanno serializzando. Se il tuo codice ha una comprensione dei vari tipi di dati da serializzare, puoi ottenere un po 'di robustezza senza fare troppo lavoro.

Supponiamo di avere un oggetto serializzato simile al seguente:

ObjectType
{
  m_name = "a string"
  m_size = { 1.2, 2.1 }
  m_someStruct = {
    m_deeperInteger = 5
    m_radians = 3.14
  }
}

Dovrebbe essere facile vedere che il tipo ObjectTypeha chiamato membri di dati m_name, m_sizeem_someStruct . Se è possibile eseguire il ciclo o enumerare i membri dei dati durante il runtime (in qualche modo), durante la lettura di questo file è possibile leggere un nome di membro e abbinarlo a un membro effettivo all'interno dell'istanza dell'oggetto.

Durante questa fase di ricerca se non trovi un membro di dati corrispondente puoi tranquillamente ignorare questa parte del file di salvataggio. Ad esempio, dire che la versione 1.0 di SomeStructaveva un m_namemembro dati. Quindi patch e questo membro di dati è stato rimosso completamente. Quando carichi il tuo file di salvataggio, ti imbatteraim_name un membro corrispondente e non troverai alcuna corrispondenza. Il codice può semplicemente passare al membro successivo nel file senza arresti anomali. Ciò consente di rimuovere i membri dei dati senza preoccuparsi di rompere i vecchi file di salvataggio.

Allo stesso modo se aggiungi un nuovo tipo di membro di dati e provi a caricare da un vecchio file di salvataggio il tuo codice potrebbe non inizializzare il nuovo membro. Questo può essere utilizzato a vantaggio: i nuovi membri dei dati possono essere inseriti manualmente nei file di salvataggio durante l'applicazione di patch, magari introducendo valori predefiniti (o con mezzi più intelligenti).

Questo formato consente inoltre di manipolare o modificare manualmente i file di salvataggio; l'ordine in cui i membri dei dati non hanno molto a che fare con la validità della routine di serializzazione. Ogni membro viene consultato e inizializzato in modo indipendente. Questa potrebbe essere una bontà che aggiunge un po 'più di robustezza.

Tutto ciò può essere ottenuto attraverso una qualche forma di introspezione di tipo. Dovrai essere in grado di eseguire una query su un membro di dati mediante la ricerca di stringhe e di sapere quale sia il tipo effettivo di dati del membro di dati. Ciò può essere ottenuto in C ++ utilizzando una forma di introspezione personalizzata e altre lingue potrebbero avere strutture di introspezione integrate.


Ciò sarà utile per rendere più robusti dati e classi. (In .NET la funzione si chiama "riflesso"). Mi chiedo le raccolte ... la mia IA è complicata e utilizza molte raccolte temporanee per elaborare i dati. Dovrei cercare di evitare di salvarli ...? Forse limitare il salvataggio in "punti sicuri" in cui l'elaborazione è terminata.
Pane di segale,

@aman Se si salva una raccolta, è possibile scrivere i dati effettivi in ​​queste raccolte come nel mio esempio originale, tranne in un "formato array", come in molti di essi di fila. È comunque possibile applicare la stessa idea a ogni singolo elemento di un array o qualsiasi altro contenitore. Dovrai solo scrivere un "serializzatore di array" generico, un "serializzatore di elenchi" ecc. Se desideri un "serializzatore di contenitori" generico, probabilmente avrai bisogno di un abstract SerializingIteratordi qualche tipo e questo iteratore verrebbe implementato per ogni tipo di contenitore.
RandyGaul,

1
Oh e sì, dovresti cercare di evitare il più possibile il salvataggio di raccolte complicate con puntatori. Spesso questo può essere evitato con un sacco di pensiero e design intelligente. La serializzazione è qualcosa che può diventare molto complicato, quindi pagherà per cercare di semplificarlo il più possibile. @aman
RandyGaul,

C'è anche il problema di deserializzare un oggetto quando la classe è cambiata ... Penso che il deserializzatore .NET si arresti in modo anomalo in molti casi.
Pane di segale,

2

Questo è un problema che esiste non solo sui giochi, ma anche su qualsiasi applicazione di scambio di file. Certamente, non ci sono soluzioni perfette e cercare di creare un formato file che si adatti a qualsiasi tipo di modifica è probabilmente impossibile, quindi è probabilmente una buona idea prepararsi per il tipo di modifiche che potresti aspettarti.

Il più delle volte, probabilmente aggiungerai / rimuoverai semplicemente campi e valori, mantenendo intatta la struttura generale dei tuoi file. In tal caso, puoi semplicemente scrivere il tuo codice per ignorare i campi sconosciuti e utilizzare le impostazioni predefinite sensibili quando un valore non può essere compreso / analizzato. L'implementazione è abbastanza semplice e lo faccio molto.

Tuttavia, a volte vorrai cambiare la struttura del file. Dire da testo a binario; o da campi fissi a dimensioni-valore. In tal caso, molto probabilmente vorrai congelare l'origine del vecchio lettore di file e crearne uno nuovo per il nuovo tipo di file, come nella soluzione di Sean. Assicurati di isolare l'intero lettore legacy o potresti finire per modificare qualcosa che lo influenza. Lo consiglio solo per importanti cambiamenti nella struttura dei file.

Questi due metodi dovrebbero funzionare nella maggior parte dei casi, ma tieni presente che non sono le uniche modifiche che potresti incontrare. Ho avuto un caso in cui ho dovuto cambiare l'intero livello di caricamento del codice da lettura completa a streaming (per la versione mobile del gioco, che dovrebbe funzionare su dispositivi con larghezza di banda e memoria significativamente ridotte). Un cambiamento come questo è molto più profondo e molto probabilmente richiederà cambiamenti in molte altre parti del gioco, alcune delle quali richiedono cambiamenti nella struttura del file stesso.


0

A un livello superiore: se stai aggiungendo nuove funzionalità al gioco, disponi di una funzione "Indovina nuovi valori" che può prendere le vecchie funzionalità e indovinare quali saranno i nuovi valori.

Un esempio potrebbe rendere questo più chiaro. Supponiamo che le città modellino un gioco e che la versione 1.0 tenga traccia del livello generale di sviluppo delle città, mentre la versione 1.1 aggiunge edifici specifici simili alla civiltà. (Personalmente, preferisco tenere traccia dello sviluppo complessivo, in quanto meno irrealistico; ma sto divagando.) GuessNewValues ​​() per 1.1, dato un file di salvataggio 1.0, inizierebbe con una vecchia figura del livello di sviluppo, e indovina, in base a ciò, cosa gli edifici sarebbero stati costruiti in città - forse guardando la cultura della città, la sua posizione geografica, il fulcro del suo sviluppo, quel genere di cose.

Spero che questo possa essere comprensibile in generale - che se stai aggiungendo nuove funzionalità a un gioco, caricare un file di salvataggio che non ha ancora quelle funzionalità richiede di fare le migliori ipotesi su quali saranno i nuovi dati e combinarli con i dati caricati.

Per quanto riguarda le cose di basso livello, appoggerei la risposta di Sean Middleditch (che ho votato a favore): mantenere la logica di carico esistente, possibilmente anche mantenendo le vecchie versioni delle classi pertinenti, e prima chiamandola, poi un convertitore.


0

Suggerirei di usare qualcosa come XML (se salvi i file sono molto piccoli) in questo modo hai solo bisogno di 1 funzione per gestire il markup, indipendentemente da ciò che hai inserito. Il nodo radice di quel documento potrebbe dichiarare la versione che ha salvato il gioco e, se necessario, consentire di scrivere codice per aggiornare il file all'ultima versione.

<save version="1">
  <player name="foo" score="10" />
  <data>![CDATA[lksdf9owelkjlkdfjdfgdfg]]</data>
</save>

Questo significa anche che puoi applicare una trasformazione se vuoi convertire i dati in un "formato di versione corrente" prima di caricare i dati, quindi invece di avere molte funzioni di versione attorno a te avresti semplicemente un set di file xsl tra cui scegliere per fare la conversione. Questo può richiedere molto tempo se non hai familiarità con xsl.

Se i tuoi file di salvataggio sono enormi xml potrebbe essere un problema, in genere ho i file di lavoro funzionano molto bene dove basta scaricare le coppie di valori chiave nel file in questo modo ...

version=1
player=foo
data=lksdf9owelkjlkdfjdfgdfg
score=10

Quindi quando leggi da questo file scrivi sempre e leggi una variabile allo stesso modo, se hai bisogno di una nuova variabile crei una nuova funzione per scriverla e leggerla. potresti semplicemente scrivere una funzione per i tipi di variabili in modo da avere un "lettore di stringhe" e un "lettore di int", questo funzionerebbe solo se cambiassi un tipo di variabile tra le versioni ma non dovresti mai farlo perché la variabile significa qualcos'altro in questo punto quindi dovresti creare una nuova variabile invece con un nome diverso.

L'altro modo, ovviamente, è utilizzare un formato di tipo di database o qualcosa di simile a un file CSV, ma dipende dai dati che si stanno salvando.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.