web-dev-qa-db-ja.com

C ++のコンポジションでカプセル化を維持する方法は?

他の複数のクラスMasterABaseおよびCから構成されるクラスDを設計しています。これらの4つのクラスはMaster以外ではまったく使用されず、その機能を管理可能な論理的に分割されたパッケージに分割することを目的としています。また、Baseの場合のように、クライアントから継承できる拡張可能な機能も提供します。

しかし、この設計でMasterのカプセル化をどのように維持できますか?


これまでのところ、私は2つのアプローチを持っていますが、どちらも完璧とはほど遠いものです。

1.すべてのアクセサーを複製します。

Masterを構成するすべてのクラスのすべてのアクセサメソッドのアクセサメソッドを記述するだけです。 Masterの実装の詳細は表示されませんが、これは完全なカプセル化につながりますが、非常に退屈であり、クラス定義を巨大なものにします。また、コンポーズの1つに機能を追加するには(それも単語ですか?)、これらすべてのメソッドをMasterに書き直す必要があります。追加の問題は、Baseの継承者が変更できるだけで機能を追加できないことです。

2.割り当て不可、コピー不可のメンバーアクセサーを使用します。

コピー、移動、または割り当てができないが、_accessor<T>_をオーバーライドして、基になる_operator->_にアクセスするクラス_shared_ptr_があるため、次のような呼び出し

_Master->A()->niceFunction();
_

可能になります。これに関する私の問題は、Masterの実装を変更してniceFunction()の機能に別のクラスを使用することができないため、カプセル化が壊れることです。それでも、これは醜い最初のアプローチを使用せずに得た最も近いものです。また、継承の問題も非常にうまく修正されます。このようなクラスがstdまたはboostにすでに存在しているかどうかという小さな疑問が生じます。


編集:コードの壁

ここで、説明したクラスのヘッダーファイルのコードを投稿します。少しわかりづらいかもしれませんが、全力で説明させていただきます。

1. GameTree.h

そのすべての基礎。これは基本的には二重リンクされたツリーであり、後で取得するGameObject- instancesを保持します。また、独自のカスタムイテレータGTIteratorもありますが、簡潔にするために省略しました。 WResultSUCCESSFAILEDの値を持つ列挙型ですが、それほど重要ではありません。

_class GameTree
{
public:
    //Static methods for the root. Only one root is allowed to exist at a time!
    static void ConstructRoot(seed_type seed, unsigned int depth);
    inline static bool rootExists(){ return static_cast<bool>(rootObject_); }
    inline static weak_ptr<GameTree> root(){ return rootObject_; }

    //delta is in ms, this is used for velocity, collision and such
    void tick(unsigned int delta);


    //Interaction with the tree
    inline weak_ptr<GameTree> parent() const { return parent_; }

    inline unsigned int numChildren() const{ return static_cast<unsigned int>(children_.size()); }
    weak_ptr<GameTree> getChild(unsigned int index) const;
    template<typename GOType>
    weak_ptr<GameTree> addChild(seed_type seed, unsigned int depth = 9001){
        GOType object{ new GOType(seed) };
        return addChildObject(unique_ptr<GameTree>(new GameTree(std::move(object), depth)));
    }                                           
    WResult moveTo(weak_ptr<GameTree> newParent);
    WResult erase();

    //Iterators for for( : ) loop
    GTIterator& begin(){
        return *(beginIter_ = std::move(make_unique<GTIterator>(children_.begin())));
    }
    GTIterator& end(){
        return *(endIter_ = std::move(make_unique<GTIterator>(children_.end())));
    }

    //unloading should be used when objects are far away
    WResult unloadChildren(unsigned int newDepth = 0);
    WResult loadChildren(unsigned int newDepth = 1);

    inline const RenderObject& renderObject() const{ return gameObject_->renderObject(); }

    //Getter for the underlying GameObject (I have not tested the template version)
    weak_ptr<GameObject> gameObject(){
        return gameObject_;
    }
    template<typename GOType>
    weak_ptr<GOType> gameObject(){
        return dynamic_cast<weak_ptr<GOType>>(gameObject_);
    }

    weak_ptr<PhysicsObject> physicsObject() {
        return gameObject_->physicsObject();
    }

private:
    GameTree(const GameTree&); //copying is only allowed internally
    GameTree(shared_ptr<GameObject> object, unsigned int depth = 9001);

    //pointer to root
    static shared_ptr<GameTree> rootObject_;

    //internal management of a child
    weak_ptr<GameTree> addChildObject(shared_ptr<GameTree>);
    WResult removeChild(unsigned int index);

    //private members
    shared_ptr<GameObject> gameObject_;
    shared_ptr<GTIterator> beginIter_;
    shared_ptr<GTIterator> endIter_;

    //tree stuff
    vector<shared_ptr<GameTree>> children_;
    weak_ptr<GameTree> parent_;
    unsigned int selfIndex_; //used for deletion, this isn't necessary
    void initChildren(unsigned int depth); //constructs children
};
_

2. GameObject.h

これを理解するのは少し難しいですが、GameObjectは基本的に次のように機能します。

GameObjectを作成するときは、その基本的な属性と、_vector<unique_ptr<Construction>>_を含むCResult- instanceを作成します。 Construction- structには、GameObjectを構築するために必要なすべての情報が含まれています。これはシードであり、ファクトリによって構築時に適用される関数オブジェクトです。これにより、GameObjectと同様に、GameTreesの動的なロードとアンロードが可能になります。また、GameObjectを継承する場合は、そのファクトリを定義する必要があります。この継承は、GameTreeにテンプレート関数_gameObject<GOType>_がある理由でもあります。

GameObjectにはRenderObjectPhysicsObjectを含めることができます。これらについては後で説明します。

とにかく、これがコードです。

_class GameObject;
typedef unsigned long seed_type;


//this declaration magic means that all GameObjectFactorys inherit from GameObjectFactory<GameObject>
template<typename GOType>
struct GameObjectFactory;

template<>
struct GameObjectFactory<GameObject>{
    virtual unique_ptr<GameObject> construct(seed_type seed) const = 0;
};

template<typename GOType>
struct GameObjectFactory : GameObjectFactory<GameObject>{
    GameObjectFactory() : GameObjectFactory<GameObject>(){}
    unique_ptr<GameObject> construct(seed_type seed) const{
        return unique_ptr<GOType>(new GOType(seed));
    }
};

//same as with the factories. this is important for storing them in vectors
template<typename GOType>
struct Construction;

template<>
struct Construction<GameObject>{
    virtual unique_ptr<GameObject> construct() const = 0;
};

template<typename GOType>
struct Construction : Construction<GameObject>{
    Construction(seed_type seed, function<void(GOType*)> func = [](GOType* null){}) :
        Construction<GameObject>(),
        seed_(seed),
        func_(func)
    {}

    unique_ptr<GameObject> construct() const{
        unique_ptr<GameObject> gameObject{ GOType::factory.construct(seed_) };
        func_(dynamic_cast<GOType*>(gameObject.get()));
        return std::move(gameObject);
    }

    seed_type seed_;
    function<void(GOType*)> func_;
};


typedef struct CResult
{
    CResult() :
        constructions{}
    {}

    CResult(CResult && o) :
        constructions(std::move(o.constructions))
    {}

    CResult& operator= (CResult& other){
        if (this != &other){
            for (unique_ptr<Construction<GameObject>>& child : other.constructions){
                constructions.Push_back(std::move(child));
            }
        }
        return *this;
}

    template<typename GOType>
    void Push_back(seed_type seed, function<void(GOType*)> func = [](GOType* null){}){
        constructions.Push_back(make_unique<Construction<GOType>>(seed, func));
    }

    vector<unique_ptr<Construction<GameObject>>> constructions;
} CResult;



//finally, the GameObject
class GameObject
{
public:
    GameObject(seed_type seed);
    GameObject(const GameObject&);

    virtual void tick(unsigned int delta);

    inline Matrix4f trafoMatrix(){ return physicsObject_->transformationMatrix(); }


    //getter
    inline seed_type seed() const{ return seed_; }
    inline CResult& properties(){ return properties_; }
    inline const RenderObject& renderObject() const{ return *renderObject_; }
    inline weak_ptr<PhysicsObject> physicsObject() { return physicsObject_; }

protected:
    virtual CResult construct_(seed_type seed) = 0;

    CResult properties_;
    shared_ptr<RenderObject> renderObject_;
    shared_ptr<PhysicsObject> physicsObject_;
    seed_type seed_;
};
_

3. PhysicsObject

それは少し簡単です。これは、位置、速度、および加速を担当します。将来的には衝突も処理します。 3つのTransformationオブジェクトが含まれ、そのうち2つはオプションです。 PhysicsObjectクラスにアクセサーを含めるつもりはありません。最初のアプローチを試してみましたが、それは純粋な狂気(30を超える関数)だからです。また、動作が異なるPhysicsObjectsを構築する名前付きコンストラクターも欠落しています。

_class Transformation{
    Vector3f translation_;
    Vector3f rotation_;
    Vector3f scaling_;
public:
    Transformation() :
        translation_{ 0, 0, 0 },
        rotation_{ 0, 0, 0 },
        scaling_{ 1, 1, 1 }
    {};
    Transformation(Vector3f translation, Vector3f rotation, Vector3f scaling);

    inline Vector3f translation(){ return translation_; }
    inline void translation(float x, float y, float z){ translation(Vector3f(x, y, z)); }
    inline void translation(Vector3f newTranslation){
        translation_ = newTranslation;
    }
    inline void translate(float x, float y, float z){ translate(Vector3f(x, y, z)); }
    inline void translate(Vector3f summand){
        translation_ += summand;
    }

    inline Vector3f rotation(){ return rotation_; }
    inline void rotation(float pitch, float yaw, float roll){ rotation(Vector3f(pitch, yaw, roll)); }
    inline void rotation(Vector3f newRotation){
        rotation_ = newRotation;
    }
    inline void rotate(float pitch, float yaw, float roll){ rotate(Vector3f(pitch, yaw, roll)); }
    inline void rotate(Vector3f summand){
        rotation_ += summand;
    }

    inline Vector3f scaling(){ return scaling_; }
    inline void scaling(float x, float y, float z){ scaling(Vector3f(x, y, z)); }
    inline void scaling(Vector3f newScaling){
        scaling_ = newScaling;
    }
    inline void scale(float x, float y, float z){ scale(Vector3f(x, y, z)); }
    void scale(Vector3f factor){
        scaling_(0) *= factor(0);
        scaling_(1) *= factor(1);
        scaling_(2) *= factor(2);
    }

    Matrix4f matrix(){
        return WMatrix::Translation(translation_) * WMatrix::Rotation(rotation_) * WMatrix::Scale(scaling_);
    }
};

class PhysicsObject;

typedef void tickFunction(PhysicsObject& self, unsigned int delta);

class PhysicsObject{
    PhysicsObject(const Transformation& trafo) :
        transformation_(trafo),
        transformationVelocity_(nullptr),
        transformationAcceleration_(nullptr),
        tick_(nullptr)
    {}
    PhysicsObject(PhysicsObject&& other) :
        transformation_(other.transformation_),
        transformationVelocity_(std::move(other.transformationVelocity_)),
        transformationAcceleration_(std::move(other.transformationAcceleration_)),
        tick_(other.tick_)
    {}

    Transformation transformation_;
    unique_ptr<Transformation> transformationVelocity_;
    unique_ptr<Transformation> transformationAcceleration_;

    tickFunction* tick_;

public:
    void tick(unsigned int delta){ tick_ ? tick_(*this, delta) : 0; }

    inline Matrix4f transformationMatrix(){ return transformation_.matrix(); }
}
_

4. RenderObject

RenderObjectは、レンダリングできるさまざまなタイプの物、つまりメッシュ、光源、またはスプライトの基本クラスです。 免責事項:私はこのコードを記述していません。他の誰かとこのプロジェクトに取り組んでいます。

_class RenderObject
{
public:
    RenderObject(float renderDistance);
    virtual ~RenderObject();

    float renderDistance() const { return renderDistance_; }
    void setRenderDistance(float rD) { renderDistance_ = rD; }


protected:
    float renderDistance_;
};

struct NullRenderObject : public RenderObject{
    NullRenderObject() : RenderObject(0.f){};
};

class Light : public RenderObject{
public:
    Light() : RenderObject(30.f){};
};

class Mesh : public RenderObject{
public:
    Mesh(unsigned int seed) :
        RenderObject(20.f)
    {
        meshID_ = 0;
        textureID_ = 0;
        if (seed == 1)
            meshID_ = Model::getMeshID("EM-208_heavy");
        else
            meshID_ = Model::getMeshID("cube");
    };

    unsigned int getMeshID() const { return meshID_; }
    unsigned int getTextureID() const { return textureID_; }

private:
    unsigned int meshID_;
    unsigned int textureID_;
};
_

これは私の問題を非常にうまく示していると思います:GameObjectには、メンバーのメンバーにアクセスするために_weak_ptr_ sを返すいくつかのアクセサーが表示されますが、それは本当に私が望んでいることではありません。

また、これは[〜#〜] not [〜#〜]であることを覚えておいてください。これは単なるプロトタイプであり、クラスの不整合や不必要なpublic部分などがある場合があります。

6
iFreilicht

ゲーム業界全体が、コンポーネントベースの設計エンジン(再利用可能なエンジンのみ)に切り替えました。データ駆動型エンジン)。これを調べる必要があります。これは、さまざまなコンポーネントのセットによって定義された非常に異種のエンティティを作成するための最良の方法です。

コンポーネントシステムを設定するには、さまざまな方法があります。私は 本のゲームエンジンアーキテクチャ をポイントする必要があります。これには、さまざまな種類のコンポーネントベースのシステムを含む、あなたが抱えている問題を解決するために必要なさまざまな種類のセットアップに関する章全体があります。また、この件に関する多くの記事があります:

(最近のC++でコンポーネントシステムを実装する方法についてgithubでも多くの実験があります。概念ベースの型消去を使用して、継承なしで最近行いました)

あなたが抱えている問題は、継承の使用に関連しており、それにより、アーキテクチャーの柔軟性が低下します。継承を削除し、コンポーネントをまとめて整理すると、最大限の柔軟性が得られます。

もちろん、その柔軟性は必ずしも最終的に必要なものではありませんが、ここで提示している問題は解決するはずです。

5
Klaim

私はクライムの細かい答えをいくつかの代替の詳細で補足したいと思いました。

しかし、この設計でマスターのカプセル化をどのように維持しますか?

私は文脈を正しく理解していなければ理解していません。これはやや独断的に聞こえる声明ですが、ゲームエンジンの柔軟でプログラム可能なオープンアーキテクチャの要件に直面してオブジェクトを設計および実装するまったく新しい方法を誰かが理解しない限り、次のような概念をモデル化することは現実的ではありません。 World、またはUniverse、または最も一般的に使用される用語Scene、およびそのパブリックインターフェースが、その中にあるすべての小さなものを公開します。

ここでの一般的なアプローチは、「漏れやすい抽象化」と呼ばれるものを優先し、SceneまたはUniverseコンテナと見なすことです。ある種のものを保存し、そのインターフェイスのユーザーが内部にあるものにアクセスできるようにするという主な責任があります。不変条件を維持しながら、最強のカプセル化と情報の隠蔽を優先して、そのような詳細を隠そうとすることは、絶えず変化する要件に伴って成長し、成長し、途方もないモノリシックな責任に成長する傾向があります。

Masterを構成するすべてのクラスのすべてのアクセサメソッドのアクセサメソッドを記述するだけです。マスターの実装の詳細が表示されないため、これは完全なカプセル化につながります[...]

私はその「完全なカプセル化」を考えていません。それは少し主観的かもしれませんが、カプセル化の最も強力なレベルは、その状態が(アクセサ関数またはその他の何かを通じて)最初に公開する必要さえないクラスであると私が考えるものです。私の考えでは、それはそれ自体についての最小限の情報を公開するクラスです。たとえば、読み取り専用アクセス以外のために内部に格納されているものを公開する必要さえないコンテナのようなものです。これは、その状態に対して不変条件を維持する最も強力な方法は、変更可能な形式でそれを公開しないことです。 Universeクラス/インターフェイスでUniverseレベルの不変条件を維持する場合、改ざんのためにその内部を公開することはできません。もちろん、前述のように、「ユニバース」の種類をモデル化したいゲームエンジンに課せられるシフト要件のタイプを使用して「ユニバース」のレベルで物事をモデル化する場合、それはあまり実用的ではありません。単純化されていますが、ゲーム開発者は独自のミニチュアユニバースを作成しています。あえて言うと、それはプログラミングデザインの観点から行うのが最も難しい課題の1つです。マリオまたはアンリアル4)。

種類の異なるもの

そして、これがあなたが問題を見つけたと私が思う場所です(Master、またはGameTreeで、私があなたがそれを呼ぶと収集したので)。 UniverseまたはSceneをコンテナーにしたとしても、非常に同種のタイプのものを含めたくありません。

私たちは光を放つ星を持っているかもしれません、私たちはそのような光に依存して成長する植物を持っているかもしれません、そしてその不在下で枯れて死んでいるかもしれません、私たちは人、動物をお互いに食べ、植物、車、機関銃、人工照明、カメラ、ビルなど.

したがって、これらすべてを統一するための非常に均一な概念やデザイン、インターフェースはありません。したがって、ゲームは通常、Scene内の個々のものをEntity(これはGameObjectのような)と呼ばれる「コンテナ」自体に似たものにすることでこの問題を解決します。

そして、セキュリティステーションからそれを見通すことができるように、カメラコンポーネントを持つ歩哨エンティティがあるかもしれません、それはそれが見ている方向に赤外線を照らすように指向性ライトコンポーネントがあるかもしれません、それは武器コンポーネントを持つかもしれません侵入者が視界に入ったときに発砲し、楽しみのために、生き残るために日光を必要とする植物コンポーネント(サイバネティックプラントのようなもの)を持つ有機植物である可能性があり、その武器コンポーネントはスパイクされた針を撃つように設計されています侵入者。

ヒューストン:まだ問題があります

ああ、私たちはこれらのゲームEntitiesに不均一な収集問題を転送しました。 Entityからカメラコンポーネントから植物コンポーネントのように異なるものをフェッチするにはどうすればよいですか?また、上記のEntityのインターフェースは、その実装にいくつかのダウンキャストを含む傾向があります(ただし、安全のために実行時に集中的にチェックされます)。

_// Fetch a plant component from the entity, or nullptr if
// if the entity does not provide one.
PlanetComponent* plant = some_entity.get<PlantComponent>();
If (plant)
{
     // Do something with the plant.
}
_

そしてそれを実装する方法は大きく異なりますが、多くの場合、上記の類似のgetメソッドの実装のどこかに中心的なダウンキャストがあります(_dynamic_cast_の可能性があります。ここにいくつかの答えがあります)可能な実装を1つ提案しました)。実装は通常、ポリモーフィックベースポインターコンテナーとダウンキャストを組み合わせます(これは避けられますが、少なくともシステム内の1つの場所に集中させることができます)。

システム

ここまで進んだら、コードベースを「システム」に整理したいと思うかもしれません。 NatureSystemがあり、その唯一の責任は、植物のコンポーネントを提供する私たちの宇宙のすべてのエンティティをループし、それらが成長するために利用可能な太陽光があることを確認することです。そうしないと、枯れて死ぬかもしれません。

そのようなシステムがどれほど頻繁にそのようなことをしたいのかを考えると、Sceneにpublicメソッドを追加して、ユニバース内のすべての植物コンポーネントをフェッチすることができます。

_for (PlantComponent& plant: scene.query<PlantComponent>())
{
     // Make sure plant component has had sufficient available 
     // sunlight or else make it start withering and dying.
}
_

このようにして、エンティティをループして、それらがプラントコンポーネントを提供しているかどうかをチェックする必要さえありません。ループで処理できるプラントコンポーネントをシーンに尋ねます。

効率

上記のようなコードのプロファイリングを開始すると、scene.query<SomeComponentType>()に多数のホットスポットが見つかり、「ユニバース」内のすべてのエンティティをループして、特定のタイプのコンポーネントを提供しているかどうかを確認する必要がある場合があります。 、植物コンポーネントのように。

したがって、コンポーネントを実際にコンテナのように実装する必要があるかどうかという問題が生じる可能性があります。そのため、私はゲームプログラミングの高学年に相談しようとしましたが、聖典を正しく覚えていれば、彼らの答えは次のようになりました。

番号。

代わりに、ループを実行するこれらのシステムが特定のタイプの各コンポーネントをScene(またはユニバース)に連続して格納し、エンティティが概念的に「持っている」コンポーネントに関連付け/マップされるようにする方がはるかに効率的です。 。

enter image description here

これで、システムが「ユニバース」またはScene内のすべての植物コンポーネントを必要とする場合、エンティティを通過する必要がなくなります。シーン内の植物コンポーネントのコレクションに直接アクセスし、それを反復処理することができます(参照の局所性を保持する連続した適切な表現を使用)。エンティティ内にコンポーネントをstoreしません。エンティティをコンポーネントに関連付けします。

純粋なインターフェース

簡単な説明ですが、IPlantのように純粋なインターフェイスを使用しないのはなぜでしょうか。その場合、エンティティが次のような「プラント機能」を提供しているかどうかを確認できます。

_IPlant* plant = entity.get<IPlant>();
_

または、次のようなシーンでプラントインターフェイスを提供するすべてのものをフェッチします。

_for (IPlant& plant: scene.query<IPlant>())
{
     ...
}
_

これにより、そのようなインターフェースのユーザーに多くの柔軟性が提供されますが、そのアプローチの問題は、エンティティーがこれらの純粋なインターフェースを直接実装するか、そうするコンポーネントを直接格納することを望むことです。そのようなアーキテクチャー設計の性質により、開発者はLiskov置換(ほとんどコードの重複のみ)に関してほとんど利点のないインターフェースを冗長的に実装するように誘惑されたり、抽象的な基本クラスを導入して、ポイント全体と代替可能性を大きく損なう冗長性を排除したりする可能性があります。純粋なインターフェースの。

ここで、このような柔軟性は、特定のコンポーネントの実装の代替可能性ではなく、コンポーネントとそれらを処理するロジック(システム)の分離から生じます。私たちは、C++テンプレートに見られるような、アヒルのタイピングのような柔軟性を得ることになります。翼がある場合、羽ばたき、飛行を開始できます。それが翼を持っているという事実を除いて、それを超えているものについては気にしません。翼は、いくつかの抽象的なインターフェースではなく単なるデータでさえあるかもしれません。

結論

以上のことから、今日のゲームエンジンで人気のあるエンティティコンポーネントシステムアーキテクチャにたどり着きました(現在、私がVFXソフトウェアでそれを使用している私の知識に対する唯一の奇妙なボールです)。そして、上記の「進化の物語」が真実かどうかはわかりません。私はちょうどそれを作りました。しかし、私はそれを語るのが好きで、ボーランドターボCで始まった元のゲーム開発者として、「ユニバース」のモデリング方法に沿ってまったく同じ設計問題に取り組んでいるのは理にかなっていますが、ECSを見つけるのはこれまでに見つけた中で最も良いものです私がここ数年直面してきたこれらの同じ設計問題に。私はこのような話をするのが好きです。私のユーモアのセンスが文章にうまく反映されているかどうかはわかりません。個人的には私がはるかに得意です。酔っ払った女の子はそれを掘り下げるようです(まあ、私はそれらのケースではストーリーをプログラミングしないことを意味します。通常、ナイトクラブの女の子にあなたがプログラマーであることを、彼らがあなたを好きになり始めるまでは伝えたくないでしょう)。

1
Dragon Energy