ゲームデータ/ロジックをレンダリングから分離する


21

私はC ++とOpenGL 2.1を使用してゲームを書いています。データ/ロジックをレンダリングから分離する方法を考えていました。現時点では、描画を実装するための純粋な仮想メソッドを提供する基本クラス「Renderable」を使用しています。しかし、すべてのオブジェクトには特別なコードがあり、シェーダーのユニフォームを適切に設定し、頂点配列バッファーデータを整理する方法を知っているのはオブジェクトだけです。コード全体で多くのgl *関数呼び出しが発生します。オブジェクトを描画する一般的な方法はありますか?


4
コンポジションを使用して、レンダリング可能なオブジェクトを実際にオブジェクトにアタッチし、オブジェクトをそのm_renderableメンバーと対話させます。そうすれば、ロジックをより適切に分離できます。物理学、ai、その他もある一般的なオブジェクトにレンダリング可能な「インターフェース」を強制しないでください。その後、レンダリング可能物を個別に管理できます。さらに物事を切り離すために、OpenGL関数呼び出しを抽象化するレイヤーが必要です。したがって、優れたエンジンがさまざまなレンダリング可能な実装内でGL API呼び出しを行うことを期待しないでください。マイクロナットシェルでそれだけです。
テオドロン

1
@teodron:なぜそれを答えにしないのですか?
タピオ

1
@Tapio:それはあまり答えではないからです。代わりに提案されたものです。
テオドロン

回答:


20

アイデアは、訪問者のデザインパターンを使用することです。小道具をレンダリングする方法を知っているRenderer実装が必要です。すべてのオブジェクトがレンダラーインスタンスを呼び出して、レンダリングジョブを処理できます。

数行の擬似コードで:

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 *スタッフはレンダラーのメソッドによって実装され、オブジェクトはレンダリングに必要なデータ、位置、テクスチャタイプ、サイズなどのみを保存します。

また、異なるレンダラー(debugRenderer、hqRendererなど)をセットアップし、オブジェクトを変更せずにこれらを動的に使用できます。

これは、エンティティ/コンポーネントシステムと簡単に組み合わせることができます。


1
これはかなり良い答えです!Entity/Componentジオメトリプロバイダーを他のエンジンパーツ(AI、物理学、ネットワーク、または一般的なゲームプレイ)から分離するのに役立つため、この代替案をもう少し強調することもできます。+1!
テオドロン

1
@ teodron、E / Cの代替案については説明しません。しかし、私はあなたが変更する必要があるObjectAと思うし、ObjectBper DrawableComponentADrawableComponentB、そしてrenderメソッド内で、必要であれば他のコンポーネントを使用position = component->getComponent("Position");します。
ジェン

関数とレンダリング可能なすべてのオブジェクトが実装されたインターフェース(などRenderable)だけでdraw(Renderer&)実装しないのはなぜですか?どちらの場合Rendererでも、共通のインターフェースを実装して呼び出すオブジェクトを受け入れる関数が1つだけ必要renderable.draw(*this);ですか?
ヴィーテファルコン

1
@ViteFalcon、明確にしないとすみませんが、詳細な説明のために、より多くのスペースとコードが必要です。基本的に、私のソリューションはgl_*関数をレンダラーに移動します(ロジックをレンダリングから分離します)が、ソリューションはgl_*呼び出しをオブジェクトに移動します。
ジェン

この方法では、gl *関数は実際にオブジェクトコードから移動しますが、バッファー/テクスチャID、ユニフォーム/属性の場所など、レンダリングで使用されるハンドル変数を保持しています。
フェリー

4

あなたはすでにZhenの答えを受け入れていることは知っていますが、他の人に役立つ場合に備えて、そこに別のものを入れたいと思います。

問題を繰り返すために、OPは、レンダリングコードをロジックやデータから分離しておく機能を求めています。

私の解決策は、異なるクラスを一緒に使用してコンポーネントをレンダリングすることです。これはRenderer、論理クラスとは別です。最初にRenderable関数を持つインターフェイスが必要でbool render(Renderer& renderer);あり、Rendererクラスはs Renderableのリストが与えられ、インスタンスGameObjectを持つオブジェクトをレンダリングするビジターパターンを使用してすべてのインスタンスを取得しRenderableます。このように、レンダラーはそこにあるすべてのオブジェクトタイプを知る必要はなくRenderablegetRenderable()関数を介してそれを通知するのは各オブジェクトタイプの責任です。または、RenderableVisitorすべてのGameObjectを訪問するクラスを作成し、個々のGameObject条件に基づいて、ビジターにレンダリング可能を追加するかどうかを選択できます。いずれにせよ、主な要点はgl_*呼び出しはすべてオブジェクト自体の外部にあり、の一部ではなく、オブジェクト自体の詳細を知っているクラスに存在しますRenderer

免責事項:これらのクラスをエディターで手書きしたので、コード内の何かを見逃した可能性は十分にありますが、うまくいけばアイデアが得られるでしょう。

(部分的な)例を表示するには:

Renderable インタフェース

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

GameObject クラス:

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

(部分)Rendererクラス。

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 クラス:

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 クラス:

// 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 クラス:

#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

レンダリングコマンドシステムを構築します。両方へのアクセス持つ高レベルのオブジェクト、OpenGLRendererおよびシーングラフ/ゲームオブジェクトは、シーングラフやゲームオブジェクトを反復し、バッチ構築しRenderCmds、その後に提出され、OpenGLRenderer順番にそれぞれを描画し、それによって全てのOpenGL含みます関連するコード。

これには単なる抽象化よりも多くの利点があります。最終的には、レンダリングの複雑さが増すにつれて、各レンダリングコマンドをテクスチャまたはシェーダーごとにソートおよびグループ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

それは、すべてのレンダリング可能なエンティティに共通するものについて推測できるかどうかに完全に依存します。私のエンジンでは、すべてのオブジェクトが同じ方法でレンダリングされるため、vbo、テクスチャ、および変換を提供するだけで済みます。次に、レンダラーがそれらすべてをフェッチするため、異なるオブジェクトでOpenGL関数呼び出しはまったく必要ありません。


1
天気=雨、太陽、暑い、寒い:P->天気
トビアスキンツラー

3
@TobiasKienzler彼のスペルを修正する場合は、正しいかどうかをスペルしてみてください:-)
TASagent

@TASagentなに、そしてマフリーの法則にブレーキをかけるm- /
トビアスキーンズラー

1
タイプミスを修正
-danijar

2

レンダリングコードとゲームロジックを異なるクラスに確実に配置します。(テオドロンが示唆したように)構成がおそらくこれを行うための最良の方法です。ゲーム世界の各エンティティには、独自のレンダリング可能オブジェクト、またはおそらくそれらのセットがあります。

Renderableの複数のサブクラスがまだある場合があります。たとえば、基本的なテクスチャライティングシェーダーに加えて、骨格アニメーション、パーティクルエミッター、複雑なシェーダーを処理するためです。Renderableクラスとそのサブクラスには、レンダリングに必要な情報、つまりジオメトリ、テクスチャ、シェーダーのみを含める必要があります。

さらに、特定のメッシュのインスタンスをメッシュ自体から分離する必要があります。スクリーン上に同じメッシュを使用する100本の木があるとします。ジオメトリを1回だけ保存したいのですが、ツリーごとに個別の場所と回転行列が必要になります。アニメーション化されたヒューマノイドなどのより複雑なオブジェクトには、追加の状態情報(スケルトン、現在適用されているアニメーションのセットなど)もあります。

レンダリングするための単純なアプローチは、すべてのゲームエンティティを反復処理し、レンダリングするように指示することです。あるいは、各エンティティ(スポーンするとき)は、レンダリング可能なオブジェクトをシーンオブジェクトに挿入できます。次に、レンダリング関数がシーンにレンダリングするよう指示します。これにより、ゲームエンティティまたは特定のレンダリング可能なサブクラスにコードを埋め込むことなく、シーンで複雑なレンダリング関連の処理を実行できます。


2

このアドバイスは、レンダリングに特化したものではありませんが、物事を大きく分離するシステムを考え出すのに役立つはずです。まず、位置情報とは別に「GameObject」データを保持してみてください。

単純なXYZ位置情報はそれほど単純ではない可能性があることに注意してください。物理エンジンを使用している場合、位置データはサードパーティエンジン内に保存できます。それらの間で同期する必要があり(多くの無意味なメモリコピーが必要になります)、エンジンから直接情報を照会します。ただし、すべてのオブジェクトに物理が必要なわけではありません。一部は固定されているため、単純なフロートのセットがそこで動作します。一部は他のオブジェクトにアタッチされている場合もあるため、それらの位置は実際には別の位置のオフセットです。高度なセットアップでは、コンピューター側でスクリプト、ストレージ、およびネットワークレプリケーションが必要な場合にのみ、GPUにのみ位置を保存できます。そのため、位置データにはいくつかの選択肢があります。ここでは、継承を使用するのが理にかなっています。

オブジェクトがその位置を所有するのではなく、そのオブジェクト自体がインデックスデータ構造によって所有される必要があります。たとえば、「レベル」にはオクトリー、または物理エンジンの「シーン」があります。レンダリングする(またはレンダリングシーンを設定する)場合は、カメラから見えるオブジェクトの特別な構造を照会します。

これは、適切なメモリ管理にも役立ちます。この方法では、実際にエリア内にないオブジェクトは、0.0座標またはエリア内で最後にあったときの座標を返すのではなく、意味のある位置すらありません。

object.getX()の代わりにオブジェクトの座標を保持しなくなった場合、level.getX(object)になります。レベルでオブジェクトを検索することの問題は、すべてのオブジェクトを調べて、クエリを実行するオブジェクトと一致する必要があるため、操作が遅くなる可能性があります。

それを避けるために、おそらく特別な「リンク」クラスを作成します。レベルとオブジェクトの間をバインドするもの。私はそれを「場所」と呼びます。これには、xyz座標と、レベルのハンドルとオブジェクトのハンドルが含まれます。このリンククラスは空間構造/レベルに格納され、オブジェクトはそれへの弱い参照を持ちます(レベル/場所が破壊された場合、オブジェクトの参照はnullに更新する必要があります。レベルが削除された場合、そのようにオブジェクトを「所有」します。特別なインデックス構造、含まれる場所、およびそのオブジェクトも同様です。

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

これで、位置情報は1か所にのみ保存されます。オブジェクト、空間インデックス構造、レンダラーなどの間で複製されません。

Octreesのような空間データ構造は、多くの場合、格納するオブジェクトの座標さえ必要としません。位置は、構造自体のノードの相対位置に格納されます(これは一種の不可逆圧縮と考えることができ、高速なルックアップ時間の精度を犠牲にします)。Octreeのロケーションオブジェクトを使用すると、クエリが完了すると、実際の座標がその中に見つかります。

または、物理エンジンを使用してオブジェクトの場所または2つの場所の混合を管理している場合、Locationクラスはすべてのコードを1か所に保持しながら透過的に処理する必要があります。

もう1つの利点は、レベルへの位置と参照が同じ場所に保存されることです。object.TeleportTo(other_object)を実装して、レベルを越えて機能させることができます。同様に、AIの経路探索は別の領域に何かをたどることができます。

レンダリングに関して。レンダーは、Locationに同様のバインディングを持つことができます。そこにレンダリング固有のものがあるだろうことを除いて。この構造に「オブジェクト」または「レベル」を保存する必要はおそらくないでしょう。オブジェクトは、カラーピッキングなどのことをしようとしている場合や、その上にフローティングするヒットバーをレンダリングしようとしている場合などに役立ちますが、そうでない場合、レンダラーはメッシュなどのみを考慮します。RenderableStuffはメッシュで、バウンディングボックスなどもあります。

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

フレームごとにこれを行う必要はないかもしれませんが、カメラが現在示しているよりも大きな領域を確保することができます。キャッシュし、オブジェクトの動きを追跡して、バウンディングボックスが範囲内にあるかどうかを確認し、カメラの動きを追跡します。ただし、ベンチマークを行うまでは、そのようなものをいじり始めないでください。

物理エンジン自体も同様の抽象化を持つ可能性があります。これは、オブジェクトデータを必要とせず、衝突メッシュと物理プロパティのみを必要とするためです。

コアオブジェクトデータに含まれるのは、オブジェクトが使用するメッシュの名前です。ゲームエンジンは、オブジェクトクラスにレンダリング固有のもの(レンダリングAPI、つまりDirectX対OpenGLなど)の負担をかけることなく、好きな形式でこれを読み込むことができます。

また、さまざまなコンポーネントを分離します。これにより、物理エンジンの交換などを簡単に行えるようになります。これは、ほとんどのものが1つの場所に含まれているためです。また、ユニットテストがはるかに簡単になります。必要なのはLocationクラスだけなので、実際の偽オブジェクトを設定しなくても、物理クエリなどをテストできます。簡単に最適化することもできます。どのクラスと単一の場所でそれらを最適化するためにどのクエリを実行する必要があるかがより明確になります(たとえば、上記のlevel.getVisibleObjectは、カメラがあまり動かない場合にキャッシュできる場所です)。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.