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

Наш основной проект на момент написания этой заметки – игра Witchcraft – написан на Ruby on Rails. Начать разработку онлайн-игры именно на Rails не было каким-то сознательным решением, скорее так исторически сложилось, что мне очень нравилось программировать на Rails и я готов был все что угодно делать с помощью этой технологии, не взирая на уместность. У текущей реализации проекта есть один несомненный плюс – она работает. Но есть и ряд существенных минусов, от которых я сильно хочу избавиться и которые мы стараемся исключить при разработке нового продукта.

Минусы текущей реализации

  • Игровая механика жестко ограничена соглашениями Rails. Каждое действие игрока может генерировать только один результат, только один ответ от сервера. Например, при клике по кнопке миссии можно только вернуть результат выполнения миссии, но не событие о получении нового уровня или об открытии новой миссии. Конечно, все можно сделать, однако код для такой логике абсолютно противоречит структуре традиционного rails-приложения.
  • Возможности общения сервера с клиентом ограничены ответами на HTTP-запросы. Любая попытка реализовать отправку сообщения с сервера на клиент вне контекста конкретного запроса выглядит совершенно ортогонально концепции Rails.
  • Использование базы данных для хранения игрового контента и данных игрока жесточайше и главное совершенно бесполезно растрачивает серверные ресурсы на загрузку/сохранение данных при выполнении мелких действий, из которых собственно и строится интерактивный игровой процесс.
  • Завязанность визуализации игрового процесса на сервер. В Witchcraft подавляющая часть всего визуального представления игры генерируется сервером. Все страницы, результаты действий, сообщения пользователю, диалоги – все это в виде HTML поступает с сервера. На клиенте происходит самый минимум – навешивание событий на кнопки и ссылки, которые опять же вызывают сервер для генерации результатов событий. В отличие от бизнес-приложений, где один клиент генерирует 10-15 запросов за сессию, игрок выполняет 50-1000 действий за игровую сессию, потребляя ощутимое количество ресурсов.

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

Событийный игровой сервер

События могут быть сгенерированы игроком – клики мышкой, нажатия на кнопки – а могут и поступать от сервера – построилось здание, на игрока напали, пришло сообщение. И те, и другие события могут происходить вне зависимости друг от друга, между ними нет взаимосвязи, то есть нельзя точно сказать, например, что в результате данного конкретного нажатия на кнопку построилось здание. События должны поступать от игрока на сервер и от сервера к игроку без какой-либо привязки к запросу.

В процессе работы над прототипом игрового сервера у меня выкристаллизовалась система, состоящая из клиента, транспорта, очереди событий и игрового демона.

Игровой клиент

Решив не отходить сильно далеко от Rails, я решил разрабатывать игровой клиент на JavaScript с использованием фреймворка Backbone.js – это оказалось наболее простым решением. В качестве шаблонизатора используется Handlebars.js – шаблоны получаются весьма простыми и с минимумом логики. Клиент собирается и отдается в браузер Rails-приложением, которое помимо отдачи клиента будет отвечать за такие полезные в хозяйстве вещи как регистрация пользователей, настройки и прием платежей.

Транспорт

Для доставки событий от клиента на сервер и обратно я решил испробовать WebSocket – эта технология оказалась наиболее подходящей для асинхронного обмена данными. На стороне сервера я поставил Rack middleware прямо на Rails-приложение, обработку websocket-ов реализовал с помощью гема faye-websocket. Клиент подключается к серверу и отправляет сообщения о событиях, сервер сваливает полученные сообщения в очередь событий для обработки игровым демоном, выгребает из очереди события, сгенерированные игровым демоном для данного игрока, и отправляет их на клиент. Обмен данными происходит асинхронно.

Очередь событий

Хранение очередей входящих и исходящих игровых событий я пока решил реализовать через Redis, так как с ним я более-менее знаком и в нем хорошо реализована возможность создания списков объектов. События пишутся в ключи, соответствующие игровой сессии – ‘game_123_incoming’ для всех входящих от всех игроков, ‘game_123_ougoing_player_*’ – для исходящих, адресованных конкретным игрокам.

Игровой демон

Непосредственная обработка игрового процесса происходит в отдельно запущенном демоне на базе EventMachine. Демон загружает в себя игровые сессии по мере их добавления в хранилище, для хранения данных сессий опять же пока что использую Redis. Затем по таймеру (100 раз в секунду) демон опрашивает очередь на предмет наличия новых событий от игроков, а так же запускает так называемый game loop – обработчик игрового процесса. Этот обработчик производит действия в соответствии с командами игроков и отсылает игрокам игровые события, произошедшие в результате их действий или в связи с внутренними алгоритмами. Все изменения параметров игровой сессии происходят в памяти игрового демона и периодически (раз в 10 секунд) записываются в хранилище.

Предолагаемые проблемы

На данный момент это экспериментальная схема, но она весьма неплохо работает, по крайней мере на девелоперской машине :) Однако я заранее предвижу несколько проблем, которые могут возникнуть:

  • JS-клиент может достаточно сильно тормозить при активном рендере. Это частично можно скомпенсировать пре-рендером части шаблонов, а так же переходом на какие-либо более быстрые средства рендеринга – Flash, Unity, нативный клиент.
  • Websocket – новая технология, поддерживается не всеми браузерами. Эту проблему можно обойти, используя Flash-сокеты, уже даже есть готовые решения. На крайний случай доставку событий от клиента можно реализовать обычными HTTP-запросами, а от сервера клиенту – через long-polling.
  • Redis – не самый подходящий инструмент для хранения очередей. При необходимости его можно заменить на что-то более подходящее для организации очередей. Например, RabbitMQ.
  • Игровой демон на ruby может оказаться достаточно медленным при большом количестве игровых сессий. Это решается распределением сессий между несколькими демонами и/или переписыванием игровой логики на более быстрые языки – Java, Erlang, C++.

Преимущества

У данной схемы помимо недостатков есть и определенные преимущества:

  • Слабая связность компонентов позволяет менять технологии и инструменты одного компонента без особого влияния на все остальные. При желании JS-клиента можно отдавать не рельсами, а голым nginx-ом, websocket-ы обрабатывать сервером на C++, игровой демон переписать на Java, а очередь хранить RabbitMQ.
  • Относительная универсальность системы позволяет разрабатывать игры совершенно разных жанров – стратегии, rpg, тетрисы и кроссворды – используя одну и ту же инфраструктуру. Все отличия заключаются в двух компонентах – логика игровой сессии и клиент. Все остальное остается неизменным.

В целом, эксперимент сам по себе оказался весьма интересен и позволил немного отстраниться от привычной схемы разработки приложений.