Como posso projetar vários tipos de ataque diferentes que podem ser combinados?

17

Estou fazendo um jogo 2D de cima para baixo e quero ter vários tipos de ataque diferentes. Eu gostaria de tornar os ataques muito flexíveis e combináveis ​​da maneira que The Binding of Isaac funciona. Aqui está uma lista de todos os itens colecionáveis ​​do jogo . Para encontrar um bom exemplo, vejamos o item Spoon Bender .

Spoon Bender dá a Isaac a capacidade de atirar em lágrimas.

Se você olhar a seção "sinergias", verá que ela pode ser combinada com outros itens colecionáveis ​​para obter efeitos interessantes, porém intuitivos. Por exemplo, se combinar com o Olho Interior , "permitirá que Isaac dê vários tiros de volta ao mesmo tempo". Isso faz sentido, porque The Inner Eye

Dá um tiro triplo a Isaac

O que é uma boa arquitetura para projetar coisas assim? Aqui está uma solução de força 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 ...

Mas isso ficará fora de controle muito rápido. Qual é a melhor maneira de projetar um sistema como esse?

Daniel Kaplan
fonte
Pessoalmente, eu apenas manteria todos os itens equipados em uma lista e os implementaria algum tipo de interface comum que leva um objeto que você muda de objeto para objeto. "para cada item, modifique o ataque planejado", para que um item possa duplicar a quantidade de projéteis, adicione um tom e altere o dano (por isso, se um item fizer parafusos vermelhos e outro amarelo, você terá um ataque laranja depois que ambos modificarem o ataque) . Você também pode ter apenas uma classe de item genérica que possui parâmetros para decidir como ela modifica o ataque planejado.
Benjamin Danger Johnson
2
Eu gostaria de salientar que Kirby 64 adotou a abordagem da força bruta e programou diferentes efeitos para todas as combinações possíveis de habilidades, por isso é factível.
Kevin - Restabelece Monica

Respostas:

16

Você não precisa totalmente de combinações de códigos de mão. Em vez disso, você pode se concentrar nas propriedades que cada item fornece. Por exemplo, o item A é definido Projectile=Fireball,Targetting=Homing. Item B conjuntos FireMode=ArcShot,Count=3. A ArcShotlógica é responsável por enviar o Countnúmero de Projectileitens em um arco.

Esses dois itens podem ser combinados com outros itens que modificam essas (ou outras) propriedades livremente. Se você adicionar um novo tipo de projétil, ele funcionará automaticamente ArcShote, se você adicionar um novo modo de disparo, ele funcionará automaticamente com Fireballprojéteis. Da mesma forma, Targettingé uma propriedade que define o controlador para os projéteis enquanto FireModecria os projéteis, para que eles possam ser combinados de maneira fácil e trivial em qualquer combinação que faça sentido.

Você também pode definir dependências de propriedade e tal. Por exemplo, ArcShotrequer que você tenha um provedor de Projectile(que pode ser apenas o padrão). Você pode definir prioridades para que, se você tiver dois itens ativos, ambos forneçam Projectileo código, saiba qual deles usar. Ou você pode fornecer uma interface do usuário para permitir que o usuário selecione qual tipo de projétil usar, ou simplesmente exigir que o jogador desequipar itens de alta prioridade que ele não deseja, ou usar o item mais recente etc. Você também pode permitir um sistema de incompatibilidades , por exemplo, para que dois itens que ambos modificam Projectilenão possam ser equipados simultaneamente.

Em geral, quando possível, prefira qualquer tipo de abordagem orientada a dados (ou declarativa ) do que as abordagens procedurais (o grande se-else mexe) quando se trata de objetos e coisas do seu jogo. A lógica genérica de nível superior configurável por dados simples é muito preferível a listas codificadas de regras de casos especiais.

Sean Middleditch
fonte
Nit pequeno, mas você não parece ter as propriedades corretas para os exemplos que está usando. "Spoon Bender" adiciona a posição inicial e "Inner Eye" adiciona tiro triplo. Nem adicione arco e ambos são lágrimas. Se você estiver usando propriedades arbitrárias em seu exemplo para abstrair o design, seria mais fácil ler se elas não fossem nomeadas de maneiras enganosas. Eu preferiria "Item A" e "Item B" a isso.
precisa
1
@tieTYT: generalizei os nomes e expandi o exemplo para incluir modos de direcionamento, além de tipos de projéteis e modos de disparo. Nunca joguei BoI por mais de alguns minutos, então não tenho os nomes tão internalizados quanto os outros. :)
Sean Middleditch 11/11
Parece-me que a parte complicada é identificar as categorias de propriedades. por exemplo: segmentação é uma, FireMode é outra, Count pode ser um terceiro ... ou não. Talvez 3 projéteis possam ser uma bola de fogo e 5 possam ser granadas, mesmo que seja uma arma.
21413 Daniel Kaplan
@tieTYT: absolutamente. Você pode estender o sistema ainda mais para permitir combinações especiais ou lógica, certamente, mas essa deve ser a exceção e não a regra. Otimize para o caso comum, não o canto.
Sean Middleditch
Aqui está um ótimo vídeo sobre por que você está errado em relação à programação procedural youtu.be/QM1iUe6IofM , também a maneira como você descreve os mapas mapeia qualquer bloco if / else em conjuntos de dados individuais, basicamente não fazendo nada para reduzir quantos cenários de casos especiais você precisa. programa, como você tem que programar todos eles ...
RenaissanceProgrammer
8

Se você estiver usando uma linguagem OOP, isso parece um bom lugar para empregar o Padrão Decorador . Quando você quiser modificar como um ataque ocorre, decore-o com o aumento apropriado.

Exemplo bruto de c ++:

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);
}

Esse método seria melhor se você tiver um número muito grande de ataques e precisar que todos eles se comportem da mesma maneira. Se você quiser alterar substancialmente a maneira como o ataque acontece com o modificador (por exemplo, nova animação com modificador), esse método não é para você.

Milha náutica
fonte
Se eu quiser mudar a animação, por que isso não é para mim? Além disso, e se eu estiver trabalhando em uma linguagem mais funcional? Você conhece um padrão de design adequado para eles?
Daniel Kaplan
É mais rígido porque o modificador sempre acaba chamando o Attackmétodo do objeto que agrega. A TripleAttackturma não deve saber sobre a TearAttackturma. Se isso fosse verdade, levaria a tantas dores de cabeça quanto o else-ifbloqueio. Isso significa que qualquer animação de lágrima precisa residir dentro do TearAttackBehaviourobjeto. Este objeto não (e não deveria) saber que foi decorado por um TripleAttackobjeto. O resultado é que as três animações de lágrima procedem independentemente, porque são independentes.
NauticalMile
Estou tendo dificuldade em explicar isso com palavras, se alguém quiser dar uma facada, fique à vontade.
NauticalMile
Quanto a implementar isso em uma linguagem mais funcional, vou pensar um pouco e alterar minha resposta quando estiver pronto.
NauticalMile
1

Como fã de Binding of Isaac, eu também me perguntei como fazer algo assim. O sistema no jogo é robusto o suficiente, onde comportamentos emergentes surgem da combinação de efeitos (o que me vem à mente é obter espelho, dobrador de colher e alguns reforçadores de alcance resultam em uma parede de lágrimas em volta de Isaac, estilo Magneto ) O grande número deles tornaria um bloco "se" impraticável.

Minha conclusão é que Isaac e suas lágrimas são duas entidades no centro de uma enorme estrutura componente-entidade . As entidades têm algumas estatísticas básicas (velocidade de movimento, vida, alcance, dano, sprite, etc.) e cada componente traria um modificador de estatística e um verbo.

No código, Isaac e suas lágrimas teriam uma lista que conteria coisas de uma interface. Isaac teria uma lista de coisas que assinam a interface do IsaacMutator, e suas lágrimas rasgam oMutator. IsaacMutator teria funções para modificar a saúde, velocidade, alcance, aparência e algum verbo especial do Isaacs. O TearMutator seria semelhante. Uma vez por loop de jogo, Isaac passaria por todos os IsaacMutators que ele possui, e todas as lágrimas vivas também. Para seguir seu exemplo em inglês, seria como:

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

e assim por diante. Como os tipos são aditivos, você pode empilhar, adicionar e remover o conteúdo do seu coração.

Kirbinator
fonte
-5

Eu acho que seu caminho funciona melhor. Cada um desses tipos de itens fornece uma condição; se usados ​​juntos, produzem uma condição diferente, você precisaria efetivamente das três condições possíveis definidas.

Você também pode criar um novo tipo de definição quando os dois itens estiverem presentes, mas isso realmente aumenta a convolução:

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

if spoon bender inner eye then ...
RenaissanceProgrammer
fonte