Separación del estado mundial y la animación en un juego por turnos

9

¿Cómo manejas la separación de la animación del estado mundial dentro de un juego por turnos? Actualmente estoy trabajando en un juego basado en cuadrícula 2D. El siguiente código está simplificado para explicarlo mejor.

Cuando un actor se mueve, quiero pausar el flujo de turnos mientras la criatura se anima y se mueve a la nueva posición. De lo contrario, la pantalla podría quedar muy por detrás del estado mundial, lo que causaría una apariencia visual extraña. También quiero tener animaciones que no bloqueen el flujo del juego: un efecto de partículas podría desarrollarse en múltiples turnos sin afectar el juego.

Lo que he hecho es presentar dos tipos de animaciones, que llamo animaciones de bloqueo y animaciones sin bloqueo. Cuando el jugador quiere moverse, el código que se ejecuta es

class Actor {
    void move(PositionInfo oldPosition, PositionInfo newPosition) {
        if(isValidMove(oldPosition, newPosition) {
             getGame().pushBlockingAnimation(new MoveAnimation(this, oldPosition, newPosition, ....));
             player.setPosition(newPosition);
        }
    }
}

Entonces el ciclo principal de actualización hace:

class Game {
    void update(float dt) {
        updateNonBlockingAnimations(dt); //Effects like particle explosions, damage numbers, etc. Things that don't block the flow of turns.
        if(!mBlockingAnimations.empty()) {
            mBlockingAnimations.get(0).update(dt);
        } else {
             //.. handle the next turn. This is where new animations will be enqueued..//
        }
        cleanUpAnimations(); // remove finished animations
    }
}

... donde la animación actualiza la posición de la pantalla del actor.

Otra idea que estoy implementando es tener una animación de bloqueo concurrente, donde múltiples animaciones de bloqueo se actualizarían simultáneamente, pero el siguiente turno no sucedería hasta que se hicieran todas.

¿Parece esto una forma sensata de hacer las cosas? ¿Alguien tiene alguna sugerencia, o incluso referencias a cómo otros juegos similares hacen tal cosa?

mdkess
fuente

Respuestas:

2

Si su sistema funciona para usted, no veo ninguna razón para no hacerlo.

Bueno, una razón: puede ser complicado cuando no puedes juzgar fácilmente las consecuencias de "player.setPosition (newPosition)"; nunca más.

Ejemplo (solo inventando cosas): imagina que mueves al jugador con tu setPosition encima de alguna trampa. La llamada a "player.setPosition" en sí activará alguna otra llamada a ... digamos un código de escucha de trampa que a su vez empujará una animación de bloqueo por sí misma que muestra una "salpicadura dolorosa" encima de sí mismo.

Usted ve el problema: el chapoteo se muestra simultáneamente con el movimiento del jugador hacia la trampa. (Supongamos aquí que no quieres esto, pero quieres que la animación de la trampa se reproduzca justo después de que termine el movimiento;)).

Por lo tanto, siempre que pueda tener en cuenta todas las consecuencias de sus acciones que hace después de esas llamadas "pushBlockingAnimation", todo está bien.

Es posible que deba comenzar a ajustar la lógica del juego en "bloquear algo" - clases para que puedan ponerse en cola para ejecutarse después de que finalice la animación. O pasa una devolución de llamada "callThisAfterAnimationFinishes" a pushBlockingAnimation y mueve setPosition a la función de devolución de llamada.

Otro enfoque serían los lenguajes de script. Por ejemplo, Lua tiene "coroutine.yield" que devuelve la función con un estado especial para que pueda llamarse más tarde para continuar en la siguiente instrucción. De esta manera, puede esperar fácilmente hasta el final de la animación para ejecutar la lógica del juego sin saturar el aspecto de flujo de programa "normal" de su código. (significa que su código aún se vería un poco como "playAnimation (); setPosition ();" en lugar de pasar devoluciones de llamada y saturar la lógica del juego en múltiples funciones).

Unity3D (y al menos otro sistema de juego que conozco) presenta la declaración C # "return return" para simular esto directamente en el código C #.

// instead of simple call to move(), you have to "iterate" it in a foreach-like loop
IEnumerable<Updatable> move(...) {
    // playAnimation returns an object that contain an update() and finished() method.
    // the caller will not iterate over move() again until the object is "finished"
    yield return getGame().playAnimation(...)
    // the next statement will be executed *after* the animation finished
    player.setPosition(newPosition);
}

Por supuesto, en este esquema, la llamada a move () obtiene todo el trabajo sucio ahora. Probablemente aplique esta técnica solo para funciones específicas donde sepa exactamente quién las llama desde dónde. Así que tome esto solo como una idea básica de cómo están organizados otros motores; probablemente sea demasiado complicado para su problema específico actual.

Le recomiendo que se apegue a su idea de pushBlockingAnimation hasta que tenga problemas en el mundo real. Luego modifícalo. ;)

Imi
fuente
Muchas gracias por tomarse el tiempo para escribir esta respuesta, realmente lo aprecio.
mdkess