Comment concevoir un AssetManager?

26

Quelle est la meilleure approche pour concevoir un AssestManager qui contiendra des références aux graphiques, aux sons, etc. d'un jeu?

Ces actifs doivent-ils être stockés dans une paire de cartes clé / valeur? C'est-à-dire que je demande un élément "d'arrière-plan" et que la carte renvoie le bitmap associé? Existe-t-il un moyen encore meilleur?

Plus précisément, j'écris un jeu Android / Java, mais les réponses peuvent être génériques.

Bryan Denny
la source

Réponses:

16

Cela dépend de l'étendue de votre jeu. Un gestionnaire d'actifs est absolument essentiel pour les grands titres, moins pour les petits jeux.

Pour les titres plus volumineux, vous devez gérer des problèmes tels que les suivants:

  • Ressources partagées - cette texture de brique est-elle utilisée par plusieurs modèles?
  • Durée de vie de l'actif - cet actif que vous avez chargé il y a 15 minutes n'est-il plus nécessaire? Référence comptant vos actifs pour vous assurer que vous savez quand quelque chose est terminé, etc.
  • Dans DirectX 9 si certains types d'actifs sont chargés et que votre périphérique graphique est 'perdu' (cela se produit si vous appuyez sur Ctrl + Alt + Suppr entre autres) - votre jeu devra les recréer
  • Chargement des actifs avant d'en avoir besoin - vous ne pourriez pas créer de grands jeux en monde ouvert sans cela
  • Chargement en masse d'actifs - Nous regroupons souvent de nombreux actifs dans un seul fichier pour améliorer les temps de chargement - la recherche autour du disque prend beaucoup de temps

Pour les petits titres, ces choses sont moins problématiques, les cadres comme XNA ont des gestionnaires d'actifs en eux - il est très inutile de le réinventer.

Si vous avez besoin d'un gestionnaire d'actifs, il n'y a pas vraiment de solution unique, mais j'ai trouvé qu'une carte de hachage avec la clé comme hachage * du nom de fichier (abaissé et séparateurs tous «fixes») fonctionne bien pour les projets sur lesquels j'ai travaillé.

Il n'est généralement pas conseillé de coder en dur les noms de fichiers dans votre application, il est généralement préférable qu'un autre format de données (tel que xml) représente les noms de fichiers en «ID».

  • Comme note latérale amusante, vous obtenez normalement une collision de hachage par projet.
icStatic
la source
Le fait que vous ayez besoin de gérer des actifs ne nécessite pas AssetManagers, un nom important capitalisé qui a probablement trop de méthodes, de mauvaises performances et une sémantique de mémoire boueuse. À titre de comparaison, pensez à ce qui se passe si vous avez beaucoup de gestion de projet (généralement bonne), puis lorsque vous avez beaucoup de chefs de projet (généralement mauvaise).
2
@Joe Wreschnig - comment répondriez-vous aux cinq exigences mentionnées par icStatic sans recourir à un gestionnaire d'actifs?
antinome
8

(Essayer d'éviter la discussion "n'utilisez pas de gestionnaire d'actifs" ici, car je considère que c'est hors sujet.)

Une carte clé / valeur est une approche très utilisable.

Nous avons une implémentation ResourceManager où les usines pour différents types de ressources peuvent s'inscrire.

La méthode "getResource" utilise des modèles pour trouver la fabrique correcte pour le type de ressources souhaité et renvoie un ResourceHandle spécifique (en utilisant à nouveau le modèle pour renvoyer un SpecificResourceHandle).

Les ressources sont recomptées par le ResourceManager (à l'intérieur du ResourceHandle) et libérées lorsqu'elles ne sont plus nécessaires.

Le premier addon que nous avons écrit était la méthode "reload (XYZ)", qui nous permet de changer les ressources de l'extérieur du moteur en cours d'exécution sans changer de code ni recharger le jeu. (Ceci est essentiel lorsque les artistes travaillent sur des consoles;))

La plupart du temps, nous n'avons que sur l'instance du ResourceManager, mais parfois nous créons une nouvelle instance juste pour un niveau ou une carte. De cette façon, nous pouvons simplement appeler «shutdown» sur le levelResourceManager et nous assurer que rien ne fuit.

(bref) exemple

// 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
Andreas
la source
6

Les classes de gestionnaire dédié ne sont presque jamais le bon outil d'ingénierie. Si vous n'avez besoin de l'actif qu'une seule fois (comme un arrière-plan ou une carte), vous ne devez le demander qu'une seule fois et le laisser mourir normalement lorsque vous en avez terminé. Si vous devez mettre en cache un type particulier d'objet, vous devez utiliser une fabrique qui vérifie d'abord une antémémoire et charge autrement quelque chose, la place dans la mémoire cache, puis la renvoie - et cette fabrique peut simplement être une fonction statique accédant à une variable statique , pas un type à part.

Steve Yegge (parmi beaucoup, beaucoup d'autres) a écrit une bonne histoire sur la façon dont les classes de manager inutiles, par le biais du modèle singleton, finissent par être. http://sites.google.com/site/steveyegge2/singleton-considered-stupid


la source
2
D'accord, bien sûr. Mais dans des cas comme Android (ou d'autres jeux), vous devez charger de nombreux graphiques / sons en mémoire avant de commencer le jeu, pas pendant. Comment puis-je utiliser ce que vous dites (usines) pour le faire pendant un écran de chargement? Il suffit de frapper chaque objet en usine sur l'écran de chargement pour qu'il les mette en cache?
Bryan Denny
Je ne connais pas les détails d'Android mais je n'ai aucune idée de ce que vous entendez par "avant de commencer le jeu". Est-il vraiment impossible de charger une ressource lorsque vous en avez besoin (ou quand vous en aurez besoin «bientôt») plutôt que lorsque vous démarrez le programme? Je trouve cela extrêmement improbable, sinon, par exemple, vous ne pourriez jamais avoir plus de textures que dans la maigre RAM d'Android.
@Joe jetez un œil à mon autre question sur les "écrans de chargement": gamedev.stackexchange.com/questions/1171/… Atteindre un cache vide signifie de longs délais pour aller sur le disque et pourrait entraîner des résultats de performances FPS lors de ces premiers appels . Si vous savez déjà ce que vous allez frapper à l'avance, autant le frapper pendant le chargement pour le pré-mettre en cache, non?
Bryan Denny
Encore une fois, je ne peux pas parler à Android, mais en général, aller sur le disque est exactement ce que vous pouvez faire sans prendre de coups FPS, car le thread qui va sur le disque n'utilisera aucun processeur. Il vous suffit de prévoir un budget suffisamment à l'avance pour ne pas avoir de pop-in. Si vous allez tout mettre en antémémoire parce que vous savez à l'avance ce dont vous avez besoin, vous n'avez vraiment pas besoin d'un AssetManager, car vous n'avez pas du tout besoin de gérer les actifs - ils sont déjà tous à portée de main.
1
@Joe, une usine n'est-elle pas également un "gestionnaire dédié"?
MSN
2

J'ai toujours pensé qu'un bon gestionnaire d'actifs devrait avoir plusieurs modes de fonctionnement. Ces modes seraient très probablement des modules source distincts adhérant à une interface commune. Les deux modes de fonctionnement de base seraient:

  • Mode de production - tous les actifs sont locaux et dépouillés de toutes les métadonnées
  • Mode de développement - les tests sont stockés dans une base de données (par exemple MySQL, etc.) avec des métadonnées supplémentaires. La base de données serait un système à deux niveaux avec une base de données locale mettant en cache une base de données partagée. Les créateurs de contenu pourraient éditer et mettre à jour la base de données partagée et les mises à jour automatiquement propagées aux systèmes développeur / AQ. Il devrait également être possible de créer du contenu d'espace réservé. Puisque tout est dans une base de données, des requêtes peuvent être faites sur la base de données et des rapports générés pour analyser l'état de la production.

Vous auriez besoin d'un outil qui peut récupérer toutes les analyses de la base de données partagée et créer l'ensemble de données de production.

Pendant mes années en tant que développeur, je n'ai jamais rien vu de tel, même si je n'ai travaillé que pour une poignée d'entreprises, mon point de vue n'est donc pas vraiment représentatif.

Mise à jour

OK, quelques votes négatifs. Je développerai cette conception.

Tout d'abord, vous n'avez pas vraiment besoin de classes d'usine car si vous avez:

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

vous connaissez le type, alors faites simplement:

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

mais ensuite, ce que j'essayais de dire ci-dessus, c'est que vous n'utiliseriez pas de toute façon des noms de fichiers explicites, la texture à charger serait spécifiée par le modèle sur lequel la texture est utilisée, donc vous n'avez pas réellement besoin d'un nom lisible par l'homme, il peut s'agir d'une valeur entière de 32 bits, ce qui est beaucoup plus facile à gérer pour le processeur. Ainsi, dans le constructeur de TextureHandle, vous auriez:

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 utilise le paramètre resource_id pour trouver l'emplacement des données. La façon dont cela a été fait dépendrait de l'environnement dans lequel vous exécutez:

En développement: le flux recherche l'ID dans une base de données (en utilisant SQL par exemple) pour obtenir un nom de fichier puis ouvre le fichier, le fichier peut être mis en cache localement, ou extrait d'un serveur si le fichier local n'existe pas ou est périmé.

Dans la version: le flux recherche l'ID dans une table clé / valeur pour obtenir un décalage / taille dans un grand fichier compressé (comme le fichier WAD de Doom).

Skizz
la source
Je vous ai rejeté parce que vous avez suggéré de tout ranger dans une table SQL avec des clés primaires plutôt que d'utiliser un vrai VCS. J'envisage également d'utiliser des ID opaques plutôt qu'une optimisation prématurée des noms de chaîne. J'ai utilisé des chaînes sur deux grands projets pour tous les actifs autres que les clés de traduction, dont nous avions des centaines de milliers de clés de chaîne très longues (et ensuite uniquement à porter sur des consoles). Ils étaient généralement normalisés afin que nous puissions utiliser des comparaisons de pointeurs plutôt que des comparaisons de chaînes, mais les comparaisons de chaînes sont souvent dominées par le coût de l'extraction de la mémoire et non par la comparaison réelle de toute façon.
@Joe: Je n'ai donné que SQL à titre d'exemple et seulement dans un environnement de développement, vous pouvez également utiliser un VCS. Je n'ai suggéré que la base de données SQL car vous pouvez ensuite ajouter des informations supplémentaires aux objets stockés et utiliser les fonctions SQL pour interroger les informations de la base de données (plus un gain de gestion qu'autre chose). Quant aux identifiants opaques comme optimisation prématurée - certains pourraient le voir de cette façon, je suppose, mais je pense qu'il serait plus facile de commencer avec cela plutôt que de le montrer à un stade ultérieur du développement. Je ne pense pas que cela affecterait beaucoup le développement si vous utilisiez des ID ou des chaînes.
Skizz
2

Ce que j'aime faire pour les actifs, c'est mettre en place un gestionnaire forfaitaire . Inspirés par le moteur Doom, les blocs sont des éléments de données qui contiennent des actifs, stockés dans un fichier forfaitaire qui déclare les noms des blocs, les longueurs, le type (bitmap, son, shader, etc.) et le type de contenu (fichier, un autre bloc, à l'intérieur le fichier forfaitaire lui-même). Au démarrage, ces grumeaux sont entrés dans un arbre binaire, mais pas encore chargés. Chaque carte (qui est également un bloc) a une liste de dépendances, qui sont simplement les noms des blocs dont la carte a besoin pour fonctionner. Ces morceaux, à moins qu'ils n'aient déjà été chargés, sont chargés au moment du chargement de la carte. De plus, les morceaux des cartes adjacentes de la carte sont chargés, mais pas en même temps, mais lorsque le moteur tourne au ralenti pour une raison quelconque. Cela peut rendre les cartes transparentes et il n'y a pas d'écran de chargement.

Ma méthode est parfaite pour les cartes du monde ouvert, mais un jeu basé sur le niveau ne bénéficiera pas de la fluidité de cette méthode. J'espère que cela t'aides!

Marcus Cramer
la source