37

オブジェクトを扱い、お互いに話をする良い方法はありますか?ゲームオブジェクトはお互いに話し合います

今まで私のゲームの趣味や学生はすべて小さくなっていたので、この問題は一般的にかなり醜い方法で解決され、緊密な統合と循環依存をもたらしました。私がやっていたプロジェクトの規模にとってはうれしいです。

しかし、私のプロジェクトはますます大きくなり、複雑さが増しています。今ではコードを再利用して、頭を簡単にしたいと思っています。

私が持っている主な問題は、Map知ってPlayerニーズの線に沿って、一般的ですので、Enemyはない、これは通常のポインタの多くを設定すると、依存関係の多くを持つに降りたが、これはすぐに混乱になります。

私はメッセージスタイルシステムのラインに沿って考えました。しかし私は実際にどこでもポインタを送信しているので、この依存関係がどのように減少するかは実際はわかりません。

PS:これは前に議論されていたと思いますが、私が必要としているものが何であるかはわかりません。

答えて

41

EDIT:以下、私は何度も繰り返し使ってきた基本的なイベントメッセージングシステムについて説明します。そして、両方の学校プロジェクトはオープンソースでウェブ上にあります。 http://sourceforge.net/projects/bpfat/には、このメッセージングシステムの2番目のバージョンがあります(もう少し詳しく)。システムの詳細については、お楽しみください。

私は一般的なメッセージングシステムを作成し、それをPSPとエンタープライズレベルのアプリケーションソフトウェアでリリースされたいくつかのゲームに導入しました。メッセージングシステムのポイントは、使用する用語に応じて、メッセージやイベントを処理するために必要なデータのみを渡すことであり、オブジェクトはお互いに知る必要はありません。

A

struct TEventMessage 
{ 
    int _iMessageID; 
} 

class IEventMessagingSystem 
{ 
    Post(int iMessageId); 
    Post(int iMessageId, float fData); 
    Post(int iMessageId, int iData); 
    // ... 
    Post(TMessageEvent * pMessage); 
    Post(int iMessageId, void * pData); 
} 

typedef float(*IEventMessagingSystem::Callback)(TEventMessage * pMessage); 

class CEventMessagingSystem 
{ 
    Init  (); 
    DNit  (); 
    Exec  (float fElapsedTime); 

    Post  (TEventMessage * oMessage); 

    Register (int iMessageId, IEventMessagingSystem* pObject, FObjectCallback* fpMethod); 
    Unregister (int iMessageId, IEventMessagingSystem* pObject, FObjectCallback * fpMethod); 
} 

#define MSG_Startup   (1) 
#define MSG_Shutdown   (2) 
#define MSG_PlaySound   (3) 
#define MSG_HandlePlayerInput (4) 
#define MSG_NetworkMessage  (5) 
#define MSG_PlayerDied   (6) 
#define MSG_BeginCombat  (7) 
#define MSG_EndCombat   (8) 

そして今、説明のビット:これを達成するために使用されるオブジェクトのリストの下に迅速に実行はの線に沿って何かです。最初のオブジェクトTEventMessageは、メッセージングシステムによって送信されたデータを表す基本オブジェクトです。デフォルトでは、あなたはあなたができることを期待していたメッセージを受け取っているかどうかを確認するために、常にメッセージのIDを送信します(通常はデバッグでのみ行います)。

次は、コールバックの実行中にメッセージングシステムがキャストに使用する汎用オブジェクトを与えるInterfaceクラスです。さらに、これは、メッセージングシステムに異なるデータ型をPost()するための使いやすいインタフェースを提供します。

その後、我々はコールバック型のtypedefを持っています。単にインターフェイスクラスの型のオブジェクトを期待して、TEventMessageポインタを渡します...オプションでパラメータconstを作ることができますが、Iveはトリクルアップ処理をスタックデバッグなどのメッセージングシステムのようなものです。

最後に、CEventMessagingSystemオブジェクトがコアにあります。このオブジェクトには、コールバックオブジェクトスタック(またはリンクされたリストまたはキュー、またはデータを格納したい)の配列が含まれています。上には示されていないコールバックオブジェクトは、オブジェクトへのポインタと、そのオブジェクトを呼び出すメソッドを維持する必要があります(また、それらによって一意に定義されます)。 Register()を実行すると、メッセージIDの配列位置にあるオブジェクトスタックにエントリが追加されます。 Unregister()を実行すると、そのエントリが削除されます。

これは基本的には..ここには、すべてがIEventMessagingSystemとTEventMessageオブジェクトについて知る必要があるという規定があります。このオブジェクトはそれを頻繁に変更するべきではなく、呼び出されるイベントによって決定されるロジックに不可欠な情報の部分だけを渡すべきです。この方法では、プレイヤーはマップや敵を直接知り、イベントをオフに送信する必要はありません。管理対象オブジェクトは、それについて何かを知る必要なしに、より大きいシステムにもAPIを呼び出すことができます。

例:敵が死亡したときに、効果音を鳴らしたい場合。 IEventMessagingSystemインターフェイスを継承するサウンドマネージャがあると仮定すると、TEventMessagePlaySoundEffectまたはそのilkの何かを受け入れるメッセージングシステムのコールバックを設定します。 Sound Managerは、サウンドエフェクトが有効になっているときにこのコールバックを登録します(オン/オフ機能を簡単にするためにすべてのサウンドエフェクトをミュートする場合はコールバックの登録を解除します)。次に、敵のオブジェクトもIEventMessagingSystemから継承させ、TEventMessagePlaySoundEffectオブジェクトを組み込みます(メッセージIDにMSG_PlaySound、次に再生するサウンドエフェクトのIDが必要です)。これはint IDまたはサウンドの名前ですエフェクト)、単にポスト(& oEventMessagePlaySoundEffect)を呼び出します。

これは実装されていない非常に単純なデザインです。すぐに実行できるのであれば、TEventMessageオブジェクトをバッファリングする必要はありません(主にコンソールゲームで使用したもの)。マルチスレッド環境にある場合、これは、別々のスレッドで実行されているオブジェクトとシステムが互いに通信するための非常に明確な方法ですが、処理時にデータを利用できるようにTEventMessageオブジェクトを保持する必要があります。

これ以外の変更は、データをPost()にする必要があるオブジェクトに対してのみ、IEventMessagingSystemで静的メソッドセットを作成して継承する必要がないようにすることができます(アクセスやコールバック機能Post()呼び出しには直接的には必要ありません)。

MVCについて言及したすべての人にとって、これは非常に良いパターンですが、さまざまな方法でさまざまなレベルで実装できます。私が専門的に取り組んでいる現在のプロジェクトは、約3回以上のMVCセットアップです。アプリケーション全体のグローバルMVCがあり、それぞれのMVとCも組み込みのMVCパターンです。ここでは、ビューに入る必要性を排除してMのほぼすべてのタイプを処理するのに十分な一般的なCを作成する方法を説明します。

たとえば、オブジェクトが「死んでいる」場合は再生したい場合がありますサウンドエフェクト.. TEventMessageから継承し、サウンドエフェクトIDを追加するTEventMessageSoundEffectのようなサウンドシステム用の構造体を作成します(プリロードされたInt、またはsfxファイルの名前ですが、システムでトラッキングされます) )。次に、すべてのオブジェクトは、適切なDeathノイズを持つTEventMessageSoundEffectオブジェクトをまとめてポスト(& oEventMessageSoundEffect)を呼び出す必要があります。あなたがサウンドマネージャの登録を解除したいだろうか(音がミュートされていないと仮定すると、...オブジェクト

EDIT:以下のコメントに関してのビット、これを明確にする:メッセージを送信または受信する どれでもオブジェクトがちょうどする必要がありますIEventMessagingSystemインターフェースについて知っていて、これはEventMessagingSystemが他のすべてのオブジェクトを知る必要がある唯一のオブジェクトです。これがデタッチメントを提供するものです。メッセージを受信したいオブジェクトは単純にRegister(MSG、Object、Callback)それから、オブジェクトがPost(MSG、Data)を呼び出すと、それはそれを知っているインターフェイスを介してEventMessagingSystemに送信し、EMSは登録された各オブジェクトにイベントを通知します。プレイヤーはMSG_PlaySound、MSG_Respawnなどを呼び出すことで、それらのメッセージを聞いているものがそれらに作用するようにすることができます。ゲームエンジン内の異なるシステムへの抽象APIとしてのPost(MSG、Data)。

ああ!私に指摘されたもう1つのこと..私が上で説明したシステムは、与えられた他の答えのObserverパターンに適合します。だから、より一般的な説明をして欲しいのであれば、もう少し理解してください。

これが役立ちます。

+1

+1徹底的な説明はありますが、私も発言をしています。プレイヤーはイベントを送るためにマップ*について知る必要はないと述べましたが、あなたの例は、死にかけている敵は、通知する必要があるプログラムの一部。私は単に「ただ死んでしまった」というメッセージを送信し、メッセージングシステムにこのイベントに興味のあるリスナー(サウンド再生、スコアの更新など)を通知させることを期待していました。このようにして、どのエンティティも単一のイベント(サウンドを再生し、スコアを上げる)のためにたくさんのメッセージを送信する必要があるように見えます。それとも間違っていたのですか? – Groo

+1

@Groo私は自分の応答を十分に短くすることができなかったので、私は上記の私の答えにそれを編集しました。 – James

+0

こんにちは、あなたの答えから5年以上経ちましたが、単純なpubsubのアイデアを探していたときにこのポストが出てきました。私はソースをダウンロードしました。コーディング標準を除いて、 C++は2005年以来少し進歩していますが、このコードは非常に興味深いものです。私はC#のゲームにEMSスケルトンを使用しました。あなたが3人でやったことは本当に驚くほど難しくて、もっと学ぶことを願っています! –

4

これはゲームクラスだけでなく、一般的な意味でのクラスにも当てはまります。 MVC(model-view-controller)パターンと推奨メッセージポンプが必要です。

"Enemy"と "Player"はおそらくMVCのModel部分に収まりますが、大したことではありませんが、すべてのモデルとビューがコントローラを介してやりとりされています。したがって、この「コントローラ」クラスの他のすべてのクラスインスタンス(ほとんどの場合)への参照(ポインタよりも良い)を保持したい場合は、ControlDispatcherという名前を付けてください。メッセージポンプを追加する(コード化するプラットフォームによって異なります)、最初に(他のクラスの前に他のオブジェクトを追加する)、最後に(他のオブジェクトをControlDispatcherの参照として保存します)。

もちろん、ControlDispatcherクラスは、ファイルあたりのコードを700〜800行(これは少なくとも私にとっては限界です)に保つために、さらに特殊化されたコントローラに分割する必要があります。あなたのニーズに応じて、より多くのスレッドがメッセージをポンピングし、処理します。

乾杯

MVCの
+0

+1、私は同意する:

は、だからここにこれを使用する方法についての例です。 – Groo

-1

@kellogsの提案が有効であり、Webアプリケーションやフレームワークにそのずっとより一般的なものの、いくつかのゲームで使用されています。それは過剰なことかもしれませんし、あまりにも多くの場合、このためです。

私はあなたのデザインを考え直すでしょう、なぜプレイヤーは敵と話をする必要がありますか?彼らは両方ともActorクラスから継承できませんでしたか?なぜアクターは地図と話す必要がありますか?

私が書いたことを読んだとき、それはMVCフレームワークに収まるようになっています...最近、あまりにも多くのレールを作業してきました。しかし、私は喜んで賭けることができ、彼らはただのようなものを知る必要があり、彼らは別の俳優と衝突しており、彼らは地位を持っています。

ここで私が取り組んだAsteroidsの実装です。あなたはゲームが複雑かもしれませんが、おそらく複雑です。密結合を回避オブジェクト間の通信のための

+0

プレイヤーと敵ナビゲーションするマップについて知っておく必要があります。 –

15

一般的な解決策:

  1. Mediator pattern
  2. Observer pattern
+1

MVC(コントローラがメディエータ)にメディエータパターンがあります。 +1オブザーバーパターン。一部のプラットフォームで頻繁に使用されます。 – kellogs

+0

えええええええええええええええええええええええええええええええええええええええええええええええええええええええええええええええええええええええええっと、それはシングルトンの一種であると考えられています。シングルトンは誰もが知っているものです。この記事では、個々のオブジェクト( 'Customer.AddOrder'、' Customer.RemoveOrder')のメンバメソッドを示して、内部を「マネージャ」に公開してからマネージャがそれらの作業を行うことを許可しています。 OOPはどこに行きましたか?また、顧客に単一の注文を追加することをテストするために、マネージャークラス全体を模擬することになっています。私はあなたが最初の2つのリンクだけを保持したいと思います。 – Groo

+0

よろしくお願いします。私はリンクを削除;-)。 –

0

は、「メッセージ・スタイル系」と注意してください、それはおそらく、実装に依存するが、通常、静的型チェックが緩くなり、デバッグするのが非常に困難になります。オブジェクトのメソッドを呼び出すことは、すでにというメッセージに似たシステムであることに注意してください。

おそらく、ナビゲーションのためにいくつかのレベルの抽象化が欠けているだけで、Playerは地図自体についてすべて知っているのではなくナビゲータを使用することができます。あなたはまた、this has usually descended into setting lots of pointersと言っています、それらのポインタは何ですか?おそらく、それらを間違った抽象化にしていますか?オブジェクトをインタフェースや中間体を経由せずに直接他のものについて知ることは、緊密に結合された設計を得るための直接の方法です。

+0

はい私はそれらを直接割り当てていますが、私の問題だと思います。 –

0

メッセージングシステムは確かに素晴らしい方法ですが、メッセージングシステムには大きな違いがあります。あなたのクラスをきれいに保つためには、メッセージングシステムを知らずに、ILocketServiceのような単純なものに依存関係を持たせて、Mapクラスなどの情報を公開/要求するように実装するようにしてください。より多くの授業で終わる一方、小さくてシンプルでクリーンなデザインを奨励します。

メッセージングは​​デカップリングだけではなく、より非同期で並行したリアクションアーキテクチャに移行することもできます。エンタープライズ統合のパターンGregor Hopheは、良いメッセージングパターンについて語る素晴らしい本です。 Erlang OTPやScalaのActorパターンの実装は、私に多くの指導を提供してくれました。

3

ここでは、使用できるC++ 11向けのきれいなイベントシステムを示します。それはテンプレートとスマートポインタだけでなく、代理人のためのラムダを使用します。非常に柔軟です。下に例があります。これについて質問がある場合は[email protected]までメールしてください。

これらのクラスは、任意のデータが添付されたイベントを送信する方法で、システムがキャストして変換した引数型を受け入れる関数を直接バインドし、代理人を呼び出す前に正しい変換を確認する簡単な方法です。

基本的に、すべてのイベントはIEventDataクラスから派生しています(必要に応じてIEventと呼びます)。イベントシステムがすべてのデリゲートをループし、各イベントタイプをサブスクライブした他のシステムによって提供されたデリゲートを呼び出すProcessEvents()を呼び出す各「フレーム」。各イベントタイプに固有のIDがあるため、誰がどのイベントを購読したいのかを選ぶことができます。また、このようなイベントをサブスクライブするためにラムダを使用することができますのaddListener(MyEvent :: ID()、[&](のshared_ptr EV){ があなたのことを行う} ..

とにかく、ここでのクラスは、すべての実装であります:

#pragma once 

#include <list> 
#include <memory> 
#include <map> 
#include <vector> 
#include <functional> 

class IEventData { 
public: 
    typedef size_t id_t; 
    virtual id_t GetID() = 0; 
}; 

typedef std::shared_ptr<IEventData> IEventDataPtr; 
typedef std::function<void(IEventDataPtr&)> EventDelegate; 

class IEventManager { 
public: 
    virtual bool AddListener(IEventData::id_t id, EventDelegate proc) = 0; 
    virtual bool RemoveListener(IEventData::id_t id, EventDelegate proc) = 0; 
    virtual void QueueEvent(IEventDataPtr ev) = 0; 
    virtual void ProcessEvents() = 0; 
}; 


#define DECLARE_EVENT(type) \ 
    static IEventData::id_t ID(){ \ 
     return reinterpret_cast<IEventData::id_t>(&ID); \ 
    } \ 
    IEventData::id_t GetID() override { \ 
     return ID(); \ 
    }\ 

class EventManager : public IEventManager { 
public: 
    typedef std::list<EventDelegate> EventDelegateList; 

    ~EventManager(){ 
    } 
    //! Adds a listener to the event. The listener should invalidate itself when it needs to be removed. 
    virtual bool AddListener(IEventData::id_t id, EventDelegate proc) override; 

    //! Removes the specified delegate from the list 
    virtual bool RemoveListener(IEventData::id_t id, EventDelegate proc) override; 

    //! Queues an event to be processed during the next update 
    virtual void QueueEvent(IEventDataPtr ev) override; 

    //! Processes all events 
    virtual void ProcessEvents() override; 
private: 
    std::list<std::shared_ptr<IEventData>> mEventQueue; 
    std::map<IEventData::id_t, EventDelegateList> mEventListeners; 

}; 

//! Helper class that automatically handles removal of individual event listeners registered using OnEvent() member function upon destruction of an object derived from this class. 
class EventListener { 
public: 
    //! Template function that also converts the event into the right data type before calling the event listener. 
    template<class T> 
    bool OnEvent(std::function<void(std::shared_ptr<T>)> proc){ 
     return OnEvent(T::ID(), [&, proc](IEventDataPtr data){ 
      auto ev = std::dynamic_pointer_cast<T>(data); 
      if(ev) proc(ev); 
     }); 
    } 
protected: 
    typedef std::pair<IEventData::id_t, EventDelegate> _EvPair; 
    EventListener(std::weak_ptr<IEventManager> mgr):_els_mEventManager(mgr){ 

    } 
    virtual ~EventListener(){ 
     if(_els_mEventManager.expired()) return; 
     auto em = _els_mEventManager.lock(); 
     for(auto i : _els_mLocalEvents){ 
      em->RemoveListener(i.first, i.second); 
     } 
    } 

    bool OnEvent(IEventData::id_t id, EventDelegate proc){ 
     if(_els_mEventManager.expired()) return false; 
     auto em = _els_mEventManager.lock(); 
     if(em->AddListener(id, proc)){ 
      _els_mLocalEvents.push_back(_EvPair(id, proc)); 
     } 
    } 
private: 
    std::weak_ptr<IEventManager> _els_mEventManager; 
    std::vector<_EvPair>  _els_mLocalEvents; 
    //std::vector<_DynEvPair> mDynamicLocalEvents; 
}; 

そしてCPPファイル:

#include "Events.hpp" 

using namespace std; 

bool EventManager::AddListener(IEventData::id_t id, EventDelegate proc){ 
    auto i = mEventListeners.find(id); 
    if(i == mEventListeners.end()){ 
     mEventListeners[id] = list<EventDelegate>(); 
    } 
    auto &list = mEventListeners[id]; 
    for(auto i = list.begin(); i != list.end(); i++){ 
     EventDelegate &func = *i; 
     if(func.target<EventDelegate>() == proc.target<EventDelegate>()) 
      return false; 
    } 
    list.push_back(proc); 
} 

bool EventManager::RemoveListener(IEventData::id_t id, EventDelegate proc){ 
    auto j = mEventListeners.find(id); 
    if(j == mEventListeners.end()) return false; 
    auto &list = j->second; 
    for(auto i = list.begin(); i != list.end(); ++i){ 
     EventDelegate &func = *i; 
     if(func.target<EventDelegate>() == proc.target<EventDelegate>()) { 
      list.erase(i); 
      return true; 
     } 
    } 
    return false; 
} 

void EventManager::QueueEvent(IEventDataPtr ev) { 
    mEventQueue.push_back(ev); 
} 

void EventManager::ProcessEvents(){ 
    size_t count = mEventQueue.size(); 
    for(auto it = mEventQueue.begin(); it != mEventQueue.end(); ++it){ 
     printf("Processing event..\n"); 
     if(!count) break; 
     auto &i = *it; 
     auto listeners = mEventListeners.find(i->GetID()); 
     if(listeners != mEventListeners.end()){ 
      // Call listeners 
      for(auto l : listeners->second){ 
       l(i); 
      } 
     } 
     // remove event 
     it = mEventQueue.erase(it); 
     count--; 
    } 
} 

私はイベントをリッスンしたいすべてのクラスの基本クラスとして便宜上のEventListenerクラスを使用し、あなたのリスニングのクラスを派生した場合。このクラスから、それをwあなたのイベントマネージャーでは、非常に便利な関数OnEvent(..)を使用してイベントを登録することができます。そして、基底クラスは、破棄されたすべてのイベントから派生クラスを自動的に退会させます。クラスが破棄されたときにイベントマネージャからデリゲートを削除するのを忘れると、プログラムがクラッシュする可能性が非常に高いため、これは非常に便利です。

クラスの静的関数を宣言し、そのアドレスをintにキャストするだけで、イベントの一意の型IDを取得するきちんとした方法です。すべてのクラスは異なるアドレスでこのメソッドを持つため、クラスイベントの一意の識別に使用できます。必要に応じて、typename()をintにキャストして一意のIDを取得することもできます。これを行うにはさまざまな方法があります。ものを再発明する必要はありません

#include <functional> 
#include <memory> 
#include <stdio.h> 
#include <list> 
#include <map> 

#include "Events.hpp" 
#include "Events.cpp" 

using namespace std; 

class DisplayTextEvent : public IEventData { 
public: 
    DECLARE_EVENT(DisplayTextEvent); 

    DisplayTextEvent(const string &text){ 
     mStr = text; 
    } 
    ~DisplayTextEvent(){ 
     printf("Deleted event data\n"); 
    } 
    const string &GetText(){ 
     return mStr; 
    } 
private: 
    string mStr; 
}; 

class Emitter { 
public: 
    Emitter(shared_ptr<IEventManager> em){ 
     mEmgr = em; 
    } 
    void EmitEvent(){ 
     mEmgr->QueueEvent(shared_ptr<IEventData>(
      new DisplayTextEvent("Hello World!"))); 
    } 
private: 
    shared_ptr<IEventManager> mEmgr; 
}; 

class Receiver : public EventListener{ 
public: 
    Receiver(shared_ptr<IEventManager> em) : EventListener(em){ 
     mEmgr = em; 

     OnEvent<DisplayTextEvent>([&](shared_ptr<DisplayTextEvent> data){ 
      printf("It's working: %s\n", data->GetText().c_str()); 
     }); 
    } 
    ~Receiver(){ 
     mEmgr->RemoveListener(DisplayTextEvent::ID(), std::bind(&Receiver::OnExampleEvent, this, placeholders::_1)); 
    } 
    void OnExampleEvent(IEventDataPtr &data){ 
     auto ev = dynamic_pointer_cast<DisplayTextEvent>(data); 
     if(!ev) return; 
     printf("Received event: %s\n", ev->GetText().c_str()); 
    } 
private: 
    shared_ptr<IEventManager> mEmgr; 
}; 

int main(){ 
    auto emgr = shared_ptr<IEventManager>(new EventManager()); 


    Emitter emit(emgr); 
    { 
     Receiver receive(emgr); 

     emit.EmitEvent(); 
     emgr->ProcessEvents(); 
    } 
    emit.EmitEvent(); 
    emgr->ProcessEvents(); 
    emgr = 0; 

    return 0; 
} 
関連する問題