Войти
ПрограммированиеСтатьиОбщее

Простая система событий на С++11

Автор:

При проектировании своих игр/программ/систем мы стараемся как можно больше следовать принципу SRP (Single responsibility principle) [1] - то есть каждая сущность инкапсулирует в себя одну конкретную обязанность.
И очень замечательно если получается избегать прямого общения сущностей между собой, однако в геймдеве этого, порой, бывает сложно добиться, и некоторым сущностям хотелось бы дать возможность мгновенного реагирования на те или иные события.

Это можно решить, например, использовав паттерн Observer [2]. Однако данный паттерн не очень удобен, в первую очередь тем, что приходится использовать множественное наследование, а так же включать заголовочные файлы класса-слушателя. Хочется решать задачу более «чистым» способом.

И тут мы приходим к идее Событий (Events) [3]. В целом, идея не  нова, и в том или ином виде используется давно и много кем. Частный случай событийной системы – система сигнал-слот, которая хоть и избавляет нас от множественного наследования, однако все же требует некоторых знаний о слушателе.
Мы же с вами рассмотрим «чистую» систему событий (events system) – не требующую знаний о том кто будет источником/получателем нащих событий, и есть ли таковые вообще. То есть посылающий события вообще ничего не знает о том куда они уходят и что с этой информацией будут делать. Эдакая система коллбеков 80-го уровня ;)

До недавнего времени для построения такой системы требовалось включение тяжелых библиотек а-ля Boost и целого вороха макросов. С появлением стандарта С++11 жить стало проще, и теперь реализовать такую систему можно в пару десятков строчек чистого С++ кода.

Примечание: расмотренная ниже система используется автором в нескольких небольших домашних проектах, и полностью удовлетворяет оного как скоростью, так и удобством. Если вы захотите использовать приведенный код в своих проектах – помните, автор не несет никакой ответственности за вред/срыв сроков/кранчи/увольнения причененные данным кодом. Используйте на свой страх и риск, и с благословления ближайшей к вам церкви.

Наша событийная система базируется на 2-х фичах нового С++11 стандарта – std::function и variadic templates. Предполагается у читателя базовых знаний о С++11 и данных его фичах.

std::function [4] – это практически клон boost::function, и в свою очередь является высокоуровневой оберткой над функциями и функторами. Для захвата методов классов мы будем использовать std::bind.

Variadic templates (или parameters pack) [5] – фича, которую многие (включая меня) давно ждали, и которая пришла из замечательного языка D, где ее возможности еще шире.
Суть данной фичи в том, чтобы шаблоны могли принимать переменное количество аргументов. Типичный пример variadic template – std::tuple – контейнер фиксированного размера, способный содержать элементы различных типов.

template <typename... Params> class tuple;


Для «распаковки» аргументов был введен оператор ... (троеточие). При этом все разделительные запятые будут проставлены автоматически. Кто пробовал реализовать потодобно на макросах знают сколько кода приходилось писать для этого.
Для того чтобы узнать количество аргументов был введен оператор sizeof...().

Теперь перейдем к деталям реализации.

Главный класс – EventSystem – держит все зарегистрированные события в контейнере типа std::unordered_map, так как частые вставки-удаления не планируются, но нам нужна быстрая выборка по ключу. Данный класс является синглтоном (Singleton) [6]
Основные 4 метода класса (все шаблонные, в качестве шаблонного параметра принимают специальную структуру-описатель события (EventTrait)):
•  RegisterEvent – регистрирует событие в системе событий.
•  SubscribeToEvent – подписывает объект на событие. В качестве параметров принимает указатель на объект класса и метод класса, который будет реагировать на событие.
•  UnsibscribeFromEvent – отписывает объект от события. В качестве параметра принимает указатель на объект класса.
•  RaiseEvent – вызывает событие, по цепочке будут оповещены все подписавшиеся. Все полученные аргументы будут переданы подписчикам (за это отвечает std::forward).

Для визуальной красоты рекомендуется пользоваться функциями-помощниками:
•  ev::register_event
•  ev::subscribe_event
•  ev::unsunscribe_event
•  ev::raise_event

Для хранения событий в контейнере все события наследуются от общего абстрактного класса BasicEvent не имеющего методов.
Класс SpecEvent является специализированным шаблонным классом для событий, специализатором для него является все та же структура-описатель EventTrait.
Основные методы:
•  Bind – производит захват метода класса и указателя на объект класса и сохраняет их в списке подписчиков.
•  Unbind – удаляет объект класса из списка слушателей.
•  Call – вызывает по цепочке сохраненные методы классов-слушателей передавая им аргументы. Для того чтобы не терять аттрибуты аргументов (особенно для ссылочных аргументов) – используется move-семантика [7]

Как уже говорилось ранее – для захвата методов класса используется std::bind, которая вернет полноценный объект-фунцию. Использование std::bind необходимо так же для захвата первого, неявного, параметра метода-функции – указателя на объект класса. Для того чтобы опустить захват остальных аргументов мы используем т.н. placeholders [8].
И все бы хорошо, однако мы в момент биндинга должны передавать определенное количество плейсхолдеров. Можно, конечно, написать специализированные биндинг-функции для каждого количества аргументов, а можно использовать возможности variadic templates.

Для этого напишем свою последовательность заглушек используя самописный аналог integer_sequence из C++14.  Реализация подсмотрена в http://loungecpp.wikidot.com/tips-and-tricks%3aindices

    template <std::size_t... Is>                struct indices {};
    template <std::size_t N, std::size_t... Is> struct build_indices : build_indices<N - 1, N - 1, Is...> {};
    template <std::size_t... Is>                struct build_indices<0, Is...> : indices<Is...> {};

    // placeholders generator declaration
    template <std::size_t N> struct placeholders_generator {};


Теперь нам нужно заставить std::bind принимать наши заглушки как плейсхолдеры. Для этого специализируем std::is_placeholder.

namespace std {
    template <std::size_t N> struct is_placeholder<ev::detail::placeholders_generator<N>> : std::integral_constant<std::size_t, N + 1>{};
} // namespace std


N + 1 здесь нужен чтобы последовательность всегда стартовала от 1 а не от 0.

И теперь рассмотрим ту самую структуру-описатель событий – EventTrait. Для каждого события она должна быть своя, и так как в С++ нет аналога mixins из D, то здесь пришлось использовать пережиток коммунизма макрос:

#define DECLARE_EVENT_TRAIT(eventTrait, ...)                                    \
    struct eventTrait {                                                         \
        typedef std::tuple<__VA_ARGS__> ParamsTuple;                            \
        typedef std::function<void(__VA_ARGS__)> DelegateType;                  \
        static const std::size_t numArgs = std::tuple_size<ParamsTuple>::value; \
    };                                                                          \
    namespace ev { template <> const char* get_event_trait_name<eventTrait>() { return #eventTrait; } }


Данная структура определяет тип std::tuple способной хранить в себе аргументы события. В данной реализации не используется, сделано как задел на будущее для реализации некоего подобия RPC.
Так же структура определяет тип std::function способной хранить захваченные методы.
Структура так же хранит в себе количество аргументов для события. Используется для compile-time проверки и оповещения программиста (чтобы можно было быстро понять суть ошибки, а не читать простыни template-hell).

static_assert(EventTrait::numArgs == sizeof...(Args), "Incorrect arguments number!");


Функция get_event_trait_name<EventTrait> возвращает имя сруктуры-описателя. Используется так же для построения хэш-ключа для хранения событий в unordered_map.

Ну и наконец – пример использования. Пример надуманный, сделан лишь для демонстрации, в реальных проектах ссылочные аргументы лучше не использовать, а передавать весь объект и изменять его в методе подписчика.

#include "eventsys.h"

DECLARE_EVENT_TRAIT(Event_GameObjectMoved, float, float, unsigned int&);

struct GameObject {
    float posX, posY;
    unsigned int levelHash;

    void SetPos(const float x, const float y) {
        posX = x;
        posY = y;

        ev::raise_event<Event_GameObjectMoved>(posX, posY, levelHash);
    }
};

struct GameLevel {
    void GlobalPos2LevelHash(const float globX, const float globY, unsigned int& outHash) {
        unsigned int lvlX = static_cast<int>(std::floorf(globX)) >> 2;
        unsigned int lvlY = static_cast<int>(std::floorf(globY)) >> 2;
        outHash = (lvlY & 0xffff) << 16 | (lvlX & 0xffff);
    }

    void Initialize() {
        ev::subscribe_event<Event_GameObjectMoved>(this, &GameLevel::GlobalPos2LevelHash);
    }

    void Deinitialize() {
        ev::unsubscribe_event<Event_GameObjectMoved>(this);
    }
};

void RegisterGameEvents() {
    ev::register_event<Event_GameObjectMoved>();
}

int main(int, char**) {
    RegisterGameEvents();

    GameLevel level;
    level.Initialize();

    GameObject hero;
    hero.levelHash = 0;
    hero.SetPos(1037.0f, 576.0f);   // this should trigger Level::GlobalPos2LevelHash

    level.Deinitialize();

    hero.SetPos(576.0f, 1037.0f);   // this should not trigger Level::GlobalPos2LevelHash

    return 0;
}


Полный исходный код: - https://bitbucket.org/iOrange/eventsystem/src


Полезные ссылочки:
1.  https://en.wikipedia.org/wiki/Single_responsibility_principle
2.  https://en.wikipedia.org/wiki/Observer_pattern
3.  https://en.wikipedia.org/wiki/Event-driven_programming
4.  http://www.cplusplus.com/reference/functional/function/
5.  http://en.cppreference.com/w/cpp/language/parameter_pack
6.  https://en.wikipedia.org/wiki/Singleton_pattern
7.  http://www.cprogramming.com/c++11/rvalue-references-and-move-sema… in-c++11.html
8.  http://www.cplusplus.com/reference/functional/placeholders/

#C++, #GameDev

2 декабря 2015

Комментарии [103]