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?
fuente
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 undelegate
C #. 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.fuente