Oyun Programlarında Doğru Zamanlama
(1020 kelime) (405 okuma)
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
|