37

Что такое хороший способ борьбы с объектами и общение с ними?Объекты игры, говорящие друг другу

До сих пор все мои хобби/ученики были маленькими, поэтому эта проблема была в целом решена довольно уродливо, что привело к жесткой интеграции и круговым зависимостям. Это было прекрасно для размеров проектов, которые я делал.

Однако мои проекты стали больше по размеру и сложности, и теперь я хочу начать повторное использование кода и сделать мою голову более простым местом.

Основная проблема у меня есть, как правило, вдоль линий Player нужно знать о Map и так же Enemy, это, как правило, происходят в настройке много указателей и имеющих много зависимостей, и это становится беспорядок быстро.

Я думал о линиях системы сообщений. но я не могу понять, как это уменьшает зависимости, поскольку я все равно буду посылать указатели повсюду.

PS: Я предполагаю, что это обсуждалось ранее, но я не знаю, как его зовут только то, что у меня есть.

ответ

41

EDIT: Ниже я описываю базовую систему обмена сообщениями, которую я использовал снова и снова. И мне казалось, что оба школьных проекта имеют открытый исходный код и в Интернете. Вы можете найти вторую версию этой системы обмена сообщениями (и немного больше) на http://sourceforge.net/projects/bpfat/ .. Наслаждайтесь и читайте ниже для более подробного описания системы!

Я написал общую систему обмена сообщениями и представил ее в несколько игр, выпущенных на 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, является базовым объектом для представления данных, отправленных системой обмена сообщениями. По умолчанию он всегда будет иметь идентификатор отправляемого сообщения, поэтому, если вы хотите удостовериться, что получили сообщение, которое ожидаете, вы можете (обычно я делаю это только при отладке).

Следующий класс - это интерфейс, который дает общий объект для системы обмена сообщениями, используемой для каста при выполнении обратных вызовов. Кроме того, это также обеспечивает «простой в использовании» интерфейс для Post() для разных типов данных для системы обмена сообщениями.

После этого у нас есть наш callback typedef, просто предположим, что объект типа класса интерфейса будет проходить вдоль указателя TEventMessage ... При желании вы можете сделать параметр const, но Ive использовал обработку подкачки, прежде чем такие вещи, как отладка стека и такая система обмена сообщениями.

Последнее, а в основе лежит объект CEventMessagingSystem. Этот объект содержит массив стеков объектов обратного вызова (или связанных списков или очередей или, тем не менее, вы хотите хранить данные). Объекты обратного вызова, не показанные выше, должны поддерживать (и однозначно определяться) указателем на объект, а также метод вызова этого объекта. Когда вы Регистрируете(), вы добавляете запись в стек объекта под позицией массива идентификатора сообщения. Когда вы отмените регистрацию(), вы удалите эту запись.

Это в основном это .. Теперь у этого есть условие, что все должно знать об IEventMessagingSystem и объекте TEventMessage ...но этот объект не должен меняться, что часто и только передает части информации, которые жизненно важны для логики, продиктованной вызываемым событием. Таким образом, игроку не нужно знать о карте или враге непосредственно для отправки событий на него. Управляемый объект может также вызвать API для более крупной системы, не требуя ничего знать об этом.

Например: Когда враг умирает, вы хотите, чтобы он воспроизводил звуковой эффект. Предполагая, что у вас есть диспетчер звуков, который наследует интерфейс IEventMessagingSystem, вы должны настроить обратную связь для системы обмена сообщениями, которая будет принимать TEventMessagePlaySoundEffect или что-то подобное. Затем Sound Manager зарегистрирует этот обратный вызов, когда звуковые эффекты будут включены (или отмените регистрацию, когда вы хотите отключить все звуковые эффекты для удобства включения/выключения). Затем вы также должны наследовать объект-противник из IEventMessagingSystem, объединив объект TEventMessagePlaySoundEffect (для его идентификатора сообщения потребуется MSG_PlaySound, а затем идентификатор звукового эффекта для воспроизведения, будь то int ID или имя звука эффект) и просто вызвать сообщение (& oEventMessagePlaySoundEffect).

Теперь это просто простой дизайн без реализации. Если у вас есть немедленное выполнение, вам не нужно буферизовать объекты TEventMessage (что я использовал в основном в консольных играх). Если вы находитесь в многопоточной среде, то это очень четко определенный способ для объектов и систем, работающих в отдельных потоках, разговаривать друг с другом, но вы захотите сохранить объекты TEventMessage, чтобы данные были доступны при обработке.

Другие изменения для объектов, которые когда-либо нужны только для данных Post(), вы можете создать статический набор методов в IEventMessagingSystem, чтобы им не было наследовать от них (это используется для удобства доступа и возможностей обратного вызова , непрямо- необходимый для вызовов Post()).

Для всех людей, которые упоминают MVC, это очень хороший образец, но вы можете реализовать его так много разных манер и на разных уровнях. Текущий проект, над которым я работаю профессионально, - это установка MVC примерно в 3 раза, есть глобальный MVC всего приложения, а затем дизайн мудрый каждый MV и C также является самодостаточным шаблоном MVC. Так что я пытался сделать здесь объясняется, как сделать C, который является достаточно общим для обработки практически любого типа M без необходимости входить в представление ...

Например, объект, когда он «умирает», может захотеть воспроизвести звуковой эффект. Вы создали бы структуру для звуковой системы, например TEventMessageSoundEffect, которая наследуется от TEventMessage и добавляет в идентификатор звукового эффекта (будь то предварительно загруженный Int или имя файла sfx, однако они отслеживаются в вашей системе). Затем всему объекту просто нужно собрать объект TEventMessageSoundEffect с соответствующим шумом смерти и вызвать сообщение (& oEventMessageSoundEffect); объект .. Предполагая, что звук не отключен (то, что вы хотели бы отменить регистрацию менеджеров звука.

EDIT: Прояснить это немного относительно комментария ниже: Любой объект для отправки или получения сообщения просто необходим знать об интерфейсе IEventMessagingSystem, и это единственный объект, который EventMessagingSystem должен знать обо всех других объектах. Это то, что дает вам отрыв. Любой объект, который хочет получить сообщение, просто зарегистрируется (MSG, Object, Callback) s для Затем, когда объект вызывает Post (MSG, Data), он отправляет это в EventMessagingSystem через интерфейс, о котором он знает, затем EMS уведомляет каждый зарегистрированный объект события. Вы можете сделать MSG_PlayerDied, который обрабатывают другие системы, или плеер может вызывать MSG_PlaySound, MSG_Respawn и т. д., чтобы позволить вещам слушать эти сообщения, чтобы воздействовать на них. Post (MSG, Data) в качестве абстрактного API для разных систем в игровом движке.

Oh! Еще одна вещь, которая была указана мне. Система, описанная выше, соответствует шаблону Observer в другом ответе. Поэтому, если вы хотите, чтобы более общее описание сделало мой способ более понятным, это короткая статья, которая дает хорошее описание.

Надеюсь, что это поможет и понравится!

+1

+1 за подробное объяснение, но у меня также есть замечание: вы заявили, что * игроку не нужно знать о карте *, чтобы отправлять ему события, но ваш пример означает, что умирающий враг должен знать обо всех других часть программы, которая должна быть уведомлена. Я ожидал, что он просто отправит сообщение «Я только что умер», а затем пусть ваша система обмена сообщениями сообщит слушателям, которые интересуются этим событием (воспроизведение звука, оценка обновления и т. Д.). Таким образом, похоже, что любой объект должен отправить кучу сообщений для одного события (воспроизведение звука, увеличение балла). Или я ошибаюсь? – Groo

+1

@Groo Мне не удалось сократить свой ответ, поэтому я отредактировал его в своем ответе выше. – James

+0

Привет, человек, прошло более 5 лет с момента вашего ответа, но этот пост появился, когда я искал простую идею pubsub, и я должен сказать, что я загрузил источники, и кроме стандартов кодирования i ' m не используется и тот факт, что C++ немного продвинулся с 2005 года, код очень интересен для исследования, и я использовал некоторый скелет EMS для моей игры на C#. Это действительно потрясающе и сложно, что вы, три парня, сделали, и я надеюсь, что я узнаю больше от него! –

4

Это, вероятно, относится не только к игровым классам, но и к классам в общем смысле. шаблон MVC (модель-view-controller) вместе с вашим предлагаемым насосом сообщений - это все, что вам нужно.

«Враг» и «Игрок», вероятно, вписываются в часть модели MVC, это не имеет большого значения, но эмпирическое правило состоит в том, что все модели и представления взаимодействуют через контроллер. Таким образом, вы хотели бы сохранить ссылки (лучше, чем указатели) на (почти) все экземпляры других классов из этого класса «controller», назовем его ControlDispatcher. Добавьте сообщение к нему (зависит от того, на какой платформе вы кодируетесь), сначала создавайте его (перед любыми другими классами и другими объектами) или, наконец, (и сохраняйте другие объекты в качестве ссылок в ControlDispatcher).

Конечно, класс ControlDispatcher, вероятно, придется разделить на более специализированные контроллеры, чтобы сохранить код в файле примерно 700-800 строк (это ограничение для меня хотя бы), и оно может даже иметь больше потоков накачки и обработки сообщений в зависимости от ваших потребностей.

Приветствия

+0

+1 Там нет необходимости изобретать вещи, я согласен. – Groo

-1

@kellogs внушение MVC является действительным, и используется в нескольких играх, хотя его много более распространенным в веб-приложений и фреймворков. Это может быть излишним и слишком много для этого.

Я бы переосмыслил ваш дизайн, почему игроку нужно поговорить с врагами? Разве они не могли унаследовать от класса Актера? Зачем актерам нужно разговаривать с Картой?

Как я читал то, что я написал, он начинает вписываться в рамки MVC ... Я, очевидно, сделал слишком много рельсов в последнее время. Тем не менее, я был бы готов поспорить, им нужно только знать такие вещи, как, они сталкиваются с другим Актером, и у них есть позиция, которая должна быть относительно Карты в любом случае.

Вот реализация Asteroids, над которой я работал. Вы играете, возможно, сложной.

+0

Игроку и врагу Нужно знать о карте для навигации, это был просто грубый упрощенный пример. –

15

общие решения для связи между объектами, избегая тесную связь:

  1. Mediator pattern
  2. Observer pattern
+1

Схема посредника находится прямо в MVC (где контроллер является посредником). +1 для шаблона наблюдателя.Сильно используется на некоторых платформах. – kellogs

+0

Hmmm .. Из статьи, с которой вы связались, «Менеджер отношений» выглядит немного вонючим с первого взгляда, он кажется божественным объектом. Предполагается, что это какой-то синглтон, который знает все обо всех. В статье показаны методы членов отдельных объектов ('Customer.AddOrder',' Customer.RemoveOrder'), раскрывающие их внутренности «менеджеру», а затем позволяя менеджеру выполнять работу для них. Где же тогда был ООП? Кроме того, чтобы проверить добавление одного заказа клиенту, вы должны высмеять весь класс менеджера. Я бы предпочел, чтобы вы сохранили только первые две ссылки. – Groo

+0

Хорошее замечание вас. Я удаляю ссылку ;-). –

0

Будьте осторожны с «системой типа сообщения», это, вероятно, зависит от реализации, но, как правило вы потеряете проверку статического типа и можете сделать некоторые ошибки очень сложными для отладки. Обратите внимание, что методы вызова объекта - это уже система сообщений.

Возможно, вам просто не хватает некоторых уровней абстракции, например, для навигации игрок может использовать навигатор вместо того, чтобы знать все о самой карте. Вы также говорите, что this has usually descended into setting lots of pointers, что это за указатели? Наверное, вы даете им неправильную абстракцию? .. Ознакомиться с объектами напрямую о других, не проходя через интерфейсы и промежуточные продукты, - это прямой способ получить плотно связанный дизайн.

+0

Да, я назначил их напрямую, и я думаю, это моя проблема. –

0

Обмен сообщениями, безусловно, отличный способ, но системы обмена сообщениями могут иметь множество отличий. Если вы хотите, чтобы ваши классы были чистыми и чистыми, напишите им, чтобы они не знали о системе обмена сообщениями, и вместо этого они берут зависимости от чего-то простого, такого как «ILocationService», который затем может быть реализован для публикации/запроса информации из таких вещей, как класс карты , В то время как у вас будет больше классов, они будут небольшими, простыми и поощряют чистый дизайн.

Сообщения - это нечто большее, чем просто развязка, а также позволяет перейти к более асинхронной, параллельной и реактивной архитектуре. Шаблоны Enterprise Integration от Gregor Hophe - отличная книга, в которой рассказывается о хороших шаблонах обмена сообщениями. Erlang OTP или реализация Scala в шаблоне Actor предоставили мне много рекомендаций.

3

Вот эта аккуратная система событий, написанная для C++ 11, которую вы можете использовать. Он использует шаблоны и интеллектуальные указатели, а также лямбды для делегатов. Он очень гибкий. Ниже вы также найдете пример. Если у вас есть вопросы по этому поводу, напишите мне на [email protected]

То, что эти классы дают вам, это способ отправки событий с произвольными данными, прикрепленными к ним, и простой способ прямого привязки функций, которые принимают уже преобразованные типы аргументов, которые система выполняет, и проверяет правильное преобразование перед вызовом вашего делегата.

В принципе, каждое событие происходит из класса IEventData (вы можете назвать его IEvent, если хотите). Каждый «кадр» вы вызываете ProcessEvents(), после чего система событий проходит через все делегаты и вызывает делегатов, которые были предоставлены другими системами, которые подписались на каждый тип события. Любой может выбрать, на какие события они хотели бы подписаться, поскольку каждый тип события имеет уникальный идентификатор. Вы можете также использовать лямбды для подписки на события, как это: AddListener (MyEvent :: ID(), [&] (shared_ptr эв) { сделать вещь} ..

Во всяком случае, здесь есть класс со всеми реализациями :

#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. Поскольку каждый класс будет иметь этот метод по разным адресам, его можно использовать для уникальной идентификации событий класса. Вы также можете указать typename() в int, чтобы получить уникальный идентификатор, если хотите. Существуют разные способы сделать это.

Так вот пример того, как использовать это:

#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; 
} 
Смежные вопросы