Pola desain apa yang paling sesuai menangani pegangan untuk objek, tanpa melewati pegangan atau Manajer di sekitar?

8

Saya sedang menulis game di C ++ menggunakan OpenGL.

Bagi mereka yang tidak tahu, dengan OpenGL API Anda melakukan banyak panggilan ke hal-hal seperti glGenBuffersdan glCreateShaderlain-lain. Jenis pengembalian ini GLuintadalah pengidentifikasi unik untuk apa yang baru saja Anda buat. Benda yang diciptakan hidup dengan memori GPU.

Mengingat Memori GPU terkadang terbatas, Anda tidak ingin membuat dua hal yang sama ketika digunakan oleh banyak objek.

Misalnya, Shader. Anda menautkan Program Shader dan kemudian Anda memiliki GLuint. Setelah selesai dengan Shader, Anda harus menelepon glDeleteShader(atau sesuatu yang mempengaruhi).

Sekarang, katakanlah saya memiliki hierarki kelas yang dangkal seperti:

class WorldEntity
{
public:
    /* ... */
protected:
    ShaderProgram* shader;
    /* ... */
};

class CarEntity : public WorldEntity 
{
    /* ... */
};

class PersonEntity: public WorldEntity
{
    /* ... */
};

Kode apa pun yang pernah saya lihat akan mengharuskan semua Konstruktor memiliki ShaderProgram*izin untuk disimpan dalam WorldEntity. ShaderProgramadalah kelas saya yang merangkum pengikatan GLuintke keadaan shader saat ini dalam konteks OpenGL serta beberapa hal bermanfaat lainnya yang perlu Anda lakukan dengan Shader.

Masalah yang saya miliki dengan ini adalah:

  • Ada banyak parameter yang diperlukan untuk membangun a WorldEntity(pertimbangkan bahwa mungkin ada mesh, shader, sekelompok tekstur dll, yang semuanya dapat dibagikan, sehingga dilewatkan sebagai petunjuk)
  • Apa pun yang menciptakan WorldEntitykebutuhan untuk mengetahui apa ShaderProgramyang dibutuhkan
  • Ini mungkin membutuhkan semacam kelas tegukan EntityManager yang tahu contoh apa yang ShaderProgramharus dilewatkan ke entitas yang berbeda.

Jadi sekarang karena ada Managerkelas-kelas yang perlu mendaftar sendiri EntityManagerbersama dengan ShaderPrograminstance yang mereka butuhkan, atau saya perlu keledai besar switchdi manajer yang perlu saya perbarui untuk setiap WorldEntityjenis turunan baru .

Pikiran pertama saya adalah membuat ShaderManagerkelas (saya tahu, Manajer buruk) yang saya lewati dengan referensi atau penunjuk ke WorldEntitykelas sehingga mereka dapat membuat apa pun yang ShaderProgrammereka inginkan, melalui ShaderManagerdan ShaderManagerdapat melacak ShaderPrograms yang sudah ada , sehingga dapat kembalikan yang sudah ada atau buat yang baru jika perlu.

(Saya bisa menyimpan ShaderPrograms melalui hash dari nama file ShaderProgramkode sumber aktual s)

Jadi sekarang:

  • Saya sekarang meneruskan pointer ke ShaderManagerbukan ShaderProgram, jadi masih ada banyak parameter
  • Saya tidak memerlukan EntityManager, entitas itu sendiri akan tahu apa yang ShaderProgramharus dibuat, dan ShaderManagerakan menangani yang sebenarnya ShaderProgram.
  • Tapi sekarang saya tidak tahu kapan ShaderManagerbisa dengan aman menghapus ShaderProgramyang dimilikinya.

Jadi sekarang saya telah menambahkan penghitungan referensi ke ShaderProgramkelas saya yang menghapus internal GLuintmelalui glDeleteProgramdan saya tidak melakukannya ShaderManager.

Jadi sekarang:

  • Objek dapat membuat apa pun ShaderProgramyang dibutuhkannya
  • Tapi sekarang ada duplikat ShaderProgramkarena tidak ada Manajer eksternal yang melacak

Akhirnya saya datang untuk membuat satu dari dua keputusan:

1. Kelas Statis

A static classyang dipanggil untuk membuat ShaderPrograms. Itu membuat trek internal ShaderProgramberdasarkan hash dari nama file - ini berarti saya tidak perlu lagi melewati pointer atau referensi ke ShaderProgramatau ShaderManagersekitar, jadi lebih sedikit parameter - WorldEntitiesMemiliki semua pengetahuan tentang contoh yang ShaderProgramingin mereka buat

static ShaderManagerKebutuhan baru ini untuk:

  • pertahankan hitungan berapa kali a ShaderProgramdigunakan dan saya ShaderProgramtidak membuat salinan ATAU
  • ShaderProgrammenghitung referensi mereka dan hanya memanggil glDeleteProgramdestruktor mereka ketika hitungan itu 0DAN ShaderManagersecara berkala memeriksa untuk ShaderProgramdengan hitungan 1 dan membuangnya.

Kelemahan dari pendekatan ini yang saya lihat adalah:

  1. Saya memiliki kelas statis global yang mungkin menjadi masalah. Konteks OpenGL perlu dibuat sebelum memanggil glXfungsi apa pun . Jadi berpotensi, WorldEntitymungkin dibuat dan mencoba untuk membuat ShaderProgramsebelum pembuatan Konteks OpenGL, yang akan mengakibatkan crash.

    Satu-satunya cara untuk mengatasi hal ini adalah kembali menyerahkan semua yang ada sebagai pointer / referensi, atau memiliki kelas global GLContext yang dapat ditanyakan, atau memegang segala sesuatu di kelas yang menciptakan Konteks pada konstruksi. Atau mungkin hanya boolean global IsContextCreatedyang dapat diperiksa. Tapi saya khawatir ini memberi saya kode jelek di mana-mana.

    Apa yang bisa saya lihat adalah pindah ke:

    • Kelas besar Engineyang memiliki setiap kelas lain tersembunyi di dalamnya sehingga dapat mengontrol urutan konstruksi / dekonstruksi dengan tepat. Ini tampak seperti kekacauan besar kode antarmuka antara pengguna mesin dan mesin, seperti pembungkus di atas pembungkus
    • Seluruh kelas "Manajer" yang melacak instance dan menghapus hal-hal saat diperlukan. Ini mungkin kejahatan yang perlu?

DAN

  1. Kapan sebenarnya membersihkan ShaderPrograms static ShaderManager? Setiap beberapa menit? Setiap Loop Game? Saya dengan anggun menangani penyusunan ulang shader dalam kasus ketika sebuah ShaderProgramdihapus tetapi kemudian baru WorldEntitymemintanya; tapi saya yakin ada cara yang lebih baik.

2. Metode yang lebih baik

Itulah yang saya minta di sini

NeomerArcana
sumber
2
Hal yang terlintas dalam pikiran ketika Anda mengatakan "Ada banyak parameter yang diperlukan untuk membangun WorldEntity" adalah bahwa pola pabrik semacam itulah yang diperlukan untuk menangani wireup. Selain itu, saya tidak mengatakan Anda perlu injeksi ketergantungan di sini, tetapi jika Anda belum melihat jalan itu sebelum Anda mungkin menemukan wawasan. "Manajer" yang Anda bicarakan di sini terdengar mirip dengan penangan ruang lingkup seumur hidup.
J Trana
Jadi, katakanlah saya menerapkan kelas pabrik untuk membangun WorldEntitys; Bukankah itu menggeser beberapa masalah? Karena sekarang kelas WorldFactory harus lulus setiap WolrdEntity ShaderProgram yang benar.
NeomerArcana
Pertanyaan bagus. Seringkali, tidak - dan inilah sebabnya. Dalam banyak kasus Anda tidak harus memiliki ShaderProgram tertentu, atau Anda mungkin ingin mengganti yang mana yang dipakai, atau mungkin Anda ingin menulis unit test dengan ShaderProgram yang sepenuhnya disimulasikan. Sebuah pertanyaan yang akan saya tanyakan adalah: apakah benar-benar penting bagi entitas yang memiliki program shader? Dalam beberapa kasus mungkin, tetapi karena Anda menggunakan pointer ShaderProgram daripada pointer MySpecificShaderProgram, itu mungkin tidak. Juga, masalah ruang lingkup ShaderProgram sekarang dapat bergeser ke tingkat pabrik, memungkinkan untuk perubahan antar lajang dll dengan mudah.
J Trana

Jawaban:

4
  1. Metode yang lebih baik Itulah yang saya minta di sini

Permintaan maaf untuk necromancy tapi saya telah melihat begitu banyak tersandung masalah serupa dengan mengelola sumber daya OpenGL, termasuk saya di masa lalu. Dan begitu banyak kesulitan yang saya perjuangkan dengan yang saya kenali pada orang lain datang dari godaan untuk membungkus dan kadang-kadang abstrak dan bahkan merangkum sumber daya OGL yang diperlukan untuk beberapa entitas game analog yang akan diberikan.

Dan "cara yang lebih baik" yang saya temukan (setidaknya satu yang mengakhiri perjuangan khusus saya di sana) adalah melakukan hal-hal yang sebaliknya. Artinya, jangan khawatirkan diri Anda dengan aspek tingkat rendah OGL dalam mendesain entitas dan komponen gim Anda dan menjauhlah dari ide-ide seperti itu yang harus Anda Modelsimpan seperti segitiga dan simpul primitif dalam bentuk objek yang dibungkus atau bahkan abstraksi VBO.

Masalah Rendering vs. Masalah Desain Game

Ada beberapa konsep level yang sedikit lebih tinggi daripada tekstur GPU, misalnya, dengan persyaratan manajemen yang lebih sederhana seperti gambar CPU (dan Anda tetap membutuhkannya, setidaknya untuk sementara, sebelum Anda bahkan dapat membuat dan mengikat tekstur GPU). Tidak ada masalah rendering model mungkin cukup hanya menyimpan properti yang menunjukkan nama file yang digunakan untuk file yang berisi data untuk model. Anda dapat memiliki komponen "material" yang tingkatnya lebih tinggi dan lebih abstrak dan menjelaskan sifat-sifat material itu daripada shader GLSL.

Dan kemudian hanya ada satu tempat dalam basis kode yang berkaitan dengan hal-hal seperti shader dan tekstur GPU dan VAOs / VBOs dan konteks OpenGL, dan itulah implementasi dari sistem rendering . Sistem rendering mungkin loop melalui entitas dalam adegan game (dalam kasus saya ini berjalan melalui indeks spasial, tetapi Anda dapat memahami lebih mudah dan memulai dengan loop sederhana sebelum menerapkan optimasi seperti frustum culling dengan indeks spasial), dan itu menemukan komponen tingkat tinggi Anda seperti "bahan" dan "gambar" dan nama file model.

Dan tugasnya adalah untuk mengambil data tingkat yang lebih tinggi yang tidak secara langsung berkaitan dengan GPU dan memuat / membuat / mengasosiasikan / mengikat / menggunakan / memisahkan / menghancurkan sumber daya OpenGL yang diperlukan berdasarkan apa yang ditemukan di tempat kejadian dan apa yang terjadi pada tempat kejadian. Dan itu menghilangkan godaan untuk menggunakan hal-hal seperti lajang dan versi statis "manajer" dan apa yang tidak, karena sekarang semua manajemen sumber daya OGL Anda terpusat ke satu sistem / objek dalam basis kode Anda (walaupun tentu saja Anda dapat menguraikannya menjadi objek lebih lanjut yang dienkapsulasi oleh renderer untuk membuat kode lebih mudah dikelola). Ini juga secara alami menghindari beberapa poin tersandung dengan hal-hal seperti mencoba menghancurkan sumber daya di luar konteks OGL yang valid,

Menghindari Perubahan Desain

Selanjutnya yang menawarkan banyak ruang bernapas untuk menghindari perubahan desain pusat yang mahal, karena katakan Anda menemukan di belakang bahwa beberapa bahan memerlukan beberapa rendering pass (dan beberapa shader) untuk diurai, seperti pass hamburan dan shader untuk permukaan bawah permukaan untuk bahan kulit, padahal sebelumnya Anda ingin mengurutkan bahan dengan shader GPU tunggal. Dalam hal ini tidak ada perubahan desain yang mahal untuk antarmuka pusat yang digunakan oleh banyak hal. Yang Anda lakukan hanyalah memperbarui implementasi sistem rendering secara lokal untuk menangani kasus yang sebelumnya tidak terduga ini ketika berhadapan dengan sifat-sifat kulit pada komponen material tingkat tinggi Anda.

Strategi Keseluruhan

Dan itulah strategi keseluruhan yang saya gunakan sekarang, dan itu terutama menjadi semakin membantu semakin kompleks masalah rendering Anda. Sebagai sisi negatifnya memang membutuhkan sedikit pekerjaan di muka daripada seperti menyuntikkan entitas game Anda dengan shader dan VBO dan hal-hal seperti itu, dan itu juga pasangan renderer Anda lebih ke mesin game khusus Anda (atau abstraksinya, meskipun dalam pertukaran tingkat yang lebih tinggi entitas dan konsep game menjadi sepenuhnya terpisah dari masalah rendering tingkat rendah). Dan penyaji Anda mungkin perlu hal-hal seperti callback untuk memberi tahu ketika entitas dihancurkan sehingga dapat membatalkan dan menghancurkan data yang terkait dengannya (Anda dapat menggunakan penghitungan ulang di sini ataushared_ptruntuk sumber daya bersama, tetapi hanya secara lokal di dalam renderer). Dan Anda mungkin ingin beberapa cara yang efisien untuk mengaitkan dan melepaskan semua jenis data yang dirender ke entitas apa pun dalam waktu konstan (ECS cenderung memberikan ini kelelawar untuk setiap sistem dengan cara Anda dapat mengaitkan jenis komponen baru dengan cepat jika Anda memiliki ECS - jika tidak seharusnya tidak terlalu sulit) ... tetapi pada sisi baiknya semua hal-hal ini kemungkinan akan berguna untuk sistem selain renderer pula.

Memang implementasi nyata mendapatkan lebih banyak nuansa dari ini dan mungkin mengaburkan hal-hal ini sedikit lebih, seperti mesin Anda mungkin ingin berurusan dengan hal-hal seperti segitiga dan simpul di bidang selain rendering (mis: fisika mungkin ingin data seperti itu untuk melakukan deteksi tabrakan ). Tetapi di mana kehidupan mulai menjadi jauh lebih mudah (setidaknya bagi saya) adalah merangkul pola pikir dan strategi pembalikan seperti ini sebagai titik awal.

Dan mendesain penyaji waktu nyata sangat sulit dalam pengalaman saya - hal tersulit yang pernah saya rancang (dan terus merancang ulang) dengan perubahan cepat pada perangkat keras, kemampuan peneduhan, teknik yang ditemukan. Tetapi pendekatan ini tidak menghilangkan kekhawatiran segera ketika sumber daya GPU dapat dibuat / dihancurkan dengan memusatkan semua itu ke implementasi rendering, dan bahkan lebih menguntungkan bagi saya adalah bahwa itu menggeser apa yang seharusnya menjadi mahal dan perubahan desain cascading (yang dapat tumpah ke dalam kode tidak langsung berkaitan dengan rendering) hanya implementasi renderer itu sendiri. Dan pengurangan biaya perubahan dapat menambah penghematan besar dengan sesuatu yang bergeser dalam persyaratan setiap atau dua tahun secepat rendering waktu nyata.

Contoh Bayangan Anda

Cara saya menangani contoh naungan Anda adalah bahwa saya tidak mementingkan diri sendiri dengan hal-hal seperti shader GLSL dalam hal-hal seperti entitas mobil dan orang. Saya menyibukkan diri dengan "bahan" yang merupakan objek CPU yang sangat ringan yang hanya berisi properti yang menggambarkan jenis bahan apa itu (kulit, cat mobil, dll). Dalam kasus saya yang sebenarnya, ini sedikit canggih karena saya memiliki DSEL yang mirip dengan Unreal Blueprints untuk pemrograman shader menggunakan bahasa visual, tetapi bahan tidak menyimpan pegangan shader GLSL.

ShaderProgram menghitung referensi mereka dan hanya memanggil glDeleteProgram di destructor mereka ketika hitungannya 0 DAN ShaderManager secara berkala memeriksa ShaderProgram dengan hitungan 1 dan membuangnya.

Saya dulu melakukan hal serupa ketika saya menyimpan dan mengelola sumber daya semacam ini "di luar angkasa" di luar penyaji karena upaya naif saya yang paling awal yang hanya mencoba untuk langsung menghancurkan sumber daya tersebut dalam sebuah destructor sering mencoba untuk menghancurkan sumber daya di luar konteks GL yang valid (dan kadang-kadang saya bahkan secara tidak sengaja mencoba membuatnya dalam skrip atau sesuatu ketika saya tidak berada dalam konteks yang valid), jadi saya perlu menunda pembuatan dan penghancuran pada kasus-kasus di mana saya dapat menjamin saya berada dalam konteks yang valid yang mengarah ke desain "manajer" serupa yang Anda gambarkan.

Semua masalah ini hilang jika Anda menyimpan sumber daya CPU di tempatnya dan meminta penyaji menangani masalah pengelolaan sumber daya GPU. Saya tidak bisa menghancurkan shader OGL di mana pun, tetapi saya dapat menghancurkan materi CPU di mana saja dan dengan mudah digunakan shared_ptrdan sebagainya tanpa membuat saya kesulitan.

Kapan benar-benar menghapus ShaderProgram dari ShaderManager statis? Setiap beberapa menit? Setiap Loop Game? Saya dengan anggun menangani kompilasi ulang shader dalam kasus ketika ShaderProgram dihapus tapi kemudian WorldEntity baru memintanya; tapi saya yakin ada cara yang lebih baik.

Sekarang kekhawatiran itu sebenarnya sulit, bahkan dalam kasus saya jika Anda ingin mengelola sumber daya GPU secara efisien dan mengeluarkannya ketika tidak lagi diperlukan. Dalam kasus saya, saya bisa berurusan dengan adegan besar dan saya bekerja di VFX daripada permainan di mana artis mungkin memiliki konten yang intens tidak dioptimalkan untuk rendering realtime (tekstur epik, model yang mencakup jutaan poligon, dll).

Ini sangat berguna untuk kinerja bukan hanya untuk menghindari rendering ketika mereka offscreen (keluar dari tampilan frustrasi) tetapi juga membongkar sumber daya GPU ketika tidak lagi diperlukan untuk sementara waktu (katakanlah pengguna tidak melihat sesuatu jalan keluar di kejauhan ruang untuk sementara waktu).

Jadi solusi yang paling sering saya gunakan adalah solusi "timestamped", meskipun saya tidak yakin seberapa bisa diterapkannya dengan game. Ketika saya mulai menggunakan / mengikat sumber daya untuk rendering (mis: mereka lulus tes culling frustum), saya menyimpan waktu saat ini dengan mereka. Kemudian secara berkala ada pemeriksaan untuk melihat apakah sumber daya tersebut tidak pernah digunakan untuk sementara waktu, dan jika demikian, mereka dibongkar / dihancurkan (meskipun data CPU asli yang digunakan untuk menghasilkan sumber daya GPU disimpan sampai entitas sebenarnya yang menyimpan komponen-komponen tersebut dihancurkan) atau sampai komponen-komponen tersebut dikeluarkan dari entitas). Ketika jumlah sumber daya meningkat dan lebih banyak memori digunakan, sistem menjadi lebih agresif dalam membongkar / menghancurkan sumber daya tersebut (jumlah waktu idle yang diperbolehkan untuk yang lama,

Saya membayangkan itu sangat tergantung pada desain game Anda. Karena jika Anda memiliki game dengan pendekatan yang lebih tersegmentasi dengan level / zona yang lebih kecil, maka Anda mungkin dapat (dan menemukan waktu termudah menjaga frame rate stabil) memuat semua sumber daya yang diperlukan untuk level tersebut di muka dan membongkar mereka ketika pengguna pergi ke level berikutnya. Sedangkan jika Anda memiliki beberapa permainan dunia terbuka besar yang mulus seperti itu, Anda mungkin perlu strategi yang jauh lebih canggih untuk mengontrol kapan membuat dan menghancurkan sumber daya ini, dan mungkin ada tantangan yang lebih besar di sana untuk melakukan ini semua tanpa gagap. Dalam domain VFX saya sedikit cegukan untuk membingkai tingkat tidak sebesar masalah besar (saya mencoba untuk menghilangkan mereka dalam alasan) karena pengguna tidak akan bermain-main sebagai akibatnya.

Semua kerumitan ini dalam kasus saya masih terisolasi ke sistem rendering, dan sementara saya telah menggeneralisasi kelas dan kode untuk membantu mengimplementasikannya, tidak ada kekhawatiran tentang konteks GL yang valid dan godaan untuk menggunakan global atau semacamnya.

Energi Naga
sumber
1

Daripada melakukan penghitungan referensi di ShaderProgramkelas itu sendiri, lebih baik untuk mendelegasikan itu ke kelas pointer cerdas, seperti std::shared_ptr<>. Dengan begitu, Anda memastikan bahwa setiap kelas hanya memiliki satu pekerjaan yang harus dilakukan.

Untuk menghindari melelahkan sumber daya OpenGL Anda secara tidak sengaja, Anda dapat membuat ShaderProgramnon-copyable (swasta / dihapus copy-constructor dan operator penugasan copy).
Untuk menyimpan repositori terpusat dari ShaderPrograminstance yang dapat dibagi, Anda bisa menggunakan SharedShaderProgramFactory(mirip dengan manajer statis Anda, tetapi dengan nama yang lebih baik) seperti ini:

class SharedShaderProgramFactory {
private:
  std::weak_ptr<ShaderProgram> program_a;

  std::shared_ptr<ShaderProgram> get_progam_a()
  {
    shared_ptr<ShaderProgram> temp = program_a.lock();
    if (!temp)
    {
      // Requested program does not currently exist, so (re-)create it
      temp = new ShaderProgramA();
      program_a = temp; // Save for future requests
    }
    return temp;
  }
};

Kelas pabrik dapat diimplementasikan sebagai kelas statis, Singleton atau ketergantungan yang dilewati jika diperlukan.

Bart van Ingen Schenau
sumber
-3

Opengl telah dirancang sebagai pustaka C, dan memiliki karakteristik perangkat lunak prosedural. Salah satu aturan pembuka yang berasal dari menjadi pustaka C terlihat seperti ini:

"Ketika kompleksitas adegan Anda meningkat, Anda akan memiliki lebih banyak pegangan yang perlu diberikan di sekitar kode"

Ini adalah fitur api terbuka. Pada dasarnya ini mengasumsikan bahwa seluruh kode Anda berada di dalam fungsi main (), dan semua pegangan itu dilewatkan melalui variabel lokal main ().

Kepastian aturan ini adalah sebagai berikut:

  1. Anda seharusnya tidak mencoba untuk meletakkan tipe atau antarmuka ke jalur data-passing. Alasannya adalah bahwa jenis ini tidak stabil, membutuhkan perubahan konstan ketika kompleksitas adegan Anda meningkat.
  2. Jalur data-passing harus dalam fungsi main ().
tp1
sumber
Jika tertarik untuk mengetahui mengapa ini menarik suara. Sebagai seseorang yang tidak terbiasa dengan topik itu, akan sangat membantu untuk mengetahui apa yang salah dengan jawaban ini.
RubberDuck
1
Saya tidak memilih di sini dan ingin tahu juga, tapi mungkin gagasan bahwa OGL dirancang di seluruh kode yang ada di dalam maintampaknya agak rumit bagi saya (setidaknya dalam kata-kata).
Dragon Energy