¿Qué puedo hacer para evitar marcas y controles únicos en todo mi código?

17

Considere un juego de cartas, como Hearthstone .

¡Hay cientos de tarjetas que hacen una gran variedad de cosas, algunas de las cuales son únicas incluso para una sola tarjeta! ¡Por ejemplo, hay una carta (llamada Nozdormu) que reduce los turnos de los jugadores a solo 15 segundos!

Cuando tienes una variedad tan amplia de efectos potenciales, ¿cómo evitas los números mágicos y los controles únicos en todo tu código? ¿Cómo se evita el método "Check_Nozdormu_In_Play" en la clase PlayerTurnTime? ¿Y cómo se puede organizar el código de modo que cuando agrega aún más efectos, no necesita refactorizar los sistemas centrales para admitir cosas que nunca antes han tenido que soportar?

Soñador Sable
fuente
¿Es esto realmente un problema de rendimiento? Quiero decir, puedes hacer una gran cantidad de cosas con CPU modernas en muy poco tiempo ...
Jari Komppa
11
¿Quién dijo algo sobre problemas de rendimiento? El principal problema que vería es la necesidad constante de ajustar todo el código cada vez que crea una nueva tarjeta.
jhocking
2
así que agregue un lenguaje de script y escriba cada tarjeta.
Jari Komppa
1
No hay tiempo para dar una respuesta adecuada, pero en lugar de tener, por ejemplo, el control Nozdormu y el ajuste de 15 segundos dentro del código de clase "PlayerTurnTime" que maneja los turnos de jugador, puede codificar la clase "PlayerTurnTime" para llamar a [class-, si lo desea ] función suministrada desde el exterior en puntos específicos. Entonces el código de la tarjeta Nozdormu (y todas las otras tarjetas que deben afectar el mismo lugar) pueden implementar una función para ese ajuste e inyectar esa función en la clase PlayerTurnTime cuando sea necesario. Puede ser útil leer sobre el patrón de estrategia y la inyección de dependencia del clásico libro Design Patterns
Peteris, del
2
En algún momento tengo que preguntarme si agregar verificaciones ad-hoc a los bits de código relevantes es la solución más simple.
user253751

Respuestas:

12

¿Ha examinado los sistemas de componentes de la entidad y las estrategias de mensajería de eventos?

Los efectos de estado deben ser componentes de algún tipo que puedan aplicar sus efectos persistentes en un método OnCreate (), expirar sus efectos en OnRemoved () y suscribirse a los mensajes de eventos del juego para aplicar efectos que ocurren como reacción a algo que sucede.

Si el efecto es persistente condicional (dura X turnos, pero se aplica solo bajo ciertas circunstancias), es posible que deba verificar esas condiciones en varias fases.

Luego, solo asegúrate de que tu juego no tenga números mágicos predeterminados también. Asegúrese de que todo lo que se pueda cambiar sea una variable basada en datos en lugar de valores predeterminados codificados con variables utilizadas para cualquier excepción.

De esta manera, nunca asumes cuál será la longitud del giro. Siempre es una variable constantemente verificada que puede modificarse por cualquier efecto y posiblemente deshacerse más tarde por el efecto cuando caduca. Nunca verifica las excepciones antes de omitir su número mágico.

RobStone
fuente
2
"Asegúrese de que todo lo que se pueda cambiar sea una variable basada en datos en lugar de valores predeterminados codificados con variables utilizadas para cualquier excepción". - Ooh, me gusta bastante eso. ¡Eso ayuda mucho, creo!
Sable Dreamer el
¿Podría elaborar sobre "aplicar sus efectos persistentes"? ¿Suscribirse a turnStarted y luego cambiar el valor de Longitud hace que el código sea indebible y, o peor aún, produce resultados inconsistentes (al interactuar entre efectos similares)?
wondra
Solo para suscriptores que asumirían un período de tiempo determinado. Tienes que modelar con cuidado. Puede ser bueno que el tiempo de turno actual sea diferente del tiempo de turno del jugador. PTT se verificaría para crear un nuevo turno. CTT podría ser verificado por tarjetas. Si un efecto aumentara el tiempo actual, la IU del temporizador debería seguir su ejemplo naturalmente si no tiene estado.
RobStone
Para responder mejor a la pregunta. Nada más almacena el tiempo de giro ni nada basado en eso. Siempre verifícalo.
RobStone
11

RobStone está en el camino correcto, pero quería dar más detalles, ya que esto es exactamente lo que hice cuando escribí Dungeon Ho !, un Roguelike que tenía un sistema de efectos muy complejo para armas y hechizos.

Cada carta debe tener un conjunto de efectos adjuntos, definidos de tal manera que pueda indicar cuál es el efecto, a qué se dirige, cómo y por cuánto tiempo. Por ejemplo, un efecto de "dañar al oponente" podría verse más o menos así;

Effect type: deal damage (enumeration, string, what-have-you)
Effect amount: 20
Source: my weapon
Target: opponent
Effect Cost: 20
Cost Type: Mana

Luego, cuando se active el efecto, haga que una rutina genérica se encargue del procesamiento del efecto. Como un idiota, utilicé una gran declaración de caso / cambio:

switch (effect_type)
{
     case DAMAGE:

     break;
}

Pero una forma mucho mejor y más modular de hacerlo es a través del polimorfismo. Cree una clase de efecto que envuelva todos estos datos, cree una subclase para cada tipo de efecto y luego haga que esa clase anule un método onExecute () específico de la clase.

class Effect
{
    Object source;
    int amount;

    public void onExecute(Object target)
    {
          // Do nothing
    }
}

class DamageEffect extends Effect
{
    public void onExecute(Object target)
    {
          target.health -= amount;
    }
}

Entonces tendríamos una clase de efecto básica, luego una clase DamageEffect con un método onExecute (), así que en nuestro código de procesamiento simplemente iríamos;

Effect effect = card.getActiveEffect();

effect.onExecute();

La forma de lidiar con saber lo que está en juego es crear un Vector / Array / lista vinculada / etc. de efectos activos (de tipo Efecto, la clase base) adjuntos a cualquier objeto (incluido el campo de juego / "juego"), por lo que en lugar de tener que verificar si un efecto particular está en juego, simplemente recorre todos los efectos adjuntos a el objeto (s) y dejarlos ejecutar. Si un efecto no está asociado a un objeto, no está en juego.

Effect effect;

for (int o = 0; o < objects.length; o++)
{
    for (int e = 0; e < objects[o].effects.length; e++)
    {
         effect = objects[o].effects[e];

         effect.onExecute();
    }
}
Sandalfoot
fuente
Así es exactamente como lo hice. La belleza aquí es que tiene esencialmente un sistema basado en datos, y puede ajustar la lógica con bastante facilidad por efecto. Por lo general, tendrá que hacer algunas comprobaciones de condición en la lógica de ejecución del efecto, pero aún es mucho más coherente ya que estas comprobaciones son solo para el efecto en cuestión.
manabreak
1

Ofreceré un puñado de sugerencias. Algunos de ellos se contradicen entre sí. Pero tal vez algunos sean útiles.

Considere listas versus banderas

Puede recorrer el mundo y marcar una bandera en cada elemento para decidir si se debe hacer la bandera. O puede mantener una lista de solo aquellos elementos que deberían hacer la bandera.

Considere listas y enumeraciones

Puede seguir agregando campos booleanos a su clase de elemento, isAThis y isAThat. O puede tener una lista de cadenas o elementos de enumeración, como {"isAThis", "isAThat"} o {IS_A_THIS, IS_A_THAT}. De esa manera, puede agregar nuevos en la enumeración (o consts de cadena) sin agregar campos. No es que haya nada realmente malo al agregar campos ...

Considerar punteros de función

En lugar de una lista de indicadores o enumeraciones, podría tener una lista de acciones para ejecutar ese elemento en diferentes contextos. (Entidad-ish ...)

Considerar objetos

Algunas personas prefieren enfoques basados ​​en datos, guiones o entidades componentes. Pero también vale la pena considerar las jerarquías de objetos anticuados. La clase base necesita aceptar las acciones, como "jugar esta carta para la fase de turno B" o lo que sea. Luego, cada tipo de tarjeta puede anular y responder según corresponda. Probablemente también haya un objeto de jugador y un objeto de juego, por lo que el juego puede hacer cosas como, si (player-> isAllowedToPlay ()) {hace el juego ...}.

Considere la capacidad de depuración

Una vez que lo bueno de una pila de campos de banderas es que puede examinar e imprimir el estado de cada elemento de la misma manera. Si el estado está representado por diferentes tipos, o bolsas de componentes, o punteros de función, o está en diferentes listas, puede que no sea suficiente solo mirar los campos del elemento. Todo son compensaciones.

Eventualmente, refactorización: considere las pruebas unitarias

No importa cuánto generalice su arquitectura, podrá imaginar cosas que no cubre. Entonces tendrás que refactorizar. Quizás un poco, quizás mucho.

Una forma de hacer esto más seguro es con un cuerpo de pruebas unitarias. De esa manera, puede estar seguro de que, aunque haya reorganizado las cosas debajo (¡tal vez por mucho!), La funcionalidad existente aún funciona. Cada prueba unitaria generalmente se ve así:

void test1()
{
   Game game;
   game.addThis();
   game.setupThat(); // use primary or backdoor API to get game to known state

   game.playCard(something something).

   int x = game.getSomeInternalState;
   assertEquals(“did it do what we wanted?”, x, 23); // fail if x isn’t 23
}

Como puede ver, mantener estables las llamadas de API de nivel superior en el juego (o jugador, tarjeta, etc.) es clave para la estrategia de prueba de la unidad.

David Van Brink
fuente
0

En lugar de pensar en cada carta individualmente, comience a pensar en términos de categorías de efectos, y las cartas contienen una o más de estas categorías. Por ejemplo, para calcular la cantidad de tiempo en un turno, puede recorrer todas las cartas en juego y verificar la categoría "manipular duración del turno" de cada carta que contiene esa categoría. Cada carta incrementa o sobrescribe la duración del turno según las reglas que hayas decidido.

Este es esencialmente un sistema de mini componentes, donde cada objeto "tarjeta" es simplemente un contenedor para un grupo de componentes de efectos.

jhocking
fuente
Dado que las tarjetas, y las tarjetas futuras también, pueden hacer casi cualquier cosa, esperaría que cada tarjeta lleve un guión. Aún así, estoy bastante seguro de que este no es un problema de rendimiento real ...
Jari Komppa
44
según los comentarios principales: Nadie (aparte de usted) ha dicho nada sobre problemas de rendimiento. En cuanto a las secuencias de comandos completas como alternativa, explique eso en una respuesta.
jhocking