Come progettare un AssetManager?


26

Qual è l'approccio migliore alla progettazione di un AssestManager che conterrà riferimenti alla grafica, ai suoni, ecc. Di un gioco?

Queste risorse devono essere archiviate in una coppia di mappe chiave / valore? Cioè chiedo asset "di sfondo" e la mappa restituisce la bitmap associata? C'è un modo ancora migliore?

In particolare sto scrivendo un gioco Android / Java, ma le risposte possono essere generiche.

Risposte:


16

Dipende dall'ambito del tuo gioco. Un gestore patrimoniale è assolutamente essenziale per titoli più grandi, meno per giochi più piccoli.

Per i titoli più grandi devi gestire problemi come i seguenti:

  • Risorse condivise: la trama in mattoni viene utilizzata da più modelli?
  • Durata delle risorse: quella risorsa che hai caricato 15 minuti fa non è più necessaria? Fai riferimento al conteggio delle tue risorse per assicurarti di sapere quando qualcosa è finito, ecc
  • In DirectX 9 se alcuni tipi di risorse vengono caricati e il dispositivo grafico viene 'perso' (ciò accade se si preme Ctrl + Alt + Canc tra le altre cose), il gioco dovrà ricrearli
  • Caricare le risorse prima di averne bisogno - non puoi costruire grandi giochi open world senza questo
  • Caricamento in blocco di risorse: spesso raggruppiamo molte risorse in un singolo file per migliorare i tempi di caricamento. La ricerca del disco richiede molto tempo

Per i titoli più piccoli queste cose sono meno problematiche, quadri come XNA hanno gestori patrimoniali al loro interno - non ha molto senso reinventarlo.

Se ti accorgi che hai bisogno di un gestore patrimoniale, non esiste davvero una soluzione unica per tutti, ma ho scoperto che una mappa hash con la chiave come hash * del nome file (abbassata e separatori tutti 'fissi') funziona bene per i progetti a cui ho lavorato.

Di solito non è consigliabile codificare i nomi dei file nella tua app, di solito è meglio avere un altro formato di dati (come xml) che descriva i nomi dei file in "ID".

  • Come nota a margine divertente, normalmente si ottiene una collisione di hash per progetto.

Solo perché è necessario gestire le risorse non è necessario AssetManagers, un nome importante maiuscolo che probabilmente ha troppi metodi, scarse prestazioni e semantica della memoria fangosa. Per un confronto, pensa a cosa succede se hai un sacco di project management (di solito buono), e poi quando hai molti project manager (di solito cattivo).

2
@Joe Wreschnig - come affronteresti i cinque requisiti menzionati da icStatic senza utilizzare un gestore patrimoniale?
antinome,

8

(Cercando di evitare la discussione "non usare un gestore patrimoniale" qui, poiché lo considero offtopico.)

Una mappa chiave / valore è un approccio molto utilizzabile.

Abbiamo un'implementazione di ResourceManager in cui è possibile registrare fabbriche per diversi tipi di risorsa.

Il metodo "getResource" utilizza i modelli per trovare la Factory corretta per il tipo di risorsa desiderato e restituisce uno specifico ResourceHandle (usando nuovamente il modello per restituire uno SpecificResourceHandle).

Le risorse vengono ricontattate da ResourceManager (all'interno di ResourceHandle) e rilasciate quando non sono più necessarie.

Il primo addon che abbiamo scritto è stato il metodo "ricaricare (XYZ)", che ci consente di cambiare le risorse dall'esterno del motore in esecuzione senza cambiare alcun codice o ricaricare il gioco. (Questo è essenziale quando gli artisti lavorano su console;))

Il più delle volte abbiamo solo un'istanza di ResourceManager, ma a volte creiamo una nuova istanza solo per un livello o una mappa. In questo modo possiamo semplicemente chiamare "shutdown" sul levelResourceManager e assicurarci che non vi siano perdite.

(breve) esempio

// very abbreviated!
// this code would never survive our coding guidelines ;)

ResourceManager* pRm = new ResourceManager;
pRm->initialize( );
pRm->registerFactory( new TextureFactory );
// [...]
TextureHandle tex = pRm->getResource<Texture>( "test.otx" ); // in real code we use some macro magic here to use CRCs for filenames
tex->storeToHardware( 0 ); // channel 0

pRm->releaseResource( pRm );

// [...]
pRm->shutdown(); // will log any leaked resource

6

Le classi di Manager dedicati non sono quasi mai lo strumento di ingegneria giusto. Se hai bisogno dell'asset una sola volta (come uno sfondo o una mappa), dovresti richiederlo solo una volta e lasciarlo morire normalmente quando hai finito con esso. Se è necessario memorizzare nella cache un particolare tipo di oggetto, è necessario utilizzare una factory che prima controlla una cache e carica altrimenti qualcosa, la mette nella cache e quindi la restituisce - e quella factory può essere solo una funzione statica che accede a una variabile statica , non un tipo a sé stante.

Steve Yegge (tra molti, molti altri) ha scritto una bella storia su come inutili classi manageriali, attraverso il modello singleton, finiscano per essere. http://sites.google.com/site/steveyegge2/singleton-considered-stupid


2
Certo, sicuramente. Ma in casi come Android (o altri giochi) è necessario caricare un sacco di grafica / suoni nella memoria prima di iniziare il gioco, non durante. Come posso usare quello che stai dicendo (fabbriche) per farlo durante una schermata di caricamento? Basta colpire tutti gli oggetti in fabbrica nella schermata di caricamento in modo che li memorizzi nella cache?
Bryan Denny,

Non ho familiarità con i dettagli di Android, ma non ho idea di cosa intendi per "prima di iniziare il gioco". È davvero impossibile caricare una risorsa quando ne hai bisogno (o quando ne avrai bisogno 'presto') piuttosto che quando avvii il programma? Trovo che sia estremamente improbabile, altrimenti, ad esempio, non potresti mai avere più trame di quelle che si adattano alla scarsa RAM di Android.

@Joe date un'occhiata all'altra mia domanda sul "caricamento delle schermate": gamedev.stackexchange.com/questions/1171/… Colpire una cache vuota significa molto tempo per passare al disco e potrebbe causare alcuni hit delle prestazioni FPS in quelle prime chiamate . Se sai già cosa colpirai in anticipo, potresti anche colpirlo durante il caricamento per pre-memorizzarlo nella cache, giusto?
Bryan Denny,

Ancora una volta non posso parlare con Android, ma di solito andare su disco è esattamente ciò che puoi fare senza prendere hit FPS, perché il thread che va su disco non consumerà alcuna CPU. Devi solo fare un budget facendo abbastanza in anticipo per non essere pop-in. Se hai pre-cache tutto perché sai in anticipo ciò di cui hai bisogno, non hai davvero bisogno di un AssetManager, perché non devi assolutamente gestire le risorse: sono già tutte a portata di mano.

1
@Joe, una fabbrica non è anche un "Dedicated Manager"?
MSN

2

Ho sempre pensato che un buon gestore patrimoniale dovrebbe avere diverse modalità operative. Molto probabilmente queste modalità sarebbero moduli sorgente separati che aderiscono a un'interfaccia comune. Le due modalità operative di base sarebbero:

  • Modalità di produzione: tutti gli asset sono locali e privati ​​di tutti i metadati
  • Modalità di sviluppo: i test sono archiviati in un database (ad es. MySQL, ecc.) Con metadati aggiuntivi. Il database sarebbe un sistema a due livelli con un database locale che memorizza nella cache un database condiviso. I creatori di contenuti sarebbero in grado di modificare e aggiornare il database condiviso e gli aggiornamenti automaticamente proposti ai sistemi di sviluppo / QA. Dovrebbe anche essere possibile creare contenuti segnaposto. Poiché tutto è contenuto in un database, è possibile eseguire query sul database e generare report per analizzare lo stato della produzione.

Avresti bisogno di uno strumento in grado di catturare tutti i test dal database condiviso e creare il set di dati di produzione.

Nei miei anni come sviluppatore, non ho mai visto nulla di simile, anche se ho lavorato solo per una manciata di aziende, quindi il mio punto di vista non è davvero rappresentativo.

Aggiornare

OK, alcuni voti negativi. Espanderò su questo disegno.

In primo luogo, non hai davvero bisogno delle classi di fabbrica perché se hai:

TextureHandle tex = pRm->getResource<Texture>( "test.otx" );

conosci il tipo, quindi basta:

TextureHandle tex = new TextureHandle ("test.otx");

ma poi, quello che stavo cercando di dire sopra è che non avresti comunque usato nomi di file espliciti, la trama da caricare sarebbe specificata dal modello su cui la trama è usata, quindi in realtà non hai bisogno di un nome leggibile dall'uomo, potrebbe essere un valore intero a 32 bit, che è molto più facile da gestire per la CPU. Quindi, nel costruttore di TextureHandle avresti:

if (texture already loaded)
  update texture reference count
else
  asset_stream = new AssetStream (resource_id)
  asset_stream->ReadBytes
  create texture
  set texture ref count to 1

AssetStream utilizza il parametro resource_id per trovare la posizione dei dati. Il modo in cui lo ha fatto dipenderà dall'ambiente in cui si esegue:

In sviluppo: lo stream cerca l'ID in un database (usando SQL per esempio) per ottenere un nome file e quindi apre il file, il file potrebbe essere memorizzato nella cache locale o estratto da un server se il file locale non esiste o è obsoleto.

Nella versione: lo stream cerca l'ID in una tabella chiave / valore per ottenere un offset / dimensione in un file grande e compresso (come il file WAD di Doom).


Ti ho votato perché hai suggerito di inserire tutto in una tabella SQL con chiavi primarie anziché utilizzare un VCS reale. Inoltre, considero l'utilizzo di ID opachi anziché l'ottimizzazione prematura dei nomi di stringa. Ho usato stringhe su due grandi progetti per tutte le risorse diverse dalle chiavi di traduzione, di cui avevamo centinaia di migliaia di chiavi di stringa molto lunghe (e quindi solo per il port su console). Di solito erano normalizzati in modo da poter usare i confronti dei puntatori anziché i confronti delle stringhe, ma i confronti delle stringhe sono spesso dominati dal costo del recupero della memoria e non dal confronto effettivo comunque.

@Joe: ho solo fornito SQL come esempio e quindi solo in un ambiente di sviluppo, è possibile utilizzare un VCS. Ho solo suggerito un database SQL poiché puoi quindi aggiungere ulteriori informazioni agli oggetti archiviati e utilizzare le funzioni SQL per eseguire query sulle informazioni dal database (più un vantaggio di gestione che altro). Per quanto riguarda gli ID opachi come ottimizzazione prematura - alcuni potrebbero vederlo in questo modo, immagino, ma penso che sarebbe più facile iniziare con questo piuttosto che mostrarlo in un secondo momento dello sviluppo. Non penso che influenzerebbe molto lo sviluppo se usassi ID o stringhe.
Skizz,

2

Quello che mi piace fare per le risorse è creare un gestore di capitale . Ispirati dal motore Doom, i grumi sono pezzi di dati che contengono risorse, memorizzati in un file di grumi che dichiara i nomi dei grumi, le lunghezze, il tipo (bitmap, suono, shader, ecc.) E il tipo di contenuto (file, un altro grumo, all'interno il file lump stesso). All'avvio, questi grumi vengono inseriti in un albero binario, ma non ancora caricati. Ogni mappa (che è anche un nodulo) ha un elenco di dipendenze, che sono semplicemente i nomi di noduli che la mappa deve funzionare. Questi grumi, a meno che non siano già stati caricati, vengono caricati al momento del caricamento della mappa. Inoltre, i grumi delle mappe adiacenti della mappa vengono caricati, non solo allo stesso tempo, ma quando il motore è al minimo per qualche motivo. Questo può rendere le mappe senza soluzione di continuità e non esiste una schermata di caricamento.

Il mio metodo è perfetto per le mappe del mondo aperto, ma un gioco basato su livelli non trarrà beneficio dalla continuità di questo metodo. Spero che sia di aiuto!

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.