Ações do jogo que levam vários quadros para serem concluídas

20

Eu realmente nunca fiz muita programação de jogos antes, uma pergunta bastante direta.

Imagine que estou construindo um jogo Tetris, com o loop principal parecido com isso.

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

Até agora, tudo no jogo acontece instantaneamente - as coisas são geradas instantaneamente, as linhas são removidas instantaneamente etc. Mas e se eu não quiser que as coisas aconteçam instantaneamente (ou seja, animar coisas)?

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

No meu clone Pong, isso não foi um problema, pois todos os quadros que eu estava apenas movendo a bola e procurando colisões.

Como posso entender minha questão? Certamente, a maioria dos jogos envolve alguma ação que leva mais do que um quadro, e outras coisas param até a ação ser concluída.


fonte

Respostas:

11

A solução tradicional para isso é uma máquina de estados finitos, que está sendo sugerida em vários comentários.

Eu odeio máquinas de estado finito.

Claro, eles são simples, são suportados em todos os idiomas, mas são uma dor incrível de se trabalhar. Toda manipulação requer uma tonelada de código copie e cole com correção de erros, e ajustar o efeito em pequenas maneiras pode ser uma grande mudança no código.

Se você pode usar um idioma que os suporte, recomendo corotinas. Eles permitem que você escreva um código parecido com:

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, é um pseudo-crocodilo, mas deve ficar claro que isso não apenas é uma descrição linear simples do efeito especial, mas também nos permite soltar um novo bloco enquanto a animação ainda está terminando . Conseguir isso com uma máquina de estado geralmente será horrível.

Que eu saiba, essa funcionalidade não está facilmente disponível em C, C ++, C #, Objective C ou Java. Esta é uma das principais razões pelas quais uso Lua em toda a minha lógica de jogo :)

ZorbaTHut
fonte
Você também pode implementar algo nesse sentido em outras linguagens OOP. Imagine algum tipo de Actionclasse e uma fila de ações a serem executadas. Quando uma ação estiver concluída, remova-a da fila e execute a próxima ação etc. Muito mais flexível que uma máquina de estado.
22411 bummzack
3
Isso funciona, mas você deve derivar do Action para cada ação única. Ele também pressupõe que seu processo se encaixa perfeitamente em uma fila - se você deseja ramificações ou loops com condições finais indefinidas, a solução da fila é interrompida rapidamente. É certamente mais limpo do que a abordagem de máquina de estado, mas acho que coroutines ainda trunfo lo em legibilidade :)
ZorbaTHut
É verdade, mas para o exemplo Tetris deve ser suficiente :)
bummzack
As co-rotinas são ótimas - mas Lua como uma linguagem é péssima de tantas outras maneiras, eu simplesmente não posso recomendar.
DeadMG
Contanto que você precise apenas produzir no nível superior (e não a partir de uma chamada de função aninhada), você pode realizar a mesma coisa em C #, mas sim, as corotinas Lua agitam.
munificent
8

Estou tirando isso do Game Coding Complete de Mike McShaffry.

Ele fala sobre um 'Process Manager', que se resume a uma lista de tarefas que precisam ser realizadas. Por exemplo, um processo controlaria a animação para desenhar uma espada (AnimProcess), abrir uma porta ou, no seu caso, fazer a linha desaparecer.

O processo seria adicionado à lista de gerenciadores de processos, que seria iterada a cada quadro e Update () chamado em cada um. Entidades muito parecidas, mas para ações. Haveria um sinalizador de morte para remover da lista quando terminar.

A outra coisa interessante sobre eles é como eles podem vincular, tendo um ponteiro para o próximo processo. Dessa forma, seu processo de linha animada pode realmente consistir em:

  • Um AnimationProcess para a linha desaparecendo
  • Um MovementProcess para remover as peças
  • Um ScoreProcess para adicionar pontos à pontuação

(Como os processos podem ser de uso único, condicionalmente lá ou lá por um período de tempo X)

Se você quiser mais detalhes, pergunte.

O Pato Comunista
fonte
3

Você pode usar uma fila de ações prioritárias. Você empurra uma ação e um tempo. A cada quadro, você obtém o tempo e libera todas as ações que têm um tempo especificado antes desse horário e as executa. Bônus: A abordagem é paralela, e você pode implementar quase toda a lógica do jogo dessa maneira.

DeadMG
fonte
1

Você sempre precisa saber a diferença de horário entre o quadro anterior e o atual, para fazer duas coisas.

Decida quando atualizar seu modelo: por exemplo. no tetris, quando uma remoção de linha começa, você não deseja mais que as coisas colidam com a linha; portanto, você remove a linha do 'modelo' do seu aplicativo.

-Você precisa manipular o objeto que está em um estado de transição para uma classe separada que resolve a animação / evento por um período de tempo. No exemplo do tetris, a linha desapareceria lentamente (altere um pouco a opacidade de cada quadro). Após a opacidade ser 0, você transfere todos os blocos no topo da linha um para baixo.

Isso pode parecer um pouco complicado no começo, mas você entenderá isso, apenas certifique-se de abstrair muito em diferentes classes, isso facilitará. Verifique também se os eventos que demoram algum tempo, como a remoção de uma linha no tetris, são do tipo "Fire and Forget", basta criar um novo objeto que lide com tudo o que precisa ser feito automaticamente e que, quando tudo estiver pronto, remove-se do seu cenário.

Roy T.
fonte
Além disso, em alguns casos, cálculos pesados ​​podem exceder o tempo permitido para uma etapa do tempo da física (por exemplo, detecção de colisão e planejamento de trajetos). Nesses casos, você pode pular fora dos cálculos quando o tempo alocado tiver sido usado e continuar o cálculo no próximo quadro.
Nailer
0

Você precisa pensar no jogo como uma "máquina de estados finitos". O jogo pode estar em um dos vários estados: no seu caso, "espera de entrada", "peça descendo", "linha explodindo".

Você faz coisas diferentes, dependendo do estado. Por exemplo, durante a "descida da peça", você ignora a entrada do jogador e anima a peça da linha atual para a próxima linha. Algo assim:

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
fonte