Comment dois-je structurer un système extensible de chargement d'actifs?

19

Pour un moteur de jeu de loisir en Java, je veux coder un gestionnaire d'actifs / ressources simple mais flexible. Les actifs sont des sons, des images, des animations, des modèles, des textures, etc. Après quelques heures de navigation et quelques expériences de code, je ne sais toujours pas comment concevoir cette chose.

Plus précisément, je cherche comment concevoir le gestionnaire de manière à ce qu'il résume comment les types d'actifs spécifiques sont chargés et d'où les actifs sont chargés. J'aimerais pouvoir prendre en charge à la fois le système de fichiers et le stockage RDBMS sans que le reste du programme ait besoin de le savoir. De même, je voudrais ajouter un élément de description d'animation (FPS, images à restituer, référence à l'image-objet, et cetera) qui est XML. Je devrais pouvoir écrire une classe pour cela avec la fonctionnalité pour trouver et lire un fichier XML et créer et renvoyer une AnimationAssetclasse avec ces informations. Je recherche une conception basée sur les données .

Je peux trouver beaucoup d'informations sur ce qu'un gestionnaire d'actifs doit faire, mais pas sur la façon de le faire. Les génériques impliqués semblent entraîner une forme de cascade de classes ou une forme de classes auxiliaires. Cependant, je n'ai pas vu d'exemple clair qui ne ressemble pas à un hack personnel ou à un point de consensus.

user8363
la source

Réponses:

23

Je commencerais par ne pas penser à un gestionnaire d' actifs . Penser votre architecture dans des termes vaguement définis (comme «gestionnaire») a tendance à vous laisser glisser mentalement de nombreux détails sous le tapis, et par conséquent, il devient plus difficile de trouver une solution.

Concentrez-vous sur vos besoins spécifiques, ce qui semble être lié à la création d'un mécanisme de chargement des ressources qui résume le stockage d'origine sous-jacent et permet l'extensibilité de l'ensemble de types pris en charge. Il n'y a vraiment rien dans votre question concernant, par exemple, la mise en cache des ressources déjà chargées - ce qui est bien, car conformément à la principe de responsabilité unique, vous devriez probablement créer un cache d'actifs en tant qu'entité distincte et agréger les deux interfaces ailleurs , selon le cas.

Pour répondre à votre préoccupation spécifique, vous devez concevoir votre chargeur de sorte qu'il ne fasse pas le chargement des actifs lui-même, mais délègue plutôt cette responsabilité à des interfaces adaptées au chargement de types spécifiques d'actifs. Par exemple:

interface ITypeLoader {
  object Load (Stream assetStream);
}

Vous pouvez créer de nouvelles classes qui implémentent cette interface, chaque nouvelle classe étant adaptée au chargement d'un type spécifique de données à partir d'un flux. En utilisant un flux, le chargeur de type peut être écrit sur une interface commune, indépendante du stockage, et n'a pas besoin d'être codé en dur pour charger à partir du disque ou d'une base de données; cela vous permettrait même de charger vos actifs à partir de flux réseau (ce qui peut être très utile pour implémenter le rechargement à chaud des actifs lorsque votre jeu s'exécute sur une console et vos outils d'édition sur un PC connecté au réseau).

Votre chargeur d'actifs principal doit pouvoir enregistrer et suivre ces chargeurs spécifiques au type:

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

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

La "clé" utilisée ici peut être ce que vous voulez - et ce n'est pas nécessairement une chaîne, mais celles-ci sont faciles à démarrer. La clé tiendra compte de la façon dont vous vous attendez à ce qu'un utilisateur identifie un actif particulier et sera utilisée pour rechercher le chargeur approprié. Parce que vous voulez masquer le fait que l'implémentation utilise peut-être un système de fichiers ou une base de données, vous ne pouvez pas avoir d'utilisateurs référençant des actifs par un chemin de système de fichiers ou quelque chose comme ça.

Les utilisateurs doivent se référer à un actif avec un strict minimum d'informations. Dans certains cas, un seul nom de fichier suffirait à lui seul, mais j'ai constaté qu'il est souvent souhaitable d'utiliser une paire type / nom, donc tout est très explicite. Ainsi, un utilisateur peut se référer à une instance nommée de l'un de vos fichiers XML d'animation comme "AnimationXml","PlayerWalkCycle".

Ici, ce AnimationXmlserait la clé sous laquelle vous vous êtes inscrit AnimationXmlLoader, qui implémente IAssetLoader. De toute évidence, PlayerWalkCycleidentifie l'actif spécifique. Étant donné un nom de type et un nom de ressource, votre chargeur d'actifs peut interroger son stockage persistant pour les octets bruts de cet actif. Puisque nous recherchons ici une généralité maximale, vous pouvez l'implémenter en transmettant au chargeur un moyen d'accès au stockage lorsque vous le créez, vous permettant de remplacer le support de stockage par tout ce qui peut fournir un flux plus tard:

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 fournisseur de flux très simple rechercherait simplement dans un répertoire racine d'actif spécifié un sous-répertoire nommé typeet chargerait les octets bruts du fichier nomméname dans un flux et le retournerait.

En bref, ce que vous avez ici est un système où:

  • Il existe une classe qui sait lire les octets bruts d'une sorte de stockage backend (un disque, une base de données, un flux réseau, etc.).
  • Il existe des classes qui savent comment transformer un flux d'octets bruts en un type spécifique de ressource et le renvoyer.
  • Votre "chargeur d'actifs" réel a juste une collection de ce qui précède et sait comment diriger la sortie du fournisseur de flux dans le chargeur spécifique au type et ainsi produire un actif concret. En exposant les moyens de configurer le fournisseur de flux et les chargeurs spécifiques au type, vous disposez d'un système qui peut être étendu par les clients (ou vous-même) sans avoir à modifier le code du chargeur d'actifs réel.

Quelques mises en garde et notes finales:

  • Le code ci-dessus est essentiellement C #, mais devrait se traduire dans à peu près n'importe quel langage avec un minimum d'effort. Pour faciliter cela, j'ai omis beaucoup de choses comme la vérification des erreurs ou l'utilisation correcte IDisposableet d'autres idiomes qui peuvent ne pas s'appliquer directement dans d'autres langues. Ceux-ci sont laissés comme devoirs pour le lecteur.

  • De même, je retourne l'actif concret comme objectci-dessus, mais vous pouvez utiliser des génériques ou des modèles ou autre pour produire un type d'objet plus spécifique si vous le souhaitez (vous devriez, c'est agréable de travailler avec).

  • Comme ci-dessus, je ne traite pas du tout de la mise en cache ici. Cependant, vous pouvez ajouter la mise en cache facilement et avec le même type de généralité et de configurabilité. Essayez-le et voyez!

  • Il y a beaucoup, beaucoup et beaucoup de façons de le faire, et il n'y a certainement pas de voie unique ou de consensus, c'est pourquoi vous n'avez pas pu en trouver une. J'ai essayé de fournir suffisamment de code pour faire passer les points spécifiques sans transformer cette réponse en un mur de code douloureusement long. C'est déjà extrêmement long. Si vous avez des questions à clarifier, n'hésitez pas à commenter ou à me retrouver dans le chat .


la source
1
Bonne question et bonne réponse qui orientent la solution non seulement vers une conception axée sur les données, mais aussi sur la façon de commencer à penser de manière axée sur les données.
Patrick Hughes
Réponse très agréable et approfondie. J'adore la façon dont vous avez interprété ma question et m'avez dit exactement ce que je devais savoir pendant que je la formulais si mal. Merci! Par hasard, pourriez-vous m'indiquer quelques ressources sur Streams?
user8363
Un "flux" n'est qu'une séquence (potentiellement sans fin déterminable) d'octets ou de données. Je pensais spécifiquement au Stream de C # , mais vous êtes probablement plus intéressé par les classes de stream de Java - bien que soyez averti que je ne connais pas trop Java donc ce n'est peut-être pas une classe idéale à utiliser.
Les flux sont généralement avec état, car un objet de flux donné a généralement une position de lecture ou d'écriture actuelle dans le flux, et tout IO que vous effectuez dessus se produit à partir de cette position - c'est pourquoi je les ai utilisés comme entrées pour les interfaces d'actif ci-dessus, parce qu'ils disent essentiellement "voici quelques données brutes et où commencer à lire, lire et faire votre chose."
Cette approche respecte certains des principes fondamentaux de SOLID et OOP . Bravo.
Adam Naylor