エンティティシステムとレンダリング


10

わかった、これまで私が知っていること。エンティティには、次のような情報を保持するコンポーネント(データストレージ)が含まれています。-テクスチャ/スプライト-シェーダー-など

そして、これをすべて描画するレンダラーシステムがあります。しかし、私が理解していないのは、レンダラーの設計方法です。「ビジュアルタイプ」ごとに1つのコンポーネントが必要ですか。シェーダーなしのコンポーネント、シェーダー付きのコンポーネントなど

これを行うための「正しい方法」についての入力が必要です。注意すべきヒントと落とし穴。


2
物事を一般化しすぎないようにしてください。エンティティがSpriteコンポーネントではなくShaderコンポーネントを持つのは奇妙に思えるので、ShaderはSpriteコンポーネントの一部である必要があります。当然、その場合、必要なレンダリングシステムは1つだけです。
ジョナサンコネル

回答:


7

エンティティコンポーネントシステムをどのように構成するかについては、だれもが独自のアイデアを持っているため、これは答えるのが難しい質問です。私にできる最善のことは、私にとって最も役立つとわかったいくつかのことをあなたと共有することです。

エンティティ

私はECSにファットクラスのアプローチをとっています。おそらく、プログラミングの極端な方法が(人間の生産性の観点から)非常に非効率的であることがわかったためです。そのために、私にとってのエンティティは、より専門的なクラスによって継承される抽象クラスです。エンティティには、いくつかの仮想プロパティと、このエンティティが存在する必要があるかどうかを示す単純なフラグがあります。レンダーシステムに関する質問と比較すると、これはEntity次のようになります。

public abstract class Entity {
    public bool IsAlive = true;
    public virtual SpatialComponent   Spatial   { get; set; }
    public virtual ImageComponent     Image     { get; set; }
    public virtual AnimationComponent Animation { get; set; }
    public virtual InputComponent     Input     { get; set; }
}

部品

コンポーネントは、彼らがいないという点で、「愚か」ですやる知っている何かを。それらは他のコンポーネントへの参照がなく、通常は関数がありません(私はC#で作業するので、プロパティを使用してゲッター/セッターを処理します-関数がある場合、それらは保持しているデータの取得に基づいています)。

システム

システムはそれほど「愚か」ではありませんが、まだ馬鹿げたオートマトンです。それらには、システム全体のコンテキストがなく、他のシステムへの参照がなく、個別の処理を行うために必要ないくつかのバッファーを除いて、データを保持していません。システムによっては、専用のUpdateDrawメソッド、または場合によってはその両方がある場合があります。

インターフェース

インターフェースは私のシステムの重要な構造です。それらはSystem、何が処理できるか、何ができるかを定義するために使用されますEntity。レンダリングに関連するインターフェースは、IRenderableおよびIAnimatableです。

インターフェースは、使用可能なコンポーネントをシステムに通知するだけです。たとえば、レンダリングシステムは、エンティティの境界ボックスと描画する画像を知っている必要があります。私の場合、それは次のようになりますSpatialComponentImageComponent。したがって、次のようになります。

public interface IRenderable {
    SpatialComponent Component { get; }
    ImageComponent   Image     { get; }
}

RenderingSystem

では、レンダリングシステムはどのようにしてエンティティを描画するのでしょうか。それは実際には非常に単純なので、簡単なクラスを表示してアイデアを提供します。

public class RenderSystem {
    private SpriteBatch batch;
    public RenderSystem(SpriteBatch batch) {
        this.batch = batch;
    }
    public void Draw(List<IRenderable> list) {
        foreach(IRenderable obj in list) {
            this.batch.draw(
                obj.Image.Texture,
                obj.Spatial.Position,
                obj.Image.Source,
                Color.White);
        }
    }
}

クラスを見ると、レンダーシステムは何であるかさえ知りませんEntity。それが知っているすべてはそれでありIRenderable、それは単にそれらを描くためにそれらのリストが与えられます。

すべての仕組み

新しいゲームオブジェクトを作成する方法と、それらをシステムに供給する方法も理解するのに役立ちます。

エンティティの作成

すべてのゲームオブジェクトはEntityと、そのゲームオブジェクトが実行できることを説明する適切なインターフェイスを継承します。画面上でアニメーション化されるほぼすべては次のようになります。

public class MyAnimatedWidget : Entity, IRenderable, IAnimatable {}

システムへの給餌

と呼ばれる単一のリストに、ゲームの世界に存在するすべてのエンティティのリストを保持しますList<Entity> gameObjects。次に、各フレームでそのリストをふるいにかけ、オブジェクトの参照をList<IRenderable> renderableObjects、やなどのインターフェイスタイプに基づいて、より多くのリストにコピーしますList<IAnimatable> animatableObjects。このように、異なるシステムが同じエンティティを処理する必要がある場合、それらを処理できます。次に、それらのリストをシステムUpdateまたはDrawメソッドのそれぞれに渡し、システムに機能を任せます。

アニメーション

あなたはアニメーションシステムがどのように機能するか知りたいかもしれません。私の場合、IAnimatableインターフェースを確認したい場合があります。

public interface IAnimatable {
    public AnimationComponent Animation { get; }
    public ImageComponent Image         { get; set; }
}

ここで注目すべき重要なImageComponent点は、IAnimatableインターフェースの側面が読み取り専用ではないことです。それはセッターを持っています。

ご想像のとおり、アニメーションコンポーネントはアニメーションに関するデータのみを保持しています。フレーム(画像コンポーネント)、現在のフレーム、描画される1秒あたりのフレーム数、最後のフレーム増分からの経過時間、およびその他のオプションのリスト。

アニメーションシステムは、レンダリングシステムと画像コンポーネントの関係を利用します。エンティティの画像コンポーネントを変更するだけで、アニメーションのフレームが増加します。このようにして、アニメーションはレンダリングシステムによって間接的にレンダリングされます。


おそらく、これがエンティティコンポーネントシステムと呼ばれているものに近いかどうかは本当にわからないことに注意してください。コンポジションベースのデザインを実装しようとしたとき、私はこのパターンに陥っていました。
Cypher、

面白い!私はあなたのエンティティの抽象クラスにあまり熱心ではありませんが、IRenderableインターフェイスは良いアイデアです!
ジョナサンコネル

5

参照してくださいこの回答私が話しているシステムの種類を確認するために。

コンポーネントには、何を描画し、どのように描画するかの詳細が含まれている必要があります。レンダリングシステムはこれらの詳細を取得し、コンポーネントで指定された方法でエンティティを描画します。大幅に異なる描画テクノロジを使用する場合にのみ、個別のスタイルに個別のコンポーネントを使用できます。


3

ロジックをコンポーネントに分割する主な理由は、エンティティで組み合わせると有用で再利用可能な動作を生成する一連のデータを作成することです。たとえば、エンティティをPhysicsComponentとRenderComponentに分離することは、すべてのエンティティがPhysicsを持つわけではなく、一部のエンティティがSpriteを持たない可能性があるため、理にかなっています。

あなたの質問に答えるために、あなたはあなたのアーキテクチャを見て、あなた自身に2つの質問をする必要があります:

  1. テクスチャのないシェーダーがあることは理にかなっていますか
  2. ShaderをTextureから分離すると、コードの重複を回避できますか?

コンポーネントを分割する際に重要なのは、この質問をすることです。1の答えが「はい」の場合、おそらく1つはシェーダーを持ち、もう1つはテクスチャーを持つ別々のコンポーネントを作成するのに適した候補です。通常、2。に対する答えは、複数のコンポーネントが位置を使用できるPositionのようなコンポーネントに対してはyesです。

たとえば、PhysicsとAudioの両方が同じ位置を使用する可能性があります。重複する位置を格納する両方のコンポーネントが1つのPositionComponentにリファクタリングするのではなく、PhysicsComponent / AudioComponentを使用するエンティティにもPositionComponentが必要です。

あなたが私たちに提供した情報に基づいて、シェーダーは完全にテクスチャーに依存し、他には何も依存しないため、RenderComponentはTextureComponentとShaderComponentに分割するのに適した候補ではないようです。

T-Machine:Entity Systemsのようなものを使用しているとすると、C ++でのRenderComponentとRenderSystemのサンプル実装は次のようになります。

struct RenderComponent {
    Texture* textureData;
    Shader* shaderData;
};

class RenderSystem {
    public:
        RenderSystem(EntityManager& manager) :
            m_manager(manager) {
            // Initialize Window, rendering context, etc...
        }

        void update() {
            // Get all the entities with RenderComponent
            std::vector<RenderComponent>& components = m_manager.getComponents<RenderComponent>();

            for(auto component = components.begin(); entity != components.end(); ++components) {
                // Do something with the texture
                doSomethingWithTexture(component->textureData);

                // Do something with the shader if it's not null
                if(component->shaderData != nullptr) {
                    doSomethingWithShader(component->shaderData);
                }
            }
        }
    private:
        EntityManager& m_manager;
}

それは完全に間違っています。コンポーネントの全体のポイントは、エンティティからそれらを分離することであり、レンダリングシステムにエンティティを検索してそれらを見つけることではありません。レンダリングシステムは、独自のデータを完全に制御する必要があります。PS std :: vector(特にインスタンスデータの場合)をループに入れないでください。これはひどい(遅い)C ++です。
snake5

@ snake5あなたは両方の点で正しいです。私は頭の上からコードを入力しましたが、指摘してくれてありがとう、いくつかの問題がありました。影響を受けるコードの速度が遅くなり、エンティティシステムのイディオムを正しく使用するように修正しました。
ジェイクウッズ

2
@ snake5フレームごとにデータを再計算するのではなく、getComponentsは、既知のm_managerが所有するベクトルを返し、コンポーネントを追加/削除したときにのみ変更されます。これは、同じエンティティの複数のコンポーネントを使用したいシステム(たとえば、PositionComponentとPhysicsComponentを使用したいPhysicsSystem)がある場合に有利です。他のシステムではおそらく位置が必要であり、PositionComponentを使用することで、データが重複することはありません。主に、コンポーネントの通信方法の問題を解決します。
ジェイクウッズ

5
@ snake5問題は、ECシステムのレイアウトやパフォーマンスです。問題はレンダーシステムのセットアップです。ECシステムを構築する方法は複数あります。ここでは、互いにパフォーマンスの問題に追いつかないようにしてください。OPは、おそらくどちらの回答とも完全に異なるEC構造を使用しています。この回答で提供されるコードは、例をよりよく示すためのものであり、パフォーマンスについて批判されることはありません。パフォーマンスに関する質問の場合、おそらくこれは「役に立たない」と答えますが、そうではありません。
MichaelHouse

2
サイファーよりも、この回答に示されているデザインの方がずっと好きです。それは私が使うものと非常によく似ています。変数が1つまたは2つしかない場合でも、小さいコンポーネントほどimoが優れています。それらは、エンティティの側面を定義する必要があります。たとえば、「Damagable」コンポーネントには2つ、おそらく4つの変数(各ヘルスとアーマーの最大値と現在値)があります。これらのコメントは長くなっています。もっと議論したい場合はチャットに移りましょう。
ジョンマクドナルド

2

落とし穴#1:過剰に設計されたコード。かなり長い間、それと共存する必要があるため、実装するすべてのものが本当に必要かどうかを検討してください。

落とし穴#2:オブジェクトが多すぎる。自動化処理が難しくなるだけなので、オブジェクトが多すぎるシステム(タイプごと、サブタイプごとに1つ)は使用しません。私の意見では、(1つの機能ではなく)各オブジェクトが特定の機能セットを制御する方がはるかに優れています。たとえば、レンダリングに含まれるデータの各ビットのコンポーネント(テクスチャコンポーネント、シェーダーコンポーネント)の作成はあまりに分割されています。通常、とにかくそれらすべてのコンポーネントを一緒に持つ必要があるでしょうね。

落とし穴#3:外部制御が厳しすぎる。オブジェクトはレンダラー/テクスチャタイプ/シェーダーフォーマット/何でも変更できるため、シェーダー/テクスチャオブジェクトの名前を変更することをお勧めします。名前は単純な識別子です-それらから何を作成するかはレンダラーが決定します。ある日、プレーンシェーダーの代わりにマテリアルが必要になる場合があります(たとえば、データからシェーダー、テクスチャ、ブレンドモードを追加します)。テキストベースのインターフェースを使用すると、実装がはるかに簡単になります。

レンダラーに関しては、コンポーネントによって作成されたオブジェクトを作成/破棄/維持/レンダリングするシンプルなインターフェースにすることができます。それの最も原始的な表現は次のようなものになるでしょう:

class Renderer {
    function Draw() { ... }
    function AddSprite( ... ) { ... return sprite; }
    function RemoveSprite( sprite ) { ... }
    ...
};

これにより、コンポーネントからこれらのオブジェクトを管理し、好きなようにレンダリングできるように十分な距離を保つことができます。

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