Acciones del juego que requieren varios cuadros para completar

20

Nunca antes había hecho mucha programación de juegos, una pregunta bastante directa.

Imagina que estoy construyendo un juego de Tetris, con el bucle principal más o menos así.

for every frame
    handle input
    if it's time to make the current block move down a row
        if we can move the block
            move the block
        else
            remove all complete rows
            move rows down so there are no gaps
            if we can spawn a new block
                spawn a new current block
            else
                game over

Hasta ahora, todo en el juego ocurre instantáneamente: las cosas se generan instantáneamente, las filas se eliminan instantáneamente, etc. ¿Pero qué pasa si no quiero que las cosas sucedan instantáneamente (es decir, animar cosas)?

for every frame
    handle input
    if it's time to make the current block move down a row
        if we can move the block
            move the block
        else
            ?? animate complete rows disappearing (somehow, wait over multiple frames until the animation is done)
            ?? animate rows moving downwards (and again, wait over multiple frames)
            if we can spawn a new block
                spawn a new current block
            else
                game over

En mi clon de Pong esto no fue un problema, ya que cada cuadro solo estaba moviendo la pelota y buscando colisiones.

¿Cómo puedo resolver este problema? Seguramente, la mayoría de los juegos implican alguna acción que toma más de un marco, y otras cosas se detienen hasta que se realiza la acción.


fuente

Respuestas:

11

La solución tradicional a esto es una máquina de estados finitos, que se sugiere en varios comentarios.

Odio las máquinas de estados finitos.

Claro, son simples, son compatibles en todos los idiomas, pero es un dolor increíble trabajar con ellos. Cada manipulación requiere una tonelada de código de copiar y pegar propenso a errores, y ajustar el efecto en pequeñas formas puede ser un gran cambio en el código.

Si puede usar un lenguaje que los admita, le recomiendo corutinas. Te permiten escribir código que se parece a:

function TetrisPieceExplosion()
  for brightness = 0, 1, 0.2 do
    SetExplosionBrightness(brightness)
    coroutine.yield()
  end

  AllowNewBlockToFall()

  SpawnABunchOfParticles()

  RemoveBlockPhysics()

  for transparency = 0, 1, 0.5 do
    SetBlockTransparency(transparency)
    coroutine.yield()
  end

  RemoveBlockGraphics()
end

Obviamente, se trata de una pseudocódigo, pero debe quedar claro que no solo se trata de una descripción lineal simple del efecto especial, sino que también nos permite soltar un nuevo bloque mientras la animación aún está terminando . Lograr esto con una máquina de estado generalmente será horrible.

Que yo sepa, esta funcionalidad no está fácilmente disponible en C, C ++, C #, Objective C o Java. Esta es una de las principales razones por las que uso Lua para toda mi lógica de juego :)

ZorbaTHut
fuente
También podría implementar algo en este sentido en otros lenguajes OOP. Imagine algún tipo de Actionclase y una cola de acciones para realizar. Cuando se completa una acción, retírela de la cola y realice la siguiente acción, etc. Mucho más flexible que una máquina de estado.
bummzack
3
Eso funciona, pero entonces estás buscando derivar de Action para cada acción única. También supone que su proceso encaja perfectamente en una cola: si desea bifurcación o bucles con condiciones finales indefinidas, la solución de cola se descompone rápidamente. Ciertamente es más limpio que el enfoque de máquina de estado, pero creo que las corutinas aún superan la legibilidad :)
ZorbaTHut
Es cierto, pero para el ejemplo de Tetris debería ser suficiente :)
bummzack
Co-rutinas rock, pero Lua como idioma apesta de muchas otras maneras, simplemente no puedo recomendarlo.
DeadMG
Siempre que solo necesite ceder en el nivel superior (y no de una llamada de función anidada), puede lograr lo mismo en C #, pero sí, Lua coroutines rock.
munificente
8

Estoy tomando esto de Game Coding Complete por Mike McShaffry.

Habla de un 'Administrador de procesos', que se reduce a una lista de tareas que deben hacerse. Por ejemplo, un proceso controlaría la animación para dibujar una espada (AnimProcess), abrir una puerta o, en su caso, hacer que la fila desaparezca.

El proceso se agregaría a la lista del administrador de procesos, que se iteraría en cada cuadro y Update () llamado en cada uno. Entidades muy parecidas, pero para acciones. Habría una bandera de matar para eliminar de la lista cuando haya terminado.

La otra cosa interesante acerca de ellos es cómo pueden vincularse, al tener un puntero al siguiente proceso. De esta manera, su proceso de fila animada puede consistir en:

  • Un proceso de animación para la fila que desaparece
  • Un proceso de movimiento para eliminar las piezas.
  • Un proceso de puntaje para agregar puntos al puntaje

(Dado que los procesos pueden ser cosas de un solo uso, condicionalmente allí o allí durante X cantidad de tiempo)

Si quieres más detalles, pregunta.

El pato comunista
fuente
3

Puede usar una cola de acciones prioritaria. Empujas una acción y un tiempo. En cada cuadro, obtienes el tiempo y eliminas todas las acciones que tienen un tiempo especificado antes de ese tiempo y las ejecutas. Bonificación: el enfoque es muy similar y puedes implementar casi toda la lógica del juego de esta manera.

DeadMG
fuente
1

Siempre necesita saber la diferencia horaria entre el marco anterior y el actual, luego debe hacer dos cosas.

-Decida cuándo actualizar su modelo: por ejemplo. en tetris, cuando comienza la eliminación de una fila, ya no desea que las cosas choquen con la fila, por lo que elimina la fila del "modelo" de su aplicación.

-A continuación, debe manejar el objeto que está en un estado de transición a una clase separada que resuelve la animación / evento durante un período de tiempo. En el ejemplo de tetris, la fila se desvanecería lentamente (cambie un poco la opacidad de cada fotograma). Después de que la opacidad es 0, transfiere todos los bloques en la parte superior de la fila uno hacia abajo.

Esto puede parecer un poco complicado al principio, pero entenderás esto, solo asegúrate de abstraer mucho en diferentes clases, esto lo hará más fácil. También asegúrese de que los eventos que toman tiempo, como la eliminación de una fila en tetris, sean del tipo "Fire and Forget", simplemente cree un nuevo objeto que maneje todo lo que debe hacerse automáticamente y que, cuando todo esté hecho, se elimina de su escenagrama.

Roy T.
fuente
Además, en algunos casos, los cálculos pesados ​​pueden exceder el tiempo permitido para un paso de tiempo de física (por ejemplo, detección de colisión y planificación de ruta). En estos casos, puede salirse de los cálculos cuando se haya utilizado el tiempo asignado y continuar el siguiente cuadro de cálculo.
Clavador
0

Debes pensar en el juego como una "máquina de estados finitos". El juego puede estar en uno de varios estados: en su caso, "esperando entrada", "pieza moviéndose hacia abajo", "explosión de fila".

Haces cosas diferentes según el estado. Por ejemplo, durante la "pieza que se mueve hacia abajo", ignora la entrada del jugador y, en cambio, anima la pieza desde su fila actual a la siguiente. Algo como esto:

if state == ACCEPTING_INPUT:
    if player presses any key:
        handle input
    row_timer = row_timer - time_since_last_frame
    if row_timer < 0:
        state = MOVING_PIECE_DOWN
elif state == MOVING_PIECE_DOWN:
    piece.y = piece.y + piece.speed*time_since_last_frame
    if piece.y >= target_piece_y:
        piece.y = target_piece_y
        state = ACCEPTING_INPUT
ggambett
fuente