43

オブジェクトを処理し、それらを相互に通信させるための良い方法は何ですか?

これまでの私のゲームの趣味/学生はすべて小さいので、この問題は一般的にかなり醜い方法で解決され、緊密な統合と循環依存につながりました。これは、私が行っていたプロジェクトのサイズには問題ありませんでした。

しかし、私のプロジェクトはサイズと複雑さが増してきており、今度はコードを再利用して、頭をよりシンプルな場所にしたいと思っています。

Player私が抱えている主な問題は、一般的に、について知る必要のある線に沿っていることですMapEnemyこれは通常、多くのポインタを設定し、多くの依存関係を持つことになり、これはすぐに混乱します。

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

PS:これは以前に議論されたと思いますが、それが私が持っている必要性だけを何と呼んでいるのかわかりません。

4

7 に答える 7

48

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

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

これを達成するために使用されるオブジェクトのリストの簡単な要約は、次のようなものです。

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にすることができますが、以前はトリクルアップ処理を使用しました。メッセージングシステムのスタックデバッグなど。

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

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

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

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

もう1つの変更は、Post()データのみが必要なオブジェクトの場合です。IEventMessagingSystemに静的なメソッドのセットを作成して、それらから継承する必要がないようにすることができます(これは、直接ではなく、アクセスとコールバック機能を容易にするために使用されます) --Post()呼び出しに必要)。

MVCに言及するすべての人にとって、これは非常に優れたパターンですが、さまざまな方法でさまざまなレベルで実装できます。私が専門的に取り組んでいる現在のプロジェクトは、約3倍以上のMVCセットアップであり、アプリケーション全体のグローバルMVCがあり、各MVとCも自己完結型のMVCパターンです。だから私がここでやろうとしたのは、ビューに入る必要なしにほぼすべてのタイプのMを処理するのに十分な汎用性のあるCを作成する方法を説明することです...

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

編集:以下のコメントに関してこれを少し明確にするために:メッセージを送信または受信するオブジェクトは、IEventMessagingSystemインターフェイスについて知る必要があります。これは、EventMessagingSystemが他のすべてのオブジェクトについて知る必要がある唯一のオブジェクトです。これはあなたに分離を与えるものです。メッセージを受信したいオブジェクトは、単に登録(MSG、オブジェクト、コールバック)します。次に、オブジェクトがPost(MSG、Data)を呼び出すと、それを認識しているインターフェイスを介してEventMessagingSystemに送信し、EMSは登録されている各オブジェクトにイベントを通知します。他のシステムが処理するMSG_PlayerDiedを実行するか、プレーヤーがMSG_PlaySound、MSG_Respawnなどを呼び出して、それらのメッセージをリッスンしているものがそれらに作用するようにすることができます。Post(MSG、Data)は、ゲームエンジン内のさまざまなシステムへの抽象化されたAPIと考えてください。

おー!私に指摘されたもう一つのこと。私が上で説明したシステムは、与えられた他の答えのオブザーバーパターンに適合します。したがって、私のものをもう少し意味のあるものにするために、より一般的な説明が必要な場合は、それはそれに良い説明を与える短い記事です。

これがお役に立てば幸いです。

于 2011-01-02T20:06:24.283 に答える
15

密結合を回避するオブジェクト間の通信の一般的なソリューション:

  1. メディエーターパターン
  2. オブザーバーパターン
于 2011-01-01T12:48:35.303 に答える
5

これは、C++11用に作成された優れたイベントシステムです。テンプレートとスマートポインター、およびデリゲートのラムダを使用します。それは非常に柔軟です。以下に例もあります。これについて質問がある場合は、info@fortmax.seまでメールでお問い合わせください。

これらのクラスが提供するのは、任意のデータが添付されたイベントを送信する方法と、システムがキャストし、デリゲートを呼び出す前に正しい変換をチェックする、すでに変換された引数タイプを受け入れる関数を直接バインドする簡単な方法です。

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

とにかく、ここにすべての実装を持つクラスがあります:

#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クラスを使用します。このクラスからリスニングクラスを派生させ、イベントマネージャーに提供すると、非常に便利な関数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; 
}
于 2014-03-28T02:44:11.527 に答える
4

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

「Enemy」と「Player」はおそらくMVCのモデル部分に収まります。それほど重要ではありませんが、経験則では、すべてのモデルとビューがコントローラーを介して相互作用します。したがって、この「コントローラー」クラスからの他のすべてのクラスインスタンスへの(ポインターよりも優れた)参照を保持する必要があります。ControlDispatcherという名前を付けましょう。メッセージポンプを追加し(コーディングしているプラ​​ットフォームによって異なります)、最初にインスタンス化し(他のクラスの前に、他のオブジェクトをその一部にします)、最後にインスタンス化します(他のオブジェクトを参照としてControlDispatcherに保存します)。

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

乾杯

于 2011-01-01T12:32:27.560 に答える
0

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

おそらく、いくつかのレベルの抽象化が欠落しているだけです。たとえば、ナビゲーションの場合、プレーヤーはマップ自体についてすべてを知る代わりにナビゲーターを使用できます。あなたはまたthis has usually descended into setting lots of pointers、それらのポインタは何ですか?おそらく、あなたはそれらに間違った抽象化を与えているのでしょうか?..インターフェースや中間体を経由せずに、オブジェクトに他のオブジェクトを直接知らせることは、緊密に結合されたデザインを取得するための簡単な方法です。

于 2011-01-01T15:21:23.143 に答える
0

メッセージングは​​間違いなく素晴らしい方法ですが、メッセージングシステムには多くの違いがあります。クラスをきれいに保ちたい場合は、メッセージングシステムを認識しないようにクラスを記述し、代わりに「ILocationService」のような単純なものに依存させて、Mapクラスなどから情報を公開/要求するように実装できます。 。最終的にはクラスが増えますが、クラスは小さく、シンプルで、すっきりとしたデザインになります。

メッセージングは​​単なるデカップリングではなく、より非同期で同時かつリアクティブなアーキテクチャに移行することもできます。グレゴール・ホーフによるエンタープライズ統合のパターンは、優れたメッセージングパターンについて説明している素晴らしい本です。Erlang OTPまたはScalaによるアクターパターンの実装は、私に多くのガイダンスを提供してくれました。

于 2011-01-04T06:58:45.683 に答える
-1

@kellogsによるMVCの提案は有効であり、いくつかのゲームで使用されていますが、Webアプリやフレームワークでははるかに一般的です。それはやり過ぎであり、これには多すぎるかもしれません。

私はあなたのデザインを再考します、なぜプレイヤーは敵と話す必要があるのですか?どちらもActorクラスから継承できませんでしたか?アクターがマップと話す必要があるのはなぜですか?

私が書いたものを読むと、それはMVCフレームワークに適合し始めます...私は明らかに最近あまりにも多くのレール作業を行いました。しかし、私は賭けても構わないと思っています。彼らは、他のアクターと衝突している、とにかくマップに対して相対的な位置を持っているなどのことを知る必要があるだけです。

これが私が取り組んだ小惑星の実装です。あなたのゲームは複雑かもしれませんし、おそらく複雑です。

于 2011-01-01T12:43:32.213 に答える