¿Cómo diseñar un AssetManager?

26

¿Cuál es el mejor enfoque para diseñar un AssestManager que contenga referencias a gráficos, sonidos, etc. de un juego?

¿Deberían almacenarse estos activos en un par de mapa clave / valor? Es decir, solicito un activo de "fondo" y el mapa devuelve el mapa de bits asociado ¿Hay una forma aún mejor?

Específicamente, estoy escribiendo un juego para Android / Java, pero las respuestas pueden ser genéricas.

Bryan Denny
fuente

Respuestas:

16

Depende del alcance de tu juego. Un administrador de activos es absolutamente esencial para títulos más grandes, menos para juegos más pequeños.

Para títulos más grandes, debe gestionar problemas como los siguientes:

  • Activos compartidos: ¿esa textura de ladrillo está siendo utilizada por múltiples modelos?
  • Vida útil del activo: ¿ese activo que cargó hace 15 minutos ya no es necesario? Haga referencia al recuento de sus activos para asegurarse de saber cuándo se termina algo con etc.
  • En DirectX 9 si se cargan ciertos tipos de activos y su dispositivo gráfico se 'pierde' (esto sucede si presiona Ctrl + Alt + Supr, entre otras cosas): su juego necesitará recrearlos
  • Carga de activos antes de necesitarlos: no podrías construir grandes juegos de mundo abierto sin esto
  • Activos de carga masiva: a menudo empaquetamos muchos activos en un solo archivo para mejorar los tiempos de carga; buscar alrededor del disco lleva mucho tiempo

Para títulos más pequeños, estas cosas son menos problemáticas, los marcos como XNA tienen administradores de activos dentro de ellos; no tiene mucho sentido reinventarlo.

Si necesita un administrador de activos, no existe una solución única para todos, pero he encontrado un mapa de hash con la clave como hash * del nombre de archivo (se bajan y los separadores están 'arreglados') funciona bien para los proyectos en los que he trabajado.

Por lo general, no es aconsejable codificar los nombres de archivo en su aplicación, por lo general es mejor tener otro formato de datos (como xml) que represente los nombres de archivo en 'ID'.

  • Como nota al margen divertida, normalmente obtienes una colisión de hash por proyecto.
icStatic
fuente
El hecho de que necesite administrar activos no requiere AssetManagers, un sustantivo importante en mayúscula que probablemente tiene demasiados métodos, bajo rendimiento y semántica de memoria turbia. Para una comparación, piense en lo que sucede si tiene mucha gestión de proyectos (generalmente buena), y luego cuando tiene muchos gerentes de proyecto (generalmente malos).
2
@ Joe Wreschnig: ¿cómo abordaría los cinco requisitos mencionados por icStatic sin utilizar un administrador de activos?
antinome
8

(Intentando evitar la discusión "no usar un administrador de activos" aquí, ya que lo considero fuera de tema).

Un mapa clave / valor es un enfoque muy útil.

Tenemos una implementación de ResourceManager donde las fábricas para diferentes tipos de recursos pueden registrarse.

El método "getResource" usa plantillas para encontrar la Fábrica correcta para el tipo de recurso deseado y devuelve un ResourceHandle específico (nuevamente usando la plantilla para devolver un SpecificResourceHandle).

ResourceManager vuelve a contar los recursos (dentro de ResourceHandle) y los libera cuando ya no los necesita.

El primer complemento que escribimos fue el método "reload (XYZ)", que nos permite cambiar los recursos desde fuera del motor en ejecución sin cambiar ningún código o recargar el juego. (Esto es esencial cuando los artistas trabajan en consolas;))

La mayoría de las veces solo tenemos una instancia del ResourceManager, pero a veces creamos una nueva instancia solo para un nivel o un mapa. De esta manera podemos simplemente llamar "apagado" en el levelResourceManager y asegurarnos de que nada se filtre.

(breve) ejemplo

// 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
fuente
6

Las clases dedicadas de Gerente casi nunca son la herramienta de ingeniería adecuada. Si solo necesita el activo una vez (como un fondo o mapa), solo debe solicitarlo una vez y dejar que muera normalmente cuando haya terminado con él. Si necesita almacenar en caché un tipo particular de objeto, debe usar una fábrica que primero verifica un caché y, de lo contrario, carga algo, lo coloca en el caché y luego lo devuelve, y esa fábrica puede ser una función estática que accede a una variable estática , no un tipo propio.

Steve Yegge (entre muchos, muchos otros) ha escrito una buena historia sobre cómo las clases de manager inútiles, a través del patrón singleton, terminan siendo http://sites.google.com/site/steveyegge2/singleton-considered-stupid


fuente
2
Bien, seguro. Pero en casos como Android (u otros juegos) necesita cargar muchos gráficos / sonidos en la memoria antes de comenzar el juego, no durante. ¿Cómo puedo usar lo que estás diciendo (fábricas) para hacer esto durante una pantalla de carga? ¿Simplemente golpea cada objeto en la fábrica en la pantalla de carga para que los almacene en caché?
Bryan Denny
No estoy familiarizado con los detalles de Android, pero no tengo idea de lo que quieres decir con "antes de comenzar el juego". ¿Es realmente imposible cargar un recurso cuando lo necesita (o cuando lo necesite "pronto") en lugar de cuando inicia el programa? Me parece extremadamente improbable, de lo contrario, por ejemplo, nunca podría tener más texturas de las que caben en la escasa RAM de Android.
@ Joe eche un vistazo a mi otra pregunta sobre "pantallas de carga": gamedev.stackexchange.com/questions/1171/... Golpear un caché vacío significa mucho tiempo para ir al disco y podría dar lugar a algunos resultados de rendimiento de FPS en esas primeras llamadas . Si ya sabe lo que va a golpear con anticipación, también podría golpearlo durante la carga para almacenarlo en caché, ¿verdad?
Bryan Denny
Una vez más, no puedo hablar con Android, pero generalmente ir al disco es exactamente lo que puedes hacer sin recibir golpes de FPS, porque el hilo que va al disco no usará ninguna CPU. Solo necesita presupuestarlo con suficiente anticipación para que no aparezca una ventana emergente. Si va a almacenar previamente todo en caché porque sabe de antemano lo que necesita, realmente no necesita un AssetManager, porque no necesita administrar activos en absoluto, ya están a mano.
1
@ Joe, ¿no es una fábrica también un "Gerente dedicado"?
MSN
2

Siempre he pensado que un buen administrador de activos debería tener varios modos de operación. Es muy probable que estos modos sean módulos fuente separados que se adhieran a una interfaz común. Los dos modos básicos de operación serían:

  • Modo de producción: todos los activos son locales y despojados de todos los metadatos
  • Modo de desarrollo: las evaluaciones se almacenan en una base de datos (por ejemplo, MySQL, etc.) con metadatos adicionales. La base de datos sería un sistema de dos niveles con una base de datos local que almacena en caché una base de datos compartida. Los creadores de contenido podrían editar y actualizar la base de datos compartida y las actualizaciones automáticamente propagadas a los sistemas de desarrollador / control de calidad. También debería ser posible crear contenido de marcador de posición. Como todo está en una base de datos, se pueden realizar consultas en la base de datos y generar informes para analizar el estado de la producción.

Necesitaría una herramienta que pueda tomar todas las evaluaciones de la base de datos compartida y crear el conjunto de datos de producción.

En mis años como desarrollador, nunca he visto algo así, aunque solo he trabajado para un puñado de empresas, por lo que mi punto de vista no es realmente representativo.

Actualizar

OK, algunos votos negativos. Ampliaré este diseño.

En primer lugar, realmente no necesitas clases de fábrica porque si tienes:

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

sabes el tipo, así que solo hazlo:

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

pero luego, lo que estaba tratando de decir anteriormente es que no usarías nombres de archivo explícitos de todos modos, la textura para cargar estaría especificada por el modelo en el que se usa la textura, por lo que no necesitas un nombre legible por humanos, podría ser un valor entero de 32 bits, que es mucho más fácil de manejar para la CPU. Entonces, en el constructor de TextureHandle tendrías:

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 usa el parámetro resource_id para encontrar la ubicación de los datos. La forma en que lo hizo dependería del entorno en el que se esté ejecutando:

En Desarrollo: la secuencia busca la ID en una base de datos (usando SQL, por ejemplo) para obtener un nombre de archivo y luego abre el archivo, el archivo podría almacenarse en caché localmente o extraerse de un servidor si el archivo local no existe o es fuera de plazo.

En la versión: la secuencia busca la ID en una tabla de clave / valor para obtener un desplazamiento / tamaño en un archivo grande y empaquetado (como el archivo WAD de Doom).

Skizz
fuente
Lo rechacé porque sugirió calzar todo en una tabla SQL con claves primarias en lugar de usar un VCS real. También considero usar ID opacos en lugar de nombres de cadena de optimización prematura. Utilicé cadenas en dos proyectos grandes para todos los activos que no sean claves de traducción, de los cuales teníamos cientos de miles de claves de cadena muy largas (y luego solo para portar a consolas). Por lo general, se normalizaron para que pudiéramos usar comparaciones de puntero en lugar de comparaciones de cadenas, pero las comparaciones de cadenas a menudo están dominadas por el costo de la recuperación de memoria y no la comparación real de todos modos.
@ Joe: Solo di SQL como ejemplo y luego solo en un entorno de desarrollo, igualmente podría usar un VCS. Solo sugerí la base de datos SQL, ya que puede agregar información adicional a los objetos almacenados y usar las funciones SQL para consultar información de la base de datos (más una ganancia de administración que cualquier otra cosa). En cuanto a las ID opacas como la optimización prematura, supongo que algunos lo verán de esa manera, pero creo que sería más fácil comenzar con eso en lugar de mostrarlo en un momento posterior del desarrollo. No creo que afectaría mucho el desarrollo si usaras ID o cadenas.
Skizz
2

Lo que me gusta hacer por los activos es configurar un administrador global . Inspirados en el motor Doom, los grumos son datos que contienen activos, almacenados en un archivo de grumos que declara los nombres, las longitudes, el tipo (mapa de bits, sonido, sombreador, etc.) y el tipo de contenido (archivo, otro bulto, dentro el archivo global). Al inicio, estos grumos se ingresan en un árbol binario, pero aún no se cargan. Cada mapa (que también es un bulto) tiene una lista de dependencias, que son simplemente los nombres de grumos que el mapa necesita para funcionar. Estos grumos, a menos que ya se hayan cargado, se cargan en el momento en que se carga el mapa. Además, los bultos de los mapas contiguos del mapa se cargan, no solo al mismo tiempo, sino cuando el motor está inactivo por algún motivo. Esto puede hacer que los mapas sean fluidos, y no hay pantalla de carga.

Mi método es perfecto para mapas de mundo abierto, pero un juego basado en niveles no se beneficiará de la fluidez de este método. ¡Espero que esto ayude!

Marcus Cramer
fuente