Постик про архитектуру игр в @MadMudBot.
Как и обещал, рассказываю про внутреннее устройство приложения. Устроено всё относительно просто и стандартно: приложение реализовано на java 8 при помощи каркаса Spring Boot, и развёрнуто в JBoss Wildfly в IaaS-провайдере Openshift. Почему использую сервер приложений — очень удобен для управления, предоставляет полезные сервисы JavaEE: транзакции, встроенный IMDG (in-memory data grid) Infinispan, пулинг и управление соединениями к БД — всё там есть.
Если кто не знает, развёртывание в OpenShift выглядит так — пушишь код в репозиторий, там срабатывает триггер и развёртывает master на сервере.
Основной нюанс архитектуры связан с тем, что я решил отказаться от использования реляционной СУБД для хранения оперативных данных (при этом я по прежнему использую реляционную БД Postgres для хранения данных билллинга — для надёжности). Почему так сделал ? Типовые задачи онлайн игры "вынуть игровой мир" - "обновить" - "положить игровой мир обратно" не очень хорошо ложатся на паттерн работы с реляционной БД, и требуют при написании кода подключения головного мозга, а не только спинного. Лениво.
Поэтому было решено для оперативного хранения взять кэш-провайдер Infinispan, встроенный в сервер приложений. Этот IMDG, который предоставляет в java интерфейс обычной хэш-мапы, хранящей объекты по ключу. Ключом, как вы понимаете, является идентификатор игрока. С "задней стороны" кэш подстрахован (стандартный механизм infinispan) технологией персистирования объектов в кэше в постоянное хранилище — на случай перезагрузок сервера и т.п. Сохраняется всё в большую плоскую таблицу в БД. Это был самый простой вариант, хотя можно настроить и файл, и что-то ещё. Сохранение данных выполняется кэшом тогда, когда ему кажется нужным, т.к. асинхронно и незаметно для клиентов infinispan. Так реализовано сохранение игрового мира в процессе игры. Заработало всё сразу же.
Создание же игрового мира выполняется из текстового файла, в котором описана структура лабиринта, а дополнительная логика (зомби, аптечки, инвентарь) реализованы кодом (50 строк на java).
Самое большое, что теряет приложение при отказе от реляционной БД — это изоляцию, когда можно закрыть глаза на многопоточность и притвориться, как будто у тебя один пользователь. В случае с хранением всех данных в БД, как это делает типовое приложение, в ситуации конфликтов транзакционная система помогает поддерживать согласованность (в крайней ситуации прерывая транзакцию, которую необходимо будет повторить или отбросить). Когда всё хранится в памяти, о многопоточном доступе приходится заботиться по-другому (расскажу ниже, как сделал).
Для более сложной игры я бы взял для реализации какой-нибудь агентный фреймворк (типа Akka), там сериализация доступа выполняется просто и понятно. В данной ситуации я поступил по-другому: обмен с Telegram API выполняется при помощи очередей сообщений, которые наполняют и разбирают специальные I/O потоки, а вот превращением входной команды пользователя в выходное сообщение (входная очередь -> выходная очередь) занимается один выделенный рабочий поток. Т.е. на самом деле несколько потоков никогда не работают со структурами игры, всегда только один, тем достигается согласованность и отсутствие конфликтов.
Планирование разных отложенных действий (например, оповещений пользователя) сделано на базе стандартных шедулеров java 8, который позволяет это сделать экономно и не требует блокирования множества потоков на ожидании таймера и т.п.
Вот и все основные моменты. Надеюсь, было интересно
Как и обещал, рассказываю про внутреннее устройство приложения. Устроено всё относительно просто и стандартно: приложение реализовано на java 8 при помощи каркаса Spring Boot, и развёрнуто в JBoss Wildfly в IaaS-провайдере Openshift. Почему использую сервер приложений — очень удобен для управления, предоставляет полезные сервисы JavaEE: транзакции, встроенный IMDG (in-memory data grid) Infinispan, пулинг и управление соединениями к БД — всё там есть.
Если кто не знает, развёртывание в OpenShift выглядит так — пушишь код в репозиторий, там срабатывает триггер и развёртывает master на сервере.
Основной нюанс архитектуры связан с тем, что я решил отказаться от использования реляционной СУБД для хранения оперативных данных (при этом я по прежнему использую реляционную БД Postgres для хранения данных билллинга — для надёжности). Почему так сделал ? Типовые задачи онлайн игры "вынуть игровой мир" - "обновить" - "положить игровой мир обратно" не очень хорошо ложатся на паттерн работы с реляционной БД, и требуют при написании кода подключения головного мозга, а не только спинного. Лениво.
Поэтому было решено для оперативного хранения взять кэш-провайдер Infinispan, встроенный в сервер приложений. Этот IMDG, который предоставляет в java интерфейс обычной хэш-мапы, хранящей объекты по ключу. Ключом, как вы понимаете, является идентификатор игрока. С "задней стороны" кэш подстрахован (стандартный механизм infinispan) технологией персистирования объектов в кэше в постоянное хранилище — на случай перезагрузок сервера и т.п. Сохраняется всё в большую плоскую таблицу в БД. Это был самый простой вариант, хотя можно настроить и файл, и что-то ещё. Сохранение данных выполняется кэшом тогда, когда ему кажется нужным, т.к. асинхронно и незаметно для клиентов infinispan. Так реализовано сохранение игрового мира в процессе игры. Заработало всё сразу же.
Создание же игрового мира выполняется из текстового файла, в котором описана структура лабиринта, а дополнительная логика (зомби, аптечки, инвентарь) реализованы кодом (50 строк на java).
Самое большое, что теряет приложение при отказе от реляционной БД — это изоляцию, когда можно закрыть глаза на многопоточность и притвориться, как будто у тебя один пользователь. В случае с хранением всех данных в БД, как это делает типовое приложение, в ситуации конфликтов транзакционная система помогает поддерживать согласованность (в крайней ситуации прерывая транзакцию, которую необходимо будет повторить или отбросить). Когда всё хранится в памяти, о многопоточном доступе приходится заботиться по-другому (расскажу ниже, как сделал).
Для более сложной игры я бы взял для реализации какой-нибудь агентный фреймворк (типа Akka), там сериализация доступа выполняется просто и понятно. В данной ситуации я поступил по-другому: обмен с Telegram API выполняется при помощи очередей сообщений, которые наполняют и разбирают специальные I/O потоки, а вот превращением входной команды пользователя в выходное сообщение (входная очередь -> выходная очередь) занимается один выделенный рабочий поток. Т.е. на самом деле несколько потоков никогда не работают со структурами игры, всегда только один, тем достигается согласованность и отсутствие конфликтов.
Планирование разных отложенных действий (например, оповещений пользователя) сделано на базе стандартных шедулеров java 8, который позволяет это сделать экономно и не требует блокирования множества потоков на ожидании таймера и т.п.
Вот и все основные моменты. Надеюсь, было интересно