OpenGL'de bir oyun döngüsü oluşturmak için iyi bir yol


31

Şu anda okulda OpenGL öğrenmeye başlıyorum ve geçen gün basit bir oyun yapmaya başladım (kendi başıma okul için değil). Freeglut kullanıyorum ve bunu C'ye yapıyorum, bu yüzden oyun döngüm için glutIdleFunctüm çizimleri ve fiziği bir seferde güncellemek için yaptığım bir işlevi gerçekten kullanıyordum . Bu, kare hızını çok fazla önemsemediğim basit animasyonlar için gayet iyiydi, ancak oyun çoğunlukla fizik temelli olduğu için, güncellemenin ne kadar hızlı olduğunu gerçekten bilmek istiyorum.

Bu yüzden ilk girişimim, önceki çağrımdan ne kadar zaman geçtiğini takip etmek için glutIdleFunc( myIdle()) işlevine geçmek ve her milisaniyede bir fiziği (ve şu anda grafikleri) güncellemek oldu. Bunu timeGetTime()(kullanarak <windows.h>) yapardım . Ve bu beni düşündürdü, boşta işlevini kullanmak gerçekten oyun döngüsüne devam etmek için iyi bir yol mu?

Sorum şu, OpenGL'de oyun döngüsünü uygulamak için daha iyi bir yol nedir? Boşta işlevini kullanmaktan kaçınmalı mıyım?



Bu soru OpenGL'ye daha spesifik olduğunu hissediyorum ve döngü GLUT olsun tavsiye edilir
Jeff

1
Adamım , daha büyük projeler için GLUT kullanmayın . Yine de küçük olanlar için iyi.
bobobobo

Yanıtlar:


29

Basit cevap hayır, glutIdleFunc geri çağrıyı bir tür simülasyonu olan bir oyunda kullanmak istemezsiniz. Bunun nedeni, bu işlevin animasyonu boşa çıkarması ve Windows olayı işlemeden kodu eşzamanlı olarak çizmemesidir. Başka bir deyişle, pencere olaylarını almak ve teslim etmek, stant kodunu (ya da bu geri aramaya ne koyduysanız), bu etkileşimli bir uygulama için mükemmeldir (etkileşim, sonra yanıt), ancak fizik veya oyun durumunun ilerlemesi gereken bir oyun için mükemmel etkileşimden veya oluşturma süresinden bağımsız.

Girdi işlemeyi, oyun durumunu ve kodunu tamamen ayırmak istiyorsunuz. Doğrudan grafik kütüphanesini içermeyen, bunun için kolay ve temiz bir çözüm vardır (örn. Taşınabilir ve görselleştirmesi kolaydır); Tüm oyun döngüsünün zaman üretmesini ve simülasyonun üretilen zamanı (parçalarla) tüketmesini istiyorsunuz. Ancak anahtar, simülasyonunuzun harcadığı süreyi animasyonunuza entegre etmektir.

Bu konuda bulduğum en iyi açıklama ve öğretici, Glenn Fiedler'in Zaman Aşımını Düzelt

Bu eğitimde tam bir tedavi vardır, ancak gerçek bir fizik simülasyonuna sahip değilseniz, gerçek entegrasyonu atlayabilirsiniz ancak temel döngü hala (aşağılık sözde kodda) değerine kadar kaymaktadır:

// The amount of time we want to simulate each step, in milliseconds
// (written as implicit frame-rate)
timeDelta = 1000/30
timeAccumulator = 0
while ( game should run )
{
  timeSimulatedThisIteration = 0
  startTime = currentTime()

  while ( timeAccumulator >= timeDelta )
  {
    stepGameState( timeDelta )
    timeAccumulator -= timeDelta
    timeSimulatedThisIteration += timeDelta
  }

  stepAnimation( timeSimulatedThisIteration )

  renderFrame() // OpenGL frame drawing code goes here

  handleUserInput()

  timeAccumulator += currentTime() - startTime 
}

Bu şekilde yaparak, render kodunuzdaki, giriş işleminizdeki veya işletim sisteminizdeki duraklamalar oyun durumunuzun gerisinde kalmasına neden olmaz. Bu yöntem aynı zamanda taşınabilir ve grafik kütüphanesinden bağımsızdır.

GLUT iyi bir kütüphanedir, ancak kesinlikle olaya dayalıdır. Geri aramaları kaydedersiniz ve ana döngüden çıkarsınız. GLUT kullanarak ana döngünüzün kontrolünü her zaman devredersiniz. Etrafında dolaşmak için kesmek vardır, ayrıca zamanlayıcıları ve benzeri şeyleri kullanarak harici bir döngü taklit edebilirsiniz, ancak başka bir kütüphane muhtemelen daha iyi (daha kolay) bir yoldur. Pek çok alternatif var, işte birkaçı (iyi belgeler ve hızlı öğreticiler ile):

  • Girdi olaylarını satır içi (kendi ana döngünüzde) alabilmenizi sağlayan GLFW .
  • Ancak SDL , vurgulanması özellikle OpenGL değil.

İyi makale. Yani bunu OpenGL'de yapmak için glutMainLoop'u kullanmam, yerine kendim? Bunu yaparsam, glutMouseFunc ve diğer işlevleri kullanabilir miyim? Bu mantıklı umarım, hala tüm bunlar için oldukça yeniyim
Jeff

Ne yazık ki değil. GLUT'a kaydedilen tüm geri aramalar, olay kuyruğu boşaltılır ve tekrarlanırken glutMainLoop içinden çıkarılır. Cevap organındaki alternatiflere bazı bağlantılar ekledim.
charstar

1
Başka bir şey için GLUT'u kesmek için +1. Bildiğim kadarıyla, GLUT hiçbir zaman test uygulamalarından ibaret değildi.
Jari Komppa

Yine de sistem olaylarını ele almanız gerekiyor, değil mi? Anladığım kadarıyla glutIdleFunc kullanarak sadece (Windows'ta) GetMessage-TranslateMessage-DispatchMessage döngüsünü idare eder ve alınacak bir mesaj olmadığında işlevinizi çağırır. Yine de kendin yaparsın ya da başvurunun tepkisiz olarak işaretlenmesinde OS'nin sabırsızlığına kapılıyorsun, değil mi?
Ricket

@Ricket Teknik olarak, glutMainLoopEvent (), gelen geri çağrıları arayarak gelen olayları yönetir. glutMainLoop () etkili bir şekilde {glutMainLoopEvent (); IdleCallback (); } Burada önemli olan nokta burada yeniden çizilmesi, yani , aynı zamanda olay döngüsü içinde ele bir geri arama. Sen edebilir bir zaman odaklı bir gibi bir olay kaynaklı kütüphane davranmaya yapmak ama Maslow'un Çekiç ve bütün bu ...
charstar

4

Bence glutIdleFuncsadece iyi; Eğer kullanmamış olsaydın, yine de el ile yapmaktan vazgeçeceğin şey bu. Sabit bir düzende adlandırılmayan sıkı bir döngüye ne sahip olursanız olun, sabit zaman döngüsüne dönüştürmek ya da değişken zaman döngüsüne dönüştürmek isteyip istemediğinizi seçmeniz gerekir. Bütün matematik hesaplarının enterpolasyon süresi için olduğundan emin ol. Ya da bir şekilde bunların bir karışımı.

Kesinlikle myIdlegeçeceğiniz bir işleve sahip olabilirsiniz glutIdleFuncve bunun içinde geçen zamanı ölçebilir, sabit zamanlamanıza göre bölebilir ve birçok kez başka bir işlev çağırabilirsiniz.

İşte budur değil yapmak:

void myFunc() {
    // (calculate timeDifference here)
    int numOfTimes = timeDifference / 10;
    for(int i=0; i<numOfTimes; i++) {
        fixedTimestep();
    }
}

fixedTimestep her 10 milisaniyede bir çağrılmaz. Aslında, gerçek zamandan daha yavaş çalışacaktır ve sorunun asıl nedenini timeDifference10'a böldüğünüzde geri kalanının kaybında yatarsa ​​telafi etmek için geçiştirme sayıları ile sonuçlanabilirsiniz .

Bunun yerine, böyle bir şey yapın:

long queuedMilliseconds; // static or whatever, this must persist outside of your loop

void myFunc() {
    // (calculate timeDifference here)
    queuedMilliseconds += timeDifference;
    while(queuedMilliseconds >= 10) {
        fixedTimestep();
        queuedMilliseconds -= 10;
    }
}

Şimdi bu döngü yapısı fixedTimestep, herhangi bir milisaniyeyi geri kalanı veya bunun gibi bir şey kaybetmeden 10 milisaniyede bir işlevinizin çağrılmasını sağlar .

İşletim sistemlerinin çok görevli olması nedeniyle, her 10 milisaniyede bir çağrılan bir işleve güvenemezsiniz; ancak yukarıdaki döngü yeterince hızlı olacaktı, umarım yeterince yakın olacaktır ve her çağrının fixedTimestepsimülasyonunuzu 10 milisaniye değerinde artıracağını varsayabilirsiniz .

Lütfen bu cevabı ve özellikle cevapta verilen bağlantıları okuyunuz .

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.