Игровые объекты, говорящие друг другу

Что такое хороший способ борьбы с объектами и общение с ними?

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

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

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

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

Спасибо.

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

Ответ 1

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

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

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

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

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

Ответ 3

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

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

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

Приветствия

Ответ 4

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

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

В принципе, каждое событие происходит из класса IEventData (вы можете назвать его IEvent, если хотите). Каждый "кадр" вы вызываете ProcessEvents(), после чего система событий проходит через все делегаты и вызывает делегатов, которые были предоставлены другими системами, которые подписались на каждый тип события. Любой может выбрать, на какие события они хотели бы подписаться, поскольку каждый тип события имеет уникальный идентификатор. Вы также можете использовать lambdas для подписки на такие события: 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 для удобства в качестве базового класса для любого класса, который хотел бы прослушивать события. Если вы вывели свой класс слушания из этого класса и предоставили его менеджеру событий, вы можете использовать очень удобную функцию 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 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; 
}

Ответ 5

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

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

Ответ 6

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

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

Ответ 7

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

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

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

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