O que posso fazer para evitar sinalizações e verificações pontuais em todo o meu código?

17

Considere um jogo de cartas, como Hearthstone .

Existem centenas de cartões que fazem uma grande variedade de coisas, algumas das quais são únicas até para um único cartão! Por exemplo, existe um cartão (chamado Nozdormu) que reduz o turno do jogador para apenas 15 segundos!

Quando você tem uma variedade tão grande de efeitos em potencial, como evita números mágicos e verificações pontuais em todo o seu código? Como evitar um método "Check_Nozdormu_In_Play" na classe PlayerTurnTime? E como organizar o código de forma que, quando você adiciona ainda mais efeitos, não precise refatorar os sistemas principais para dar suporte a coisas que eles nunca tiveram que suportar antes?

Sonhador Sable
fonte
Isso é realmente um problema de desempenho? Quero dizer, você pode fazer quantidade louca de coisas com CPUs modernas em quase nenhum momento ..
Jari Komppa
11
Quem disse algo sobre problemas de desempenho? O principal problema que eu vejo é a necessidade constante de ajustar todo o seu código toda vez que você cria um novo cartão.
Jhocking 03/08/2015
2
adicione uma linguagem de script e escreva cada cartão.
Jari Komppa
1
Não há tempo para dar uma resposta adequada, mas, em vez de ter, por exemplo, verificação Nozdormu e ajuste de 15 segundos dentro do código de classe "PlayerTurnTime" que lida com turnos de jogador, você pode codificar a classe "PlayerTurnTime" para chamar uma [class-, se desejar ] função fornecida de fora em pontos específicos. O código do cartão Nozdormu (e todos os outros cartões que precisam afetar a mesma posição) pode implementar uma função para esse ajuste e injetar essa função na classe PlayerTurnTime quando necessário. Pode ser útil para ler sobre padrão de estratégia e injeção de dependência dos Design Patterns livro clássico
Peteris
2
Em algum momento, tenho que me perguntar se a adição de verificações ad-hoc aos bits relevantes de código é a solução mais simples.
user253751

Respostas:

12

Você analisou os sistemas de componentes da entidade e as estratégias de mensagens de eventos?

Os efeitos de status devem ser componentes de algum tipo que possam aplicar seus efeitos persistentes em um método OnCreate (), expirar seus efeitos em OnRemoved () e assinar mensagens de eventos do jogo para aplicar efeitos que ocorrem como uma reação a algo acontecendo.

Se o efeito for persistentemente condicional (dura X turnos, mas se aplica apenas em determinadas circunstâncias), talvez seja necessário verificar essas condições em várias fases.

Então, apenas verifique se o seu jogo também não possui números mágicos padrão. Certifique-se de que tudo o que pode ser alterado seja uma variável orientada a dados, em vez de padrões codificados com variáveis ​​usadas para quaisquer exceções.

Dessa forma, você nunca assume qual será o comprimento do turno. É sempre uma variável constantemente verificada que pode ser alterada por qualquer efeito e possivelmente desfeita posteriormente pelo efeito quando ela expira. Você nunca verifica exceções antes de usar o número mágico como padrão.

RobStone
fonte
2
"Verifique se tudo o que pode ser alterado é uma variável orientada a dados, em vez de padrões codificados com variáveis ​​usadas para quaisquer exceções." - Eu gosto bastante disso. Isso ajuda muito, eu acho!
Sable Dreamer
Você poderia elaborar sobre "aplicar seus efeitos persistentes"? A assinatura de turnStarted e a alteração do valor de Length tornariam o código inabalável e, ou pior, produziriam resultados inconsistentes (ao interagir entre efeitos semelhantes)?
Wondra
Somente para assinantes que assumiriam um determinado período de tempo. Você tem que modelar com cuidado. Pode ser bom ter o tempo de curva atual diferente do tempo de curva do jogador. O PTT seria verificado para criar um novo turno. Os CTT podem ser verificados por cartões. Se um efeito aumentar o tempo atual, a interface do usuário do timer naturalmente seguirá o exemplo, se não tiver estado.
RobStone
Para responder melhor à pergunta. Nada mais armazena o tempo de turno ou qualquer coisa com base nisso. Sempre verifique.
RobStone
11

RobStone está no caminho certo, mas eu queria elaborar, pois foi exatamente o que fiz quando escrevi Dungeon Ho !, um Roguelike que tinha um sistema de efeitos muito complexo para armas e feitiços.

Cada carta deve ter um conjunto de efeitos, definidos de forma que possam indicar qual é o efeito, o que é direcionado, como e por quanto tempo. Por exemplo, um efeito "prejudicar o oponente" pode ser algo assim;

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

Em seguida, quando o efeito for acionado, faça com que uma rotina genérica processe o processamento do efeito. Como um idiota, usei uma enorme declaração case / switch:

switch (effect_type)
{
     case DAMAGE:

     break;
}

Mas uma maneira muito melhor e mais modular de fazer isso é via polimorfismo. Crie uma classe Effect que agrupe todos esses dados, crie uma subclasse para cada tipo de efeito e faça com que essa classe substitua um método onExecute () específico para a classe.

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

Portanto, teríamos uma classe Effect básica, depois uma classe DamageEffect com um método onExecute (), portanto, em nosso código de processamento, apenas iríamos;

Effect effect = card.getActiveEffect();

effect.onExecute();

A maneira de lidar com o que está em jogo é criar uma lista Vector / Array / vinculada / etc. dos efeitos ativos (do tipo Efeito, a classe base) anexados a qualquer objeto (incluindo o campo de jogo / "jogo"), portanto, em vez de verificar se um efeito específico está em jogo, basta percorrer todos os efeitos associados a o (s) objeto (s) e deixe-os executar. Se um efeito não está anexado a um objeto, ele não está em jogo.

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
fonte
Foi exatamente assim que eu fiz. A vantagem aqui é que você tem essencialmente um sistema orientado a dados e pode ajustar a lógica facilmente com base no efeito. Normalmente, você precisará fazer uma verificação de condição na lógica de execução do efeito, mas ainda é muito mais coerente, pois essas verificações são apenas para o efeito em questão.
manabreak
1

Vou oferecer algumas sugestões. Alguns deles se contradizem. Mas talvez alguns sejam úteis.

Considere listas versus sinalizadores

Você pode percorrer o mundo e verificar uma bandeira em cada item para decidir se deve fazer a bandeira. Ou você pode manter uma lista apenas dos itens que devem fazer o sinalizador.

Considere listas e enumerações

Você pode continuar adicionando campos booleanos à sua classe de item, isAThis e isAThat. Ou você pode ter uma lista de strings ou elementos de enumeração, como {"isAThis", "isAThat"} ou {IS_A_THIS, IS_A_THAT}. Dessa forma, você pode adicionar novos na enumeração (ou string consts) sem adicionar campos. Não que haja algo realmente errado em adicionar campos ...

Considere ponteiros de função

Em vez de uma lista de sinalizadores ou enumerações, pode haver uma lista de ações a serem executadas para esse item em diferentes contextos. (Entidade-ish…)

Considere objetos

Algumas pessoas preferem abordagens de entidade orientada a dados, com script ou componente. Mas também vale a pena considerar as hierarquias de objetos à moda antiga. A classe base precisa aceitar as ações, como "jogar esta carta na fase B do turno" ou o que for. Cada tipo de cartão pode substituir e responder conforme apropriado. Provavelmente também existe um objeto de jogador e um objeto de jogo, para que o jogo possa fazer coisas como, se (player-> isAllowedToPlay ()) {faça ​​a jogada ...}.

Considere a capacidade de depuração

Uma coisa interessante sobre uma pilha de campos de bandeira é que você pode examinar e imprimir o estado de cada item da mesma maneira. Se o estado é representado por diferentes tipos, bolsas de componentes ou ponteiros de função, ou estar em listas diferentes, pode não ser suficiente apenas olhar para os campos do item. São todas as trocas.

Eventualmente, refatoração: considere testes de unidade

Não importa o quanto você generalize sua arquitetura, poderá imaginar coisas que ela não cobre. Então você terá que refatorar. Talvez um pouco, talvez muito.

Uma maneira de tornar isso mais seguro é com um corpo de testes de unidade. Dessa forma, você pode ter certeza de que, mesmo reorganizando as coisas por baixo (talvez muito!), A funcionalidade existente ainda funciona. Cada teste de unidade é, geralmente, assim:

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 você pode ver, manter essas chamadas de API de nível superior no jogo (ou jogador, cartão etc.) é essencial para a estratégia de teste de unidade.

david van brink
fonte
0

Em vez de pensar em cada cartão individualmente, comece a pensar em termos de categorias de efeitos, e os cartões contêm uma ou mais dessas categorias. Por exemplo, para calcular a quantidade de tempo em um turno, você pode percorrer todas as cartas em jogo e verificar a categoria "manipular duração do turno" de cada carta que contém essa categoria. Cada carta aumenta ou sobrescreve a duração do turno com base nas regras que você decidiu.

Este é essencialmente um sistema de minicomponentes, onde cada objeto "cartão" é simplesmente um contêiner para vários componentes de efeito.

jhocking
fonte
Como os cartões - e os futuros cartões também - podem fazer praticamente qualquer coisa, eu esperaria que cada cartão carregasse um script. Ainda assim, eu tenho certeza que isso não é um problema de desempenho real ..
Jari Komppa
4
conforme os comentários principais: Ninguém (além de você) disse nada sobre problemas de desempenho. Quanto ao script completo como alternativa, elabore isso em uma resposta.
Jhocking 03/08/2015