Oyun döngülerinde oluşturma hakkında bilgi edinin

Bir oyun döngüsünü uygulamanın çok popüler bir yolu şu şekildedir:

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

Bununla ilgili birkaç sorun vardır. En temel düşünce, oyunun bir "çerçeve"nin ne olduğunu tanımlayabileceği fikridir. Farklı ekranlar farklı hızlarda yenilenir ve bu hız zamanla değişebilir. Kareleri ekranın gösterebileceğinden daha hızlı oluşturursanız kareleri ara sıra bırakmanız gerekir. Bunları çok yavaş oluşturursanız SurfaceFlinger düzenli aralıklarla alınacak yeni bir tampon bulamaz ve önceki kareyi yeniden gösterir. Bu durumların her ikisi de gözle görülür arızalara neden olabilir.

Yapmanız gereken, ekranın kare hızını eşleştirmek ve bir önceki kareden bu yana geçen süreye göre oyun durumunu ilerletmektir. Bu konuda izleyebileceğiniz birkaç yol vardır:

  • Android Frame Pacing kitaplığını kullan (önerilir)
  • BufferQueue'yu doldurun ve "arabellek değiştirme" geri basıncından yararlanın
  • Koreograf'ı kullanın (API 16+)

Android Frame Pacing kitaplığı

Bu kitaplığı kullanma hakkında bilgi edinmek için Uygun kare hızına ulaşma bölümüne bakın.

Sıra doldurma

Bunu uygulamak çok kolaydır: Tamponları mümkün olduğunca hızlı bir şekilde değiştirin. Android'in erken sürümlerinde bu durum, SurfaceView#lockCanvas() tarafından 100 ms boyunca uyku moduna geçeceğiniz bir cezaya neden olabilir. Şu anda Tempo BufferQueue tarafından ayarlanıyor ve BufferQueue, SurfaceFlinger'ın mümkün olduğu kadar hızlı bir şekilde boşaltılıyor.

Bu yaklaşımın bir örneğini Android Breakout'ta görebilirsiniz. Uygulamanın onDrawFrame() geri çağırmasını çağıran bir döngüde çalışan ve daha sonra arabelleği değiştiren GLSurfaceView'u kullanır. BufferQueue doluysa eglSwapBuffers() çağrısı, bir arabellek bulunana kadar bekler. SurfaceFlinger, bunları yayınlamak için yeni bir tane aldıktan sonra arabellekleri kullanıma sunduğunda kullanılabilir hale gelir. Bu durum VSYNC'te gerçekleştiği için çizim döngüsü zamanlaması yenileme hızıyla eşleşir. Çoğunlukla.

Bu yaklaşımda birkaç sorun vardır. İlk olarak, uygulama SurfaceFlinger etkinliğine bağlıdır. Bu, ne kadar iş yapılacağına ve diğer işlemlerle CPU zamanını mücadele edip etmediğine bağlı olarak farklı süreler alır. Oyun durumunuz arabellek değişiklikleri arasındaki süreye göre ilerlediği için animasyonunuz istikrarlı bir hızda güncellenmez. Ancak zaman içinde ortalaması tutarsızlıklar dikkate alınarak 60 fps'de çalışırken büyük olasılıkla ani yükselişleri fark etmezsiniz.

İkincisi, BufferQueue henüz dolmadığı için arabellek değişiminin ilk birkaçı çok hızlı bir şekilde gerçekleşir. Kareler arasındaki hesaplanan süre neredeyse sıfıra yakın olur. Bu nedenle, oyun hiçbir şey olmazsa birkaç kare oluşturur. Her yenilemede ekranı güncelleyen Breakout gibi bir oyunda, oyunun ilk başlatıldığı (veya duraklatılmadığı) durumlar hariç olmak üzere sıra her zaman dolu olur. Bu nedenle, değişikliğin etkisi fark edilmez. Animasyonu ara sıra duraklatıp ardından mümkün olduğunca hızlı moduna dönen bir oyunda garip kesintiler görülebilir.

Koreograf

Choreographer, bir sonraki VSYNC'te tetiklenen bir geri çağırmayı ayarlamanıza olanak tanır. Gerçek VSYNC süresi bağımsız değişken olarak aktarılır. Böylece uygulamanız hemen uyanmasa bile ekranın yenilenme süresinin ne zaman başladığına dair doğru bir fikir edinirsiniz. Geçerli saat yerine bu değeri kullanırsanız oyun durumu güncelleme mantığınız için tutarlı bir zaman kaynağı elde edilir.

Ne yazık ki her VSYNC'ten sonra geri arama almanız, geri çağırmanızın zamanında yürütüleceğini veya bu konuda yeterince hızlı bir şekilde harekete geçebileceğinizi garanti etmez. Uygulamanızın, nerede durduğunu algılaması ve kareleri manuel olarak indirmesi gerekir.

Bunun bir örneğini, Grafika’daki "Record GL uygulaması" etkinliğinde görebilirsiniz. Bazı cihazlarda (ör. Nexus 4 ve Nexus 5) sadece oturup izlerseniz etkinlik kareleri bırakmaya başlar. GL oluşturma işlemi önemsizdir, ancak bazen Görünüm öğeleri yeniden çizilir ve cihaz düşük güç moduna geçerse ölçüm/düzen geçişi çok uzun sürebilir. (systrace'e göre, Android 4.4'te saatler yavaşladığında 6 ms yerine 28 ms. sürer. Parmağınızı ekran üzerinde sürüklerseniz etkinlikle etkileşime girdiğinizi düşünerek saat hızı yüksek kalır ve hiçbir zaman kare düşmez.)

Basit çözüm, VSYNC zamanından sonra geçerli süre N milisaniyeden fazla olursa Choreographer geri çağırmasında kareden kurtulmak gerekiyordu. İdeal olarak, N değeri daha önce gözlemlenen VSYNC aralıklarına göre belirlenir. Örneğin, yenileme süresi 16,7 ms (60 fps) ise 15 ms.den daha uzun bir süre gecikirseniz kareden çıkarabilirsiniz.

"Record GL uygulaması"nın çalıştığını izlerseniz, bırakılan kare sayacının arttığını ve hatta kareler düştüğünde kenarlıkta yanıp sönen kırmızı bir ışık görürsünüz. Yine de gözleriniz çok iyi olmadığı sürece animasyon takılmaz. 60 fps'de uygulama, animasyon sabit bir hızda ilerlemeye devam ettiği sürece hiç kimse fark etmeden ara sıra kareleri bırakabilir. Ne kadar ilerleyebileceğiniz, bir ölçüde ne çizdiğinize, ekranın özelliklerine ve uygulamayı kullanan kişinin duraklamaları algılamada ne kadar iyi olduğuna bağlıdır.

İleti dizisi yönetimi

Genel olarak, SurfaceView, GLSurfaceView veya TextureView'da oluşturma yapıyorsanız bu işlemi özel bir iş parçacığında yapmanız gerekir. Kullanıcı arayüzü iş parçacığı üzerinde hiçbir zaman "ağır iş" veya belirsiz miktarda işlem yapmayın. Bunun yerine, oyun için iki iş parçacığı oluşturun: bir oyun ileti dizisi ve bir oluşturma ileti dizisi. Daha fazla bilgi için Oyununuzun performansını iyileştirme konusuna bakın.

Breakout ve "Record GL uygulaması", özel oluşturucu ileti dizileri kullanır ve bu iş parçacığındaki animasyon durumunu da günceller. Oyun durumu hızlı bir şekilde güncellenebildiği sürece bu, makul bir yaklaşımdır.

Diğer oyunlar, oyun mantığını ve oluşturma işlemini tamamen ayırır. 100 ms'de bir blok taşımaktan başka hiçbir şey yapmayan basit bir oyununuz varsa bunu az önce yapan özel bir iş parçacığınız olabilirdi:

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

(Sürükmeyi önlemek için uyku süresini sabit bir saate dayandırmak isteyebilirsiniz. sleep() tamamen tutarlı değildir vemoveBlock() sıfır dışında bir zaman alır-- yine de bunun anlamını biliyorsunuz.)

Çizim kodu uyandığında kilidi ele geçirir, blokun o anki konumunu alır, kilidi açar ve çizer. Çerçeveler arası delta sürelerine göre kesirli hareketler yapmak yerine, yalnızca nesneleri hareket ettiren bir iş parçacığınız ve çizim başladığında öğeleri bulundukları yere çeken başka bir iş parçanız olur.

Karmaşık bir düzende, yaklaşan etkinliklerin listesini uyanma zamanına göre sıralanmış olarak oluşturabilir ve bir sonraki etkinliğin zamanı gelene kadar uyku moduna geçebilirsiniz.