Cómo evitar la codificación dura en los motores de juego

22

Mi pregunta no es una pregunta de codificación; Se aplica a todo el diseño del motor del juego en general.

¿Cómo se evita la codificación rígida?

Esta pregunta es mucho más profunda de lo que parece. Digamos, si quieres ejecutar un juego que carga los archivos necesarios para la operación, ¿cómo evitas decir algo como load specificfile.waden el código del motor? Además, cuando se carga el archivo, ¿cómo evita decir load aspecificmap in specificfile.wad?

Esta pregunta se aplica a casi todo el diseño del motor, y lo menos posible del motor debe estar codificado. ¿Cuál es la mejor manera de lograr eso?

Marcus Cramer
fuente

Respuestas:

42

Codificación basada en datos

Todo lo que mencionas es algo que se puede especificar en los datos. ¿Por qué estás cargando?aspecificmap ? Porque la configuración del juego dice que es el primer nivel cuando un jugador inicia un nuevo juego, o porque ese es el nombre del punto de guardado actual en el archivo de guardado del jugador que acaba de cargar, etc.

Cómo lo encuentras aspecificmap ? Porque está en un archivo de datos que enumera identificadores de mapas y sus recursos en disco.

Solo debe existir un conjunto particularmente pequeño de recursos "básicos" que sean legítimamente difíciles o imposibles de evitar. Con un poco de trabajo, esto puede limitarse a un solo nombre de activo predeterminado codificado comomain.wad o similar. Este archivo puede modificarse potencialmente en tiempo de ejecución pasando un argumento de línea de comandos al juego, también conocido como game.exe -wad mymain.wad.

Escribir código basado en datos se basa en algunos otros principios. Por ejemplo, se puede evitar que los sistemas o módulos soliciten un recurso en particular y en su lugar inviertan esas dependencias. Es decir, no DebugDrawercargue debug.fonten su código de inicialización; en cambio, tenerDebugDrawer tome un controlador de recursos en su código de inicialización. Ese identificador podría cargarse desde el archivo de configuración del juego principal.

Como ejemplos concretos de nuestra base de código, tenemos un objeto de "datos globales" que se carga desde la base de datos de recursos (que por sí solo es la./resources carpeta pero se puede sobrecargar con un argumento de línea de comandos). El ID de la base de datos de recursos de estos datos globales es el único nombre de recurso codificado necesario en la base de código (tenemos otros porque a veces los programadores se vuelven vagos, pero generalmente terminamos arreglando / eliminando esos eventualmente). Este objeto de datos global está lleno de componentes cuyo único propósito es proporcionar datos de configuración. Uno de los componentes es el componente UI Global Data, que contiene identificadores de recursos para todos los recursos principales de la IU (fuentes, archivos Flash, iconos, datos de localización, etc.) entre una serie de otros elementos de configuración. Cuando un desarrollador de IU decide cambiar el nombre del activo de IU principal de /ui/mainmenu.swfa/ui/lobby.swfsimplemente actualizan esa referencia de datos global; ningún código de motor necesita cambiar en absoluto.

Utilizamos estos datos globales para todo. Todos los personajes jugables, todos los niveles, UI, audio, activos principales, configuración de red, todo. (bueno, no todo , pero esas otras cosas son errores que se deben corregir).

Este enfoque tiene muchas otras ventajas. Por un lado, hace que el empaquetado y agrupamiento de recursos sea parte integral de todo el proceso. Las rutas de codificación dura en el motor también tienden a significar que esas mismas rutas tienen que estar codificadas en cualquier script o herramienta que empaquete los activos del juego, y esas rutas pueden salir de la sincronización. Confiando en cambio en un único activo principal y cadenas de referencia a partir de ahí, podemos construir un paquete de activos con un solo comando como bundle.exe -root config.data -out main.wady saber que incluirá todos los activos que necesitamos. Además, dado que el paquete solo estaría siguiendo referencias de recursos, sabemos que incluirá solo los activos que requerimos y omitirá toda la pelusa sobrante que inevitablemente se acumula durante la vida de un proyecto (además, podemos generar automáticamente listas de eso pelusa para poda).

Un caso complicado de todo esto está en los guiones. Hacer que el motor se base en datos es fácil desde el punto de vista conceptual, pero he visto muchos proyectos (afición a AAA) en los que los scripts se consideran datos y, por lo tanto, se les "permite" usar rutas de recursos de manera indiscriminada. No hagas eso. Si un archivo Lua necesita un recurso y solo llama a una función como textures.lua("/path/to/texture.png")entonces, la canalización de activos tendrá muchos problemas para saber que el script requiere /path/to/texture.pngfuncionar correctamente y podría considerar que esa textura no se usa y es innecesaria. Las secuencias de comandos deben tratarse como cualquier otro código: cualquier dato que necesiten, incluidos los recursos o las tablas, debe especificarse en una entrada de configuración que el motor y la canalización de recursos puedan inspeccionar en busca de dependencias. Los datos que dicen "script de carga foo.lua" deberían decir "foo.luay darle estos parámetros "donde los parámetros incluyen los recursos necesarios. Si un script genera enemigos aleatoriamente, por ejemplo, pasa la lista de posibles enemigos al script desde ese archivo de configuración. El motor puede precargar a los enemigos con el nivel ( ya que conoce la lista completa de posibles engendros) y el canal de recursos sabe agrupar a todos los enemigos con el juego (ya que están referenciados definitivamente por los datos de configuración). Si los scripts generan cadenas de nombres de ruta y simplemente llaman a una loadfunción, entonces ninguno el motor o la canalización de recursos tienen alguna forma de saber específicamente qué activos puede intentar cargar el script.

Sean Middleditch
fuente
¡Buena respuesta, muy práctica, y también explica las trampas y errores que cometen las personas al implementar esto! +1
2017
+1. Agregaría que seguir el patrón de apuntar a recursos que contienen datos de configuración también es muy útil si desea habilitar la modificación. Es muchísimo más difícil y arriesgado modificar juegos que requieren que cambies los archivos de datos originales en lugar de crear los tuyos propios y señalarlos. Aún mejor si puede apuntar a múltiples archivos con un orden de prioridad definido.
Jeutnarg
12

De la misma forma que evita la codificación en funciones generales.

Pasa parámetros y mantiene su información en archivos de configuración.

En esa situación, no hay absolutamente ninguna diferencia en la ingeniería de software entre escribir un motor y escribir una clase.

MgrAssets
public:
  errorCode loadAssetFromDisk( filePath )
  errorCode getMap( mapName, map& )

private:
  maps[name, map]

Luego, su código de cliente lee un archivo de configuración "maestro" ( este está codificado o se pasa como un argumento de línea de comando) que contiene la información que indica dónde están los archivos de activos y qué mapa contienen.

A partir de ahí, todo está controlado por el archivo de configuración "maestro".

Vaillancourt
fuente
1
Sí, eso más algún tipo de mecanismo para incorporar la lógica personalizada. Funcionalidad podría ser mediante la incorporación de un lenguaje como C #, Python etc. con el fin de extender las características principales del motor por el usuario definido
qCring
3

Me gustan las otras respuestas, así que voy a ser un poco contrario. ;)

No puede evitar codificar el conocimiento sobre sus datos en su motor. De donde sea que provenga la información, el motor debe saber buscarla. Sin embargo, puede evitar codificar la información real en su motor.

Un enfoque "puro" basado en datos le permitiría iniciar el ejecutable con los parámetros de línea de comando necesarios para que cargue la configuración inicial, pero el motor tendrá que estar codificado para saber cómo interpretar esa información. Por ejemplo, si sus archivos de configuración son JSON, debe codificar las variables que busca, por ejemplo, el motor tendrá que saber buscar "intro_movies"y"level_list" etcétera.

Sin embargo, un motor "bien construido" puede funcionar para muchos juegos diferentes simplemente intercambiando los datos de configuración y los datos a los que hace referencia.

Por lo tanto, el mantra no es tanto para evitar la codificación rígida, sino para garantizar que pueda realizar cambios con la menor cantidad de esfuerzo posible.

Para contrastar con el enfoque de los archivos de datos (que apoyo de todo corazón), puede ser que esté bien compilando los datos en su motor. Si el "costo" de hacerlo es menor, entonces no hay daño real; Si usted es el único que trabaja en ello, puede diferir el manejo de archivos para una fecha posterior y no necesariamente atornillarse. Mis primeros proyectos de juego tenían grandes tablas de datos codificados en el juego en sí, por ejemplo, una lista de armas y su variedad de datos:

struct Weapon
{
    enum IconID icon;
    enum ModelID model;
    int damage;
    int rateOfFire;
    // etc...
};

const struct Weapon g_weapons[] =
{
    { ICON_PISTOL, MODEL_PISTOL, 5, 6 },
    { ICON_RIFLE, MODEL_RIFLE, 10, 20 },
    // etc...
};

Por lo tanto, coloca estos datos en un lugar fácil de referencia y es fácil de editar según sea necesario. Lo ideal sería poner estas cosas en un archivo de configuración de algún tipo, pero luego debe analizar y traducir todo ese jazz, además de conectar referencias entre estructuras puede convertirse en un dolor adicional que realmente no desea tratar con.

dash-tom-bang
fuente
No es terriblemente difícil analizar a json. El único "costo" involucrado es el aprendizaje. (Específicamente, aprender a usar el módulo o biblioteca apropiados. Go tiene un buen soporte de json, por ejemplo).
Comodín
No es terriblemente difícil, pero requiere hacerlo más allá del simple aprendizaje. Por ejemplo, sé cómo analizar JSON técnicamente, he escrito analizadores para muchos otros formatos de archivo, pero necesito encontrar e instalar una solución de terceros (y descubrir dependencias y cómo construirla) o crear la mía. Toma más tiempo que no hacerlo.
dash-tom-bang
44
Todo lleva más tiempo que no hacerlo. Pero las herramientas que necesita ya están escritas. Al igual que no tienes que diseñar un compilador para escribir un juego o jugar con el código de la máquina, pero sí tienes que aprender un idioma para la plataforma con la que estás trabajando. Entonces, aprenda a usar un analizador json también.
Comodín el
No estoy seguro de cuál es tu argumento. En esta respuesta estoy abogando por YAGNI; Si no necesita gastar / perder el tiempo haciendo algo que no lo ayudará, entonces no lo haga. Si quieres pasar tiempo en eso, genial. Tal vez tengas que pasar el tiempo más tarde, tal vez no, pero hacerlo por adelantado solo te distrae de la tarea de hacer el juego. El desarrollo del juego es trivial; Cada tarea que se necesita para crear un juego es simple. Es solo que la mayoría de los juegos tienen un millón de tareas simples y un desarrollador responsable elige las que alcanzan ese objetivo más rápido.
dash-tom-bang
2
En realidad, voté tu respuesta; sin argumento real como tal. Solo quería señalar que JSON no es difícil de analizar. Leyendo de nuevo, supongo que estaba respondiendo principalmente al fragmento "pero luego debes analizar y traducir todo ese jazz". Pero estoy de acuerdo en que para juegos de proyectos personales y tal, YAGNI. :)
Comodín del