Scopri di più sul rendering nei cicli di gioco

Un modo molto diffuso per implementare un ciclo di gioco è il seguente:

while (playing) {
    advance state by one frame
    render the new frame
    sleep until it’s time to do the next frame
}

Ci sono alcuni problemi in questo senso, il più fondamentale è l'idea che il in un gioco può definire cos'è un "frame" dall'indirizzo IP interno. Display diversi verranno aggiornati in momenti diversi e può variare nel tempo. Se generi frame più velocemente in modo da mostrarli, dovrai eliminarne uno di tanto in tanto. Se generi troppo lentamente, SurfaceFlinger non troverà periodicamente un nuovo buffer acquisirà e mostrerà nuovamente il frame precedente. Entrambe queste situazioni che causano glitch visibili.

Devi fare corrispondere la frequenza fotogrammi del display e lo stato di avanzamento del gioco in base al tempo trascorso dal frame precedente. Esistono diversi modi per farlo:

  • Utilizza la libreria Android Frame Pacing (consigliato)
  • Riempi la BufferQueue e affidati ai "buffer di scambio" contropressione
  • Usa Choreographer (API 16+)

libreria Android Frame Pacing

Per informazioni, consulta Migliorare il pacing dei frame. sull'utilizzo di questa libreria.

Coda in eccesso

È molto facile da implementare: basta sostituire i buffer il più velocemente possibile. All'inizio di Android, ciò potrebbe causare una sanzione nel SurfaceView#lockCanvas() ti consente di dormire per 100 ms. Adesso è regolato dalla BufferQueue e la BufferQueue viene svuotata con la SurfaceFlinger è in grado.

Un esempio di questo approccio è visibile in Android Breakout. it utilizza GLSurfaceView, che viene eseguito in un loop che chiama il callback onDrawFrame() e poi scambia il buffer. Se BufferQueue è piena, la chiamata eglSwapBuffers() attenderà che sia disponibile un buffer. I buffer diventano disponibili quando SurfaceFlinger li rilascia, cosa che avviene in seguito acquistarne uno nuovo per la visualizzazione. Poiché questo avviene su VSYNC, il loop di disegno corrisponderà alla frequenza di aggiornamento. Soprattutto.

Questo approccio presenta un paio di problemi. Innanzitutto, l'app è collegata L'attività SurfaceFlinger, che richiederà tempi diversi a seconda di quanto lavoro c'è da fare e se c'è una lotta per il tempo di CPU con altri processi. Dato che lo stato del gioco avanza in base al tempo, tra uno scambio di buffer e l'altro, l'animazione non verrà aggiornata con una frequenza costante. Quando girando a 60 f/s e calcolando la media delle incoerenze nel tempo, probabilmente non noterà i picchi.

In secondo luogo, le prime due operazioni di buffer swap avvengono molto rapidamente perché la coda del buffer non è ancora piena. Il tempo calcolato tra i frame sarà vicino allo zero, quindi il gioco genererà alcuni frame in cui non succede nulla. In un gioco come Breakout, che aggiorna lo schermo a ogni aggiornamento, la coda viene sempre pieno tranne quando un gioco viene avviato (o riattivato), quindi l'effetto non è visibile. Un gioco che mette in pausa l'animazione di tanto in tanto e poi torna a la modalità Il più veloce possibile potrebbe riscontrare strani intoppi.

Choreographer

Choreographer ti consente di impostare un callback che si attiva al successivo VSYNC. La il tempo effettivo di VSYNC viene passato come argomento. Quindi anche se l'app non si riattiva immediatamente, hai comunque un'immagine precisa dell'aggiornamento di Google Cloud. L'uso di questo valore al posto dell'ora corrente produce una per la logica di aggiornamento dello stato del gioco.

Sfortunatamente, il fatto di ricevere una richiamata dopo ogni VSYNC garantire che il callback verrà eseguito in modo tempestivo o che in grado di intervenire con sufficiente rapidità. L'app dovrà rilevare in cui i frame sono in ritardo e rilasciano manualmente.

L'app "Record GL" in Grafika ne offre un esempio. Su alcune dispositivi (ad es. Nexus 4 e Nexus 5), l'attività inizierà a interrompersi se stai a guardare. Il rendering GL è banale, ma a volte può capitare che gli elementi vengono ridisegnati e la valutazione della misura o del layout può richiedere molto tempo se il dispositivo è passato alla modalità a consumo ridotto. (Secondo systrace, richiede 28 ms invece di 6 ms dopo che gli orologi rallentano su Android 4.4. Se trascini con il dito intorno allo schermo, pensa che tu stia interagendo con l'attività, così le velocità di clock sono elevate e non perderai mai un fotogramma.)

La semplice soluzione consisteva nel rilasciare un frame nel callback di Coreografo se l'attuale è superiore a N millisecondi dopo il tempo VSYNC. Idealmente il valore di N viene determinato in base agli intervalli VSYNC osservati in precedenza. Ad esempio, se di aggiornamento è di 16,7 ms (60 f/s), potresti perdere un frame se eseguivi con un ritardo di oltre 15 ms.

Se guardi "Record GL app" vedrai il contatore dei frame eliminati aumentano e vedi anche un lampo di rosso sul bordo quando i fotogrammi si riducono. A meno che gli occhi sono molto buoni, però, l'animazione non si interrompe. A 60 f/s, l'app può far cadere il frame occasionale senza che nessuno se ne accorga, purché l'animazione continua ad avanzare a una velocità costante. Quanto puoi guadagnare dipende in parte da ciò che stai disegnando, le caratteristiche il display e la capacità della persona che usa l'app di rilevare i jank.

Gestione dei thread

In generale, se il rendering viene eseguito su SurfaceView, GLSurfaceView o TextureView, esegui il rendering in un thread dedicato. Non fare mai "lavoro pesante" o qualsiasi altra cosa che richieda un periodo di tempo indeterminato sul Thread UI. Crea invece due thread per il gioco: un thread del gioco e un thread di rendering. Consulta Migliorare le prestazioni del gioco per ulteriori informazioni.

Breakout e "Record GL app" usano thread del renderer dedicati ed è inoltre possibile aggiornare lo stato dell'animazione su quel thread. Si tratta di un approccio ragionevole a condizione che lo stato del gioco può essere aggiornato rapidamente.

Altri giochi separano completamente la logica del gioco e il rendering. Se avessi un un semplice gioco che non faceva altro che spostare un blocco ogni 100 ms, potevamo thread dedicato che ha eseguito questa operazione:

run() {
    Thread.sleep(100);
    synchronized (mLock) {
        moveBlock();
    }
}

(Potresti basare il tempo di sospensione su un orologio fisso per evitare la deviazione -- dorm() non è perfettamente coerente e moveBlock() prende una quantità diversa da zero tempo, ma ti viene un'idea.)

Quando il codice di disegno si attiva, afferra il blocco e recupera la posizione corrente del blocco, rilascia il blocco e disegna. Invece di utilizzare i dati frazionari in base ai tempi delta tra fotogrammi, c'è un solo thread che si sposta e un altro filo conduttore che traccia le cose ovunque si trovino all'avvio del disegno.

Per una scena con qualsiasi complessità, vuoi creare un elenco di eventi imminenti ordinati per ora del risveglio e sonno fino alla scadenza dell'evento successivo, ma è lo stesso dell'IA.