¿Cómo debo estructurar un sistema de carga de activos extensible?

19

Para un motor de juegos de hobby en Java, quiero codificar un administrador de recursos / recursos simple pero flexible. Los activos son sonidos, imágenes, animaciones, modelos, texturas, etc. Después de algunas horas de navegación y algunos experimentos de código, todavía no estoy seguro de cómo diseñar esto.

Específicamente, estoy buscando cómo puedo diseñar el administrador de una manera que abstraiga cómo se cargan los tipos de activos específicos y desde dónde se cargan los activos. Me gustaría poder soportar tanto el sistema de archivos como el almacenamiento RDBMS sin que el resto del programa necesite saberlo. Del mismo modo, me gustaría agregar un activo de descripción de animación (FPS, marcos para renderizar, referencia a la imagen del sprite, etc.) que es XML. Debería poder escribir una clase para esto con la funcionalidad de buscar y leer un archivo XML y crear y devolver una AnimationAssetclase con esa información. Estoy buscando un diseño basado en datos .

Puedo encontrar mucha información sobre lo que debe hacer un administrador de activos, pero no sobre cómo hacerlo. Los genéricos involucrados parecen dar lugar a alguna forma de cascada de clases, o alguna forma de clases auxiliares. Sin embargo, no he visto un ejemplo claro que no parezca un truco personal o un punto de consenso.

usuario8363
fuente

Respuestas:

23

Comenzaría por no pensar en un administrador de activos . Pensar en su arquitectura en términos poco definidos (como "gerente") tiende a permitirle barrer mentalmente muchos detalles debajo de la alfombra y, en consecuencia, se hace más difícil decidirse por una solución.

Concéntrese en sus necesidades específicas, lo que parece tener que ver con la creación de un mecanismo de carga de recursos que abstraiga el almacenamiento de origen subyacente y permita la extensibilidad del conjunto de tipos admitidos. No hay nada realmente en su pregunta sobre, por ejemplo, el almacenamiento en caché de recursos ya cargados, lo cual está bien, porque de acuerdo con el principio de responsabilidad única , probablemente debería construir una caché de activos como una entidad separada y agregar las dos interfaces en otro lugar , según sea apropiado.

Para abordar su inquietud específica, debe diseñar su cargador de modo que no cargue ningún activo en sí mismo, sino que delegue esa responsabilidad a las interfaces personalizadas para cargar tipos específicos de activos. Por ejemplo:

interface ITypeLoader {
  object Load (Stream assetStream);
}

Puede crear nuevas clases que implementen esta interfaz, y cada nueva clase se adaptará para cargar un tipo específico de datos desde una secuencia. Al usar una secuencia, el cargador de tipos se puede escribir en una interfaz común, independiente del almacenamiento, y no tiene que estar codificado para cargar desde el disco o una base de datos; esto incluso le permitiría cargar sus activos desde transmisiones de red (que puede ser muy útil para implementar la recarga en caliente de activos cuando su juego se ejecuta en una consola y sus herramientas de edición en una PC conectada a la red).

Su principal cargador de activos debe poder registrar y rastrear estos cargadores específicos de tipo:

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

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

La "clave" utilizada aquí puede ser lo que quiera, y no tiene por qué ser una cadena, pero es fácil comenzar con ellas. La clave tendrá en cuenta cómo espera que un usuario identifique un activo en particular y se utilizará para buscar el cargador apropiado. Debido a que desea ocultar el hecho de que la implementación podría estar utilizando un sistema de archivos o una base de datos, no puede hacer que los usuarios se refieran a los activos mediante una ruta de sistema de archivos o algo así.

Los usuarios deben referirse a un activo con un mínimo de información. En algunos casos, solo un nombre de archivo sería suficiente, pero he descubierto que a menudo es deseable usar un par de tipo / nombre para que todo sea muy explícito. Por lo tanto, un usuario puede referirse a una instancia con nombre de uno de sus archivos XML de animación como "AnimationXml","PlayerWalkCycle".

Aquí, AnimationXmlsería la clave con la que se registró AnimationXmlLoader, que implementa IAssetLoader. Obviamente, PlayerWalkCycleidentifica el activo específico. Dado un nombre de tipo y un nombre de recurso, su cargador de activos puede consultar en su almacenamiento persistente los bytes sin procesar de ese activo. Como buscamos la máxima generalidad aquí, puede implementar esto pasando al cargador un medio de acceso al almacenamiento cuando lo cree, lo que le permite reemplazar el medio de almacenamiento con cualquier cosa que pueda proporcionar una transmisión más adelante:

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 proveedor de flujo muy simple simplemente buscaría en un directorio raíz de activos especificado un subdirectorio llamado typey cargaría los bytes sin procesar del archivo nombrado nameen un flujo y lo devolvería.

En resumen, lo que tienes aquí es un sistema donde:

  • Hay una clase que sabe cómo leer bytes sin procesar de algún tipo de almacenamiento de fondo (un disco, una base de datos, una secuencia de red, lo que sea).
  • Hay clases que saben cómo convertir una secuencia de bytes sin procesar en un tipo específico de recurso y devolverlo.
  • Su "cargador de activos" real solo tiene una colección de los anteriores y sabe cómo canalizar la salida del proveedor de flujo en el cargador específico del tipo y así producir un activo concreto. Al exponer formas de configurar el proveedor de flujo y los cargadores específicos de tipo, tiene un sistema que los clientes (o usted mismo) pueden extender sin tener que modificar el código real del cargador de activos.

Algunas advertencias y notas finales:

  • El código anterior es básicamente C #, pero debe traducirse a casi cualquier lenguaje con un mínimo esfuerzo. Para facilitar esto, omití muchas cosas como la verificación de errores o el uso adecuado IDisposabley otros modismos que pueden no aplicarse directamente en otros idiomas. Esos se dejan como tarea para el lector.

  • Del mismo modo, devuelvo el activo concreto como se indicó objectanteriormente, pero puede usar genéricos o plantillas o lo que sea para producir un tipo de objeto más específico si lo desea (debería hacerlo, es bueno trabajar con él).

  • Como arriba, no trato con el almacenamiento en caché aquí. Sin embargo, puede agregar el almacenamiento en caché fácilmente y con el mismo tipo de generalidad y configurabilidad. Pruébalo y verás!

  • Hay muchísimas formas de hacer esto, y ciertamente no hay una sola forma o consenso, por lo que no ha podido encontrar una. Intenté proporcionar suficiente código para transmitir los puntos específicos sin convertir esta respuesta en un muro de código dolorosamente largo. Ya es extremadamente largo como es. Si tiene preguntas aclaratorias, siéntase libre de comentar o encontrarme en el chat .


fuente
1
Buena pregunta y buena respuesta que impulsa la solución no solo a un diseño basado en datos, sino también a cómo comenzar a pensar de una manera basada en datos.
Patrick Hughes
Muy buena y profunda respuesta. Me encanta cómo interpretaste mi pregunta y me dijiste exactamente lo que necesitaba saber mientras la formulaba tan mal. ¡Gracias! Por casualidad, ¿podría indicarme algunos recursos sobre Streams?
user8363
Un "flujo" es solo una secuencia (potencialmente sin final determinable) de bytes o datos. Estaba pensando específicamente de C # 's corriente , pero es probable que estés más interesado en Java del flujo de clases - a pesar de ser advertido que no sé demasiado Java, por lo que puede no ser una clase ideal para su uso.
Los flujos suelen tener estado, ya que un objeto de flujo determinado generalmente tiene una posición de lectura o escritura actual dentro del flujo, y cualquier E / S que realice en él se produce desde esa posición; es por eso que los usé como entradas a las interfaces de activos anteriores, porque esencialmente dicen "aquí hay algunos datos en bruto y dónde comenzar a leer, leer y hacer lo suyo".
Este enfoque respeta algunos de los principios centrales de SOLID y OOP . Bravo.
Adam Naylor