¿Cómo puedo diseñar muchos tipos de ataque diferentes que se pueden combinar?

17

Estoy haciendo un juego 2D de arriba hacia abajo y quiero tener muchos tipos de ataque diferentes. Me gustaría hacer que los ataques sean muy flexibles y combinables como funciona The Binding of Isaac. Aquí hay una lista de todos los coleccionables del juego . Para encontrar un buen ejemplo, echemos un vistazo al artículo de Spoon Bender .

Spoon Bender le da a Isaac la capacidad de disparar lágrimas de referencia.

Si observa la sección "sinergias", verá que se puede combinar con otros objetos coleccionables para obtener efectos interesantes pero intuitivos. Por ejemplo, si se combina con The Inner Eye , "le permitirá a Isaac disparar múltiples tiros de referencia al mismo tiempo". Esto tiene sentido, porque el ojo interno

Le da a Isaac un triple disparo

¿Qué es una buena arquitectura para diseñar cosas como esta? Aquí hay una solución de fuerza bruta:

if not spoon bender and not the inner eye then ...
if spoon bender and not the inner eye then ...
if not spoon bender and the inner eye then ...
if spoon bender and the inner eye then ...

Pero eso se saldrá de control muy rápido. ¿Cuál es una mejor manera de diseñar un sistema como este?

Daniel Kaplan
fuente
Personalmente, simplemente mantendría todos los elementos equipados en una lista y les pediría que implementaran algún tipo de interfaz común que lleve un objeto que mute de un objeto a otro. "para cada elemento, modificar el ataque planificado" para que un elemento pueda duplicar la cantidad de proyectiles, uno podría agregar su tinte y cambiar el daño (por lo tanto, si un elemento hizo pernos rojos y otro hizo amarillo, tendría un ataque naranja después de que ambos modifiquen el ataque) . También podría tener una clase de elemento genérico que tenga parámetros para decidir cómo modifica el ataque planificado.
Benjamin Danger Johnson
2
Me gustaría señalar que Kirby 64 adoptó el enfoque de fuerza bruta y programó diferentes efectos para todas las combinaciones posibles de habilidades, por lo que es factible.
Kevin

Respuestas:

16

No es necesario codificar manualmente las combinaciones. En su lugar, puede centrarse en las propiedades que le da cada elemento. Por ejemplo, el artículo A establece Projectile=Fireball,Targetting=Homing. Artículo B establece FireMode=ArcShot,Count=3. La ArcShotlógica es responsable de enviar el Countnúmero deProjectile elementos en un arco.

Estos dos elementos se pueden combinar con cualquier otro elemento que modifique estas (u otras) propiedades libremente. Si agrega un nuevo tipo de proyectil, simplemente funcionará automáticamente conArcShot , y si agrega un nuevo modo de disparo, funcionará automáticamente con Fireballproyectiles. Del mismo modo, Targettinges una propiedad que establece el controlador para los proyectiles mientrasFireMode crea los proyectiles, por lo que pueden combinarse fácil y trivialmente en cualquier combinación, según sea conveniente.

También puede establecer un conjunto de dependencias de propiedad y tal. Por ejemplo,ArcShot requiere que tenga un proveedor de Projectile(que podría ser el predeterminado). Puede establecer prioridades para que si tiene dos elementos activos que proporcionan Projectileel código sepa cuál usar. O puede proporcionar la interfaz de usuario para permitir que el usuario seleccione qué tipo de proyectil usar, o simplemente requerir que el jugador desequipee los elementos de alta prioridad que no quiere, o use el elemento más reciente, etc. También puede permitir un sistema de incompatibilidades , por ejemplo, de modo que dos elementos que ambos simplemente modifican Projectileno puedan equiparse simultáneamente.

En general, cuando sea posible, prefiera cualquier tipo de enfoque basado en datos (o declarativo ) sobre enfoques de procedimiento (el gran lío si no es así) cuando se trata de los objetos y demás en su juego. La lógica genérica de nivel superior que es configurable por datos simples es mucho más preferible que las listas codificadas de reglas especiales.

Sean Middleditch
fuente
Pequeño error, pero no parece tener las propiedades correctas para los ejemplos que está utilizando. "Spoon Bender" agrega orientación y "Inner Eye" agrega triple tiro. Ninguno agrega arco y ambos son lágrimas. Si está utilizando propiedades arbitrarias en su ejemplo para abstraer el diseño, sería más fácil de leer si no se nombraran de manera engañosa. Preferiría "Artículo A" y "Artículo B" a esto.
Daniel Kaplan
1
@tieTYT: generalicé los nombres y amplié el ejemplo para incluir modos de orientación además de tipos de proyectiles y modos de disparo. Nunca jugué BoI durante más de unos minutos, así que no tengo los nombres tan internalizados como otros podrían. :)
Sean Middleditch
Me parece que la parte difícil es identificar las categorías de propiedades. Por ejemplo: apuntar es uno, FireMode es otro, Count puede ser un tercero ... o no. Tal vez 3 proyectiles podrían ser una bola de fuego y 5 podrían ser granadas aunque sea un arma.
Daniel Kaplan
@tieTYT: absolutamente. Puede extender el sistema aún más para permitir combinaciones especiales o lógica, sin duda, pero esa debería ser la excepción y no la regla. Optimice para el caso común, no para el caso de la esquina.
Sean Middleditch
Aquí hay un gran video sobre por qué está equivocado con respecto a la programación de procedimientos youtu.be/QM1iUe6IofM , también la forma impulsada por los datos que describe asigna cualquier bloque if / else en conjuntos de datos individuales, esencialmente no hace nada para reducir cuántos escenarios de casos especiales tiene que programa, ya que tienes que programarlos a todos ...
RenaissanceProgrammer
8

Si está utilizando un lenguaje OOP, este suena como un buen lugar para emplear el Patrón Decorador . Cuando desee modificar cómo ocurre un ataque, simplemente decórelo con el aumento apropiado.

Crudo c ++ Ejemplo:

class AttackBehaviour
{
    /* other code */
    virtual void Attack(double angle);
};

class TearAttack: public AttackBehaviour
{
    /* other code */
    void Attack(double angle);
};

class TripleAttack: public AttackBehaviour
{
    /* other code */
    AttackBehaviour* baseAttackBehaviour;
    void Attack(double angle);
};

void TripleAttack::Attack(angle)
{
    baseAttackBehaviour->Attack(angle-30);
    baseAttackBehaviour->Attack(angle);
    baseAttackBehaviour->Attack(angle+30);
}

Este método sería mejor si tiene una gran cantidad de ataques y necesita que todos se comporten de la misma manera. Si desea cambiar sustancialmente la forma en que ocurre el ataque con el modificador (por ejemplo, nueva animación con modificador), este método no es para usted.

Milla nautica
fuente
Si quiero cambiar la animación, ¿por qué esto no es para mí? Además, ¿qué pasa si estoy trabajando en un lenguaje más funcional? ¿Conoces un patrón de diseño que sea adecuado para ellos?
Daniel Kaplan
Es más rígido porque el modificador siempre terminará llamando al Attackmétodo del objeto que agrega. La TripleAttackclase no debe saber sobre la TearAttackclase. Si esto fuera cierto, provocaría tantos dolores de cabeza como el else-ifbloqueo. Esto significa que cualquier animación de lágrima debe residir dentro del TearAttackBehaviourobjeto. Este objeto no (y no debería) saber que ha sido decorado por un TripleAttackobjeto. El resultado es que las 3 animaciones de lágrimas proceden de forma independiente, porque son independientes.
NauticalMile
Me cuesta explicar esto con palabras, si alguien más quiere apuñalarlo, sé mi invitado.
NauticalMile
En cuanto a implementar esto en un lenguaje más funcional, lo pensaré por un tiempo y enmendaré mi respuesta cuando esté listo.
NauticalMile
1

Como fanático de Binding of Isaac, yo también me he preguntado cómo hacer algo como esto. El sistema en el juego es lo suficientemente robusto donde los comportamientos emergentes surgen de la combinación de efectos (el que me viene a la mente es obtener espejo, doblador de cuchara y algunos amplificadores de alcance dan como resultado una pared de lágrimas giratoria y giratoria alrededor de Isaac, estilo Magneto ) El gran número de ellos haría poco práctico un bloque "si".

Mi conclusión es que Isaac y sus lágrimas son dos entidades en el centro de un marco masivo de componentes y entidades. . Las entidades tienen algunas estadísticas básicas (velocidad de movimiento, vida, rango, daño, sprite, etc.) y cada componente traería consigo un modificador de estadísticas y un verbo.

En código, Isaac y sus lágrimas tendrían una lista que contendría cosas de una interfaz. Isaac tendría una lista de cosas que se suscriben a la interfaz IsaacMutator, y sus lágrimas tearMutator. IsaacMutator tendría funciones para modificar la salud, la velocidad, el alcance, el aspecto y algunos verbos especiales de Isaacs. TearMutator sería similar. Una vez por ciclo de juego, Isaac recorrería todos los IsaacMutators que tiene, y todas las lágrimas vivas también lo harían. Para seguir su ejemplo en inglés, se leería así:

Isaac has IsaacMutators:
--spoonbender which gives no stat change and: Tears are made homing
--MeatEater which give +1 health, +1 damage and: nothing
--MagicMirror which gives no stat change and: Tears are made reflecting

Tears have tearMutators:
--(depends on MeatEater) +1 damage and: nothing
--(depends on MagicMirror) no stat change and: +1 vector towards isaac
--(depnds on spoonbender) no stat change and: +1 vector towards enemytype

y así. Debido a que los tipos son aditivos, puede apilar, agregar y eliminar el contenido de su corazón.

Kirbinator
fuente
-5

Creo que tu camino funciona mejor. Este tipo de elementos dan una condición, si se usan juntos producen una condición diferente, entonces necesitaría efectivamente las 3 condiciones posibles definidas.

También podría hacerlo creando un nuevo tipo de definición cuando ambos elementos están presentes, pero esto en realidad se suma a la convolución:

if spoon bender and the inner eye then new spoon bender inner eye

if spoon bender inner eye then ...
RenaissanceProgrammer
fuente