Loop do jogo, como verificar condições uma vez, fazer alguma coisa e não fazê-lo novamente

13

Por exemplo, eu tenho uma classe de jogo e mantém uma intque rastreia a vida do jogador. Eu tenho uma condicional

if ( mLives < 1 ) {
    // Do some work.
}

No entanto, esta condição continua disparando e o trabalho é feito repetidamente. Por exemplo, quero definir um cronômetro para sair do jogo em 5 segundos. Atualmente, ele continuará definindo para 5 segundos a cada quadro e o jogo nunca termina.

Este é apenas um exemplo, e eu tenho o mesmo problema em várias áreas do meu jogo. Quero verificar se há alguma condição e, em seguida, fazer algo uma vez e apenas uma vez e depois não verificar ou executar o código na instrução if novamente. Algumas soluções possíveis que vêm à mente está tendo um boolpara cada condição e definir os boolQuando os fogos de condição. No entanto, na prática, isso fica muito confuso ao lidar com muitasbools , pois eles precisam ser armazenados como campos de classe ou estaticamente dentro do próprio método.

Qual é a solução adequada para isso (não no meu exemplo, mas no domínio do problema)? Como você faria isso em um jogo?

EddieV223
fonte
Obrigado pelas respostas, minha solução agora é uma combinação de respostas ktodisco e crancran.
EddieV223

Respostas:

13

Eu acho que você pode resolver esse problema simplesmente exercendo um controle mais cuidadoso sobre seus possíveis caminhos de código. Por exemplo, no caso em que você está verificando se o número de vidas do jogador caiu abaixo de um, por que não verificar apenas quando o jogador perde uma vida, em vez de cada quadro?

void subtractPlayerLife() {
    // Generic life losing stuff, such as mLives -= 1
    if (mLives < 1) {
        // Do specific stuff.
    }
}

Por sua vez, isso pressupõe que isso subtractPlayerLifeseria chamado apenas em ocasiões específicas, o que talvez resultasse de condições que você deve verificar em todos os quadros (colisões, talvez).

Controlando cuidadosamente a forma como seu código é executado, você pode evitar soluções confusas, como booleanos estáticos, além de reduzir, pouco a pouco, a quantidade de código executado em um único quadro.

Se há algo que parece impossível de refatorar e você realmente precisa apenas verificar uma vez, então enumbooleanos estatais (como um ) ou estáticos são o caminho a percorrer. Para evitar declarar a estática, você pode usar o seguinte truque:

#define ONE_TIME_IF(condition, statement) \
    static bool once##__LINE__##__FILE__; \
    if(!once##__LINE__##__FILE__ && condition) \
    { \
        once##__LINE__##__FILE__ = true; \
        statement \
    }

que faria o seguinte código:

while (true) {
    ONE_TIME_IF(1 > 0,
        printf("something\n");
        printf("something else\n");
    )
}

imprimir somethinge something elseapenas uma vez. Não produz o código mais bonito, mas funciona. Também tenho certeza de que há maneiras de aprimorá-lo, talvez garantindo melhor nomes de variáveis ​​exclusivos. Isso #definefuncionará apenas uma vez durante o tempo de execução. Não há como redefini-lo e não há como salvá-lo.

Apesar do truque, eu recomendo controlar melhor o fluxo de código primeiro e usá-lo #definecomo último recurso ou apenas para fins de depuração.

kevintodisco
fonte
+1 agradável uma vez se solução. Confuso, mas legal.
kender
1
existe um grande problema com o seu ONE_TIME_IF, você não pode fazer isso acontecer novamente. Por exemplo, o jogador volta ao menu principal e depois retorna à cena do jogo. Essa declaração não será acionada novamente. e há condições que você pode precisar manter entre as execuções de aplicativos, por exemplo, lista de troféus já adquiridos. seu método recompensará o troféu duas vezes se o jogador o ganhar em duas aplicações diferentes.
precisa saber é o seguinte
1
@ Gajoo Isso é verdade, é um problema se a condição precisar ser redefinida ou salva. Eu editei para esclarecer isso. É por isso que eu o recomendei como último recurso, ou apenas para depuração.
Kevintodisco
Posso sugerir o idioma do {} while (0)? Eu sei que pessoalmente estragaria algo e colocaria um ponto e vírgula onde não deveria. Macro legal, bravo.
michael.bartnett
5

Isso soa como o caso clássico de usar o padrão de estado. Em um estado, você verifica sua condição para vidas <1. Se essa condição se tornar verdadeira, você fará a transição para um estado de atraso. Esse estado de atraso aguarda a duração especificada e depois faz a transição para o estado de saída.

Na abordagem mais trivial:

switch(mCurrentState) {
  case LIVES_GREATER_THAN_0:
    if(lives < 1) mCurrentState = LIVES_LESS_THAN_1;
    break;
  case LIVES_LESS_THAN_1:
    timer.expires(5000); // expire in 5 seconds
    mCurrentState = TIMER_WAIT;
    break;
  case TIMER_WAIT:
    if(timer.expired()) mCurrentState = EXIT;
    break;
  case EXIT:
    mQuit = true;
    break;
}

Você pode considerar o uso de uma classe State juntamente com um StateManager / StateMachine para lidar com a alteração / envio / transição entre estados, em vez de uma instrução switch.

Você pode tornar sua solução de gerenciamento de estado tão sofisticada quanto desejar, incluindo vários estados ativos, um único estado pai ativo com muitos estados filhos ativos usando alguma hierarquia etc. A solução padrão de estado tornará seu código muito mais reutilizável e mais fácil de manter e seguir.

Naros
fonte
1

Se você está preocupado com o desempenho, o desempenho da verificação várias vezes geralmente não é significativo; idem por ter condições de bool.

Se você estiver interessado em outra coisa, poderá usar algo semelhante ao padrão de design nulo para resolver esse problema.

Nesse caso, vamos supor que você queira chamar um método foose o jogador não tiver vidas restantes. Você implementaria isso como duas classes, uma que chama fooe outra que não faz nada. Vamos chamá-los DoNothinge CallFooassim:

class SomeLevel {
  int numLives = 3;
  FooClass behaviour = new DoNothing();
  ...
  void tick() { 
    if (numLives < 1) {
      behaviour = new CallFoo();
    }

    behaviour.execute();
  }
}

Este é um exemplo um pouco "bruto"; os pontos principais são:

  • Acompanhe algumas instâncias das FooClassquais pode ser DoNothingou CallFoo. Pode ser uma classe base, classe abstrata ou interface.
  • Sempre chame o executemétodo dessa classe.
  • Por padrão, a classe não faz nada ( DoNothinginstância).
  • Quando a vida acaba, a instância é trocada por uma instância que realmente faz alguma coisa ( CallFooinstância).

Isso é essencialmente como uma mistura de estratégia e padrões nulos.

ashes999
fonte
Mas ele continuará criando novos CallFoo () a cada quadro, que você deve excluir antes de começar, e também continuará acontecendo, o que não resolverá o problema. Por exemplo, se eu tivesse CallFoo () para chamar um método que define um timer para ser executado em alguns segundos, esse timer seria definido para o mesmo horário em cada quadro. Até onde eu sei, sua solução é a mesma que se (numLives <1) {// faça coisas} mais {// esvazie}
EddieV223
Hmm, entendo o que você está dizendo. A solução pode ser mover a ifverificação para a FooClassprópria instância, para que seja executada apenas uma vez e o mesmo para instanciação CallFoo.
ashes999