Игровые объекты, говорящие друг с другом
Как правильно обращаться с объектами и разговаривать друг с другом?
До сих пор все мои хобби / ученики в играх были небольшими, поэтому эта проблема, как правило, решалась довольно некрасиво, что приводило к тесной интеграции и круговым зависимостям. Что было хорошо для размера проектов, которые я делал.
Однако мои проекты стали больше по размеру и сложности, и теперь я хочу начать повторное использование кода и сделать мою голову проще.
Основная проблема у меня, как правило, в соответствии с Player
нужно знать о Map
и то же самое Enemy
обычно это сводится к установке множества указателей и наличию множества зависимостей, и это быстро превращается в беспорядок.
Я думал по линии системы стилей сообщений. но я не могу понять, как это уменьшает зависимости, так как я все равно буду отправлять указатели повсюду.
PS: Я думаю, что это обсуждалось ранее, но я не знаю, как это называется, просто необходимость у меня есть.
7 ответов
РЕДАКТИРОВАТЬ: Ниже я опишу базовую систему сообщений о событиях, которую я использовал снова и снова. И мне пришло в голову, что оба школьных проекта с открытым исходным кодом и в Интернете. Вы можете найти вторую версию этой системы обмена сообщениями (и немного больше) по адресу 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, является базовым объектом для представления данных, отправленных системой обмена сообщениями. По умолчанию он всегда будет иметь идентификатор отправляемого сообщения, поэтому, если вы хотите убедиться, что получили сообщение, которое ожидали, вы можете (обычно я делаю это только в режиме отладки).
Далее следует класс Interface, который предоставляет общий объект для системы обмена сообщениями, который используется для приведения при выполнении обратных вызовов. Кроме того, это также предоставляет "простой в использовании" интерфейс для Post() различных типов данных в систему обмена сообщениями.
После этого у нас есть тип обратного вызова Callback. Проще говоря, он ожидает объект типа интерфейсного класса и будет передавать указатель TEventMessage... При желании вы можете сделать параметр const, но я раньше использовал ручную обработку для таких вещей, как отладка стека и тому подобное системы обмена сообщениями.
Последним и в основе является объект CEventMessagingSystem. Этот объект содержит массив стеков объектов обратного вызова (или связанных списков или очередей, или как вы хотите сохранить данные). Объекты обратного вызова, не показанные выше, должны поддерживать (и однозначно определяются) указатель на объект, а также метод для вызова этого объекта. При регистрации () вы добавляете запись в стек объектов под позицией массива идентификатора сообщения. Когда вы отменяете регистрацию (), вы удаляете эту запись.
Это в основном это. Теперь для этого существует условие, что все должны знать об IEventMessagingSystem и объекте TEventMessage... но этот объект не должен изменяться так часто и пропускает только те части информации, которые жизненно важны для логики, продиктованной вызываемым событием. Таким образом, игроку не нужно знать о карте или противнике напрямую, чтобы отправлять на нее события. Управляемый объект может также вызывать API для более крупной системы без необходимости что-либо знать об этом.
Например: когда враг умирает, вы хотите, чтобы он играл звуковой эффект. Предполагая, что у вас есть звуковой менеджер, который наследует интерфейс IEventMessagingSystem, вы должны настроить обратный вызов для системы обмена сообщениями, которая будет принимать TEventMessagePlaySoundEffect или что-то в этом роде. Затем Sound Manager регистрирует этот обратный вызов, когда включены звуковые эффекты (или отменяет регистрацию обратного вызова, если вы хотите отключить все звуковые эффекты для упрощения включения / выключения). Затем вы должны иметь вражеский объект, также наследуемый от IEventMessagingSystem, собрать объект TEventMessagePlaySoundEffect (потребуется MSG_PlaySound для его идентификатора сообщения и затем идентификатор звукового эффекта для воспроизведения, будь то идентификатор int или имя звука эффект) и просто вызовите Post(&oEventMessagePlaySoundEffect).
Теперь это просто очень простой дизайн без реализации. Если у вас немедленное выполнение, вам не нужно буферизовать объекты TEventMessage (что я использовал в основном в консольных играх). Если вы находитесь в многопоточной среде, то это очень хорошо определенный способ для объектов и систем, работающих в отдельных потоках, общаться друг с другом, но вы захотите сохранить объекты TEventMessage, чтобы данные были доступны при обработке.
Другое изменение касается объектов, которым когда-либо нужны только данные Post(), вы можете создать статический набор методов в IEventMessagingSystem, чтобы им не пришлось наследовать от них (это используется для простоты доступа и возможности обратного вызова, а не - напрямую). - необходимо для звонков Post().
Для всех людей, которые упоминают MVC, это очень хороший шаблон, но вы можете реализовать его очень разными способами и на разных уровнях. Текущий проект, над которым я профессионально работаю, - это установка MVC примерно в 3 раза, есть глобальный MVC всего приложения, а затем по дизайну каждый MV и C также является автономным шаблоном MVC. Итак, что я попытался сделать здесь, это объяснить, как сделать C достаточно универсальным, чтобы обрабатывать практически любой тип M без необходимости входить в View...
Например, объект, когда он "умирает", может захотеть воспроизвести звуковой эффект. Вы могли бы создать структуру для звуковой системы, такую как TEventMessageSoundEffect, которая наследуется от TEventMessage и добавляет идентификатор звукового эффекта (будь то предварительно загруженный объект Int или имя файла sfx, однако они отслеживаются в вашей системе). Затем весь объект просто должен собрать объект TEventMessageSoundEffect с соответствующим шумом смерти и вызвать Post(&oEventMessageSoundEffect); объект.. Предполагается, что звук не отключен (что вы хотите отменить регистрацию звуковых менеджеров.
РЕДАКТИРОВАТЬ: Чтобы прояснить это немного в отношении комментария ниже: Любой объект для отправки или получения сообщения просто должен знать об интерфейсе IEventMessagingSystem, и это единственный объект, который EventMessagingSystem должен знать обо всех других объектах. Это то, что дает вам отряд. Любой объект, который хочет получить сообщение, просто регистрируется (MSG, Object, Callback) для него. Затем, когда объект вызывает Post(MSG,Data), он отправляет его в EventMessagingSystem через интерфейс, о котором он знает, затем EMS уведомит каждый зарегистрированный объект о событии. Вы можете сделать MSG_PlayerDied, который обрабатывают другие системы, или игрок может вызвать MSG_PlaySound, MSG_Respawn и т. Д., Чтобы позволить вещам, прослушивающим эти сообщения, воздействовать на них. Думайте о Post(MSG,Data) как об абстрактном API для различных систем игрового движка.
Ой! Еще одна вещь, которая была указана мне. Система, которую я описал выше, соответствует шаблону Observer в другом ответе. Так что, если вы хотите, чтобы мое общее описание было более понятным, это небольшая статья, которая дает хорошее описание.
Надеюсь, что это помогает и наслаждайтесь!
Общие решения для связи между объектами, избегая жесткой связи:
Вот аккуратная система событий, написанная для C++11, которую вы можете использовать. Он использует шаблоны и умные указатели, а также лямбды для делегатов. Это очень гибкий. Ниже вы также найдете пример. Напишите мне на info@fortmax.se, если у вас есть вопросы по этому поводу.
Эти классы дают вам возможность отправлять события с произвольными данными, прикрепленными к ним, и простой способ напрямую связывать функции, которые принимают уже преобразованные типы аргументов, которые система преобразует и проверяет на правильное преобразование перед вызовом вашего делегата.
По сути, каждое событие происходит от класса IEventData (вы можете назвать его IEvent, если хотите). Каждый "кадр", который вы вызываете ProcessEvents(), в этот момент система событий проходит через все делегаты и вызывает делегаты, предоставленные другими системами, подписавшимися на каждый тип события. Каждый может выбрать, на какие события он хотел бы подписаться, поскольку каждый тип события имеет уникальный идентификатор. Вы также можете использовать лямбды, чтобы подписаться на такие события: 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'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;
}
Это, вероятно, относится не только к игровым классам, но и к классам в общем смысле. шаблон MVC (модель-представление-контроллер) вместе с вашим предложенным насосом сообщений - это все, что вам нужно.
"Враг" и "Игрок", вероятно, вписываются в часть модели MVC, это не имеет большого значения, но практическое правило заключается в том, чтобы все модели и представления взаимодействовали через контроллер. Итак, вы хотели бы сохранить ссылки (лучше, чем указатели) на (почти) все другие экземпляры класса из этого класса 'controller', назовем его ControlDispatcher. Добавьте к нему насос сообщений (зависит от того, для какой платформы вы кодируете), сначала создайте его экземпляр (перед любыми другими классами и включите в него другие объекты) или, наконец, (и сохраните другие объекты как ссылки в ControlDispatcher).
Конечно, класс ControlDispatcher, вероятно, придется разделить на более специализированные контроллеры, чтобы код каждого файла оставался на уровне 700–800 строк (это, по крайней мере, для меня ограничено), и он может даже иметь больше накачки потоков и обработка сообщений в зависимости от ваших потребностей.
ура
Будьте осторожны с "системой стилей сообщений", она, вероятно, зависит от реализации, но обычно вы теряете статическую проверку типов и затем можете сделать некоторые ошибки очень сложными для отладки. Обратите внимание, что при вызове методов объекта это уже система сообщений.
Возможно, вы просто упускаете некоторые уровни абстракции, например, для навигации игрок мог бы использовать навигатор вместо того, чтобы знать все о самой карте. Вы также говорите, что this has usually descended into setting lots of pointers
что это за указатели? Возможно, вы даете им неправильную абстракцию?.. Давать объектам информацию о других напрямую, без прохождения интерфейсов и промежуточных соединений, - это прямой путь к получению тесно связанной конструкции.
Обмен сообщениями, безусловно, отличный способ, но системы обмена сообщениями могут иметь много различий. Если вы хотите, чтобы ваши классы были чистыми и понятными, напишите их как неосведомленные о системе обмена сообщениями и вместо этого попросите их взять зависимости от чего-то простого, например, "ILocationService", который затем может быть реализован для публикации / запроса информации от таких вещей, как класс Map., Хотя у вас будет больше классов, они будут небольшими, простыми и будут поощрять чистый дизайн.
Обмен сообщениями - это больше, чем просто разделение, он также позволяет вам перейти к более асинхронной, параллельной и реактивной архитектуре. Шаблоны корпоративной интеграции Грегора Хофе - отличная книга, в которой говорится о хороших шаблонах обмена сообщениями. Erlang OTP или Scala реализация Actor Pattern дали мне много рекомендаций.
Предложение @kellogs о MVC является действительным и используется в нескольких играх, хотя оно гораздо чаще встречается в веб-приложениях и фреймворках. Это может быть излишним и слишком много для этого.
Я бы переосмыслил ваш дизайн, почему игрок должен говорить с врагами? Не могли ли они оба наследовать от актерского класса? Почему актеры должны говорить с картой?
Когда я читаю то, что я написал, он начинает вписываться в структуру MVC... В последнее время я, очевидно, слишком много работал с рельсами. Тем не менее, я был бы готов поспорить, им нужно только знать такие вещи, как, они сталкиваются с другим актером, и у них есть позиция, которая должна быть в любом случае относительно карты.
Вот реализация Астероидов, над которой я работал. Ваша игра может быть, и, вероятно, является сложной.