Game loop, cómo verificar las condiciones una vez, hacer algo, luego no volver a hacerlo

13

Por ejemplo, tengo una clase de juego y mantiene una intque rastrea la vida del jugador. Tengo un condicional

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

Sin embargo, esta condición sigue disparando y el trabajo se realiza repetidamente. Por ejemplo, quiero configurar un temporizador para salir del juego en 5 segundos. Actualmente, seguirá configurándolo en 5 segundos cada fotograma y el juego nunca termina.

Este es solo un ejemplo, y tengo el mismo problema en varias áreas de mi juego. Quiero verificar alguna condición y luego hacer algo una vez y solo una vez, y luego no verificar o hacer el código en la declaración if nuevamente. Algunas posibles soluciones que vienen a la mente son tener una boolpara cada condición y establecer boolcuándo se dispara la condición. Sin embargo, en la práctica, esto se vuelve muy complicado al manejar muchos bools, ya que deben almacenarse como campos de clase o estáticamente dentro del método mismo.

¿Cuál es la solución adecuada para esto (no para mi ejemplo, sino para el dominio del problema)? ¿Cómo harías esto en un juego?

EddieV223
fuente
Gracias por las respuestas, mi solución ahora es una combinación de respuestas ktodisco y crancran.
EddieV223

Respuestas:

13

Creo que puede resolver este problema simplemente ejerciendo un control más cuidadoso sobre sus posibles rutas de código. Por ejemplo, en el caso de que esté verificando si el número de vidas del jugador ha caído por debajo de uno, ¿por qué no verificar solo cuando el jugador pierde una vida, en lugar de cada cuadro?

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

Esto, a su vez, supone que subtractPlayerLifesolo se llamaría en ocasiones específicas, lo que tal vez resultaría de condiciones en las que debe verificar cada cuadro (colisiones, tal vez).

Al controlar cuidadosamente cómo se ejecuta su código, puede evitar soluciones desordenadas como booleanos estáticos y también reducir bit por bit la cantidad de código que se ejecuta en un solo cuadro.

Si hay algo que parece imposible de refactorizar, y realmente necesita verificar solo una vez, entonces el estado (como un enum) o los booleanos estáticos son el camino a seguir. Para evitar declarar las estadísticas usted mismo, puede usar el siguiente truco:

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

que haría el siguiente código:

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

imprimir somethingy something elsesolo una vez. No produce el código más atractivo, pero funciona. También estoy bastante seguro de que hay formas de mejorarlo, tal vez asegurando mejor nombres de variables únicos. Sin #defineembargo, esto solo funcionará una vez durante el tiempo de ejecución. No hay forma de restablecerlo, y no hay forma de guardarlo.

A pesar del truco, recomiendo encarecidamente controlar mejor el flujo de su código primero y usarlo #definecomo último recurso, o solo con fines de depuración.

kevintodisco
fuente
+1 agradable una vez si la solución. Desordenado, pero genial.
kender
1
hay un gran problema con su ONE_TIME_IF, no puede hacer que vuelva a suceder. Por ejemplo, el jugador vuelve al menú principal y luego vuelve a la escena del juego. Esa declaración no se disparará nuevamente. y hay condiciones que puede necesitar mantener entre las ejecuciones de la aplicación, por ejemplo, la lista de trofeos ya adquiridos. tu método recompensará el trofeo dos veces si el jugador lo gana en dos carreras de aplicaciones diferentes.
Ali1S232
1
@Gajoo Eso es cierto, es un problema si la condición necesita restablecerse o guardarse. He editado para aclarar eso. Es por eso que lo recomendé como último recurso, o simplemente para la depuración.
kevintodisco
¿Puedo sugerir el lenguaje do {} while (0)? Sé que personalmente arruinaría algo y pondría un punto y coma donde no debería. Genial macro, bravo.
michael.bartnett
5

Esto suena como el caso clásico de usar el patrón de estado. En un estado, verifica su condición de vidas <1. Si esa condición se cumple, pasa a un estado de retraso. Este estado de retraso espera la duración especificada y luego pasa a su estado de salida.

En el enfoque más 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;
}

Podría considerar usar una clase de estado junto con un StateManager / StateMachine para manejar el cambio / empuje / transición entre estados en lugar de una declaración de cambio.

Puede hacer que su solución de administración de estado sea tan sofisticada como desee, incluidos varios estados activos, un solo estado padre activo con muchos estados hijos activos que usen alguna jerarquía, etc. Por supuesto, utilice lo que tiene sentido para usted, pero realmente creo que La solución de patrón de estado hará que su código sea mucho más reutilizable y más fácil de mantener y seguir.

Naros
fuente
1

Si le preocupa el rendimiento, el rendimiento de las comprobaciones múltiples no suele ser significativo; lo mismo para tener condiciones bool.

Si está interesado en algo más, puede usar algo similar al patrón de diseño nulo para resolver este problema.

En este caso, supongamos que desea llamar a un método foosi al jugador no le quedan vidas. Implementaría esto como dos clases, una que llama fooy otra que no hace nada. Vamos a llamarlos DoNothingy CallFooasí:

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

    behaviour.execute();
  }
}

Este es un poco un ejemplo "en bruto"; Los puntos clave son:

  • Mantenga un registro de alguna instancia de lo FooClassque puede ser DoNothingo CallFoo. Puede ser una clase base, una clase abstracta o una interfaz.
  • Siempre llame al executemétodo de esa clase.
  • Por defecto, la clase no hace nada ( DoNothinginstancia).
  • Cuando se agotan las vidas, la instancia se intercambia por una instancia que realmente hace algo ( CallFooinstancia).

Esto es esencialmente como una combinación de estrategia y patrones nulos.

cenizas999
fuente
Pero seguirá creando nuevos CallFoo () cada fotograma, que debe eliminar antes de comenzar, y también seguirá sucediendo, lo que no soluciona el problema. Por ejemplo, si tuviera CallFoo () llamar a un método que establece un temporizador para ejecutarse en unos segundos, ese temporizador se configuraría al mismo tiempo cada fotograma. Por lo que puedo decir, su solución es la misma que si (numLives <1) {// do stuff} else {// empty}
EddieV223
Hmm, ya veo lo que estás diciendo. La solución podría ser mover el ifcheque a la FooClasspropia instancia, por lo que solo se ejecuta una vez, y lo mismo para crear instancias CallFoo.
ashes999