Come devo strutturare un sistema di caricamento delle risorse estensibile?


19

Per un motore di gioco per hobby in Java, voglio programmare un gestore di risorse / risorse semplice ma flessibile. Le risorse sono suoni, immagini, animazioni, modelli, trame, ecc. Dopo alcune ore di navigazione e alcuni esperimenti sul codice non sono ancora sicuro di come progettare questa cosa.

In particolare, sto cercando il modo in cui posso progettare il gestore in modo tale da astrarre come vengono caricati tipi di asset specifici e da dove vengono caricati gli asset. Vorrei essere in grado di supportare sia il file system che l'archiviazione RDBMS senza che il resto del programma ne abbia bisogno. Allo stesso modo, vorrei aggiungere un asset di descrizione dell'animazione (FPS, frame da renderizzare, riferimento all'immagine sprite, eccetera) che è XML. Dovrei essere in grado di scrivere una classe per questo con la funzionalità per trovare e leggere un file XML e creare e restituire una AnimationAssetclasse con tali informazioni. Sto cercando un design basato sui dati .

Posso trovare molte informazioni su cosa dovrebbe fare un gestore patrimoniale, ma non su come farlo. I generici coinvolti sembrano sfociare in qualche forma di classi a cascata, o in qualche forma di classi di aiuto. Tuttavia non ho visto un chiaro esempio che non assomigliasse a un hack personale o a un punto di consenso.

Risposte:


23

Vorrei iniziare senza pensare a un gestore patrimoniale . Pensare alla tua architettura in termini vagamente definiti (come "manager") tende a permetterti di spazzare mentalmente molti dettagli sotto il tappeto, e di conseguenza diventa più difficile accontentarsi di una soluzione.

Concentrati sulle tue esigenze specifiche, che sembrano avere a che fare con la creazione di un meccanismo di caricamento delle risorse che astragga la memoria di origine sottostante e permetta l'estensibilità del set di tipi supportati. Non c'è davvero nulla nella tua domanda riguardante, ad esempio, la memorizzazione nella cache delle risorse già caricate - il che va bene, perché in conformità con il principio della responsabilità singola dovresti probabilmente costruire una cache delle risorse come entità separata e aggregare le due interfacce altrove , a seconda dei casi.

Per rispondere alla tua specifica preoccupazione, dovresti progettare il tuo caricatore in modo che non esegua il caricamento di alcun asset stesso, ma piuttosto deleghi tale responsabilità alle interfacce su misura per il caricamento di specifici tipi di asset. Per esempio:

interface ITypeLoader {
  object Load (Stream assetStream);
}

È possibile creare nuove classi che implementano questa interfaccia, con ogni nuova classe adattata al caricamento di un tipo specifico di dati da un flusso. Usando un flusso, il caricatore di tipi può essere scritto su un'interfaccia comune, indipendente dallo spazio di archiviazione, e non deve essere codificato per caricare dal disco o da un database; ciò ti consentirebbe persino di caricare le tue risorse dagli stream di rete (che può essere molto utile nell'implementazione del hot-ricaricamento delle risorse quando il gioco è in esecuzione su una console e gli strumenti di modifica su un PC collegato in rete).

Il tuo caricatore di risorse principale deve essere in grado di registrare e tracciare questi caricatori specifici per tipo:

class AssetLoader {
  public void RegisterType (string key, ITypeLoader loader) {
    loaders[key] = loader;
  }

  Dictionary<string, ITypeLoader> loaders = new Dictionary<string, ITypeLoader>();
}

La "chiave" usata qui può essere quella che ti piace - e non è necessario che sia una stringa, ma sono facili da iniziare. La chiave determinerà il modo in cui ti aspetti che un utente identifichi una determinata risorsa e verrà utilizzato per cercare il caricatore appropriato. Poiché si desidera nascondere il fatto che l'implementazione potrebbe utilizzare un file system o un database, non è possibile avere utenti che fanno riferimento a risorse tramite un percorso del file system o qualcosa del genere.

Gli utenti devono fare riferimento a una risorsa con un minimo di informazioni. In alcuni casi, sarebbe sufficiente un solo nome di file, ma ho scoperto che spesso è preferibile utilizzare una coppia tipo / nome, quindi tutto è molto esplicito. Pertanto, un utente potrebbe fare riferimento a un'istanza denominata di uno dei file XML di animazione come "AnimationXml","PlayerWalkCycle".

Qui, AnimationXmlsarebbe la chiave con cui ti sei registrato AnimationXmlLoader, che implementa IAssetLoader. Ovviamente, PlayerWalkCycleidentifica l'asset specifico. Dato un nome di tipo e un nome di risorsa, il caricatore di risorse può eseguire una query sulla memoria permanente per i byte non elaborati di tale risorsa. Dal momento che stiamo cercando la massima generalità qui, puoi implementarlo passando al caricatore un mezzo di accesso allo storage quando lo crei, permettendoti di sostituire il supporto di archiviazione con qualsiasi cosa che possa fornire un flusso in seguito:

interface IAssetStreamProvider {
  Stream GetStream (string type, string name);
}

class AssetLoader {
  public AssetLoader (IAssetStreamProvider streamProvider) {
    provider = streamProvider;
  }

  object LoadAsset (string type, string name) {
    var loader = loaders[type];
    var stream = provider.GetStream(type, name);

    return loader.Load(stream);
  }

  public void RegisterType (string type, ITypeLoader loader) {
    loaders[type] = loader;
  }

  IAssetStreamProvider provider;
  Dictionary<string, ITypeLoader> loaders = new Dictionary<string, ITypeLoader>();
}

Un provider di stream molto semplice dovrebbe semplicemente cercare in una directory radice dell'asset specificata una sottodirectory denominata typee caricare i byte grezzi del file denominato namein uno stream e restituirlo.

In breve, quello che hai qui è un sistema in cui:

  • Esiste una classe che sa leggere byte grezzi da una sorta di memoria back-end (un disco, un database, un flusso di rete, qualunque cosa).
  • Esistono classi che sanno come trasformare un flusso di byte non elaborato in un tipo specifico di risorsa e restituirlo.
  • Il tuo "caricatore di risorse" attuale ha solo una raccolta delle suddette dimensioni e sa come convogliare l'output del provider di flussi nel caricatore specifico del tipo e quindi produrre un bene concreto. Esponendo i modi per configurare il provider di flusso e i caricatori specifici del tipo, si dispone di un sistema che può essere esteso dai client (o dall'utente) senza dover modificare il codice del caricatore di risorse effettivo.

Alcuni avvertimenti e note finali:

  • Il codice sopra è sostanzialmente C #, ma dovrebbe tradursi in quasi tutte le lingue con il minimo sforzo. Per facilitare ciò ho omesso molte cose come il controllo degli errori o l'uso corretto IDisposablee altri modi di dire che potrebbero non essere applicabili direttamente in altre lingue. Questi sono lasciati come compiti per il lettore.

  • Allo stesso modo, restituisco la risorsa concreta come objectsopra, ma puoi usare generici o modelli o qualsiasi altra cosa per produrre un tipo di oggetto più specifico se vuoi (dovresti, è bello lavorare con).

  • Come sopra, non mi occupo affatto di cache qui. Tuttavia, è possibile aggiungere la memorizzazione nella cache facilmente e con lo stesso tipo di generalità e configurabilità. Provalo e vedi!

  • Ci sono molti modi per farlo, e certamente non esiste un modo o un consenso, motivo per cui non sei stato in grado di trovarne uno. Ho provato a fornire abbastanza codice per ottenere i punti specifici senza trasformare questa risposta in un wall-of-code penosamente lungo. È già estremamente lungo così com'è. Se hai domande chiare, sentiti libero di commentare o di trovarmi nella chat .


1
Una buona domanda e una buona risposta che guidano la soluzione non solo verso una progettazione basata sui dati, ma anche su come iniziare a pensare in modo guidato dai dati.
Patrick Hughes,

Risposta molto bella e approfondita. Adoro come hai interpretato la mia domanda e mi hai detto esattamente quello che dovevo sapere mentre l'ho formulata così male. Grazie! Per caso, potresti indicarmi alcune risorse sugli stream?
user8363

Un "flusso" è solo una sequenza (potenzialmente senza fine determinabile) di byte o dati. Stavo pensando in particolare allo Stream di C # , ma probabilmente sei più interessato alle classi di stream di Java - anche se tieni presente che non conosco troppo Java, quindi potrebbe non essere una classe ideale da usare.

Gli stream sono in genere stateful, in quanto un determinato oggetto stream di solito ha una posizione di lettura o scrittura corrente all'interno dello stream e qualsiasi IO che esegui su di esso si verifica da quella posizione - ecco perché li ho usati come input per le interfacce delle risorse sopra, perché stanno essenzialmente dicendo "ecco alcuni dati non elaborati e da dove iniziare a leggere, leggere da esso e fare le tue cose".

Questo approccio onora alcuni dei principi fondamentali di SOLID e OOP . Bravo.
Adam Naylor,
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.