Qual è un buon modo per archiviare i dati di tilemap?


12

Sto sviluppando un platform 2D con alcuni amici uni. Lo abbiamo basato sullo Starter Kit XNA Platformer che utilizza file .txt per memorizzare la mappa delle tessere. Anche se questo è semplice, non ci dà abbastanza controllo e flessibilità con la progettazione di livello. Alcuni esempi: per più livelli di contenuto sono necessari più file, ogni oggetto è fissato sulla griglia, non consente la rotazione di oggetti, un numero limitato di caratteri ecc. Quindi sto facendo delle ricerche su come archiviare i dati di livello e file della mappa.

Ciò riguarda solo l'archiviazione del file system delle mappe delle tessere, non la struttura dei dati che il gioco deve usare mentre è in esecuzione. La mappa dei riquadri viene caricata in un array 2D, quindi questa domanda riguarda l'origine da cui riempire l'array.

Ragionamento per DB: Dal mio punto di vista vedo meno ridondanza di dati utilizzando un database per archiviare i dati del riquadro. Le tessere nella stessa posizione x, y con le stesse caratteristiche possono essere riutilizzate da un livello all'altro. Sembra che sarebbe abbastanza semplice scrivere un metodo per recuperare tutti i riquadri utilizzati in un determinato livello dal database.

Ragionamento per JSON / XML: file modificabili visivamente, le modifiche possono essere monitorate tramite SVN molto più facilmente. Ma c'è un contenuto ripetuto.

Ci sono degli svantaggi (tempi di caricamento, tempi di accesso, memoria ecc.) Rispetto all'altro? E cosa viene comunemente usato nel settore?

Attualmente il file è simile al seguente:

....................
....................
....................
....................
....................
....................
....................
.........GGG........
.........###........
....................
....GGG.......GGG...
....###.......###...
....................
.1................X.
####################

1 - Punto iniziale giocatore, Uscita livello X,. - Spazio vuoto, # - Piattaforma, G - Gemma


2
Quale "formato" esistente stai usando? Basta dire "file di testo" significa solo che non stai salvando i dati binari. Quando dici "controllo e flessibilità insufficienti", quali sono, in particolare , i problemi che stai riscontrando? Perché lo scontro tra XML e SQLite? Ciò determinerà la risposta. blog.stackoverflow.com/2011/08/gorilla-vs-shark
Tetrade

Opterei per JSON in quanto è più leggibile.
Den

3
Perché le persone pensano di usare SQLite per queste cose? È un database relazionale ; perché la gente pensa che un database relazionale crei un buon formato di file?
Nicol Bolas,

1
@StephenTierney: Perché questa domanda è passata improvvisamente da XML vs SQLite a JSON rispetto a qualsiasi tipo di database? La mia domanda è specificamente perché non è solo "Qual è un buon modo per archiviare i dati di tilemap?" Questa domanda X vs. Y è arbitraria e insignificante.
Nicol Bolas,

1
@Kylotan: Ma non funziona molto bene. È incredibilmente difficile da modificare, perché è possibile modificare il database solo tramite comandi SQL. È difficile da leggere, poiché le tabelle non sono davvero un modo efficace per guardare una tilemap e avere una comprensione di ciò che sta succedendo. E mentre può fare ricerche, la procedura di ricerca è incredibilmente complicata. Far funzionare qualcosa è importante, ma se renderà il processo di sviluppo del gioco molto più difficile, non vale la pena intraprendere questa strada.
Nicol Bolas,

Risposte:


13

Quindi, leggendo la tua domanda aggiornata, sembra che tu sia più preoccupato per i "dati ridondanti" sul disco, con alcune preoccupazioni secondarie sui tempi di caricamento e su ciò che l'industria utilizza.

Prima di tutto, perché sei preoccupato per i dati ridondanti? Anche per un formato gonfio come XML, sarebbe piuttosto banale implementare la compressione e ridurre le dimensioni dei tuoi livelli. È più probabile che occupi spazio con trame o suoni rispetto ai dati di livello.

In secondo luogo, è molto probabile che un formato binario venga caricato più velocemente di uno basato su testo che devi analizzare. Doppiamente se puoi semplicemente lasciarlo cadere in memoria e avere le tue strutture dati proprio lì. Tuttavia, ci sono alcuni aspetti negativi. Per uno, i file binari sono quasi impossibili da eseguire il debug (specialmente per le persone che creano contenuti). Non è possibile diffrarli o modificarli. Ma saranno più piccoli e caricheranno più velocemente.

Quello che fanno alcuni motori (e ciò che considero una situazione ideale) è implementare due percorsi di caricamento. Per lo sviluppo, si utilizza un formato basato sul testo di qualche tipo. Non importa quale sia il formato specifico che usi, purché usi una libreria solida. Per il rilascio, si passa al caricamento di una versione binaria (caricamento più piccolo, più veloce, possibilmente privato delle entità di debug). I tuoi strumenti per modificare i livelli sputano entrambi. È molto più lavoro di un solo formato di file, ma puoi ottenere il meglio da entrambi i mondi.

Detto questo, penso che stai saltando un po 'la pistola.

Il primo passo per questi problemi è sempre descrivere il problema in cui si sta eseguendo accuratamente. Se sai di quali dati hai bisogno, allora sai quali dati devi archiviare. Da lì è una domanda di ottimizzazione verificabile.

Se hai un caso d'uso da testare, puoi testare diversi metodi di archiviazione / caricamento dei dati per misurare ciò che ritieni importante (tempo di caricamento, utilizzo della memoria, dimensione del disco, ecc.). Senza saperlo, è difficile essere più specifici.


8
  • XML: facile da modificare a mano ma una volta che inizi a caricare molti dati, rallenta.
  • SQLite: questo potrebbe essere migliore se si desidera recuperare molti dati contemporaneamente o anche solo piccoli blocchi. Tuttavia, a meno che tu non stia usando questo altrove nel tuo gioco, penso che sia eccessivo per le tue mappe (e probabilmente troppo complicato).

La mia raccomandazione è di usare un formato di file binario personalizzato. Con il mio gioco, ho solo un SaveMapmetodo che passa e salva ogni campo usando un BinaryWriter. Ciò consente anche di scegliere di comprimerlo se lo si desidera e offre un maggiore controllo sulla dimensione del file. vale a dire Salva un shortinvece di un intse sai che non sarà più grande di 32767. In un ciclo di grandi dimensioni, il salvataggio di qualcosa al shortposto di un intpuò significare dimensioni di file molto più piccole.

Inoltre, se segui questa strada, raccomando che la tua prima variabile nel file sia un numero di versione.

Consideriamo ad esempio una classe di mappe (molto semplificata):

class Map {
    private const short MapVersion = 1;
    public string Name { get; set; }

    public void SaveMap(string filename) {
        //set up binary writer
        bw.Write(MapVersion);
        bw.Write(Name);
        //close/dispose binary writer
    }
    public void LoadMap(string filename) {
        //set up binary reader
        short mapVersion = br.ReadInt16();
        Name = br.ReadString();
        //close/dispose binary reader
    }
}

Ora, supponiamo che tu voglia aggiungere una nuova proprietà alla tua mappa, diciamo a Listdi Platformoggetti. Ho scelto questo perché è un po 'più coinvolto.

Prima di tutto, si incrementa MapVersione si aggiunge List:

private const short MapVersion = 2;
public string Name { get; set; }
public List<Platform> Platforms { get; set; }

Quindi, aggiorna il metodo di salvataggio:

public void SaveMap(string filename) {
    //set up binary writer
    bw.Write(MapVersion);
    bw.Write(Name);
    //Save the count for loading later
    bw.Write(Platforms.Count);
    foreach(Platform plat in Platforms) {
        //For simplicities sake, I assume Platform has it's own
        // method to write itself to a file.
        plat.Write(bw);
    }
    //close/dispose binary writer
}

Quindi, ed è qui che vedi davvero il vantaggio, aggiorna il metodo di caricamento:

public void LoadMap(string filename) {
    //set up binary reader
    short mapVersion = br.ReadInt16();
    Name = br.ReadString();
    //Create our platforms list
    Platforms = new List<Platform>();
    if (mapVersion >= 2) {
        //Version is OK, let's load the Platforms
        int mapCount = br.ReadInt32();
        for (int i = 0; i < mapCount; i++) {
            //Again, I'm going to assume platform has a static Read
            //  method that returns a Platform object
            Platforms.Add(Platform.Read(br));
        }
    } else {
        //If it's an older version, give it a default value
        Platforms.Add(new Platform());
    }
    //close/dispose binary reader
}

Come puoi vedere, il LoadMapsmetodo può non solo caricare l'ultima versione della mappa, ma anche versioni precedenti! Quando carica le mappe più vecchie, hai il controllo sui valori predefiniti che utilizza.


8

Storia breve

Un'ottima alternativa a questo problema è quella di memorizzare i livelli in una bitmap, un pixel per riquadro. Usando RGBA questo permette facilmente di memorizzare quattro diverse dimensioni (livelli, ID, rotazione, tinta, ecc.) Su una singola immagine.

Lunga storia

Questa domanda mi ha ricordato quando Notch ha trasmesso in streaming in streaming la sua voce su Ludum Dare qualche mese fa, e mi piacerebbe condividere nel caso in cui non sapessi cosa ha fatto. Ho pensato che fosse davvero interessante.

Fondamentalmente, ha usato bitmap per memorizzare i suoi livelli. Ogni pixel nella bitmap corrispondeva a una "piastrella" nel mondo (non proprio una tessera nel suo caso poiché era un gioco trasmesso in streaming ma abbastanza vicino). Un esempio di uno dei suoi livelli:

inserisci qui la descrizione dell'immagine

Pertanto, se si utilizza RGBA (8 bit per canale), è possibile utilizzare ciascun canale come un livello diverso e fino a 256 tipi di riquadri per ciascuno di essi. Sarebbe abbastanza? O ad esempio, uno dei canali potrebbe contenere la rotazione per il riquadro come hai menzionato. Inoltre, la creazione di un editor di livelli per lavorare con questo formato dovrebbe essere piuttosto banale.

E soprattutto, non hai nemmeno bisogno di alcun processore di contenuti personalizzato poiché potresti semplicemente caricarlo in XNA come qualsiasi altro Texture2D e rileggere i pixel in un ciclo.

IIRC ha usato un punto rosso puro sulla mappa per segnalare un nemico, e in base al valore del canale alfa per quel pixel determinerebbe il tipo di nemico (ad esempio un valore alfa di 255 potrebbe essere un pipistrello mentre 254 sarebbe uno zombi o qualcos'altro).

Un'altra idea sarebbe quella di suddividere i tuoi oggetti di gioco in quelli che sono fissi in una griglia e quelli che possono muoversi "finemente" tra le tessere. Mantieni i riquadri fissi nella bitmap e gli oggetti dinamici in un elenco.

Potrebbe anche esserci un modo per multiplexare ancora più informazioni in quei 4 canali. Se qualcuno ha qualche idea su questo, fammi sapere. :)


3

Probabilmente potresti cavartela con quasi tutto. Sfortunatamente i computer moderni sono così veloci che questa presumibilmente relativamente piccola quantità di dati non farebbe davvero una notevole differenza di tempo, anche se è archiviata in un formato di dati gonfio e mal costruito che brama un'enorme quantità di elaborazione da leggere.

Se vuoi fare la cosa "giusta" fai un formato binario come Drackir ha suggerito, ma se fai un'altra scelta per qualche sciocca ragione, probabilmente non tornerà e ti morderà (almeno non per quanto riguarda le prestazioni).


1
Sì, dipende sicuramente dalle dimensioni. Ho una mappa di circa 161.000 tessere. Ognuno dei quali ha 6 strati. Inizialmente lo avevo in XML, ma era di 82 MB e impiegava un'eternità a caricarsi. Ora lo salvo in un formato binario ed è circa 5 MB prima della compressione e si carica in circa 200 ms. :)
Richard Marskell - Drackir,

2

Vedi questa domanda: "XML binario" per i dati di gioco? Vorrei anche optare per JSON o YAML o addirittura XML, ma questo ha un sacco di costi generali. Per quanto riguarda SQLite, non lo userei mai per l'archiviazione dei livelli. Un database relazionale è utile se si prevede di archiviare molti dati correlati (ad esempio, ha collegamenti relazionali / chiavi esterne) e se si prevede di richiedere una grande quantità di query diverse, la memorizzazione di tilemap non sembra fare questo tipo di cose e aggiungerebbe inutili complessità.


2

Il problema fondamentale con questa domanda è che unisce due concetti che non hanno nulla a che fare l'uno con l'altro:

  1. Paradigma di archiviazione dei file
  2. Rappresentazione in memoria dei dati

Puoi archiviare i tuoi file come testo normale in un formato arbitrario, JSON, script Lua, XML, un formato binario arbitrario, ecc. E niente di tutto ciò richiederà che la tua rappresentazione in memoria di quei dati prenda una forma particolare.

È compito del tuo livello di caricamento del codice convertire il paradigma di archiviazione dei file nella tua rappresentazione in memoria. Ad esempio, dici "Vedo meno ridondanza dei dati utilizzando un database per archiviare i dati del riquadro". Se vuoi meno ridondanza, è qualcosa a cui dovrebbe guardare il tuo codice di caricamento di livello. È qualcosa che la tua rappresentazione in memoria dovrebbe essere in grado di gestire.

Non è qualcosa di cui devi preoccuparti per il tuo formato di file. Ed ecco perché: quei file devono provenire da qualche parte.

O li scriverai a mano o utilizzerai un qualche tipo di strumento che crea e modifica i dati di livello. Se le scrivi a mano, la cosa più importante di cui hai bisogno è un formato che sia facile da leggere e modificare. La ridondanza dei dati non è qualcosa che il tuo formato deve nemmeno prendere in considerazione, perché passerai la maggior parte del tuo tempo a modificare i file. Vuoi effettivamente utilizzare manualmente qualsiasi meccanismo ti venga in mente per fornire ridondanza dei dati? Non sarebbe un uso migliore del tuo tempo avere solo il tuo caricatore di livelli a gestirlo?

Se si dispone di uno strumento che li crea, il formato effettivo è importante (nella migliore delle ipotesi) per motivi di leggibilità. Puoi mettere qualsiasi cosa in questi file. Se si desidera omettere i dati ridondanti nel file, è sufficiente progettare un formato in grado di farlo e fare in modo che lo strumento utilizzi correttamente tale capacità. Il formato tilemap può includere la compressione RLE (codifica di lunghezza di esecuzione), la compressione di duplicazione, qualunque sia la tecnica di compressione desiderata, poiché è il formato di tilemap.

Problema risolto.

Esistono database relazionali (RDB) per risolvere il problema di eseguire ricerche complesse su set di dati di grandi dimensioni che contengono molti campi di dati. L'unico tipo di ricerca che fai in una mappa a tessere è "ottieni tessera in posizione X, Y". Qualsiasi array può gestirlo. Utilizzando un database relazionale per memorizzare e mantenere tilemap dati sarebbero estrema eccessivo, e non realmente vale nulla.

Anche se crei un po 'di compressione nella tua rappresentazione in memoria, sarai comunque in grado di battere facilmente gli RDB in termini di prestazioni e footprint di memoria. Sì, dovrai effettivamente implementare quella compressione. Ma se la dimensione dei dati in memoria è una tale preoccupazione che dovresti considerare un RDB, allora probabilmente vorrai implementare tipi specifici di compressione. Sarai più veloce di un RDB e allo stesso tempo con meno memoria.


1
  • XML: davvero semplice da usare, ma l'overhead diventa fastidioso quando la struttura diventa profonda.
  • SQLite: più conveniente per strutture più grandi.

Per dati davvero semplici, probabilmente seguirei il percorso XML, se hai già familiarità con il caricamento e l'analisi XML.

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.