12

小さな演習として、エンティティ(移動、基本的なAIなど)を処理するだけの非常に小さくてシンプルなゲームエンジンを作成しようとしています。

そのため、ゲームがすべてのエンティティの更新をどのように処理するかを考えようとしていますが、少し混乱しています(おそらく間違った方法で行っているためです)

そこで、この質問をここに投稿して、私の現在の考え方を示し、誰かが私にそれを行うためのより良い方法を提案できるかどうかを確認することにしました。

現在、必要な他のクラス(CWindowクラス、CEntityManagerクラスなど)へのポインターを受け取るCEngineクラスがあります。

擬似コードでは次のようなゲームループがあります(CEngineクラス内)

while(isRunning) {
    Window->clear_screen();

    EntityManager->draw();

    Window->flip_screen();

    // Cap FPS
}

私のCEntityManagerクラスは次のようになりました。

enum {
    PLAYER,
    ENEMY,
    ALLY
};

class CEntityManager {
    public:
        void create_entity(int entityType); // PLAYER, ENEMY, ALLY etc.
        void delete_entity(int entityID);

    private:
        std::vector<CEntity*> entityVector;
        std::vector<CEntity*> entityVectorIter;
};

そして、私のCEntityクラスは次のようになりました。

class CEntity() {
    public:
        virtual void draw() = 0;
        void set_id(int nextEntityID);
        int get_id();
        int get_type();

    private:
        static nextEntityID;
        int entityID;
        int entityType;
};

その後、例えば敵のクラスを作成し、それにスプライトシートや独自の機能などを与えます。

例えば:

class CEnemy : public CEntity {
    public:
        void draw(); // Implement draw();
        void do_ai_stuff();

};

class CPlayer : public CEntity {
    public:
        void draw(); // Implement draw();
        void handle_input();
};

これらはすべて、画面にスプライトを描画するだけで問題なく機能しました。

しかし、あるエンティティには存在するが別のエンティティには存在しない関数を使用するという問題に直面しました。

上記の擬似コードの例では、do_ai_stuff(); およびhandle_input();

私のゲームループからわかるように、EntityManager-> draw();への呼び出しがあります。これはentityVectorを繰り返し処理し、draw()を呼び出しました。各エンティティの関数-すべてのエンティティにdraw()があるため、これは問題なく機能しました。働き。

しかし、入力を処理する必要があるのがプレーヤーエンティティである場合はどうなるでしょうか?それはどのように機能しますか?

試したことはありませんが、敵のようなエンティティにはhandle_input()関数がないため、draw()関数のようにループすることはできないと思います。

次のように、ifステートメントを使用してentityTypeをチェックできます。

for(entityVectorIter = entityVector.begin(); entityVectorIter != entityVector.end(); entityVectorIter++) {
    if((*entityVectorIter)->get_type() == PLAYER) {
        (*entityVectorIter)->handle_input();
    }
}

しかし、私は人々が通常どのようにこのようなものを書くのかわからないので、それを行うための最良の方法がわかりません。

私はここにたくさん書いたが、具体的な質問はしなかったので、ここで探しているものを明確にします。

  • コードのレイアウト/設計方法は問題ありませんか?それは実用的ですか?
  • 自分のエンティティを更新し、他のエンティティにはない関数を呼び出すためのより効率的な方法はありますか?
  • 列挙型を使用してエンティティタイプを追跡することは、エンティティを識別するための良い方法ですか?
4

5 に答える 5

13

あなたはほとんどのゲームが実際にそれを行う方法にかなり近づいています(パフォーマンスの専門家であるcurmudgeonのMike Actonはしばしばそれについて不満を持っていますが)。

通常、このようなものが表示されます

class CEntity {
  public:
     virtual void draw() {};  // default implementations do nothing
     virtual void update() {} ;
     virtual void handleinput( const inputdata &input ) {};
}

class CEnemy : public CEntity {
  public:
     virtual void draw(); // implemented...
     virtual void update() { do_ai_stuff(); }
      // use the default null impl of handleinput because enemies don't care...
}

class CPlayer : public CEntity {
  public:
     virtual void draw(); 
     virtual void update();
     virtual void handleinput( const inputdata &input) {}; // handle input here
}

次に、エンティティマネージャが通過し、ワールド内の各エンティティに対してupdate()、handleinput()、およびdraw()を呼び出します。

もちろん、これらの関数をたくさん持っていると、それらを呼び出してもほとんど何もしませんが、特に仮想関数の場合、かなり無駄になる可能性があります。だから私は他のアプローチもいくつか見ました。

1つは、入力データをグローバルに(または、グローバルインターフェイスのメンバーとして、またはシングルトンなどとして)格納することです次に、敵のupdate()関数をオーバーライドして、敵がdo_ai_stuff()になるようにします。そして、プレーヤーのupdate()により、グローバルをポーリングして入力処理を実行します。

もう1つは、リスナーパターンにいくつかのバリエーションを使用して、入力に関係するすべてのものが共通のリスナークラスから継承されるようにし、それらすべてのリスナーをInputManagerに登録することです。次に、inputmanagerは各リスナーを順番に各フレームに呼び出します。

class CInputManager
{
  AddListener( IInputListener *pListener );
  RemoveListener( IInputListener *pListener );

  vector<IInputListener *>m_listeners;
  void PerFrame( inputdata *input ) 
  { 
     for ( i = 0 ; i < m_listeners.count() ; ++i )
     {
         m_listeners[i]->handleinput(input);
     }
  }
};
CInputManager g_InputManager; // or a singleton, etc

class IInputListener
{
   virtual void handleinput( inputdata *input ) = 0;
   IInputListener() { g_InputManager.AddListener(this); }
   ~IInputListener() { g_InputManager.RemoveListener(this); }
}

class CPlayer : public IInputListener
{
   virtual void handleinput( inputdata *input ); // implement this..
}

そして、それを実行する他のより複雑な方法があります。しかし、それらのすべてが機能し、実際に出荷および販売されたものでそれぞれを見てきました。

于 2010-11-06T11:20:59.620 に答える
8

これを継承するのではなく、コンポーネントを調べる必要があります。たとえば、私のエンジンでは、次のようになっています(簡略化)。

class GameObject
{
private:
    std::map<int, GameComponent*> m_Components;
}; // eo class GameObject

さまざまなことを行うさまざまなコンポーネントがあります。

class GameComponent
{
}; // eo class GameComponent

class LightComponent : public GameComponent // represents a light
class CameraComponent : public GameComponent // represents a camera
class SceneNodeComponent : public GameComponent // represents a scene node
class MeshComponent : public GameComponent // represents a mesh and material
class SoundComponent : public GameComponent // can emit sound
class PhysicsComponent : public GameComponent // applies physics
class ScriptComponent : public GameComponent // allows scripting

これらのコンポーネントをゲームオブジェクトに追加して、動作を誘発することができます。それらはメッセージングシステムを介して通信でき、メインループ中に更新が必要なものはフレームリスナーを登録します。それらは独立して動作し、実行時に安全に追加/削除できます。これは非常に拡張可能なシステムだと思います。

編集:お詫びします、私はこれを少し肉付けします、しかし私は今何かの真っ只中にいます:)

于 2010-11-06T11:25:09.383 に答える
7

この機能は、仮想関数を使用して実現することもできます。

class CEntity() {
    public:
        virtual void do_stuff() = 0;
        virtual void draw() = 0;
        // ...
};

class CEnemy : public CEntity {
    public:
        void do_stuff() { do_ai_stuff(); }
        void draw(); // Implement draw();
        void do_ai_stuff();

};

class CPlayer : public CEntity {
    public:
        void do_stuff() { handle_input(); }
        void draw(); // Implement draw();
        void handle_input();
};
于 2010-11-06T11:03:15.667 に答える
2

1小さなこと-なぜエンティティのIDを変更するのですか?通常、これは一定であり、構築中に初期化されます。それだけです。

class CEntity
{ 
     const int m_id;
   public:
     CEntity(int id) : m_id(id) {}
}

他のことについては、さまざまなアプローチがあります。選択は、タイプ固有の関数がいくつあるか(およびそれらをどれだけうまく再現できるか)によって異なります。


すべてに追加

最も簡単な方法は、すべてのメソッドをベースインターフェイスに追加し、それをサポートしていないクラスにno-opとして実装することです。それは悪いアドバイスのように聞こえるかもしれませんが、適用されないメソッドが非常に少なく、メソッドのセットが将来の要件で大幅に増加しないと想定できる場合は、受け入れ可能な非正規化です。

基本的な種類の「検出メカニズム」を実装することもできます。

 class CEntity
 {
   public:
     ...
     virtual bool CanMove() = 0;
     virtual void Move(CPoint target) = 0;
 }

やり過ぎないでください!この方法で開始するのは簡単で、コードに大きな混乱が生じた場合でも、それに固執することができます。「型階層の意図的な非正規化」としてシュガーコートすることもできますが、最終的には、いくつかの問題をすばやく解決できるハックですが、アプリケーションが大きくなるとすぐに傷つきます。


TrueTypeディスカバリー

とを使用すると、オブジェクトをからにdynamic_cast安全にキャストできます。エンティティが実際にである場合、結果はnullポインタになります。そうすれば、オブジェクトの実際のタイプを調べて、それに応じて反応することができます。CEntityCFastCatCReallyUnmovableBoulder

CFastCat * fastCat = dynamic_cast<CFastCat *>(entity) ;
if (fastCat != 0)
   fastCat->Meow();

このメカニズムは、タイプ固有のメソッドに関連付けられたロジックがほとんどない場合にうまく機能します。多くのタイプをプローブし、それに応じて行動するチェーンになってしまう場合、これは良い解決策ではありません

// -----BAD BAD BAD BAD Code -----
CFastCat * fastCat = dynamic_cast<CFastCat *>(entity) ;
if (fastCat != 0)
   fastCat->Meow();

CBigDog * bigDog = dynamic_cast<CBigDog *>(entity) ;
if (bigDog != 0)
   bigDog->Bark();

CPebble * pebble = dynamic_cast<CPebble *>(entity) ;
if (pebble != 0)
   pebble->UhmWhatNoiseDoesAPebbleMake();

これは通常、仮想メソッドが慎重に選択されていないことを意味します。


インターフェース

タイプ固有の機能が単一のメソッドではなく、メソッドのグループである場合、上記はインターフェースに拡張できます。これらはC++ではあまりサポートされていませんが、耐えられます。たとえば、オブジェクトにはさまざまな機能があります。

class IMovable
{
   virtual void SetSpeed() = 0;
   virtual void SetTarget(CPoint target) = 0;
   virtual CPoint GetPosition() = 0;
   virtual ~IMovable() {}
}

class IAttacker
{
   virtual int GetStrength() = 0;
   virtual void Attack(IAttackable * target) = 0;
   virtual void SetAnger(int anger) = 0;
   virtual ~IAttacker() {}
}

さまざまなオブジェクトは、基本クラスと1つ以上のインターフェースから継承します。

class CHero : public CEntity, public IMovable, public IAttacker 

また、dynamic_castを使用して、任意のエンティティのインターフェイスをプローブできます。

これは非常に拡張可能であり、通常、確信が持てない場合に最も安全な方法です。上記のソリューションよりも少し冗長ですが、予期しない将来の変更に非常にうまく対処できます。機能をインターフェースに組み込むのは簡単ではありません。その感触をつかむにはある程度の経験が必要です。


ビジターパターン

ビジターパターンには多くの入力が必要ですが、クラスを変更せずにクラスに機能を追加できます。

あなたの文脈では、それはあなたがあなたの実体構造を構築することができるが、それらの活動を別々に実行することができることを意味します。これは通常、エンティティに対して非常に明確な操作がある場合、クラスを自由に変更できない場合、またはクラスに機能を追加すると単一責任の原則に大きく違反する場合に使用されます。

これにより、実質的にすべての変更要件に対応できます(エンティティ自体が十分に考慮されている場合)。

(ほとんどの人が頭を包むのに時間がかかるので、リンクしているだけです。他の方法の制限を経験していない限り、使用することはお勧めしません)

于 2010-11-06T11:35:13.263 に答える
1

他の人が指摘しているように、一般的に、あなたのコードはかなり大丈夫です。

3番目の質問に答えるには:あなたが私たちに示したコードでは、作成を除いて型列挙型を使用しません。そこでは問題ないようです(「createPlayer()」、「createEnemy()」メソッドなどは読みやすくないのではないかと思いますが)。しかし、タイプに基づいて異なることを行うためにifまたはswitchを使用するコードを作成するとすぐに、いくつかのOOの原則に違反することになります。次に、仮想メソッドの能力を使用して、必要なことを確実に実行する必要があります。特定のタイプのオブジェクトを「検索」する必要がある場合は、作成時に特別なプレーヤーオブジェクトへのポインターを格納することをお勧めします。

一意のIDが必要な場合は、IDを生のポインターに置き換えることも検討してください。

これらは、実際に必要なものに応じて適切である可能性があるというヒントと見なしてください。

于 2010-11-06T11:38:45.350 に答える