オブジェクトを処理し、それらを互いに対話させる良い方法は何ですか?
これまでのところ、私のゲームの趣味/学生はすべて小さいので、この問題は一般にかなり醜い方法で解決され、緊密な統合と循環依存関係につながりました。私がやっていたプロジェクトの規模としては、これで十分でした。
しかし、私のプロジェクトのサイズと複雑さが大きくなっているので、今度はコードの再利用を開始し、頭をより簡単な場所にしたいと考えています。
私が抱える主な問題は、一般にPlayer
がMap
について知る必要があるということです。これはEnemy
も同様です。これは通常、多くのポインタを設定し、多くの依存関係があり、これはすぐに混乱します。
私はメッセージスタイルのシステムに沿って考えてきました。しかし、ポインタがどこにでも送信されるため、これによって依存関係がどのように減少するかは、本当にわかりません。
PS:これは以前に議論されたと思いますが、それが私が単に必要としているものとは何なのかわかりません。
編集:以下では、私が何度も使用した基本的なイベントメッセージングシステムについて説明します。そして、私は両方の学校のプロジェクトがオープンソースであり、ウェブ上にあることに気付きました。 http://sourceforge.net/projects/bpfat/ ..で、このメッセージングシステムの2番目のバージョン(およびかなり多くのバージョン)を見つけることができます。システム!
私は一般的なメッセージングシステムを作成し、それを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を呼び出すことができます。
例:敵が死亡したとき、効果音を鳴らしたい。 IEventMessagingSystemインターフェイスを継承するサウンドマネージャがあるとすると、TEventMessagePlaySoundEffectまたはそのilkの何かを受け入れるメッセージングシステムのコールバックを設定します。次に、サウンドマネージャーは、サウンドエフェクトが有効になっている場合にこのコールバックを登録します(または、オン/オフ機能を簡単にするためにすべてのサウンドエフェクトをミュートにする場合は、コールバックを登録解除します)。次に、敵オブジェクトにもIEventMessagingSystemを継承させ、TEventMessagePlaySoundEffectオブジェクトを作成します(メッセージIDのMSG_PlaySoundと、再生するサウンド効果のIDが必要です(int IDまたはサウンドの名前)。エフェクト)、そして単にPost(&oEventMessagePlaySoundEffect)を呼び出します。
これは、実装のない非常にシンプルなデザインです。すぐに実行する場合は、TEventMessageオブジェクト(コンソールゲームで主に使用したもの)をバッファリングする必要はありません。マルチスレッド環境の場合、これは、個別のスレッドで実行されているオブジェクトとシステムが互いに通信するための非常に明確な方法ですが、TEventMessageオブジェクトを保持して、処理時にデータを利用できるようにする必要があります。
もう1つの変更は、データをPost()するだけでよいオブジェクト用です。IEventMessagingSystemで静的なメソッドのセットを作成して、それらを継承する必要がないようにすることができます(これは、直接ではなく、アクセスとコールバックの機能を簡単にするために使用されます) -Post()コールに必要です)。
MVCについて言及するすべての人にとって、これは非常に良いパターンですが、非常に多くの異なる方法で、異なるレベルで実装できます。私が専門的に取り組んでいる現在のプロジェクトは、約3倍のMVCセットアップであり、アプリケーション全体のグローバルMVCがあり、M VとCの設計もそれぞれ独立したMVCパターンです。だから私がここでやろうとしているのは、ビューに入る必要なしに、ほぼすべてのタイプのMを処理するのに十分なほど一般的なCを作成する方法を説明することです...
たとえば、オブジェクトが「死んだ」ときに効果音を再生したい場合があります。TEventMessageSoundEffectのようなサウンドシステムの構造体を作成し、TEventMessageから継承して効果音IDを追加します(プリロードされたInt、またはsfxファイルの名前ですが、システムで追跡されます)。次に、すべてのオブジェクトがTEventMessageSoundEffectオブジェクトと適切なデスノイズを組み合わせてPost(&oEventMessageSoundEffect);を呼び出すだけです。オブジェクト..サウンドがミュートされていないと仮定します(サウンドマネージャの登録を解除したいもの)。
編集:これを以下のコメントに関して少し明確にするために:メッセージを送信または受信するオブジェクトは、IEventMessagingSystemインターフェイスについて知る必要があるだけであり、これは、EventMessagingSystemが他のすべてのオブジェクトについて知る必要がある唯一のオブジェクトです。これはあなたに分離を与えるものです。メッセージの受信を希望するオブジェクトは、単にそのためのRegister(MSG、Object、Callback)を行います。次に、オブジェクトがPost(MSG、Data)を呼び出すと、それが知っているインターフェイスを介してEventMessagingSystemに送信され、EMSは、登録されている各オブジェクトにイベントを通知します。MSG_PlayerDied他のシステムが処理する、またはプレーヤーがMSG_PlaySound、MSG_Respawnなどを呼び出して、それらのメッセージをリッスンしてそれらに作用させることができます。Post(MSG、Data)は、ゲームエンジン内のさまざまなシステムへの抽象APIと考えてください。
ああ!もう一つ指摘されたこと。上記で説明したシステムは、与えられた他の回答のオブザーバーパターンに適合します。ですから、もっと一般的な説明で私の説明を少しわかりやすくしたいのであれば、それは良い説明を提供する短い記事です。
これがお役に立てば幸いです!
密結合を回避するオブジェクト間の通信の一般的なソリューション:
これはおそらくゲームのクラスだけでなく、一般的な意味のクラスにも当てはまります。 MVC(model-view-controller)パターンと提案されたメッセージポンプが必要なすべてです。
「敵」と「プレイヤー」はおそらくMVCのモデル部分に収まりますが、それほど重要ではありませんが、経験則では、すべてのモデルとビューがコントローラーを介して相互作用します。したがって、この「コントローラー」クラスからの(ほぼ)他のすべてのクラスインスタンスへの参照(ポインターよりも良い)を保持したい場合は、ControlDispatcherという名前を付けます。メッセージポンプを追加し(コーディングするプラットフォームに応じて異なります)、最初にインスタンス化(他のクラスの前にインスタンス化し、他のオブジェクトの一部を含める)または最後にインスタンス化します(他のオブジェクトをControlDispatcherの参照として保存します)。
もちろん、ControlDispatcherクラスはおそらく、ファイルごとのコードを約700〜800行に保つために、さらに特殊化されたコントローラーにさらに分割する必要があります(これは少なくとも私にとっての制限です)。必要に応じてメッセージを処理します。
乾杯
以下は、使用できるC++ 11用に作成されたきちんとしたイベントシステムです。テンプレートとスマートポインター、デリゲートのラムダを使用します。それは非常に柔軟です。以下にも例があります。これについて質問がある場合は、info @ fortmax.seにメールを送ってください。
これらのクラスが提供するのは、任意のデータが添付されたイベントを送信する方法であり、デリゲートを呼び出す前に、システムがキャストし、正しい変換をチェックする変換済みの引数型を受け入れる関数を直接バインドする簡単な方法です。
基本的に、すべてのイベントはIEventDataクラスから派生します(必要に応じてIEventと呼ぶことができます)。 ProcessEvents()を呼び出す各「フレーム」では、イベントシステムがすべてのデリゲートをループし、各イベントタイプにサブスクライブしている他のシステムによって提供されているデリゲートを呼び出します。各イベントタイプには一意のIDがあるため、サブスクライブするイベントは誰でも選択できます。ラムダを使用して、次のようなイベントをサブスクライブすることもできます:AddListener(MyEvent :: ID()、[&](shared_ptr ev){do your your} ..
とにかく、ここにすべての実装を持つクラスがあります:
#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;
}
「メッセージスタイルシステム」には注意してください。これはおそらく実装に依存しますが、通常は静的な型チェックが緩くなり、一部のエラーはデバッグが非常に困難になる可能性があります。オブジェクトのメソッドの呼び出しはalreadyメッセージのようなシステムであることに注意してください。
おそらく、抽象化のいくつかのレベルが欠けている可能性があります。たとえば、ナビゲーションの場合、プレーヤーはマップ自体のすべてを知る代わりにナビゲーターを使用できます。あなたはまたthis has usually descended into setting lots of pointers
、それらのポインタは何ですか?おそらく、あなたは間違った抽象化を彼らに与えているのでしょうか?.
メッセージングは間違いなく優れた方法ですが、メッセージングシステムには多くの違いがあります。クラスをきれいに保つには、メッセージングシステムを知らないようにクラスを記述し、代わりに、「ILocationService」などの単純なものに依存関係を持たせ、Mapクラスなどから情報を発行/要求するために実装できるようにします。 。最終的にはクラスが増えますが、クラスは小さく、シンプルで、すっきりとしたデザインが奨励されます。
メッセージングは単なるデカップリング以上のものであり、より非同期で、並行的で反応的なアーキテクチャに移行することもできます。 Gregor Hopheによるエンタープライズ統合のパターンは、優れたメッセージングパターンについて語る素晴らしい本です。 Erlang OTPまたはScalaによるActor Patternの実装は、多くのガイダンスを提供してくれました。