Memisahkan data / logika game dari rendering

21

Saya sedang menulis game menggunakan C ++ dan OpenGL 2.1. Saya berpikir bagaimana saya bisa memisahkan data / logika dari rendering. Saat ini saya menggunakan kelas dasar 'Renderable' yang memberikan metode virtual murni untuk mengimplementasikan gambar. Tetapi setiap objek memiliki kode khusus, hanya objek yang tahu cara mengatur seragam shader dan mengatur data buffer vertex array dengan benar. Saya berakhir dengan banyak panggilan fungsi gl * di seluruh kode saya. Apakah ada cara umum untuk menggambar objek?

Felipe
sumber
4
Gunakan komposisi untuk benar-benar melampirkan objek yang dapat di render ke objek Anda dan berinteraksi dengan m_renderableanggota tersebut. Dengan begitu, Anda dapat memisahkan logika Anda dengan lebih baik. Jangan memaksakan "antarmuka" yang dapat di-render pada objek umum yang juga memiliki fisika, ai dan yang lainnya. Setelah itu, Anda dapat mengelola renderable secara terpisah. Anda memerlukan lapisan abstrakisasi melalui pemanggilan fungsi OpenGL untuk memisahkan hal-hal lebih banyak lagi. Jadi, jangan berharap mesin yang bagus untuk memiliki panggilan API GL di dalam berbagai implementasi renderable nya. Singkatnya, singkatnya.
teodron
1
@teodron: Mengapa Anda tidak menjawabnya?
Tapio
1
@ Tapio: karena itu bukan jawaban yang banyak; itu lebih merupakan saran saja.
teodron

Jawaban:

20

Gagasannya adalah menggunakan pola desain Pengunjung. Anda memerlukan implementasi Renderer yang tahu cara membuat alat peraga. Setiap objek dapat memanggil instance renderer untuk menangani pekerjaan render.

Dalam beberapa baris kodesemu:

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 * stuff diimplementasikan dengan metode renderer, dan objek hanya menyimpan data yang diperlukan untuk dirender, posisi, jenis tekstur, ukuran ... dll.

Anda juga dapat mengatur renderer yang berbeda (debugRenderer, hqRenderer, ... dll) dan menggunakannya secara dinamis, tanpa mengubah objek.

Ini juga dapat dengan mudah digabungkan dengan sistem Entity / Component.

Zhen
sumber
1
Ini jawaban yang agak bagus! Anda bisa lebih menekankan Entity/Componentalternatif karena dapat membantu penyedia geometri yang terpisah dari bagian-bagian mesin lainnya (AI, Fisika, Jaringan atau gameplay umum). +1!
teodron
1
@teodron, saya tidak akan menjelaskan alternatif E / C karena akan mengkompilasi hal-hal. Tapi, saya berpikir bahwa Anda harus mengubah ObjectAdan ObjectBper DrawableComponentAdan DrawableComponentB, dan di dalam membuat metode, menggunakan komponen lain jika Anda membutuhkannya, seperti: position = component->getComponent("Position");Dan di lingkaran utama, Anda memiliki daftar komponen ditarik untuk memanggil menggambar dengan.
Zhen
Mengapa tidak hanya memiliki antarmuka (seperti Renderable) yang memiliki draw(Renderer&)fungsi dan semua objek yang dapat diterjemahkan mengimplementasikannya? Dalam hal mana Rendererhanya perlu satu fungsi yang menerima objek yang mengimplementasikan antarmuka dan panggilan umum renderable.draw(*this);?
Vite Falcon
1
@ ViteFalcon, Maaf jika saya tidak membuat saya jelas, tetapi untuk penjelasan rinci, saya harus membutuhkan lebih banyak ruang dan kode. Pada dasarnya, solusi saya memindahkan gl_*fungsi ke renderer (memisahkan logika dari rendering), tetapi solusi Anda memindahkan gl_*panggilan ke objek.
Zhen
Dengan cara ini gl * fungsi memang pindah dari kode objek, tapi aku masih memegang variabel menangani digunakan dalam rendering, seperti, lokasi seragam / atribut penyangga / tekstur id.
felipe
4

Saya tahu Anda sudah menerima jawaban Zhen, tetapi saya ingin menempatkan yang lain di luar sana kalau-kalau itu membantu orang lain.

Untuk mengulangi masalah, OP ingin kemampuan untuk menjaga kode rendering terpisah dari logika dan data.

Solusi saya adalah menggunakan kelas yang berbeda secara bersamaan untuk membuat komponen, yang terpisah dari Rendererkelas logika dan. Pertama-tama perlu ada Renderableantarmuka yang memiliki fungsi bool render(Renderer& renderer);dan Rendererkelas menggunakan pola pengunjung untuk mengambil semua Renderableinstance, mengingat daftar GameObjects dan merender objek yang memiliki Renderableinstance. Dengan cara ini, Renderer tidak perlu mengetahui setiap tipe objek di luar sana dan masih menjadi tanggung jawab masing-masing tipe objek untuk menginformasikannya Renderablemelalui getRenderable()fungsi. Atau sebagai alternatif, Anda dapat membuat RenderableVisitorkelas yang mengunjungi semua GameObjects dan berdasarkan pada GameObjectkondisi individu yang mereka dapat pilih untuk menambahkan / tidak-menambahkan mereka dapat diuraikan untuk pengunjung. Either way, intinya adalah bahwagl_*panggilan semua di luar objek itu sendiri dan berada di kelas yang mengetahui detail intim objek itu sendiri, bukan yang menjadi bagian dari Renderer.

PENOLAKAN : Saya menulis sendiri kelas-kelas ini di editor sehingga ada peluang bagus bahwa saya melewatkan sesuatu dalam kode, tetapi mudah-mudahan, Anda akan mendapatkan idenya.

Untuk menampilkan contoh (sebagian):

Renderable antarmuka

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

GameObject kelas:

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;
};

RendererKelas (sebagian) .

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 kelas:

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 kelas:

// 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 kelas:

#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
    }
};
Vite Falcon
sumber
4

Bangun sistem perintah rendering. Objek tingkat tinggi, yang memiliki akses ke objek OpenGLRendererdan scenegraph / gameobjects, akan mengulangi grafik adegan atau objek game dan membangun batch RenderCmds, yang kemudian akan dikirimkan ke OpenGLRendereryang akan menggambar masing-masing secara bergantian, dan dengan demikian berisi semua OpenGL kode terkait di dalamnya.

Ada lebih banyak manfaat dari ini daripada sekadar abstraksi; akhirnya ketika kompleksitas rendering Anda bertambah, Anda dapat mengurutkan dan mengelompokkan setiap perintah render berdasarkan tekstur atau shader misalnya Render()untuk menghilangkan banyak kemacetan dalam panggilan draw yang dapat membuat perbedaan besar dalam kinerja.

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;
}
KaiserJohaan
sumber
3

Ini sepenuhnya tergantung pada apakah Anda dapat membuat asumsi tentang apa yang umum untuk semua entitas yang dapat di render atau tidak. Di mesin saya, semua objek dirender dengan cara yang sama, jadi hanya perlu menyediakan vbos, tekstur dan transformasi. Kemudian renderer mengambil semuanya, sehingga tidak ada panggilan fungsi OpenGL yang diperlukan dalam objek yang berbeda sama sekali.

danijar
sumber
1
cuaca = hujan, matahari, panas, dingin: P -> cuaca
Tobias Kienzler
3
@TobiasKienzler Jika Anda akan memperbaiki ejaannya, cobalah mengeja dengan benar :-)
TASagent
@ PASAS Apa, dan rem Hukum Muphry ? m- /
Tobias Kienzler
1
dikoreksi kesalahan ketik itu
danijar
2

Jelas menempatkan kode rendering dan logika game di kelas yang berbeda. Komposisi (seperti yang disarankan oleh teodron) mungkin merupakan cara terbaik untuk melakukan ini; setiap Entitas di dunia game akan memiliki Renderable sendiri - atau mungkin satu set dari mereka.

Anda mungkin masih memiliki beberapa subclass Renderable, misalnya untuk menangani animasi kerangka, penghasil partikel, dan shader kompleks, selain shader bertekstur-dan-menyala dasar Anda. Kelas Renderable dan subkelasnya hanya boleh berisi informasi yang diperlukan untuk rendering: geometri, tekstur, dan shader.

Selanjutnya, Anda harus memisahkan sebuah instance dari mesh yang diberikan dari mesh itu sendiri. Katakanlah Anda memiliki seratus pohon di layar, masing-masing menggunakan jaring yang sama. Anda hanya ingin menyimpan geometri satu kali, tetapi Anda membutuhkan lokasi & matriks rotasi yang terpisah untuk setiap pohon. Objek yang lebih kompleks, seperti humanoids animasi, juga akan memiliki informasi status tambahan (seperti kerangka, rangkaian animasi yang saat ini diterapkan, dll).

Untuk membuat, pendekatan naif adalah untuk mengulangi setiap entitas game dan menyuruhnya untuk membuat itu sendiri. Bergantian, setiap entitas (ketika muncul) dapat memasukkan objek yang dapat diurai ke objek pemandangan. Kemudian, fungsi render Anda memberi tahu tempat kejadian untuk merender. Ini memungkinkan adegan untuk melakukan hal-hal terkait render yang kompleks tanpa menyematkan kode tersebut ke entitas gim atau subkelas render yang spesifik.

AndrewS
sumber
2

Nasihat ini tidak benar-benar spesifik untuk rendering tetapi harus membantu memunculkan sistem yang membuat hal-hal terpisah. Pertama-tama coba dan pisahkan data 'GameObject' dari informasi posisi.

Perlu dicatat bahwa informasi posisi XYZ sederhana mungkin tidak sesederhana itu. Jika Anda menggunakan mesin fisika maka data posisi Anda dapat disimpan di dalam mesin pihak ke-3. Anda perlu menyinkronkan di antara mereka (yang akan melibatkan banyak penyalinan memori yang tidak berguna) atau meminta informasi langsung dari mesin. Tetapi tidak semua benda membutuhkan fisika, beberapa akan diperbaiki di tempat sehingga satu set float sederhana berfungsi dengan baik di sana. Beberapa bahkan mungkin melekat pada objek lain, sehingga posisinya sebenarnya merupakan offset dari posisi lain. Dalam pengaturan lanjutan Anda mungkin memiliki posisi disimpan hanya pada GPU satu-satunya waktu yang dibutuhkan sisi komputer adalah untuk scripting, penyimpanan dan replikasi jaringan. Jadi, Anda kemungkinan akan memiliki beberapa pilihan yang memungkinkan untuk data posisi Anda. Di sini masuk akal untuk menggunakan warisan.

Daripada objek yang memiliki posisi itu, sebaliknya objek itu sendiri harus dimiliki oleh struktur data pengindeksan. Misalnya 'Level' mungkin memiliki Oktree, atau mungkin 'adegan' mesin fisika. Saat Anda ingin merender (atau mengatur adegan rendering), Anda menanyakan struktur khusus Anda untuk objek yang terlihat oleh kamera.

Ini juga membantu memberikan manajemen memori yang baik. Dengan cara ini sebuah objek yang sebenarnya tidak ada di suatu area bahkan tidak memiliki posisi yang masuk akal daripada mengembalikan 0,0 coords atau coord yang dimilikinya ketika itu terakhir di suatu area.

Jika Anda tidak lagi menyimpan koordinat di objek, alih-alih object.getX () Anda akan berakhir memiliki level.getX (objek). Masalah dengan itu adalah mencari objek di tingkat kemungkinan akan menjadi operasi yang lambat karena harus melihat semua objek itu dan cocok dengan yang Anda query.

Untuk menghindari itu saya mungkin akan membuat kelas 'tautan' khusus. Yang mengikat antara level dan objek. Saya menyebutnya "Lokasi". Ini akan berisi koordinat xyz serta pegangan ke tingkat dan pegangan ke objek. Kelas tautan ini akan disimpan dalam struktur spasial / level dan objek akan memiliki referensi yang lemah untuk itu (jika level / lokasi dihancurkan, referensi objek perlu diperbarui ke nol. Mungkin juga layak memiliki kelas Lokasi sebenarnya 'memiliki' objek, dengan cara itu jika level dihapus, demikian juga struktur indeks khusus, lokasi yang dikandungnya, dan Objeknya.

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

Sekarang informasi posisi disimpan hanya di satu tempat. Tidak diduplikasi antara Object, struktur pengindeksan spasial, renderer dan sebagainya.

Struktur data spasial seperti Octrees seringkali bahkan tidak perlu memiliki koordinat objek yang mereka simpan. Posisi ada disimpan di lokasi relatif dari node dalam struktur itu sendiri (bisa dianggap sebagai semacam kompresi lossy, mengorbankan akurasi untuk waktu pencarian cepat). Dengan objek lokasi di Octree maka koordinat aktual ditemukan di dalamnya setelah kueri selesai.

Atau jika Anda menggunakan mesin fisika untuk mengelola lokasi objek Anda atau campuran antara keduanya, kelas Lokasi harus menangani itu secara transparan sambil menyimpan semua kode Anda di satu tempat.

Keuntungan lain sekarang adalah posisi dan referensi ke level disimpan di lokasi yang sama. Anda dapat mengimplementasikan object.TeleportTo (other_object) dan membuatnya bekerja lintas level. Demikian pula, pencarian jalur AI dapat mengikuti sesuatu ke area yang berbeda.

Berkenaan dengan rendering. Render Anda dapat memiliki ikatan yang sama dengan Lokasi. Kecuali itu akan memiliki hal-hal khusus rendering di sana. Anda mungkin tidak perlu 'Objek' atau 'Level' untuk disimpan dalam struktur ini. Objek bisa berguna jika Anda mencoba melakukan sesuatu seperti memilih warna, atau merender hitbar yang melayang di atasnya dan seterusnya, tetapi jika tidak, pemberi render hanya peduli dengan jala dan semacamnya. RenderableStuff akan menjadi Mesh, bisa juga memiliki kotak pembatas dan sebagainya.

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);
}

Anda mungkin tidak perlu melakukan ini setiap frame, Anda bisa memastikan Anda mengambil wilayah yang lebih besar dari yang ditunjukkan kamera saat ini. Cache, lacak pergerakan objek untuk melihat apakah ada kotak pembatas yang berada dalam jangkauan, lacak pergerakan kamera, dan sebagainya. Tapi jangan mulai mengacaukan hal-hal semacam itu sampai Anda telah membandingkannya.

Anda mesin fisika itu sendiri mungkin memiliki abstraksi yang sama, karena itu juga tidak memerlukan data Object, hanya tabrakan dan sifat fisika.

Semua data objek inti Anda akan berisi nama mesh yang digunakan objek. Mesin permainan kemudian dapat melanjutkan dan memuat ini dalam format apa pun yang disukainya tanpa membebani kelas objek Anda dengan sekelompok render hal-hal tertentu (yang mungkin khusus untuk rendering API Anda, yaitu DirectX vs OpenGL).

Itu juga membuat komponen yang berbeda terpisah. Ini membuatnya mudah untuk melakukan hal-hal seperti mengganti mesin fisika Anda karena hal-hal itu sebagian besar mandiri di satu lokasi. Ini juga membuat unittesting jauh lebih mudah. Anda dapat menguji hal-hal seperti kueri fisika tanpa harus memiliki pengaturan objek palsu yang sebenarnya karena semua yang Anda butuhkan adalah kelas Lokasi. Anda juga dapat mengoptimalkan barang dengan lebih mudah. Itu membuat lebih jelas kueri apa yang perlu Anda lakukan pada kelas apa dan lokasi tunggal untuk mengoptimalkannya (misalnya level di atas. GetVisibleObject akan menjadi tempat Anda dapat men-cache hal-hal jika kamera tidak banyak bergerak).

David C. Bishop
sumber