Ada dua hal penting untuk membuat gerakan tampak mulus, yang pertama jelas bahwa apa yang Anda render harus mencocokkan dengan kondisi yang diharapkan pada saat bingkai disajikan kepada pengguna, yang kedua adalah Anda perlu menampilkan bingkai kepada pengguna pada interval yang relatif tetap. Menyajikan bingkai pada T + 10ms, lalu yang lain pada T + 30ms, lalu yang lain pada T + 40ms, akan tampak bagi pengguna untuk menilai, bahkan jika apa yang sebenarnya ditunjukkan untuk waktu itu adalah benar sesuai dengan simulasi.
Loop utama Anda tampaknya tidak memiliki mekanisme gating untuk memastikan bahwa Anda hanya membuat secara berkala. Jadi kadang-kadang Anda mungkin melakukan 3 pembaruan antara render, kadang-kadang Anda mungkin 4. Pada dasarnya loop Anda akan merender sesering mungkin, segera setelah Anda mensimulasikan cukup waktu untuk mendorong keadaan simulasi di depan waktu saat ini, Anda akan lalu berikan status itu. Tetapi setiap variabilitas dalam berapa lama waktu yang dibutuhkan untuk memperbarui atau membuat, dan interval antara frame akan bervariasi juga. Anda memiliki stempel waktu tetap untuk simulasi Anda, tetapi stempel waktu variabel untuk rendering Anda.
Apa yang mungkin Anda butuhkan adalah menunggu tepat sebelum render Anda, yang memastikan bahwa Anda hanya akan mulai render pada awal interval render. Idealnya harus adaptif: jika Anda telah terlalu lama memperbarui / membuat dan awal interval telah berlalu, Anda harus merender segera, tetapi juga meningkatkan panjang interval, sampai Anda dapat secara konsisten membuat dan memperbarui dan masih dapat render berikutnya sebelum interval selesai. Jika Anda memiliki banyak waktu luang, maka Anda dapat secara perlahan mengurangi interval (yaitu meningkatkan frame rate) untuk membuat lebih cepat lagi.
Tapi, dan inilah kickernya, jika Anda tidak membuat frame segera setelah mendeteksi bahwa keadaan simulasi telah diperbarui menjadi "sekarang", maka Anda memperkenalkan alias sementara. Bingkai yang disajikan kepada pengguna disajikan pada waktu yang sedikit salah, dan itu sendiri akan terasa seperti gagap.
Ini adalah alasan untuk "timestep parsial" yang akan Anda lihat disebutkan dalam artikel yang telah Anda baca. Itu ada di sana untuk alasan yang bagus, dan itu karena kecuali jika Anda memperbaiki catatan waktu fisika Anda ke beberapa kelipatan terpisahkan tetap dari catatan waktu pembuatan render tetap Anda, Anda tidak bisa menampilkan bingkai pada waktu yang tepat. Anda akhirnya mempresentasikannya terlalu dini, atau terlalu terlambat. Satu-satunya cara untuk mendapatkan tingkat rendering yang tetap dan masih menyajikan sesuatu yang benar secara fisik, adalah dengan menerima bahwa pada saat interval rendering muncul, Anda kemungkinan besar akan berada di tengah-tengah antara dua catatan waktu fisika tetap Anda. Tetapi itu tidak berarti bahwa objek-objek tersebut dimodifikasi selama rendering, Hanya saja rendering harus menetapkan sementara di mana objek berada sehingga dapat membuat mereka di suatu tempat di antara di mana mereka sebelumnya dan di mana mereka setelah pembaruan. Itu penting - jangan pernah mengubah negara dunia untuk rendering, hanya pembaruan yang harus mengubah negara dunia.
Jadi untuk memasukkannya ke dalam loop pseudocode, saya pikir Anda membutuhkan sesuatu yang lebih seperti:
InitialiseWorldState();
previousTime = currentTime = 0.0;
renderInterval = 1.0 / 60.0; //A nice high starting interval
subFrameProportion = 1.0; //100% currentFrame, 0% previousFrame
while (true)
{
frameStart = ActualTime();
//Render the world state as if it was some proportion
// between previousTime and currentTime
// E.g. if subFrameProportion is 0.5, previousTime is 0.1 and
// currentTime is 0.2, then we actually want to render the state
// as it would be at time 0.15. We'd do that by interpolating
// between movingObject.previousPosition and movingObject.currentPosition
// with a lerp parameter of 0.5
Render(subFrameProportion);
//Check we've not taken too long and missed our render interval
frameTime = ActualTime() - frameStart;
if (frameTime > renderInterval)
{
renderInterval = frameTime * 1.2f; //Give us a more reasonable render interval that we actually have a chance of hitting
}
expectedFrameEnd = frameStart + renderInterval;
//Loop until it's time to render the next frame
while (ActualTime() < expectedFrameEnd)
{
//step the simulation forward until it has moved just beyond the frame end
if (previousTime < expectedFrameEnd) &&
currentTime >= expectedFrameEnd)
{
previousTime = currentTime;
Update();
currentTime += fixedTimeStep;
//After the update, all objects will be in the position they should be for
// currentTime, **but** they also need to remember where they were before,
// so that the rendering can draw them somewhere between previousTime and
// currentTime
//Check again we've not taken too long and missed our render interval
frameTime = ActualTime() - frameStart;
if (frameTime > renderInterval)
{
renderInterval = frameTime * 1.2f; //Give us a more reasonable render interval that we actually have a chance of hitting
expectedFrameEnd = frameStart + renderInterval
}
}
else
{
//We've brought the simulation to just after the next time
// we expect to render, so we just want to wait.
// Ideally sleep or spin in a tight loop while waiting.
timeTillFrameEnd = expectedFrameEnd - ActualTime();
sleep(timeTillFrameEnd);
}
}
//How far between update timesteps (i.e. previousTime and currentTime)
// will we be at the end of the frame when we start the next render?
subFrameProportion = (expectedFrameEnd - previousTime) / (currentTime - previousTime);
}
Agar hal ini berhasil, semua objek yang diperbarui perlu mempertahankan pengetahuan di mana mereka sebelumnya dan di mana mereka sekarang, sehingga rendering dapat menggunakan pengetahuan tentang di mana objek itu berada.
class MovingObject
{
Vector velocity;
Vector previousPosition;
Vector currentPosition;
Initialise(startPosition, startVelocity)
{
currentPosition = startPosition; // position at time 0
velocity = startVelocity;
//ignore previousPosition because we should never render before time 0
}
Update()
{
previousPosition = currentPosition;
currentPosition += velocity * fixedTimeStep;
}
Render(subFrameProportion)
{
Vector actualPosition =
Lerp(previousPosition, currentPosition, subFrameProportion);
RenderAt(actualPosition);
}
}
Dan mari kita paparkan timeline dalam milidetik, mengatakan rendering membutuhkan 3ms untuk menyelesaikan, memperbarui butuh 1ms, langkah waktu pembaruan Anda ditetapkan menjadi 5ms, dan catatan waktu render Anda mulai (dan tetap) pada 60ms [60Hz].
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
R0 U5 U10 U15 U20 W16 R16 U25 U30 U35 W32 R32
- Pertama kita inisialisasi pada waktu 0 (jadi currentTime = 0)
- Kami membuat dengan proporsi 1,0 (100% currentTime), yang akan menarik dunia pada waktu 0
- Ketika itu selesai, waktu sebenarnya adalah 3, dan kami tidak berharap frame berakhir hingga 16, jadi kami perlu menjalankan beberapa pembaruan
- T + 3: Kami memperbarui dari 0 hingga 5 (jadi setelah ini currentTime = 5, priorTime = 0)
- T + 4: masih sebelum frame berakhir, jadi kami memperbarui dari 5 hingga 10
- T + 5: masih sebelum frame berakhir, jadi kami memperbarui dari 10 hingga 15
- T + 6: masih sebelum frame berakhir, jadi kami memperbarui dari 15 hingga 20
- T + 7: masih sebelum akhir bingkai, tetapi currentTime hanya di luar ujung bingkai. Kami tidak ingin mensimulasikan lebih jauh karena hal itu akan mendorong kami melampaui waktu yang kami inginkan selanjutnya. Sebaliknya, kita menunggu dengan tenang untuk interval render berikutnya (16)
- T + 16: Saatnya untuk membuat lagi. priorTime adalah 15, currentTime adalah 20. Jadi jika kita ingin merender pada T + 16, kita 1ms dari jalan melalui timestep panjang 5ms. Jadi kita adalah 20% dari jalan melalui bingkai (proporsi = 0,2). Ketika kita membuat, kita menggambar objek 20% dari jalan antara posisi mereka sebelumnya dan posisi mereka saat ini.
- Kembali ke 3. dan lanjutkan tanpa batas.
Ada nuansa lain di sini tentang simulasi terlalu jauh sebelumnya, yang berarti input pengguna mungkin diabaikan meskipun terjadi sebelum bingkai benar-benar dibuat, tetapi jangan khawatir tentang hal itu sampai Anda yakin bahwa loop tersebut mensimulasikan dengan lancar.