複数のクラスが同じデータにアクセスする必要がある場合、データはどこで宣言する必要がありますか?


39

C ++で基本的な2Dタワーディフェンスゲームを持っています。

各マップは、GameStateを継承する個別のクラスです。マップは、ゲーム内の各オブジェクトにロジックと描画コードを委任し、マップパスなどのデータを設定します。擬似コードでは、ロジックセクションは次のようになります。

update():
  for each creep in creeps:
    creep.update()
  for each tower in towers:
    tower.update()
  for each missile in missiles:
    missile.update()

オブジェクト(クリープ、タワー、ミサイル)は、ポインターのベクトルに格納されます。タワーは、新しいミサイルを作成してターゲットを特定するために、クリープのベクトルとミサイルのベクトルにアクセスできる必要があります。

質問は次のとおりです。ベクトルをどこで宣言しますか?それらはMapクラスのメンバーであり、引数としてtower.update()関数に渡されるべきですか?またはグローバルに宣言されましたか?または、私が完全に欠けている他のソリューションはありますか?

複数のクラスが同じデータにアクセスする必要がある場合、データはどこで宣言する必要がありますか?


1
グローバルメンバーは「ugい」と見なされますが、迅速で開発が容易です。小さなゲームであれば、問題ありません(IMHO)。ロジック(タワーがこれらのベクターを必要とする理由)を処理し、すべてのベクターにアクセスできる外部クラスを作成することもできます。
ジョナサンコネル

-1これがゲームプログラミングに関連している場合、ピザを食べることも同様です。グラブ自分でいくつかの良いソフトウェア設計図書は
はMaik Semder

9
@Maik:ソフトウェア設計とゲームプログラミングとの関係はどうですか?プログラミングの他の分野にも適用されるからといって、トピックから外れることはありません。
BlueRaja-ダニーPflughoeft

ソフトウェアデザインパターンの@BlueRajaリストは、SOにより適しています。GD.SEは、ゲームプログラミングではなく、ソフトウェアの設計のためである
はMaik Semder

回答:


53

プログラム全体でクラスの単一インスタンスが必要な場合、そのクラスをサービスと呼びます。プログラムにサービスを実装する標準的な方法はいくつかあります。

  • グローバル変数。これらは実装が最も簡単ですが、最悪の設計です。あまりにも多くのグローバル変数を使用すると、お互いに依存しすぎているモジュール(ストロングカップリング)をすぐに作成していることに気づき、ロジックのフローを追跡するのが非常に難しくなります。グローバル変数はマルチスレッド対応ではありません。グローバル変数は、オブジェクトの存続期間の追跡をより困難にし、名前空間を混乱させます。ただし、これらは最もパフォーマンスの高いオプションであるため、使用できる場合と使用する必要がある場合がありますが、慎重に使用してください。
  • シングルトン。約10〜15年前、シングルトンであった程度知っている大きなデザインパターン。しかし、今日では彼らは軽lookedされています。マルチスレッドを使用する方がはるかに簡単ですが、一度に1つのスレッドに使用を制限する必要があります。これは常に必要なものではありません。ライフタイムの追跡は、グローバル変数の場合と同様に困難です。 典型的なシングルトンクラスは次のようになります。

    class MyClass
    {
    private:
        static MyClass* _instance;
        MyClass() {} //private constructor
    
    public:
        static MyClass* getInstance();
        void method();
    };
    
    ...
    
    MyClass* MyClass::_instance = NULL;
    MyClass* MyClass::getInstance()
    {
        if(_instance == NULL)
            _instance = new MyClass(); //Not thread-safe version
        return _instance;
    
        //Note that _instance is *never* deleted - 
        //it exists for the entire lifetime of the program!
    }
    
  • 依存性注入(DI)。これは、コンストラクターパラメーターとしてサービスを渡すことを意味します。サービスをクラスに渡すには、サービスが既に存在している必要があるため、2つのサービスが互いに依存する方法はありません。98%のケースでこれがあなたの望むものです(そして他の2%では、いつでもsetWhatever()メソッドを作成し、後でサービスを渡すことができます)。このため、DIには他のオプションと同じカップリングの問題はありません。各スレッドは単にすべてのサービスの独自のインスタンスを持つことができる(そして絶対に必要なものだけを共有する)ため、マルチスレッドで使用できます。また、気にするなら、コードをユニットテスト可能にします。

    依存性注入の問題は、より多くのメモリを消費することです。現在、クラスのすべてのインスタンスには、使用するすべてのサービスへの参照が必要です。また、サービスが多すぎる場合に使用するのは面倒です。他の言語にはこの問題を軽減するフレームワークがありますが、C ++のリフレクションがないため、C ++のDIフレームワークは、手動で行うよりもさらに多くの作業を行う傾向があります。

    //Example of dependency injection
    class Tower
    {
    private:
        MissileCreationService* _missileCreator;
        CreepLocatorService* _creepLocator;
    public:
        Tower(MissileCreationService*, CreepLocatorService*);
    }
    
    //In order to create a tower, the creating-class must also have instances of
    // MissileCreationService and CreepLocatorService; thus, if we want to 
    // add a new service to the Tower constructor, we must add it to the
    // constructor of every class which creates a Tower as well!
    //This is not a problem in languages like C# and Java, where you can use
    // a framework to create an instance and inject automatically.
    

    別の例については、このページ(C#DIフレームワークであるNinjectのドキュメントから)を参照してください

    依存性注入は、この問題の通常の解決策であり、StackOverflow.comでこのような質問に最も強く賛成する回答です。DIは、Inversion of Control(IoC)の一種です。

  • サービスロケーター。基本的に、すべてのサービスのインスタンスを保持するクラスのみです。Reflectionを使用して実行すること、新しいサービスを作成するたびに新しいインスタンスを追加することもできます。あなたはまだ以前と同じ問題を抱えています - クラスはこのロケーターにどのようにアクセスしますか?-これは上記の方法のいずれかで解決できますが、今ServiceLocatorでは何十ものサービスではなく、クラスに対してのみそれを行う必要があります。この種のことを気にする場合、このメソッドは単体テストも可能です。

    サービスロケーターは、Inversion of Control(IoC)の別の形式です。通常、自動依存性注入を行うフレームワークにもサービスロケーターがあります。

    XNA (MicrosoftのC#ゲームプログラミングフレームワーク)には、サービスロケーターが含まれています。詳細については、この回答をご覧ください。


ちなみに、私見塔はクリープについて知ってはいけません。すべてのタワーのクリープのリストを単純にループすることを計画しているのでない限り、自明ではないスペース分割を実装する必要があるでしょう。そして、その種のロジックは塔のクラスに属していません。


コメントは詳細なディスカッション用ではありません。この会話はチャットに移動さました
ジョシュ

私が今まで読んだ中で最高の、最も明確な答えの一つ。よくやった。しかし、サービスは常に共有されるはずだと思っていました。
ニコス

5

私はここでポリモーフィズムを使用します。なぜ同じベクトルを呼び出すのにmissiletowerベクター、ベクター、ベクターがあるのかcreepupdate?なぜいくつかの基底クラスへのポインタのベクトルを持っていませんEntityGameObject

設計する良い方法は、「これは所有権の観点から理にかなっている」と考えることです。明らかにタワーはそれ自体を更新する方法を所有していますが、マップはその上のすべてのオブジェクトを所有していますか?グローバルに行く場合、タワーやクリープを所有しているものはないと言っていますか?グローバルは通常、悪い解決策です-それは悪い設計パターンを促進しますが、それでも作業ははるかに簡単です。「これを終了しますか?」「再利用できるものが欲しい」

これを回避する1つの方法は、何らかの形式のメッセージングシステムです。towerメッセージを送ることができmap、それがヒットしていること(それはその所有者に多分参照へのアクセス権を持っている?) creep、そしてmapその後、伝えcreep、それがヒットしています。これは非常にクリーンで、データを分離します。

別の方法は、マップ自体を検索して、必要なものを見つけることです。ただし、ここで更新順序に問題がある可能性があります。


1
ポリモーフィズムについてのあなたの提案は、実際には関係ありません。それらを別々のベクトルに保存して、描画コード(特定のオブジェクトを最初に描画する)やコリジョンコードなど、各タイプを個別に反復処理できるようにします。
ジューシー

ここでのマップは「レベル」に類似しているため、私の目的では、マップはエンティティを所有します。メッセージについてのあなたの考えを検討します、ありがとう。
ジューシー

1
ゲームではパフォーマンスが重要です。そのため、同じオブジェクト時間のベクトルは、参照の局所性が高くなります。また、仮想ポインタを持つポリモーフィックオブジェクトは、更新ループにインライン化できないため、ひどいパフォーマンスになります。
ザンリンクス

0

これは、厳密なオブジェクト指向プログラミング(OOP)が機能しなくなるケースです。

OOPの原則に従って、クラスを使用して関連する動作でデータをグループ化する必要があります。ただし、相互に関係のないデータ(タワーとクリープ)を必要とする動作(ターゲティング)があります。この状況では、多くのプログラマーが必要なデータの一部に動作を関連付けようとします(たとえば、タワーはターゲティングを処理しますが、クリープについては知りません)が、別のオプションがあります:データと動作をグループ化しない

ターゲティング動作をタワークラスのメソッドにする代わりに、タワーとクリープを引数として受け入れる無料の関数にします。これには、タワーおよびクリープクラスに残されたメンバーをより多く公開する必要があるかもしれませんデータの非表示は便利ですが、それは手段であり、それ自体が目的ではありません。あなたはそれを奴隷にすべきではありません。また、プライベートメンバーはデータへのアクセスを制御する唯一の方法ではありません。データが関数に渡されず、グローバルでない場合、その関数から事実上隠されます。この手法を使用してグローバルデータを回避できる場合、実際にはカプセル化を改善している可能性があります。

このアプローチの極端な例は、エンティティシステムアーキテクチャです。

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