Asesoramiento sobre la arquitectura del juego / patrones de diseño

16

He estado trabajando en un 2d RPG por un tiempo ahora, y me he dado cuenta de que he tomado algunas malas decisiones de diseño. Hay algunas cosas en particular que me están causando problemas, así que me preguntaba qué tipo de diseños usaron otras personas para superarlos o usarían.

Por un poco de historia, comencé a trabajar en ello en mi tiempo libre el verano pasado. Inicialmente estaba haciendo el juego en C #, pero hace unos 3 meses, decidí cambiar a C ++. Quería tener un buen manejo de C ++ ya que ha pasado un tiempo desde que lo usé mucho, y pensé que un proyecto interesante como este sería un buen motivador. He estado usando la biblioteca de impulso ampliamente y he estado usando SFML para gráficos y FMOD para audio.

Tengo un código bastante escrito, pero estoy considerando eliminarlo y comenzar de nuevo.

Estas son las principales áreas de preocupación que tengo y quería obtener algunas opiniones sobre la forma correcta en que otros las resolvieron o resolverían.

1. Dependencias cíclicas Cuando estaba haciendo el juego en C #, realmente no tenía que preocuparme por esto, ya que no es un problema allí. Pasando a C ++, esto se ha convertido en un problema bastante importante y me hizo pensar que podría haber diseñado las cosas incorrectamente. Realmente no puedo imaginar cómo desacoplar mis clases y aún así hacer que hagan lo que quiero. Aquí hay algunos ejemplos de una cadena de dependencia:

Tengo una clase de efecto de estado. La clase tiene varios métodos (Aplicar / No aplicar, Marcar, etc.) para aplicar sus efectos contra un personaje. Por ejemplo,

virtual void TickCharacter(Character::BaseCharacter* character, Battles::BattleField *field, int ticks = 1);

Estas funciones se llamarían cada vez que el personaje infligido con el efecto de estado toma un turno. Se usaría para implementar efectos como Regen, Poison, etc. Sin embargo, también introduce dependencias en la clase BaseCharacter y la clase BattleField. Naturalmente, la clase BaseCharacter necesita realizar un seguimiento de los efectos de estado que están activos actualmente en ellos, por lo que es una dependencia cíclica. Battlefield necesita hacer un seguimiento de los grupos de lucha, y la clase de grupo tiene una lista de Personajes base que presentan otra dependencia cíclica.

2 - Eventos

En C # hice un amplio uso de delegados para conectar eventos en personajes, campos de batalla, etc. (por ejemplo, había un delegado para cuando la salud del personaje cambia, cuando cambia una estadística, cuando se agrega / elimina un efecto de estado, etc. .) y el campo de batalla / componentes gráficos se conectarían a esos delegados para hacer cumplir sus efectos. En C ++, hice algo similar. Obviamente no hay un equivalente directo para los delegados de C #, así que en su lugar creé algo como esto:

typedef boost::function<void(BaseCharacter*, int oldvalue, int newvalue)> StatChangeFunction;

y en mi clase de personaje

std::map<std::string, StatChangeFunction> StatChangeEventHandlers;

cada vez que cambiaba la estadística del personaje, iteraba y llamaba a cada StatChangeFunction en el mapa. Si bien funciona, me preocupa que este sea un mal enfoque para hacer las cosas.

3 - Gráficos

Esta es la gran cosa. No está relacionado con la biblioteca de gráficos que estoy usando, pero es más una cosa conceptual. En C #, combiné gráficos con muchas de mis clases, lo que sé que es una idea terrible. Queriendo hacerlo desacoplado esta vez probé un enfoque diferente.

Para implementar mis gráficos, estaba imaginando todo lo relacionado con los gráficos en el juego como una serie de pantallas. Es decir, hay una pantalla de título, una pantalla de estado de personaje, una pantalla de mapa, una pantalla de inventario, una pantalla de batalla, una pantalla de GUI de batalla, y básicamente podría apilar estas pantallas una encima de la otra según sea necesario para crear los gráficos del juego. Cualquiera que sea la pantalla activa es propietaria de la entrada del juego.

Diseñé un administrador de pantalla que empujaría y abriría pantallas basándose en la entrada del usuario.

Por ejemplo, si estaba en una pantalla de mapa (un controlador / visualizador de entrada para un mapa de mosaico) y presionó el botón de inicio, emitiría una llamada al administrador de pantalla para presionar una pantalla del menú principal sobre la pantalla del mapa y marcar el mapa pantalla para no ser dibujada / actualizada. El jugador navegaría por el menú, que emitiría más comandos al administrador de pantalla según corresponda para insertar nuevas pantallas en la pila de pantallas, luego las abriría cuando el usuario cambie de pantalla / cancele. Finalmente, cuando el jugador sale del menú principal, lo quito y vuelvo a la pantalla del mapa, lo comenta para dibujarlo / actualizarlo y continuar desde allí.

Las pantallas de batalla serían más complejas. Tendría una pantalla para actuar como fondo, una pantalla para visualizar a cada parte en la batalla y una pantalla para visualizar la interfaz de usuario para la batalla. La interfaz de usuario se conectaría a los eventos de caracteres y los usaría para determinar cuándo actualizar / volver a dibujar los componentes de la interfaz de usuario. Finalmente, cada ataque que tenga un script de animación disponible llamaría una capa adicional para animarse antes de salir de la pila de la pantalla. En este caso, cada capa se marca constantemente como dibujable y actualizable y obtengo una pila de pantallas que manejan mis gráficos de batalla.

Si bien todavía no he podido hacer que el administrador de pantalla funcione perfectamente, creo que puedo hacerlo con algo de tiempo. Mi pregunta al respecto es, ¿es este un enfoque que vale la pena? Si es un mal diseño, quiero saber ahora antes de invertir mucho más tiempo haciendo todas las pantallas que voy a necesitar. ¿Cómo construyes los gráficos para tu juego?

usuario127817
fuente

Respuestas:

15

En general, no diría que nada de lo que haya enumerado debería hacer que descarte el sistema y comience de nuevo. Esto es algo que todo programador quiere hacer alrededor del 50-75% del camino a través de cualquier proyecto en el que esté trabajando, pero conduce a un ciclo interminable de desarrollo y nunca termina nada. Entonces, para ese fin, algunos retroalimentan cada sección.

  1. Esto puede ser un problema, pero generalmente es más una molestia que cualquier otra cosa. ¿Estás usando #pragma una vez o #ifndef MY_HEADER_FILE_H #define MY_HEADER_FILE_H ... #endif en la parte superior o alrededor de tus archivos .h respectivamente? De esta manera, ¿el archivo .h solo existe una vez dentro de cada ámbito? Si es así, mi recomendación se convierte en eliminar todas las declaraciones #include y compilar, agregando las que sean necesarias para compilar el juego nuevamente.

  2. Soy fanático de este tipo de sistemas y no veo nada de malo en ello. Lo que es un evento en C # se reemplaza comúnmente con un sistema de eventos o un sistema de mensajería (puede buscar las preguntas aquí para obtener más información). La clave aquí es mantener esto al mínimo cuando las cosas necesitan suceder, lo cual ya parece que lo está haciendo, no debería haber preocupaciones mínimas aquí.

  3. Esto también me parece el camino correcto y es lo que hago para mis propios motores, tanto personal como profesionalmente. Esto convierte el sistema de menús en un sistema de estado que tiene el menú raíz (antes de que comience el juego) o el HUD del jugador como se muestra la pantalla 'raíz', dependiendo de cómo lo configure.

En resumen, no veo que nada reinicie digno en lo que se está encontrando. Es posible que desee un reemplazo del sistema de eventos más formal en el futuro, pero eso llegará a tiempo. Cyclic incluye es un obstáculo que todos los programadores de C / C ++ tienen que atravesar constantemente, y trabajar para desacoplar los gráficos parece lógico 'los siguientes pasos'.

¡Espero que esto ayude!

James
fuente
#ifdef no ayuda a incluir problemas circulares.
El pato comunista
Solo estaba cubriendo mi base con la expectativa de que estuviera allí antes de rastrear el cíclico incluye. Puede ser otra caldera de peces cuando tiene múltiples símbolos definidos en lugar de un archivo que necesita incluir un archivo que se incluye a sí mismo. (aunque por lo que describió si las inclusiones están en los archivos .CPP y no en los archivos .H, debería estar bien con dos objetos base que se conocieran)
James
Gracias por el consejo :) Me alegra saber que estoy en el camino correcto
usuario127817
4

Sus dependencias cíclicas no deberían ser un problema siempre y cuando esté declarando hacia adelante las clases donde puede en los archivos de encabezado y realmente #incluyéndolas en los archivos .cpp (o lo que sea).

Para el sistema de eventos, dos sugerencias:

1) Si desea mantener el patrón que está usando ahora, considere cambiar a boost :: unordered_map en lugar de std :: map. El mapeo con cadenas como teclas es lento, especialmente porque .NET hace algunas cosas buenas debajo del capó para ayudar a acelerar las cosas. El uso de unordered_map mezcla las cadenas para que las comparaciones sean generalmente más rápidas.

2) Considere cambiar a algo más poderoso como boost :: señales. Si haces eso, puedes hacer cosas agradables como hacer que los objetos de tu juego sean rastreables derivando de boost :: señales :: rastreables, y dejar que el destructor se encargue de limpiar todo en lugar de tener que cancelar manualmente el registro del sistema de eventos. También puede tener múltiples señales apuntando a cada ranura (o viceversa, no recuerdo la nomenclatura exacta), por lo que es muy similar a hacerlo +=en un delegateC #. El mayor problema con las señales boost :: es que tiene que compilarse, no se trata solo de encabezados, por lo que, dependiendo de su plataforma, puede ser difícil ponerlo en funcionamiento.

Tétrada
fuente