Oyun Programlarında Doğru Zamanlama

Oyun Programlarında Doğru Zamanlama

Oyun programlamada özellikle yeni başlayanların sıkça yaptığı bir hata, oyunlarını sadece kendi bilgisayar konfigürasyonlarında doğru hızda çalışacak şekilde programlamalarıdır. Oyundaki olayların, cisim hareket ve davranışlarının güncellenme hızlarını bilgisayar hızından bağımsız bir biçimde gerçekleştiremessek, hızlı bilgisayarlarda oyunumuzdaki olaylar yavaş bilgisayarlara göre olduka çabuk gelişecek ve bu da oynanabilirliği kötü yöde etkileyecektir. İsterseniz örnek olarak hayali bir oyun üzerinde aslında her oyunda üç aşağı beş yukarı değişmeyen oyun döngüsüne bir göz atalım ve doğru zamanlama ile yanlış zamanlama kullanımını sonuçlarıyla inceleyelim.

Sorun

Hayali oyunumuz günümüzde pek de ticari örneği yapılmayan soldan sağa hareketli, shot-em'up türünde bir oyun olsun. Uzay gemimizi ekranda hareket ettirip ekranın sağından oyuna giren ve çeşitli hareket desenleriyle ilerleyerek bize çarpmak veya attıkları mermiler ile bizi vurmaya çalışan düşman gemilerine karşı savaşıyoruz. Onlardan ve attıkları mermilerden kaçarken bir yandan da attığımız mermiler ile düşmanları vurmaya ve puanımızı arrtırmaya çalışıyoruz. Benim tasarladığım ana oyun döngüsü şu şekilde:

1: uzay gemimizi, attığımız mermileri, düşman mermilerini ve düşmanları son pozisyonlarında çiz
2: düşman mermisi, uzay gemimiz ve bizim mermilerle düşman gemileri arasındaki çarpışmaları kontrol et.
3: düşmaları yeni pozisyonlarına 1 birim hareket ettir, mermileri 2 birim hareket ettir.
4: gemi pozisyonumuzu basılan tuşlara göre 1 birim hareket edecek şekilde güncelle
5: 1. basamağa dön

Şimdi iki referans bilgisayar seçelim. Bir tanesi Pentium 200 MMX olsun.. (evet oldukça eski bir bilgisayar..) Diğeri ise son model, canavar gibi bir P4 2200.. İlk bilgisayara kısaca yavaş, ikincisine ise hızlı diyeceğim :)). Oyunumuzu iki bilgisayarda da çalıştırırsak yavaş bilgisayarda hızlı bilgisayara göre hareketlerin çok daha fazla yavaş olduğunu görürüz. Neden? .. Hayır cevap iki bilgisayarın saat hızları arasındaki uçurum değil.. Oyun döngüsünü programlamadaki yapılan hata, yani doğru zamanlamaya önem vermemek ve oyunu sabit hızda çalışacak şekilde gerçekleştirememek. Açıklayayım..

Döngüdeki 3. ve 4. basamaklar önemli. Bu basamaklarda kısaca nesnelerin hareketlerinin yani davranışların güncellenmesi yapılıyor. Buna yabancı arkadaşlar kısaca 'world update' diyorlar.. Bu olay tüm oyunlarda vardır. Kimi oyunlarda bu güncelleme sırasında karmaşık yapay zeka algoritmaları çalışırken , pong gibi nispeten basit bir oyunda topun hareket vektörüne göre sabit hızda sonraki pozisyonunun bulunması kadar basit de olabilir. Önemli olan sonuçta görsel yada işleyişe yönelik bazı değişikliklerin olmasıdır. Peki bizim yukarıda yanlış yaptığımız nokta neresi. 1 birim ve 2 birim hareket ettir tarzında bir söylem yanlış hakkında ipuçları veriyor aslında. Bakın bu oyun döngüsü hızlı çalışan bilgisayarımızda örneğin saniyede 200 defa dönüyor olabilir, yavaş bilgisayarda ise belki 20 belki de daha az dönecektir. 1 birim hareket eden düşman gemisi hızlı bilgisayarda saniyede 200 birim hareket ederken yavaş bilgisayarda ise ancak 20 birim hareket edecek. Evet böylelikle hızlı bilgisayarda oynayanlar o kadar hızlı bir oyunla karşılaşacaklar ki sanırım bu hızlı düşman karşısında şansları pek de fazla olmayacaktır.

Peki oyunlarımızı her bilgisayarda aynı hızda çalıştırmak için ne yapmalıyız. Bunu gerçekleştirmek için birkaç yöntem var. Ben bildiğim ve projelerimde uyguladığım bir yöntemi burada anlatmaya çalışacağım.

Çözüm

Bu yöntemde hareket ve davranışlar ile ilgili her tam sayı içeren bölgede sabit sayılar kullanmak yerine bir zaman çarpanı kullanacağız. Bu zaman çarpanı son oyun döngüsü başından itibaren geçen zamanı içerecek. Evet biraz karışık gibi gözüküyor fakat aslında öyle değil. Yine bir örnek ile açıklayalım.

Düşman gemisini hareket ettirmek için hayali bir düşman gemisi nesnesi üzerinde tanımlı move() yordamını çağırdığımızı düşünelim..Yani gemiyi oyunumuza göre 5 birim hareket ettirmek için şu kodu yazıyoruz:

düşmanGemi.move(5);

Bizim istediğim ise düşmanın her tür bilgisayarda eşit hızda, yani saniyede eşit bir birim hareket etmesi değil mi?. Mesela düşmanımız şöyle tasarlanmış olsun. Bu gemi saniyede 5 birim hareket edecek. Bizim oyun döngümüzün hızlı bilgisayarda saniyede 200 defa yavaşta ise saniyede 20 defa çağırıldığını hayal etmiştik. Bu durumda oyunu yavaş bir bilgisayarda yazdığımızı düşünürsek (evet genelde programcılar çok kazanmıyor :)).. kötü bir programcı olarak yaptığımız şey:

düşmanGemi.move( 5 * 1/20 );

Ana döngümüz saniyede 20 defa çağırıldığına göre kabaca düşman gemisi bir saniyede 5 birim gider.. Hmm peki hızlı bilgisayarda ne olur... Döngü saniyede 200 defa çağırılacağından gemi 10 kat daha hızlı gider.. Hızlı bilgisayarda programı yazsaydık şu satırı kullanırdık:

düşmanGemi.move( 5 * 1/200 );

Hareketteki bilgisayar hızı ile ilgili çarpanları özellikle açık bir biçimde yazdım (1/20 ve 1/200).  Doğru bir zamanlama, yani her bilgisayarda eşit hızda çalışmadaki anahtar nokta bu çarpanlarda.. Bir şekilde bu çarpanların her bilgisayara göre uygun değerleri alabilmesini sağlarsak problemimiz çözülecek. Peki bu sihirli çarpanı her bilgisayara göre nasıl bulacağız. Basit aslında. Bu çarpan ana döngünün saniyede tekrarı ile ters orantılı. Başka bir deyişle 2 ana döngü tekrarı arasında geçen süreye eşit. Dikkat ederseniz hızlı bilgisayarda bu süre saniyenin 200 de biri (1/200), yavaşta ise 20 de biri (1/20). O zaman ardaşık iki ana döngü tekrarı arasındaki zamanı bir şekilde ölçebilir ve bunu çarpan olarak kullanabilirsek sorun bitecek.

İki ana döngü tekrarı arasında geçen zamanı bulmak pek de zor değil . Bu zaman aslında bir ana döngünün tamamlanması için bilgisayarın harcadığı zamana eşit. Ana döngünün 1. adımından önce o anki zamanı alalım. Bir sonraki seferde , yani döngü bir kere döndüğünde ve tekrar 1. adıma geldiğimizde ise o anki zamanı tekrar alır ve bu değerden ilk aldığımız zaman değerini çıkarırsak iki döngü arasında geçen zamanı bulmuş oluruz. Bu noktada daha açıklayıcı olması açısından hemen örneklemek istiyorum.

BAŞ:
şimdiki_zaman = ZAMAN();
geçen_süre = şimdiki_zaman - önceki_zaman ;
önceki_zaman = şimdiki_zaman;

1. 2. ve 3. basamaklar burada gerçekleştirilir
...
düşmanGemi.move( 5 * geçen_süre );
...
diğer basamaklar buralarda gerçekleştirilir.
...
...
BAŞA DÖN;

Yukarıdaki hayali bir programlama dili ile yazılmış örnek kodu açıklamak gerekirse. ZAMAN() fonksiyonu ile o anki zaman saniye cinsinden alınıp şimdiki_zaman değişkenine aktarılıyor. Bu hayali yordamın gerçek hayattaki gerçekleştirimlerinde genelde geri dönen değer programımızın çalışmaya başladığı andan itibaren geçen zaman yada belirli bir sabit tarihten bu yana geçen zaman değeridir. Aslında ne olduğu önemli değil, çünkü bizi ilgilendiren değerin kendisi değil iki değer arasındaki fark. Diğer satırda geçen_süre değişkenine bir önceki zaman alımından beri geçen zaman değeri aktarılıyor. önceki_zaman değişkenini en son geçen döngüde güncellediğimiz gözönünde bulundurulursa geçen_süre değerinin iki ardaşık oyun döngüsü arasındaki zamana eşit olduğunu anlayabilirsiniz. Ve son olarak da önceki_zaman değeri şimdiki değeri ile güncelleniyor. Bu sayede oyunun sonuna kadar bu düzeneği sorunsuzca kullanabiliyoruz.
düşmanGemi.move( 5 * geçen_süre ); şeklindeki bir direktif ile artık gemimiz hızlı yada yavaş bilgisayar ayırt etmeksizin her sistemde eşit hızda hareket etmeyi garantiliyor. Bu yöntemde dikkatli okuyucular açık bir noktayı yakalamış olabilirler. Döngüye ilk girildiğinde önceki_zaman değişkeni henüz günlenmediğinden ilk çalışmada tutarsız bir davranış olacaktır. Bunu engellemenin en kolay yolu ise ana oyun döngüsüne girmeden hemen önce bir defaya mahsus olmak üzere önceki_zaman değerini ZAMAN() fonksionu ile güncellemek. Böylelikle bu pürüzlü durumuda bertaraf edebiliriz.

Notlar

Yukarıda anlattığım kavramların pratik kullanımlarına geçişte ilk araştıracağınız nokta uygun bir ZAMAN() fonksiyonu olacaktır. Burada size birkaç öneri vermek istiyorum. İlk önce SDL kütüphanesi kulanıyorsanız (www.libsdl.org) SDL_GetTicks() fonksiyonunu ZAMAN() fonksiyonu olarak kullanabilirsiniz. Bu fonksiyon programınızı başlattığınız andan itibaren geçen süreyi milisaniye cinsinden vermektedir. (milisaniye = saniyenin 1/1000 i..). Windows altında çalışan arkadaşlar timeGetTime() isimli yordamı kullanabilirler. Bu fonksiyon windows un restart edilmesinden itibaren geçen zamanı yine milisaniye cinsinde veriyor. Bu fonksiyonu kullanmak için winmm.lib kütüphanesini projenize eklemeyi unutmayın. Çözünürlüğü daha yüksek (yani zamanı ölçme doğruluğu daha iyi) bir fonksiyon için QueryPerformanceCounter() ve QueryPerformanceFrequency() fonksiyonlarına bir göz atın. Unix/Linux sistemleri altında çalışanlar gettimeofday() yordamını kullanabilirler.

Anlattığım kavramların gerçek bir program içinde uygulanışlarını görmek için sitemizin dökümanlar bölümünde bulunan OyunYapimi-4 isimli döküman ile birlikte gelen kod içerisinden main.cpp içindeki renderScene() yordamını incelemeniz yeterli olacaktır.

Elimden geldiğince bu oldukça önemli olan zamanlama konusunu açıklamaya çalıştım. Anlamadığınız herhangi bir konuda çekinmeden sitemiz forumlarını kullanarak bana sorularınızı yöneltebilirsiniz. Kendinize iyi  bakın.

Deniz Aydınoğlu :: 15/12/2003 :: www.oyunyapimi.org




Bu haberin geldigi yer: oyunyapimi.org
http://www.oyunyapimi.org

Bu haber icin adres:
http://www.oyunyapimi.org/modules.php?name=Sections&op=viewarticle&artid=48