Oyun verilerini / mantığı görüntülemeden ayırma


21

C ++ ve OpenGL 2.1 kullanarak bir oyun yazıyorum. Verileri / mantığı renderlemeden nasıl ayırabilirim diye düşünüyordum. Şu anda çizim uygulamak için saf bir sanal yöntem sunan 'Renderable' temel sınıfını kullanıyorum. Ancak her nesnenin çok özel bir kodu vardır, yalnızca nesne gölgelendirici üniformalarını nasıl düzgün şekilde ayarlayacağınızı ve tepe dizisi arabellek verilerini nasıl düzenleyeceğini bilir. Kodumun her yerine birçok gl * fonksiyon çağrısı ile son veriyorum. Nesneleri çizmenin genel bir yolu var mı?


4
Nesnenize gerçekten yenilebilir bir şey eklemek ve nesnenizin bu üyeyle etkileşmesini sağlamak için kompozisyon kullanın m_renderable. Bu şekilde, mantığınızı daha iyi ayırabilirsiniz. Renderable "arabirimini" aynı zamanda fiziği, AI ve ne de olsa genel nesneler üzerinde zorlamayın. Bundan sonra rendeble'ları ayrı ayrı yönetebilirsiniz. Bir şeyleri daha da fazla çözmek için OpenGL işlev çağrıları üzerinden bir soyutlama katmanına ihtiyacınız var. Bu nedenle, iyi bir motorun, çeşitli cana yakın uygulamalarının içinde herhangi bir GL API çağrısı yapmasını beklemeyin. Mikro cevizde.
teodron

1
@teodron: Neden bunu bir cevap olarak söylemedin?
Tapio

1
@Tapio: Çünkü bu pek bir cevap değil; bunun yerine bir öneri var.
teodron

Yanıtlar:


20

Bir fikir Ziyaretçi tasarım desenini kullanmaktır. Sahne oluşturmayı bilen bir Renderer uygulamasına ihtiyacınız var. Her nesne, render işini işlemek için render örneğini çağırabilir.

Sözde kodun birkaç satırında:

class Renderer {
public:
    void render( const ObjectA & obj );
    void render( const ObjectB & obj );
};


class ObjectA{
public:
    void draw( Renderer & r ){ r.render( *this ) };
}

class ObjectB{
public:
    void draw( Renderer & r ){ r.render( *this ) };
}

Gl * öğesi, işleyicinin yöntemleri tarafından uygulanır ve nesneler yalnızca işlenmesi için gereken verileri, konumu, doku türünü, boyutu ... vb. Depolar.

Ayrıca, farklı oluşturucular (debugRenderer, hqRenderer, ... vb.) Ayarlayabilir ve bunları nesneleri değiştirmeden dinamik olarak kullanabilirsiniz.

Bu aynı zamanda Varlık / Bileşen sistemleriyle birleştirilebilir.


1
Bu oldukça iyi bir cevap! Entity/ComponentGeometri sağlayıcılarını diğer motor parçalarından (AI, Fizik, Ağ veya genel oyun) ayırmaya yardımcı olabileceğinden alternatifi biraz daha vurgulamış olabilirsiniz . 1!
teodron

1
@teodron, A / C alternatifini açıklamayacağım, çünkü işleri uyuşuyordu. Ancak, bence değiştirmeli ObjectAve ObjectBper DrawableComponentAve DrawableComponentBve iç render yöntemlerinin, eğer gerekirse, diğer bileşenleri kullanmanız gerektiğini düşünüyorum, örneğin: position = component->getComponent("Position");Ve ana döngüde, beraberinde çağıracağınız çekilebilir bileşenlerin bir listesi vardır.
Zhen

Neden sadece Renderablebir draw(Renderer&)işlevi olan bir arayüze (benzeri ) sahip değilsiniz ve oluşturulabilecek tüm nesneler bunları uygulasın? Bu durumda, Renderersadece ortak arayüzü ve çağrıyı uygulayan herhangi bir nesneyi kabul eden bir fonksiyona ihtiyaç duyar renderable.draw(*this);mı?
Vite Falcon,

1
@ViteFalcon, Üzgünüm beni netleştirmezsem, ancak ayrıntılı bir açıklama için daha fazla alana ve koda ihtiyacım var. Temel olarak benim çözümüm, gl_*işlevleri işleyiciye taşır (mantığı görüntülemeden ayırarak), ancak çözümünüz gl_*çağrıları nesnelere taşır .
Zhen

Bu yolla gl * işlevleri gerçekten nesne kodundan çıkarılır, ancak tampon / doku kimliği, tekdüzen / öznitelik konumları gibi, oluşturmada kullanılan tanıtıcı değişkenlerini hala tutarım.
felipe,

4

Zhen'in cevabını zaten kabul ettiğini biliyorum ama başkasına yardım etmesi durumunda başka bir tane koymak istiyorum.

Sorunu yinelemek için, OP oluşturma kodunu mantık ve verilerden ayrı tutma yeteneğini ister.

Benim çözümüm, Rendererve mantık sınıfından ayrı olan bileşeni oluşturmak için hep birlikte farklı bir sınıf kullanmak . Öncelikle Renderable, işlevi olan bir arayüz olması gerekir bool render(Renderer& renderer);ve Renderersınıf, tüm Renderableörnekleri almak için ziyaretçi desenini kullanır, GameObjects listesi verildiğinde ve örneği olan nesneleri işler Renderable. Bu şekilde, Renderer orada bulunan her nesne tipini bilmek zorunda değildir ve yine Renderablede getRenderable()fonksiyon aracılığıyla bilgi vermek için hala her nesne tipinin sorumluluğundadır . Veya alternatif olarak, RenderableVisitortüm GameObject'leri ziyaret GameObjecteden ve ziyaretçiye katılabilirliğini eklemek / eklemek için seçebilecekleri özel şartlara göre bir sınıf oluşturabilirsiniz . Her iki durumda da, asıl esas olangl_*tüm çağrılar nesnenin dışındadır ve bunun bir parçası olmak yerine nesnenin kendine ait detaylarını bilen bir sınıfta bulunur Renderer.

YASAL UYARI : Bu sınıfları editöre el yazısı ile yazdım, bu yüzden kodda bir şeyi kaçırmış olma ihtimalim çok yüksek, ama umarım bu fikri anlayacaksınız.

Kısmi bir örnek göstermek için:

Renderable arayüzey

class Renderable {
public:
    Renderable(){}
    virtual ~Renderable(){}
    virtual void render(Renderer& renderer) const = 0;
};

GameObject sınıf:

class GameObject {
public:
    GameObject()
        : mVisible(true)
        , mMarkedForDelete(false) {}

    virtual ~GameObject(){}

    virtual Renderable* getRenderable() {
        // By default, all GameObjects are missing their Renderable
        return NULL;
    }

    void setVisible(bool visible) {
        mVisible = visible;
    }

    bool isVisible() const {
        return getRenderable() != null && !isMarkedForDeletion() && mVisible;
    }

    void markForDeletion() {
        mMarkedForDelete = true;
    }

    bool isMarkedForDeletion() const {
        return mMarkedForDelete;
    }

    // More GameObject functions

private:
    bool mVisible;
    bool mMarkedForDelete;
};

(Kısmi) Renderersınıfı.

class Renderer {
public:
    void renderObjects(std::vector<GameObject>& gameObjects) {
        // If you want to do something fancy with the renderable GameObjects,
        // create a visitor class to return the list of GameObjects that
        // are visible instead of rendering them straight-away
        std::list<GameObject>::iterator itr = gameObjects.begin(), end = gameObjects.end();
        while (itr != end) {
            GameObject* gameObject = *itr++;
            if (gameObject == null || !gameObject->isVisible()) {
                continue;
            }
            gameObject->getRenderable()->render(*this);
        }
    }

};

RenderableObject sınıf:

template <typename T>
class RenderableObject : public Renderable {
public:
    RenderableObject(T& object)
        :mObject(object) {}
    virtual ~RenderableObject(){}

    virtual void render(Renderer& renderer) {
        return render(renderer, mObject);
    }

protected:
    virtual void render(Renderer& renderer, T& object) = 0;
};

ObjectA sınıf:

// Forward delcare ObjectARenderable and make sure the constructor
// definition in the CPP file where ObjectARenderable gets included
class ObjectARenderable;

class ObjectA : public GameObject {
public:
    ObjectA()
        : mRenderable(new ObjectARenderable(*this)) {}

    // All data/logic

    Renderable* getRenderable() {
        return mRenderable.get();
    }

protected:
    // boost or std shared_ptr to make sure that the renderable instance is
    // cleaned up with the destruction of this object.
    shared_ptr<Renderable> mRenderable;
};

ObjectARenderable sınıf:

#include "ObjectA.h"

class ObjectARenderable : public RenderableObject<ObjectA> {
public:
    ObjectARenderable(ObjectA& instance) {
        : RenderableObject<ObjectA>(instance) {}

protected:
    virtual void render(Renderer& renderer, T& object) {
        // gl_* class to render ObjectA
    }
};

4

Bir oluşturma komut sistemi oluşturun. Hem OpenGLRenderersahne senaryosuna hem de hedef projelere erişimi olan yüksek seviyeli bir nesne, sahne grafiğini veya hedef projelerini yineleyecek ve RenderCmdsdaha sonra OpenGLRendererher birini sırayla çizecek ve böylece tüm OpenGL'leri içerecek şekilde gönderilecek olan bir toplu iş grubu oluşturacaktır. ilgili kod var.

Bunun sadece soyutlamadan daha fazla avantajı var; Sonunda render karmaşıklığınız büyüdükçe, her render komutunu doku veya gölgelendiriciye göre sıralayabilir ve gruplandırabilirsiniz Render();

class OpenGLRenderer
{
public:
    typedef GLuint GeometryBuffer;
    typedef GLuint TextureID;
    typedef std::vector<RenderCmd> RenderBatch; 

    void Render(const RenderBatch& renderBatch);   // set shaders, set active textures, draw geometry, ...

    MeshID CreateGeometryBuffer(...);
    TextureID CreateTexture(...);

    // ....
}

struct RenderCmd
{
    GeometryBuffer mGeometryBuffer;
    TextureID mTexture;
    Mat4& mWorldMatrix;
    bool mLightingEnabled;
    // .....
}

std::vector<GameObject> gYourGameObjects;
RenderBatch BuildRenderBatch()
{
    RenderBatch ret;

    for (GameObject& object : gYourGameObjects)
    { 
        // ....
    }

    return ret;
}

3

Tamamen, tüm kiralanabilir varlıklar için neyin ortak olduğu konusunda varsayımlarda bulunup bulunamayacağınıza bağlıdır. Motorumda, tüm nesneler aynı şekilde oluşturuluyor, bu yüzden sadece vbos, dokular ve dönüşümler sağlamanız gerekiyor. Ardından, işleyici hepsini getirir, bu nedenle farklı nesnelerde hiçbir OpenGL işlevi çağrısı gerekmez.


1
hava = yağmur, güneş, sıcak, soğuk: P ->
bağ

3
@TobiasKienzler Onun hecelemesini düzeltecekseniz, doğru hecelemeyi hecelemeye çalışın :-)
TASagent

@TASagent Ne, ve Muphry Yasasını fren mi? m- /
Tobias Kienzler

1
Bu yazım hatası düzeltildi
danijar

2

Kesinlikle oluşturma kodu ve oyun mantığını farklı sınıflara koyun. Kompozisyon (teodronun önerdiği gibi) muhtemelen bunu yapmanın en iyi yoludur; Oyun dünyasındaki her bir Varlığın kendi Yenilenebilir - veya belki de bir dizi seti olacaktır.

Temel dokulu ve aydınlatılmış gölgelendiricinize ek olarak, örneğin iskelet animasyonunu, parçacık yayıcıları ve karmaşık gölgelendiricileri işlemek için yine de Renderable alt sınıflarına sahip olabilirsiniz. Renderable sınıfı ve alt sınıfları yalnızca oluşturma için gerekli bilgileri içermelidir: geometri, dokular ve gölgelendiriciler.

Ayrıca, belirli bir ağın bir örneğini ağın kendisinden ayırmalısınız. Diyelim ki ekranda her biri aynı ağı kullanan yüz tane ağaç var. Geometriyi yalnızca bir kez saklamak istiyorsunuz, ancak her ağaç için ayrı konum ve döndürme matrislerine ihtiyacınız olacak. Animasyonlu insansılar gibi daha karmaşık nesneler de ek durum bilgisine (bir iskelet, halihazırda uygulanmış olan animasyonlar kümesi gibi) sahip olacaktır.

İşe yaramaz, saf yaklaşım, her oyun varlığı üzerinde yinelemektir ve ona kendisini oluşturmasını söyler. Alternatif olarak, her varlık (ortaya çıktığında) canlandırılabilir nesnelerini bir sahne nesnesine yerleştirebilir. Ardından, oluşturma işleviniz sahneye işlenmesini söyler. Bu, sahnenin bu kodu ya oyun varlıklarına ya da belirli bir cana yakın alt sınıfa gömmeden karmaşık render ile ilgili şeyler yapmasını sağlar.


2

Bu öneri sunmaya gerçekten özgü değildir, ancak işleri büyük ölçüde ayrı tutan bir sistem bulmaya yardımcı olmalıdır. Öncelikle 'GameObject' verilerini konum bilgisinden ayrı tutmaya çalışın.

Basit XYZ konum bilgisinin o kadar basit olmayabileceğini belirtmekte fayda var. Bir fizik motoru kullanıyorsanız, o zaman konum verilerini 3. taraf motorunda saklayabilirsiniz. Ya aralarında senkronize olmanız gerekir (ki bu çok fazla anlamsız bellek kopyalaması gerektirir) ya da bilgileri doğrudan motordan sorgulamanız gerekir. Ancak tüm nesneler fiziğe ihtiyaç duymaz, bazıları yerine sabitlenir, böylece basit bir yüzdürme takımı orada iyi çalışır. Bazıları başka nesnelere bile bağlı olabilir, bu yüzden onların konumları aslında başka bir konumun kaymasıdır. Gelişmiş bir kurulumda, yalnızca GPU'da depolanan konum, bilgisayar tarafında gerekli olan tek zaman, komut dosyası, depolama ve ağ çoğaltması içindir. Dolayısıyla, konumsal verileriniz için birkaç olası seçeneğiniz olacaktır. Burada miras kullanımı mantıklı.

Konumuna sahip olan bir nesneden ziyade, bu nesnenin kendisinin bir dizin oluşturma veri yapısına sahip olması gerekir. Örneğin, bir 'Seviye' bir Octree veya belki bir fizik motoru 'sahne' olabilir. Oluşturmak istediğinizde (veya bir görüntü oluşturma sahnesi oluştururken), özel yapınızı kamera tarafından görülebilen nesneler için sorgularsınız.

Bu aynı zamanda iyi bellek yönetimi verilmesine yardımcı olur. Bu şekilde, aslında bir alanda bulunmayan bir nesnenin, 0.0 kordonu veya bir bölgedeki en son olduğu zaman sahip olduğu kodları döndürmekten daha mantıklı bir pozisyonu bile yoktur.

Koordinatları artık nesnede tutmazsanız, object.getX () yerine level.getX (object) olur. Bununla ilgili sorun, seviyedeki nesneye bakmak muhtemelen yavaş bir işlem olacaktır, çünkü tüm nesnelerine bakmak ve sorgunuzla eşleşmek zorunda kalır.

Bundan kaçınmak için muhtemelen özel bir 'link' sınıfı oluşturacağım. Bir seviye ve bir nesne arasında bağlanan. Ben buna "Yer" diyorum. Bu, xyz koordinatlarını, aynı zamanda seviyenin tanıtıcısını ve nesnenin tanıtıcısını içerir. Bu link sınıfı, spacial yapı / seviyede depolanacak ve nesnenin kendisine zayıf bir referansı olacaktır (eğer seviye / konum tahrip edilmişse, refrence öğesinin null olarak güncellenmesi gereken nesneler. Nesne 'kendi', eğer bir seviye silinirse, özel indeks yapısı, içerdiği yerler ve Nesneleri de öyle.

typedef std::tuple<Level, Object, PositionXYZ> Location;

Şimdi konum bilgisi sadece bir yerde saklanır. Nesne, Spacial endeksleme yapısı, oluşturucu vb. Arasında çoğaltılmaz.

Octrees gibi geniş veri yapılarının çoğu zaman depoladıkları nesnelerin koordinatlarına sahip olmaları gerekmez. Buradaki konum, yapının içindeki düğümlerin göreceli konumunda depolanır (hızlı arama süreleri için doğruluktan ödün veren bir tür benzer kayıplı sıkıştırma olarak düşünülebilir). Octree'deki konum nesnesi ile sorgu yapıldıktan sonra gerçek koordinatlar içinde bulunur.

Veya nesne konumlarınızı yönetmek için bir fizik motoru kullanıyorsanız ya da ikisi arasındaki bir karışımı kullanıyorsanız, Konum sınıfı tüm kodunuzu bir yerde tutarken şeffaf bir şekilde kullanmalıdır.

Diğer bir avantaj, şimdi seviyenin pozisyonu ve referansı aynı yerde depolanır. Object.TeleportTo (other_object) öğesini uygulayabilir ve düzeyler arasında çalışmasını sağlayabilirsiniz. Benzer şekilde AI yolu bulma, farklı bir bölgedeki bir şeyi takip edebilir.

İşleme ile ilgili olarak. Renderiniz, Konum ile benzer bir şekilde bağlanabilir. Dışında orada render özel şeyler olurdu. Muhtemelen bu yapı içinde saklamak için 'Nesne' veya 'Seviye' ihtiyacınız yoktur. Nesne, renk toplama veya üstünde bir hitbar oluşturma gibi bir şey yapmaya çalışıyorsanız veya bunun gibi bir şey yapmayı deniyorsanız faydalı olabilir; aksi takdirde, işleyici yalnızca kafes ve benzeriyle ilgilenir. RenderableStuff bir Mesh olacaktır, ayrıca sınırlayıcı kutulara sahip olabilir.

typedef std::pair<RenderableStuff, PositionXYZ> RenderThing;

renderer.render(level, camera);
renderer: object = level.getVisibleObjects(camera);
level: physics.getObjectsInArea(physics.getCameraFrustrum(camera));
for(object in objects) {
    //This could be depth sorted, meshes could be broken up and sorted by material for batch rendering or whatever
    rendering_que.addObjectToRender(object);
}

Bunu her karede yapmanız gerekmeyebilir, kameranın gösterdiğinden daha büyük bir alan almanızı sağlayabilirsiniz. Önbellekleyin, sınırlayıcı kutu içeri girip girmediğini görmek için nesne hareketlerini izleyin, kamera hareketini izleyin. Ancak, kıyaslamadan bu tür şeylerle uğraşmaya başlama.

Siz fizik motorunun kendisi de benzer bir soyutlamaya sahip olabilir, çünkü Nesne verilerine de ihtiyaç duymaz, sadece çarpışma ağı ve fizik özellikleri.

Tüm temel nesne verilerinizin içereceği, nesnenin kullandığı ağın adı olacaktır. Oyun motoru daha sonra devam edebilir ve nesne sınıfınıza bir dizi render özel bir şey (renderleme API'nize özel olabilecek, yani DirectX - OpenGL gibi) yüklemeden hangi formatta yüklenirse yükleyin.

Ayrıca farklı bileşenleri ayrı tutar. Bu, fizik motorunuzu değiştirmek gibi şeyleri yapmayı kolaylaştırır, çünkü bu şeyler çoğunlukla tek bir yerde bulunur. Aynı zamanda iç içe geçmeyi çok daha kolaylaştırır. Fizik sorguları gibi şeyleri gerçek sahte nesnelerin kurulumuna gerek kalmadan test edebilirsiniz çünkü ihtiyacınız olan tek şey Konum sınıfıdır. Ayrıca işleri daha kolay hale getirebilirsiniz. Bunları optimize etmek için hangi sınıflar ve tek konumlar üzerinde hangi sorguları yapmanız gerektiğini daha açık bir şekilde ortaya koymaktadır (örneğin, yukarıdaki seviye.getVisibleObject, kamera çok fazla hareket etmediğinde bir şeyleri önbelleğe alabileceğiniz yer olurdu).

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.