Particle Engine - 2
(2619 kelime) (273 okuma)
Particle Engine 2
Giriş
PE ile çeşitli efektler
yapılabileceğinden bahsetmiştik ilk yazıda. Örnek olarak da saçma sapan
bir efekt (karıncalı ekran) gibi birşey yapmıştık. Bu yazıda, duman
efekti yapmayı deneyeceğiz. Aynı zamanda PE sistemimizi de biraz daha
kolay kullanılır hale getirmeye çalışacağız.
Duman
Öncelikle duman
efektini parçacıklarla nasıl yapacağımızdan bahsedelim. Parçacık neydi
hatırlayalım. Her zaman kamera yönüne bakan bir adet poligon (şimdilik).
Poligonumuzu (ekran kartları sevdiği için) iki adet üçgenden
oluşturuyoruz. Amacımız, aynı özelliklere sahip bir grup parçacığı
birlikte hareket ettirip dumana benzetmek. Bunun için her parçacığı
dumana benzeyen bir doku ile kaplayacağız. Dokumuz alfa değerine de
sahip, yani bazı bölgeleri geçirgen, yani dumana benzemekte. Alpha
blending konusundan bu yazıda bahsetmeyeceğim (pek alakalı olmadığından
ve gereksiz dağılmamak için), umarım bu kadar bilinmesi yeterli. Merak
edenler örnek koda göz atabilir tabi her zaman. Özetlersek,
parçacığımız, dumana benzer bir poligon yani.
Tabi tek bir
parçacığın özellikleri kadar, tüm parçacıkların birlikte, dumana benzer
bir şekilde hareket etmesini sağlamalıyız. Tüm parçacıklar aynı noktadan
hareketlerine başlayacaklar, ve belirli bir yöne doğru, farklı ufak
sapmalarla hareket edecekler. Bittiğinde, son görüntü şuna benzeyecek:
Çok güzel
olmayabilir, ama başlangıç için idare eder umarım. İlk bölümdeki
XParticle sınıfı (adı artık Particle sadece) biraz değişikliğe uğradı,
hemen bir göz atalım.
using namespace XMath; namespace XParticle{ class Particle { Vector3 m_position; // her parcacik kendine ait bir pozisyona sahip Vector3 m_velocity; // ve hiza float m_halfsize; // ve boyuta (bu henuz kullanilmiyor demoda) public: float getHalfSize() const {return m_halfsize;} void setHalfSize(float hs) {m_halfsize = hs;} const Vector3& getPos() const {return m_position;} void setPos(const Vector3& pos){m_position = pos;} const Vector3& getVelocity() const {return m_velocity;} void setVelocity(const Vector3& vel) {m_velocity = vel;} }; }
Neler değişmiş
bakalım. Öncelikle PE ile ilgili tüm sınıfları XParticle namespace'i
altına toplamaya karar verdim. Projeniz ilerledikçe ve büyüdukçe,
modüler bir yapı oturtmak oldukça faydalı, buna alışsanız iyi olur.
Namespace kavramının ne olduğundan bahsetmemeye karar verdim (önce on
satır kadar anlattıktan sonra) çünkü yazı hem dağılacak hem de uzayacak.
İyi birşey olduğunu söyleyip geçiyorum.
İkinci
değişiklik, artık sınıfın içinde yalnızca değişkenlere erişmeye yarayan
erişim fonksiyonları var (accessor functions). Eski versiyondaki Draw ve
Update fonksiyonları burada değil artık. Amacımız performans kazanmak
tabi ki yine ama aynı zamanda da kolay geliştirilebilir bir mimariye
ulaşmak. Mimari konusuna değinelim önce. Nesne tabanlı mantığa göre, bu
sınıf sadece parçacığımızın özelliklerini tutuyor, verileri yani.
Bunların nasıl değişeceğinden, ekrana nasıl çizileceğinden ise habersiz.
Böylelikle veri ve veri işleme yöntemini ayırmış oluyoruz. Örneğin çizim
işlemi için OpenGL ve DirectX kullanan ayrı iki versiyon yazabiliriz
başka sınıflarda. O sınıflar nasıl çizim işlemini yapmak isterlerse o
şekilde yapacaklar, biz Particle sınıfını değiştirmek durumunda
kalmayacağız. İkincisi, performans açısından ise, onlarca parçacık için
belki de ayrı ayrı fonksiyon çağırma işlemi zaman kaybı olabilir. Tümünü
tek bir seferde veya yerde çizmek isteyebiliriz. Poligonları önce
hafızada toparlayıp, tümünü sonradan karta göndermek isteyebiliriz.
Draw() ve Update() metodlarını ayırmak iyi fikir gibi gözüküyor kısacası.
Son belirgin
değişiklik ise, constructor ve destructor açık olarak tanımlı değil
artık sınıfta. Çünkü ihtiyacımız yok, en azından şimdilik. Parçacıkların
ilk değerlerinin atanması işini de başka bir yerde yapacağız.
Mimari
Duman meselesini
bir süreliğine kenara bırakıp mimariye dönelim biraz, hazır nesne
tabanlı falan demeye başlamışken.
İlk yazıda bazı
özellikler saymıştık, bu sistemin sahip olmasını istediğimiz. Kısaca üç
başlık altında toplayabiliriz sanırım; performans, kullanım kolaylığı,
ve geliştirilebilirlik. Performansın önemi ile kafa ütülemeyeceğim bu
sefer, o ilk sırada işte zaten. Kullanım kolaylığından kastım PE'yi
motor, demo veya oyun projemde kullanırken fazla uğraşmak istemiyorum.
CreateSmoke() gibi basit, tek bir fonksiyon ile duman sistemi yaratayım,
Render() gibi bir fonksiyon ile ekrana çizeyim, her frame'de Update()
ile de güncelleyeyim. Yeter mi? Yetmez! Kullandığımız tek efekt tabi ki
duman olmayacak (ateş esprisi?), yağmur, ateş, patlama aklıma ilk gelen
diğer efektler. Her bir farklı efekt için ayrı ayrı CreateFire(),
CreateRain() falan demektense tek bir arabirim (interface) ile tüm bu
efektleri kontrol edebilmek gayet güzel olur. Yani bir array (dizi)
içinde farklı efektleri tutalım ve kontrol edebilelim. Bir kez
yarattıktan sonra efektin detayları ile biz ilgilenmeyelim, bilmeyelim,
sistem kendi kendine duman ise duman gibi davransın, ateş ise ateş gibi. Ek
olarak, efekt yaratma işini de script dosyacıklarından okuyayım, efekt
ile ilgili başlangıç ve kullanım parametreleri bir text dosyasında
dursun, böylelikle her değişiklikte kodu yeniden derlemek zorunda
kalmayayım. Böylelikle ileride yeni efektler de sistemimizin içine
kolaylıkla ekleyebilelim (geliştirilebilirlik başlığını da çaktırmadan
halletmiş olduk bu arada). Tüm bunları başarmak hedef, ancak tabi ki tek
bir döküman ile anlatabileceğimi sanmıyorum. Adım adım ilerlemeye
başlayalım.
Bir önceki
yazıda kullandığımız XParticles sınıfı ile parçacık sistemimizin
kontrolünü hedefliyorduk. Ancak tek bir sınıf ile tüm bu efektleri
yapmak hem zor hem de çirkin. İşe bir adet “Base Class” ile
başlayalım. Tüm parçacık sistemlerimizi (duman, yağmur, vs) tek bir
sınıftan türeteceğiz. Bu sınıfı kendi başına yaratmak mümkün (abstract
base class) olmayacak ancak tüm parçacık sistemlerinin ortak
özelliklerini ve işlemlerinin imzasını taşıyacak. Sınıfın adı IParticleSystem
olsun.
Yapmak
istediğimiz şöyle bir şey;
IParticleSystem* test = new SmokeSystem; // bu duman olsun IParticleSystem* test2 = new RainSystem; // bu yagmur olsun
Böylelikle, tüm
sistemleri tek bir tip altında toplayıp, bir dizide tutabiliriz. Kontrol
etmek de çok kolaylaşır. Fazla kafa karıştırmadan IParticleSystem
sınıfının tanımını yazalım.
namespace XParticle{ class IParticleSystem { protected: std::vector<Particle*> m_particles; // parcaciklar burada dursun XMath::Vector3 m_origin; // sistemin merkezi public: virtual void reset() = 0; // tum sistemi ilk haline dondurmek icin virtual void initialize() = 0; // sistemi baslatmak icin virtual void render() const = 0; // cizim virtual void emit(unsigned int count) = 0; // yeni parcacik yaratmak virtual void update(float time_elapsed) = 0; // parcaciklari guncellemek IParticleSystem(void): m_origin(Vector3(0, 0, 130)){} // constructor virtual ~IParticleSystem(void) { // parcaciklarin temizligi std::vector<Particle*>::iterator it = m_particles.begin(); while (it != m_particles.end()) { delete *it; ++it; } } }; }
İlk satırda görüldüğü üzere, bu base
class altında Particle sınıfına işaretçilerden olusan bir std::vector
tutuyoruz. Kısacası, parçacıklar burada tutuluyor. Aynı zamanda tüm
parçacıkların merkezi de burada. İleride bu sınıfa başka eklemeler (ya
da çıkarmalar) yapacağız. Örneğin merkezi buraya eklemek yerine, siz,
kendi sisteminizde, scene graph gibi bir sistem kullanıyorsanız, IParticleSystem
sınıfımızı kendi Node sınıfinızdan türetebilirsiniz. (kafası
karışanlar dert etmesin son cümlelerimi unutup okumaya devam etsinler)
Bu sınıfın
fonksiyonlarında bir gariplik var. Tanımın başındaki virtual kelimesi
ve sondaki = 0 kısmı bazılarınıza yeni gelebilir. Virtual
fonksiyonların ne olduğunu açıklamak zor ve yazı c++ kursuna dönüşebilir
her an. Bilenler bilmeyenlere anlatsın, anlatacak bulamayanlar forumda
sorsun artık (ben cevap veririm söz). = 0 kısmına gelince, bu
tanım şekli ile fonksiyonu pure virtual olarak tanımlamış
oluyoruz, fonksiyonun içeriği için başka hiçbirşey yazmıyoruz, yani
fonksiyonun bir gövdesi yok. En az bir adet pure virtual fonksiyona
sahip sınıflar abstract base class olarak adlandırılır. Bu sınıfları
kendi başına yaratmaya çalıştığınız zaman, derleyiciniz hatayı verir.
Yani şöyle bir satır yasak;
IParticleSystem* test = new IParticleSystem; // hata
Zaten bunu da istemiyoruz değil mi? Yani
kimse gidip de ne olduğu belirsiz bir efekti yaratmaya calışmamalı, bunu
böylece önlemiş olduk işte. C++'ın enteresan noktalarından birisi kendi
kendini kısıtlamayı öğrenme kısmı bence.
Son bir nokta, bu sınıftan bir efekt
sınıfı (duman?) türetebilmek ve türettikten sonra da
IParticleSystem* test = new SmokeSystem;
satırındaki gibi yaratabilmek için bir
koşul var, tüm pure virtual fonksiyonları, SmokeSystem
sınıfının içinde yazmak. Yani reset, initialize, render,emit ve update fonksiyonlarının gövdeleriyle ve aynı
tanım şekli ile SmokeSystem sınıfında yazmalıyız. Teker teker bu
fonksiyonlara bir göz atalım hemen.
void SmokeSystem::emit(unsigned int count) { Particle* dummy(0); for(unsigned int i = 0; i < count; ++i) { dummy = new Particle; setParticle(*dummy); m_particles.push_back(dummy); } }
emit fonksiyonunda
bir miktar (count adet) Particle objesi yaratıp, ilk değerlerini atayan SmokeSystem::setParticle()
fonksiyonundan geçiriyoruz. SetParticle fonksiyonu IParticleSystem
ana sınıfında mevcut değil, sadece bu sınıfta tanımlı. Şu şekilde:
void SmokeSystem::setParticle(Particle& p) { p.setPos(m_origin); p.setHalfSize(5); p.setVelocity(Vector3(RandFloat*0.0015f-0.00075f, 0.002f+RandFloat*0.002f, RandFloat*0.0005f-0.00025f)); }
Dumana benzemeye yarayacak hız vektörü
burada belirleniyor. RandFloat makrosu, bize bir random değer atıyor.
initialize fonksiyonu sadece emit
fonksiyonun çağırıyor şimdilik. reset fonksiyonu ise henüz boş. SmokeSystem sınıfı ayrıca bir adet doku ismine sahip.
Bu doku ismi sizin göremediğiniz bir TextureManager sınıfında
önceden yüklenmiş bir adet isim. TextureManager sınıfı şimdilik sizi ilgilendirmiyor,
dokuların yüklendiği, tutuldugu, ayrı bir modül. SmokeSystem::render
fonksiyonu içinde bu doku çağırılıyor, gerekli alfa blending seçeneği
seçiliyor, ve Particle objeleri teker teker bu doku ile çiziliyor
(OpenGL kodu). Son olarak SmokeSystem::update() fonksiyonu
içinde, zaman çarpı hız eşittir alınan yol ile, her parçacığın yeni
pozisyonu hesaplanıyor, ayrıca gerekliyse yeni parçacık yaratılıyor.
Detaylar için örnek koda bakmanız yeterli.
Son olarak, bu duman
sisteminin kontrolünden bahsedelim. Bu sistem nerede nasıl yaratılıyor,
vs vs.. Başta da söylediğim gibi, tek bir efekt gösteren örnek demoda
sistemin kullanım kolaylığını göstermek zor. Aklınızda hep, farklı
efektlerle dolu bir PE yapmaya çalıştığımız bulunsun. Sistemlerin
yaratılmasını mümkünse bir script dosyasından halledelim demiştik. Şu an
için böyle bir script sistemine sahip değiliz, ama PE'mizi buna göre
ileriye dönük tasarlarsak iyi olur. Bunun için factory pattern'a
benzer bir design pattern kullanacağız. Bir adet ParticleSystemFactory
sınıfımız olacak. Bu sınıfta da tek bir adet createSystem
fonksiyonu (şimdilik yeterli) bulunacak. Dökümanı fazla uzatmamak için
kodu koymuyorum buraya, örnek koddan bakın bir yandan. createSystem fonksiyonu
bir adet string alıyor, ileride bu string scriptimizin adı olabilir,
şimdilik kullanılmıyor. Onun yerine duman sistemi için gerekli doku
yükleniyor ve SmokeSystem objesi yaratılıyor, bu nesne bir
vector'e ekleniyor (işimiz bittiğinde temizlik işi otomatik olabilsin
diye). Bu vector ParticleSystemFactory sınıfının destructor'unda
temizleniyor. Böylelikle, yeni bir parçacık sistemi yarattığınızda onu
nerede sileceğinizi düşünmeniz gerekmiyor. Son olarak da fonskiyonumuz
bir adet IParticleSystem*'i donduruyor ki dışarıda bu duman
sistemini çalıştırabilelim. Son bir nokta, bu ParticleSystemFactory
sınıfının singleton yapılmasi, şimdilik geçiyorum bu noktayı.
Sistemin ana
programdan kullanımına gelince, gayet basit.
m_pParticles = m_factory.createSystem("kullanilmayan_bir_isim"); m_pParticles->initialize();
ile sistemi yaratiyoruz. Cizim sirasinda
ise;
m_pParticles->render(); m_pParticles->update(5);
ile çizip güncelliyoruz. Hepsi bu kadar.
Sonra?
Eklenebilecek
hala pekçok şey var. Yeni efektler ve scripting yeteneğinin yanında,
mimari açıdan bakarsak Particle sınıfı da IParticleSystem gibi bir base class'a dönüştürülebilir. Daha önemlisi, IParticleSystem
içinde aslında birbirinden bağımsız süreçleri kontrol ediyoruz, bunları
da ayrı sınıf hiyerarşileri altında toplamaya çalışmak mümkün (IParticleEmitter,IParticleRenderer,
vs), ve bunlar script'den doğru şekilde yüklenip IParticleSystem sınıfına
kayıt edilebilir. Daha fazla kafanızı şişirmeyeyim, bu noktaya kadar
yazıyı okumayı sürdüren kaç kişi vardır emin değilim zaten.
Örnek Kod
Örnek programı yine SDL ve
VC++7 kullanarak hazırladım. Bu kez, bu dökümanla ilgili olmayan bazı
sınıfları başka bir yerde toparladım ve lib olarak programa entegre
ettim. Örnek kodu gönlünüzce kurcalayın, değiştirin, kullanın. Ek lib'e
fazla güvenmenizi ve kullanmanızı önermem.
[
Örnek kodu buradan indirebilirsiniz ]
Bu yazı ile ilgili, hatalar,
görüşleriniz, eleştirileriniz için mentat@cfxweb.net
adresini kullanabilirsiniz. Ve tabi www.oyunyapimi.org
forumlarını. Umarım yazdıklarım işinize yarar.
mentat :: 11.11.2003 :: www.oyunyapimi.org
|