MagicEngine использует сигналы для реализации событий.
Сигнал генерируется в ответ на некоторое событие, он несет в себе информацию об этом событии. Слот это функция, которая вызывается в ответ на определнный сигнал.
Для сигналов и слотов в движке MagicEngine были выставлены следующие требования:
- Сигналы ничего не знают о слотах в которые они отправляются
- Сигналы несут в себе некоторые обобщенные данные
- Слот знает какие данные он хочет получить от сигнала
- Слот незнает кто отправил сигнал
- Допускаются множественные соединения (сигнал может быть присоединен к разным слотам, и к 1 слоту может быть прикреплено несколько сигналов)
- Сигналы и слоты имеют имена.
При реализации данной системы в начале была рассмотрена возможность использования сигналов/слотов из библиотеки boost: http://www.boost.org/doc/libs/1_38_0/doc/html/signals.html. Однако как оказалось они слегка не укладывались в имеющиеся требования, в частсности небыло возможности обращаться к сигналам и слотам только по именам, к томуже имена это строки. Требование данное вытекает из желания иметь возможность соединять сигналы и слоты в файле описания интерфейса, оперируя понятными человеку идиентификаторами. Например код ниже соединяет сиглан shoot от источника game_scene со слотом on_shoot объекта.
[signal1]
connect on_load
signal_source game_scene
signal_name shoot
slot_name on_shoot
[/]
С точки зрения разработчика система сигнал/слот состоит из 3-х частей:
- Сигналы
- Слоты
- Менеджер соединений
Сигнал — это некий именованный объект, который может хранить в себе данные.
Слот — это, в общем случае, указатель на некоторую функцию которой будет передан объект типа Сигнал.
Менеджер соединений — это набор функций, который обеспечивает соединение сигналов и слотов и доставку сигналов по их назначению.
Самая простая в реализации часть — это сигналы. Учитывая выставляемые требования реализация сигнала примерно такова:
class Signal
{
std::string name;
std::vector<int64_t> int_args;
std::vector<float> float_args;
std::vector<std::string> string_args;
} ;
Слоты:
template<class SlotClass, class SlotMethod> class Slot : public AbstractSlot
{
public:
virtual bool _invoke( SlotArgs *params ) throw( SlotAssert )
{
return ( _slot_class_->*_slot_method_ )( params );
}
Slot( SlotClass *t_class, SlotMethod t_method) : _slot_class_( t_class ), _slot_method_( t_method ){};
private:
SlotClass *_slot_class_;
SlotMethod _slot_method_;
};
В данном случае слот это шаблон. Он хранит в себе указатель на экземпляр класса и на метод этого класса которым должнем быть доставлен сигнал. В результате в программе использование таких слотов превращается в довольно ужасные строки типа:
Slot<MyClass, bool (class::*)(SlotArgs*)> SlotName;
Разумеется использовать такие определения не очень удобно, поэтому они заменяются на макросы, которыми можно легко и непринужденно объявлять, инициализировать и описывать слоты.
Менеджер сигналов должен реализовывать набор базовой функциональности:
- Регистрация слота — необходимо чтобы менеджер узнал что такой слот вообще существует
- Отмена регистрации слота — соответственно нужен чтобы забыть о слоте. При этом здесь необходимо также отсоединить все сигналы от данного слота.
- Соединение и рассоединение сигнала и слота (connect/disconnect) — чтобы задать соответствие слота и сигнала
- Посылка сигнала — данный метод должен обеспечивать поиск слота и отправку сигнала ему. Также неплохо иметь возможность по возврату слота определить стоит ли дальше посылать этот сигнал остальным слотам.
- Опциональный метод посылки сигнала по указанному адресу. При этом получить данный сигнла должен только указанный слот.
Хранение информации о сигналах и слотах реализовано в виде двух карт:
typedef std::multimap<SignalName, SlotName> SignalMap;
typedef std::map<SlotName, AbstractSlot*> SlotMap;
Первая карта связывает множество сигналов со множеством слотов а вторая позволяет по имени слота найти соответствующий объект.
Соответственно в таком случае соединение слота сводится к одной вставке в SignalMap. А его отсоединение есть задача более сложная — надо удалить из SignalMap все записи для указанного сигнала. Функция посылки сигнала в такой схеме хранения также становится достаточно простой: необходимо найти первое и последнее вхождения имени сигнала в SignalMap и для каждого вхождения в этом диапазоне вызвать соответствующий слот через маппинг SlotMap.
В результате такой несложной реализации получается весьма мощная система управления событиями. Конечно неотъемлемый минус этой реализации это использование std::string в качестве идиентификаторов, однако за удобство надо платить
Вот чего только люди не выдумают, лишь бы на Лиспе не писать.