Azioni di gioco che richiedono più frame per il completamento


20

Non ho mai fatto molta programmazione di gioco prima, una domanda piuttosto semplice.

Immagina che sto costruendo un gioco Tetris, con il loop principale che assomiglia a qualcosa del genere.

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

Finora tutto nel gioco accade all'istante: le cose vengono generate all'istante, le righe vengono rimosse all'istante, ecc. Ma cosa succede se non voglio che le cose accadano all'istante (ad esempio animare le cose)?

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

Nel mio clone Pong questo non era un problema, dato che ogni fotogramma stavo solo muovendo la palla e controllando le collisioni.

Come posso avvolgere la testa attorno a questo problema? Sicuramente la maggior parte dei giochi prevede alcune azioni che richiedono più di un frame e altre cose si fermano fino a quando l'azione non viene eseguita.

Risposte:


11

La soluzione tradizionale a questo è una macchina a stati finiti, che viene suggerita in diversi commenti.

Odio le macchine a stati finiti.

Certo, sono semplici, sono supportati in tutte le lingue, ma sono un dolore incredibile con cui lavorare. Ogni manipolazione richiede una tonnellata di codice copia e incolla bugprone, e modificare l'effetto in piccoli modi può essere una grande modifica al codice.

Se puoi usare un linguaggio che li supporti, ti consiglio le coroutine. Ti consentono di scrivere codice simile 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

Ovviamente piuttosto pseudocodey, ma dovrebbe essere chiaro che non solo questa è una semplice descrizione lineare dell'effetto speciale, ma ci consente facilmente di rilasciare un nuovo blocco mentre l'animazione sta ancora finendo . Realizzare questo con una macchina a stati sarà generalmente orribile.

Per quanto ne so, questa funzionalità non è facilmente disponibile in C, C ++, C #, Obiettivo C o Java. Questo è uno dei motivi principali per cui utilizzo Lua per tutta la mia logica di gioco :)


Puoi anche implementare qualcosa in questo senso in altri linguaggi OOP. Immagina una sorta di Actionclasse e una coda di azioni da eseguire. Quando un'azione è completa, rimuovila dalla coda ed esegui l'azione successiva, ecc. Molto più flessibile di una macchina a stati.
Bummzack,

3
Funziona, ma poi stai guardando derivare dall'Azione per ogni singola azione unica. Presuppone inoltre che il processo si adatti perfettamente a una coda: se si desidera eseguire ramificazioni o cicli con condizioni finali non definite, la soluzione della coda si interrompe rapidamente. È certamente più pulito dell'approccio basato sulla macchina a stati, ma penso che le coroutine continuino a
battere

Vero, ma per l'esempio di Tetris dovrebbe essere sufficiente :)
bummzack,

Le co-routine rock- ma Lua come lingua fa schifo in molti altri modi, non posso proprio consigliarlo.
DeadMG

Fintanto che devi solo cedere al livello più alto (e non da una chiamata di funzione nidificata), puoi realizzare la stessa cosa in C #, ma sì, le coroutine Lua rock.
munifico

8

Lo prendo da Game Coding Complete di Mike McShaffry.

Parla di un "Process Manager", che si riduce a un elenco di attività che devono essere eseguite. Ad esempio, un processo controllerebbe l'animazione per estrarre una spada (AnimProcess) o aprire una porta o, nel tuo caso, farebbe sparire la fila.

Il processo verrebbe aggiunto all'elenco del gestore processi, che verrebbe ripetuto su ogni frame e Update () chiamato su ciascuno. Entità molto simili, ma per azioni. Ci sarebbe un flag di uccisione da rimuovere dalla lista una volta terminato.

L'altra cosa chiara su di loro è come possono collegarsi, avendo un puntatore al processo successivo. In questo modo, il processo di riga animata potrebbe effettivamente consistere in:

  • Un processo di animazione per la riga che scompare
  • Un MovementProcess per rimuovere i pezzi
  • Un ScoreProcess per aggiungere punti al punteggio

(Poiché i processi possono essere cose monouso, condizionatamente lì o lì per X quantità di tempo)

Se vuoi ulteriori dettagli, chiedi pure.


3

È possibile utilizzare una coda di azioni prioritaria. Spingi dentro un'azione e un tempo. Ogni fotogramma, ottieni il tempo e annulli tutte le azioni che hanno un tempo specificato come prima di quel tempo e le esegui. Bonus: l'approccio si parallelizza bene e puoi effettivamente implementare quasi tutta la logica di gioco in questo modo.


1

Devi sempre conoscere la differenza di tempo tra il fotogramma precedente e quello corrente, quindi devi fare due cose.

-Decidi quando aggiornare il tuo modello: ad es. in tetris quando inizia la rimozione di una riga, non si desidera più che le cose si scontrino con la riga, quindi si rimuove la riga dal "modello" dell'applicazione.

-Devi quindi gestire l'oggetto che si trova in uno stato di transizione verso una classe separata che risolve l'animazione / evento per un periodo di tempo. Nell'esempio di tetris la riga svanirà lentamente (modificando leggermente l'opacità di ciascun fotogramma). Dopo che l'opacità è 0, trasferisci tutti i blocchi in cima alla riga uno in basso.

All'inizio questo potrebbe sembrare un po 'complicato, ma ti renderai conto di ciò, assicurati solo di astrarre molto in classi diverse, questo renderà più facile. Assicurati anche che gli eventi che richiedono tempo, come la rimozione di una riga in tetris, siano del tipo "Fire and Forget", crei semplicemente un nuovo oggetto che gestisca tutto ciò che deve essere fatto automaticamente e che, quando tutto è fatto, si rimuove dal tuo scenario.


Inoltre, in alcuni casi i calcoli pesanti potrebbero superare il tempo consentito per una fase temporale della fisica (ad es. Rilevamento delle collisioni e pianificazione del percorso). In questi casi è possibile saltare fuori dai calcoli quando è stato utilizzato il tempo assegnato e continuare il calcolo al fotogramma successivo.
Nailer,

0

Devi pensare al gioco come a una "macchina a stati finiti". Il gioco può essere in uno dei diversi stati: nel tuo caso, "attesa input", "pezzo in movimento", "esplosione di file".

Fai cose diverse a seconda dello stato. Ad esempio, durante "lo spostamento verso il basso del pezzo" ignori l'input del giocatore e invece animi il pezzo dalla sua riga corrente alla riga successiva. Qualcosa come questo:

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.