Basta selezionare un frame rate e attenersi ad esso. È quello che facevano molti giochi della vecchia scuola: giravano a una velocità fissa di 50 o 60 FPS, di solito sincronizzati con la frequenza di aggiornamento dello schermo e progettavano semplicemente la loro logica di gioco per fare tutto il necessario entro quell'intervallo di tempo fisso. Se, per qualche motivo, ciò non accadesse, il gioco dovrebbe semplicemente saltare un frame (o possibilmente schiantarsi), rallentando efficacemente sia il disegno che la fisica del gioco a metà velocità.
In particolare, i giochi che utilizzavano funzionalità come il rilevamento delle collisioni degli sprite hardware dovevano praticamente funzionare in questo modo, poiché la loro logica di gioco era indissolubilmente legata al rendering, che veniva eseguito in hardware a velocità fissa.
Utilizzare un timestep variabile per la fisica del gioco. Fondamentalmente, questo significa riscrivere il tuo ciclo di gioco per assomigliare a questo:
long lastTime = System.currentTimeMillis();
while (isRunning) {
long time = System.currentTimeMillis();
float timestep = 0.001 * (time - lastTime); // in seconds
if (timestep <= 0 || timestep > 1.0) {
timestep = 0.001; // avoid absurd time steps
}
update(timestep);
draw();
// ... sleep until next frame ...
lastTime = time;
}
e, all'interno update()
, adeguando le formule di fisica per tenere conto del timestep variabile, ad esempio in questo modo:
speed += timestep * acceleration;
position += timestep * (speed - 0.5 * timestep * acceleration);
Un problema con questo metodo è che può essere difficile mantenere la fisica (principalmente) indipendente dal timestep ; non vuoi davvero che la distanza che i giocatori possano saltare dipenda dal loro frame rate. La formula che ho mostrato sopra funziona bene per un'accelerazione costante, ad esempio sotto gravità (e quella nel post collegato fa abbastanza bene anche se l'accelerazione varia nel tempo), ma anche con le formule fisiche più perfette possibili, lavorare con i galleggianti è probabile che produce un po 'di "rumore numerico" che, in particolare, può rendere impossibile la riproduzione esatta. Se è qualcosa che pensi di voler, potresti preferire gli altri metodi.
Disaccoppia l'aggiornamento e disegna i passaggi. Qui, l'idea è di aggiornare lo stato del gioco usando un timestep fisso, ma eseguire un numero variabile di aggiornamenti tra ciascun frame. Cioè, il tuo loop di gioco potrebbe assomigliare a questo:
long lastTime = System.currentTimeMillis();
while (isRunning) {
long time = System.currentTimeMillis();
if (time - lastTime > 1000) {
lastTime = time; // we're too far behind, catch up
}
int updatesNeeded = (time - lastTime) / updateInterval;
for (int i = 0; i < updatesNeeded; i++) {
update();
lastTime += updateInterval;
}
draw();
// ... sleep until next frame ...
}
Per rendere più fluido il movimento percepito, potresti anche desiderare che il tuo draw()
metodo interpoli le cose come le posizioni degli oggetti senza intoppi tra lo stato di gioco precedente e quello successivo. Ciò significa che è necessario passare l'offset di interpolazione corretto al draw()
metodo, ad esempio in questo modo:
int remainder = (time - lastTime) % updateInterval;
draw( (float)remainder / updateInterval ); // scale to 0.0 - 1.0
Dovresti anche fare in modo che il tuo update()
metodo calcoli effettivamente lo stato del gioco un passo avanti (o forse più, se desideri eseguire l'interpolazione della spline di ordine superiore) e farlo salvare le posizioni degli oggetti precedenti prima di aggiornarle, in modo che il draw()
metodo possa interpolare tra loro. (È anche possibile estrapolare le posizioni previste in base alle velocità e alle accelerazioni degli oggetti, ma questo può sembrare a scatti soprattutto se gli oggetti si muovono in modi complicati, causando spesso il fallimento delle previsioni.)
Un vantaggio dell'interpolazione è che, per alcuni tipi di giochi, può consentire di ridurre in modo significativo la frequenza di aggiornamento della logica di gioco, mantenendo comunque l'illusione di un movimento fluido. Ad esempio, potresti essere in grado di aggiornare il tuo stato di gioco solo, diciamo, 5 volte al secondo, pur disegnando da 30 a 60 fotogrammi interpolati al secondo. Nel fare ciò, potresti anche prendere in considerazione l'idea di interfogliare la logica del tuo gioco con il disegno (ovvero avere un parametro nel tuo update()
metodo che gli dice di eseguire solo x % di un aggiornamento completo prima di tornare) e / o eseguire la fisica del gioco / la logica e il codice di rendering in thread separati (attenzione ai problemi di sincronizzazione!).