OpenGLとC++で動作しているゲームエンジンを抽象化しているうさぎの穴を掘り下げました。反対側に到達する前に、現在のデザインについてお聞きしたいと思います。これは、さらに検討するほど、最終的には大きな修正が必要になるということです。ウィンドウ、入力、フレームバッファを問題から分離しましたが、レンダリング操作が問題になっています。
現在、フレームバッファーを含むすべてのタイプのレンダリング可能なオブジェクトは、共通のクラスから継承しています:Renderable
には、シェーダープログラムと頂点配列オブジェクトがあります。階層の次はEntity
(ええ、私はより良い名前を付けます..)で、モデルマトリックス、世界座標、ユニフォーム、およびそれに関連する関数があります。
その後は、特定のタイプのオブジェクトです。つまり、Dummy
とModel
です。それらは実際のバッファとデータを扱います。この例では、Model
に焦点を当てます。ファイルからモデルデータを読み込み、バッファを作成し、コマンドでモデルを描画します。各属性にバッファを使用して、メッシュを個別に処理し、インデックスバッファを使用して描画することを計画しました。
また、バッファの概念をレンダリング可能要素から分離しました。インデックスバッファーと頂点バッファー(またはインターリーブバッファー)のクラスを使用します。バッファはデータを管理します。バインドGLはバッファリングし、頂点配列ポインタなどを処理します。
GameDevでのVAOとVBOの粒度について この質問 を尋ねるまで、これはすべて問題ありませんでした。それ以来、より多くの疑問が生じています。
とすれば:
私は尋ねなければなりません:
特に、複数のメッシュとその複数の属性を1つのバッファーに組み合わせる必要がある場合、バッファーはVAOについて知る必要があり、モデルはバッファーに情報を提供する必要があるため、設計が少し複雑になるようです。
どんな洞察もいただければ幸いです!
過去に抽象化されたレンダリングが多すぎるという悪い経験をしたことがあります。 OpenGLは依然としてその中心にある非常に手続き型であり、NiceモダンOOフィルターを適用して多数の小さなオブジェクトを作成する場合、状態管理全体をデバッグおよび拡張することは非常に困難です。
さらに、フレームをレンダリングするためのさまざまな手順やコンテンツ管理/プリロードだけでも、エンジンの他の部分で使用されているアプローチにうまく対応できないため、最適化されたレンダリングコードを記述すると、グローバルビューが必要になることがよくあります。
したがって、私は機能を可能な限りレイヤーに分離するので、あなたは「ルーザー」OO OpenGL状態を扱うより長いメソッドを持つ世界、そしてあなたがいる素敵なクリーンな世界に住むことができますより抽象化された、Nice OOコンテキストで動作できます。ここにはDDDへのリンクがあり、ドメインのニーズに応じて設計された個別のドメインがあります。
私があなたが言ったことのいくつかの行の間を読み、あなたがデザインで動かそうとしている場所を推測しようと試みることができるなら、私は同様のウサギの穴を掘り下げて、より多くの灰色の髪で呪いを見つけたと思います。
私の特定のケースでは、これらすべてのレンダリングバックエンド(OpenGL、OpenGL ES、DirectX、コンソールなど)のレンダリング機能を1つの抽象レンダリングインターフェイスに抽象化して、IRenderer
のようにすべてをルール化して、 1つのレンダラーサブタイプを別のレンダラーサブタイプに交換するだけで移植の悲しみを最小限に抑える(例:RendererGl
とRendererDx
)。これは少し異なり、おそらくあなたがやっていることよりも野心的ですが、私たちの設計の頭痛には共通のテーマがあると思います。
これらすべてのバックエンド間の異なるレンダリング機能を超えてそのルートで設計するのが根本的に非常に困難であり、それらすべてに対して統一された抽象化を設計しようとすることの難しさは、レンダラーにゲームエンジンの残りの部分を知らせないようにする方法でしたそして世界。レンダラーを他のすべてのものから切り離し、ゲームエンジンに、レンダラーにエンジンに関する知識を持たせる代わりに、レンダリングインターフェイスの使用方法に関する知識を持たせるようにしました。
簡単に言うと、私のレンダラーインターフェイスは次のように巨大でした。
class IRenderer
{
public:
// boatload of functions related to rendering along
// with auxiliary abstract interfaces like ones for
// vertex buffers and textures.
};
また、パーティクルシステム、モデル、ボーン、マテリアル、ライトなどのエンジンの高レベルな抽象概念を完全に意識していませんでした。今でも、レンダリング可能なコンポーネントとレンダリングするコンポーネントを注入して関連付けることにより、ゲームロジックをレンダリングコードから切り離しました。しかし、基本的に私はレンダラーにゲームの世界やシーンを知らせないようにしようとしていました。
私がそう思ったのは、レンダラーがゲームの世界/シーン全体とその中のすべての抽象にアクセスして理解できるようにすることでした。これにより、よりオブジェクト指向のアプローチを使用する場合(この頃はECSを使用しています)、レンダラーインターフェイスは次のように非常に単純になっています。
class IRenderer
{
public:
virtual ~IRender() {}
virtual void render(const IGameScene& scene) = 0;
};
そして、それは私がシーンをレンダリングするためにその関数のこの壮大な実装を持っていることを意味します(さまざまな小さな関数と中型オブジェクトに分解されますが)、それは私のレンダラーが私のエンジン以外のエンジンで動作するようにすることができないことを意味します。各具象レンダラーは、たとえば、私の特定のエンジンのパーティクルシステムの抽象化について、そしてそれをレンダリングする方法は、具象レンダーが必要とする低レベルのメカニズムを使用する次第です。
そのようにすると、ゲームエンジンの残りの部分を好きなだけ抽象化し、データのレンダリングやバッファの保存と作成と管理に最も効果的なものと格闘することなく、高レベルのロジックの観点から考えることができます。シェーダーやテクスチャなどをバインドします。これはすべてレンダラー自体で処理されます。
ファイルから読み込まれるモデルなどの場合、レンダラーは読み込まれていないシーンで抽象的なモデルに遭遇し、ファイルを読み込んでから、その後のレンダリングに使用するVBOをそのモデルに外部的に関連付けます。レンダリングの悲惨な詳細はすべて、コンクリートレンダラー内にあります。
そして、それはまだレンダラーを実装するのは簡単ではありません(コンポーネントなどのデータを、VBOコンポーネントなどの既存のエンティティに一般的にアタッチできる場合に役立ちます)が、これまでのやり方よりもはるかに簡単でしたデザインを正しくする(実装はまだ困難でした)ため、私は自分のデザインやインターフェイスを使ってそれらを形にしようと戦う必要がなくなりました。あなたがリストしたこれらの種類の懸念は、その場合、(中心的なデザインを変更するように誘惑しないという意味で)かなり悲観的になります。なぜなら、特定のレンダラーの実装に必要なすべての呼吸の余裕があり、詳細を徹底的に把握できるからです。また、OpenGLまたはその他のバックエンドの特殊性により、ゲームの世界を構成する抽象オブジェクト/エンティティを効率的にレンダリングできます。
ここには、非常に一般化された設計の種類のレッスンがあります。何かがある場合、A
(サブシステム、インターフェース、インターフェースのコレクション、高レベルのゲームエンジンの抽象化など)は比較的簡単に正しく設計できますが、B
(レンダラーなど)は非常に難しいです正しく設計してから、A depend on B
。のような代わりに:
知識と依存関係の流れを逆転させて、B depend on A
、 そのようです:
現在、依存関係はB
に流れておらず、中心的な設計を変更せずに変更する必要があるすべての呼吸があります(「中心的な設計」は、多くのものが依存するものであり、非常にコストがかかり、困難です。変更する)。基本的に、正しく取得するのが難しいことを変更することを困難にしないでください。そのように単純明快ですが、私はほとんどの人が依存関係を本能的に抽象化し、最も簡単に設計できる方向に依存関係を流す傾向があるとは思わないので、それはやや人間的に直観に反すると思います。各抽象化の背後にある最小の実装を生成する方法で抽象化を作成し、依存関係を正しく設計するのが最も難しいものから離れるように指示するのではなく、依存関係をそのように指示する方が直感的です。それでも、上位の設計が常に私たちにそれらを再考するように誘惑するならば、より小さな実装は非難されます。