Particle Engine - 2

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




Bu yazının bulunduğu site: OyunYapimi.org
http://www.oyunyapimi.org

Bu yazı için adres:
http://www.oyunyapimi.org/modules.php?name=Sections&op=viewarticle&artid=47