Всем привет!
Хочу поделиться с хабрасообществом своим опытом написания многопользовательской браузерной стратегии с нуля до рабочего проекта. С точки зрения непосредственно программирования, архитектуры и возникших проблем. Это мой первый опыт создания игры и возможно вы заметите множество недочетов или промахов, или посоветуете чего дельного. Но не суть важно — главное я довел дело до рабочего проекта и наверняка многим будет интересно узнать подробности.
Что представляет собой игра? Видимо наиболее коротким описанием будет «клон Цивилизации» =). Но это не значит что у меня не хватило фантазии придумать что-то свое. Просто сделать «Цивилизацию» было моей мечтой. Вряд ли бы я получил столько удовлетворения от написания другой игры. Ну а фанаты Цивилизации наоборот считают, что моя игра совсем не похожа на Цивилизацию, разве что только с виду. Может это и к лучшему.
Игра называется The Fate of Nation http://fatenation.com
Расписывать архитектуру и логику работы приложения можно до бесконечности поэтому придется видимо разделить на несколько частей статью, если к ней будет интерес. Кроме того не вижу большого смысла приводить много кода, так как реализовать написанное мной можно на любом языке и платформе.
Для создания игры я использовал php и MySQL на сервере, html и javascript на клиенте. Flash не используется. Из html5 есть только видео на сайте и несколько областей с канвасом в самой игре — включая поверхность карты и мини-карту. Объем кода клиентской части в несколько раз превышает серверную часть, поэтому в основном буду рассказывать о клиентской разработке, но начнем с сервера.
Общая архитектура приложения выглядит как полностью асинхронное веб приложение на JavaScript. Перезагрузок страниц не предусмотрено. Обмен данными с сервером исключительно через Ajax и JSON. В JSON'е передаются только данные, без html кода. Html разметка загружается отдельно в начале загрузки приложения и процессится с данными через клиентский шаблонизатор по мере загрузки данных с сервера.
На сервере никаких фреймворков не использовал — хотя начинал писать с использованием Zend Framework, который выкосил потом за ненадобностью. Вместо него создал свою простую архитектуру отдаленно напоминающую контроллеры и экшены из ZendFramework.
Как видно из рисунка, на сервере одна точка входа — файл index.php. В процессе игры на сервер идут запросы вроде такого: /Unit/Move. И посылается JSON с параметрами, в данном случае это id юнита и координаты перемещения. Сервер перенаправляет этот запрос на index.php, в котором последовательно выполняется подключение к БД, проверка текущего пользователя и парсинг строки запроса для определения контроллера (Unit) и действия (Move). Если контроллер не задан то сервер выдает индексную страницу с кодом для построения клиентского приложения, но об этом позже. Если же контроллер задан то ищется файл этого контроллера, подключается его код и запускается обработка запросов этого контроллера, где соответственно ищется необходимый экшн, а в нем производится проверка входных данных и дергается бизнес логика.
Для работы с БД написан специальный класс абстракции базы данных через который проходят все запросы к БД от бизнес логики и контроллеров, экранирование данных и прочие небольшие удобства. Собственно на сервере все довольно просто с архитектурой, а зона ответственности сервера заключается лишь в проверке входных данных и выдаче информации из БД. Все остальное делает клиент.
Теперь немного о самой игре.
Первое что было сделано это карта на которой происходят почти все игровые действия: строительство городов, улучшений (посевы, дороги), перемещение юнитов и исследование карты. Размер карты составил 1000 на 1000 клеток для каждой отдельная запись в БД. Я видел игры где карта сделана бесконечной и записи о клетках динамически вставлялись тогда, когда с клеткой производились какие-либо действия. Но меня такой подход немного пугал своей непредсказуемостью. Гораздо проще планировать игру, когда точно знаешь, что у тебя есть фиксированная карта. Можно запланировать расположение игроков их количество, количество городов и юнитов, приблизительно оценить нагрузку.
Итого получилось 1000 * 1000 = 1 000 000 записей в БД для карты. До этого я не работал с таким количеством записей и меня это насторожило. Думал что будет тормозить.
Я решил перехитрить MySql и разместить карту в 10-ти таблицах по 100 000 записей в каждой с надеждой, что станет быстрее работать. В итоге пришлось написать дурацкую логику по выборке клеток из нескольких таблиц сразу, а замеры показали что производительность только упала. Вернул все назад в 1-у таблицу.
Конечно это не весь список полей, но здесь и далее для упрощения буду приводить только те поля, о которых рассказываю в статье.
Клиент написан таким образом, что он не запрашивает с сервера определенные клетки, а запрашивает их партиями по 100 штук (10 на 10), которые я назвал регионами. То есть каждая клетка принадлежит какому-то региону и клиент запрашивает регионы и не конкретные клетки. Как только игрок перемещает карту так, что становится виден новый регион, мы посылаем запрос на сервер за этим регионом и граничащими с ним. Данные каждого загруженного регионакешируются на 30 секунд на клиенте. Это позволяет легко прокручивать карту без тормозов и лишних запросов на сервер и избавляет от задержки при появлении нового региона на карте — так как мы загружаем все соседние наперед.
Когда я делал эти «регионы» я не предполагал насколько они увеличат производительность. Оказалось выделить 100 клеток фильтруя по полю региона получается многократно быстрее чем фильтруя по координатам. Несмотря на то, что я объединил x и y координаты клетки в одно поле location = 1000*x + y. Сделал это прежде всего для удобства — чтобы легче было достать одну клетку.
Затем каждую сущность (города, юниты, ресурсы), которые располагаются на карте и имеют соответственно конкретные координаты, я также пометил регионом, что увеличило производительность выборок в сотни раз. Одно дело искать значения в таблице по ключу с миллионом уникальных значений и другое дело по ключу с 10 000 значений.
Таким образом получилась такая система: клиент запрашивает регионы — сервер достает из БД карту и все сущности на ней, быстро фильтруя по регионам — клиент отрисовывает это все в браузере на канвасе. У каждой сущности есть такие поля как время до окончания битвы или время до перемещения в следующую клетку — в этом случае по истечении этих таймаутов мы обновляем локально только то что требуется. Например если мы исследуем карту то догружаем только что открытые клетки и не более. Если вражеский юнит переместился — догружаем следующую точку его перемещения.
Однако меня терзал еще один вопрос. Мне позарез хотелось сделать исследование карты — чтобы изначально она была не разведана и нужно было ходить по ней чтобы что-то увидеть.
Такого я не видел еще в браузерных играх (собственно как и юнитов передвигающихся по карте, а не по воздуху). Я принялся за расчеты. Стартовая позиция игрока расположена внутри региона. То есть максимальное количество игроков 10 000 как и регионов. Каждый игрок может разведать всю карту. Итого 10 000 * 1 000 000 = 10 миллиардов записей может быть в таблице пермишенов на клетки! Таблица карты показалась на фоне этого детским лепетом =). Конечно эта цифра завышена. Вряд ли кому-то удастся разведать всю карту — она очень большая. Но десятки и сотни миллионов записей в таблице пермишенов точно могут быть в конце игры.
И на эту таблицу нужно было джойнить все сущности включая саму карту каждый раз при выборке регионов. Здесь опять спас меня ключ по полю региона, который позволил делать эти джоины намного быстрее.
Провести нагрузочное тестирование чтобы определить на каком этапе сервер начнет тормозить не удалось еще. Максимум что я видел это чуть более 2-ух миллионов записей в таблице пермишенов.
Чтобы сделать перемещение юнитов пришлось тоже подумать и переписать логику несколько раз.
Первое что нам нужно, это точно отслеживать время открытия новых клеток чтобы можно было отфильтровывать клетки, юниты и города по этим данным. Сразу напрашивается использовать таблицу пермишенов на карту, но со спец-полем — означающим время когда эта запись станет активной. Так и было сделано. Клиент отправляет id юнита, и новую координату дислокации. Сервер просчитывает текущую позицию юнита, координаты клеток по которым он будет перемещаться, и в зависимости от территории этих клеток, типа юнита и других параметров высчитывает время когда этот юнит будет в каждой клетке. Затем дополнительно просчитываются таким же образом соседние клетки в зависимости от радиуса обзора юнита.
Все это вставляется в таблицу пермишенов на клетки и карта работает как часы. Юнит ходит по карте, при каждом его перемещении мы запрашиваем клетки вокруг него, стандартными методами, которые отфильтруют сущности уже по новым данным пермишенов учитывая время активации пермишена, где будут заветные открытые области.
Далее записи пермишенов, которые говорят о перемещении юнита мы помечаем еще 2 полями: id юнита и типом записи: 'обзорные клетки' или 'клетки по которым идет юнит'. Первое поле нужно чтобы при остановке юнита или смене пункта назначения можно было их удалить, второе нужно чтобы при выборке юнита записать ему времена смены дислокации.
Затем коллеги по работе мне подсказали еще один довольно очевидный момент: ввести поле означающее время выхода юнита с данной клетки. Я назвал его out_timestamp. Это позволило легко выбирать текущие позиции всех юнитов и соответственно фильтровать вражеских юнитов по видимым нами клеткам.
Уверен, что мой пример не самая удачная архитектура для подобной игры, но вроде работает =) В следующих статьях могу рассказать о клиентской архитектуре, кешировании, используемом фреймворке и о том, как мне удалось сделать демонстрационную версию игры работающую без запросов к серверу, чисто на клиенте.
Да, кстати, часто после различных постов об игре народ начинает хвалить графику, а не геймплей. Так что скажу сразу — я ее не рисовал!!! Это все наш художник-дизайнер Максим Кудрицкий.
P. S. Спасибо TheShock за помощь и поддержку в написании топика! =)
Хочу поделиться с хабрасообществом своим опытом написания многопользовательской браузерной стратегии с нуля до рабочего проекта. С точки зрения непосредственно программирования, архитектуры и возникших проблем. Это мой первый опыт создания игры и возможно вы заметите множество недочетов или промахов, или посоветуете чего дельного. Но не суть важно — главное я довел дело до рабочего проекта и наверняка многим будет интересно узнать подробности.
Что представляет собой игра? Видимо наиболее коротким описанием будет «клон Цивилизации» =). Но это не значит что у меня не хватило фантазии придумать что-то свое. Просто сделать «Цивилизацию» было моей мечтой. Вряд ли бы я получил столько удовлетворения от написания другой игры. Ну а фанаты Цивилизации наоборот считают, что моя игра совсем не похожа на Цивилизацию, разве что только с виду. Может это и к лучшему.
Игра называется The Fate of Nation http://fatenation.com
Расписывать архитектуру и логику работы приложения можно до бесконечности поэтому придется видимо разделить на несколько частей статью, если к ней будет интерес. Кроме того не вижу большого смысла приводить много кода, так как реализовать написанное мной можно на любом языке и платформе.
Для создания игры я использовал php и MySQL на сервере, html и javascript на клиенте. Flash не используется. Из html5 есть только видео на сайте и несколько областей с канвасом в самой игре — включая поверхность карты и мини-карту. Объем кода клиентской части в несколько раз превышает серверную часть, поэтому в основном буду рассказывать о клиентской разработке, но начнем с сервера.
Общая архитектура
Общая архитектура приложения выглядит как полностью асинхронное веб приложение на JavaScript. Перезагрузок страниц не предусмотрено. Обмен данными с сервером исключительно через Ajax и JSON. В JSON'е передаются только данные, без html кода. Html разметка загружается отдельно в начале загрузки приложения и процессится с данными через клиентский шаблонизатор по мере загрузки данных с сервера.
На сервере никаких фреймворков не использовал — хотя начинал писать с использованием Zend Framework, который выкосил потом за ненадобностью. Вместо него создал свою простую архитектуру отдаленно напоминающую контроллеры и экшены из ZendFramework.
Как видно из рисунка, на сервере одна точка входа — файл index.php. В процессе игры на сервер идут запросы вроде такого: /Unit/Move. И посылается JSON с параметрами, в данном случае это id юнита и координаты перемещения. Сервер перенаправляет этот запрос на index.php, в котором последовательно выполняется подключение к БД, проверка текущего пользователя и парсинг строки запроса для определения контроллера (Unit) и действия (Move). Если контроллер не задан то сервер выдает индексную страницу с кодом для построения клиентского приложения, но об этом позже. Если же контроллер задан то ищется файл этого контроллера, подключается его код и запускается обработка запросов этого контроллера, где соответственно ищется необходимый экшн, а в нем производится проверка входных данных и дергается бизнес логика.
Для работы с БД написан специальный класс абстракции базы данных через который проходят все запросы к БД от бизнес логики и контроллеров, экранирование данных и прочие небольшие удобства. Собственно на сервере все довольно просто с архитектурой, а зона ответственности сервера заключается лишь в проверке входных данных и выдаче информации из БД. Все остальное делает клиент.
Теперь немного о самой игре.
Карта
Первое что было сделано это карта на которой происходят почти все игровые действия: строительство городов, улучшений (посевы, дороги), перемещение юнитов и исследование карты. Размер карты составил 1000 на 1000 клеток для каждой отдельная запись в БД. Я видел игры где карта сделана бесконечной и записи о клетках динамически вставлялись тогда, когда с клеткой производились какие-либо действия. Но меня такой подход немного пугал своей непредсказуемостью. Гораздо проще планировать игру, когда точно знаешь, что у тебя есть фиксированная карта. Можно запланировать расположение игроков их количество, количество городов и юнитов, приблизительно оценить нагрузку.
Итого получилось 1000 * 1000 = 1 000 000 записей в БД для карты. До этого я не работал с таким количеством записей и меня это насторожило. Думал что будет тормозить.
Я решил перехитрить MySql и разместить карту в 10-ти таблицах по 100 000 записей в каждой с надеждой, что станет быстрее работать. В итоге пришлось написать дурацкую логику по выборке клеток из нескольких таблиц сразу, а замеры показали что производительность только упала. Вернул все назад в 1-у таблицу.
- x, y — это координаты клетки.
- terrain — тип территории (луг, лес, гора...).
- resource — ресурс если он есть на клетке (глина, лошади).
- wens9_code — название поля произошло от west-east-north… 9 — означает что изображение данной клетки зависит от территорий 8-ми рядом стоящих клеток и естественно от территории самой этой клетки — всего 9. Эту логику я спер с 3-ей цивилизации, насмотревшись их спрайтов территорий там где по 512 вариантов иконок для одной клетки!)) Потом у меня вскипел мозг разбирая зависимости по которым они выбирали иконки и я понял, какой это большой геморрой. =) И все только для одного: чтобы спрайты имели жесткие концы в виде ромбиков 128 на 64 пикселя. В конце концов мы решили использовать png24 с полупрозрачными краями накладывающиеся друг на друга и создающих в 10 раз лучший и разнообразный ландшафт, чем в описанном примере из Цив3. А выбираем иконки случайно независимо от соседних клеток. Это видно на скрине — сразу не скажешь где там одинаковые иконки полей. Вот горы по краям размыть забыли и они имеют четкие границы — что плохо смотрится.
- starting_position — означает что в этой клетке появится игрок.
Конечно это не весь список полей, но здесь и далее для упрощения буду приводить только те поля, о которых рассказываю в статье.
Регионы
Клиент написан таким образом, что он не запрашивает с сервера определенные клетки, а запрашивает их партиями по 100 штук (10 на 10), которые я назвал регионами. То есть каждая клетка принадлежит какому-то региону и клиент запрашивает регионы и не конкретные клетки. Как только игрок перемещает карту так, что становится виден новый регион, мы посылаем запрос на сервер за этим регионом и граничащими с ним. Данные каждого загруженного регионакешируются на 30 секунд на клиенте. Это позволяет легко прокручивать карту без тормозов и лишних запросов на сервер и избавляет от задержки при появлении нового региона на карте — так как мы загружаем все соседние наперед.
Когда я делал эти «регионы» я не предполагал насколько они увеличат производительность. Оказалось выделить 100 клеток фильтруя по полю региона получается многократно быстрее чем фильтруя по координатам. Несмотря на то, что я объединил x и y координаты клетки в одно поле location = 1000*x + y. Сделал это прежде всего для удобства — чтобы легче было достать одну клетку.
Затем каждую сущность (города, юниты, ресурсы), которые располагаются на карте и имеют соответственно конкретные координаты, я также пометил регионом, что увеличило производительность выборок в сотни раз. Одно дело искать значения в таблице по ключу с миллионом уникальных значений и другое дело по ключу с 10 000 значений.
Таким образом получилась такая система: клиент запрашивает регионы — сервер достает из БД карту и все сущности на ней, быстро фильтруя по регионам — клиент отрисовывает это все в браузере на канвасе. У каждой сущности есть такие поля как время до окончания битвы или время до перемещения в следующую клетку — в этом случае по истечении этих таймаутов мы обновляем локально только то что требуется. Например если мы исследуем карту то догружаем только что открытые клетки и не более. Если вражеский юнит переместился — догружаем следующую точку его перемещения.
Исследование карты
Однако меня терзал еще один вопрос. Мне позарез хотелось сделать исследование карты — чтобы изначально она была не разведана и нужно было ходить по ней чтобы что-то увидеть.
Такого я не видел еще в браузерных играх (собственно как и юнитов передвигающихся по карте, а не по воздуху). Я принялся за расчеты. Стартовая позиция игрока расположена внутри региона. То есть максимальное количество игроков 10 000 как и регионов. Каждый игрок может разведать всю карту. Итого 10 000 * 1 000 000 = 10 миллиардов записей может быть в таблице пермишенов на клетки! Таблица карты показалась на фоне этого детским лепетом =). Конечно эта цифра завышена. Вряд ли кому-то удастся разведать всю карту — она очень большая. Но десятки и сотни миллионов записей в таблице пермишенов точно могут быть в конце игры.
И на эту таблицу нужно было джойнить все сущности включая саму карту каждый раз при выборке регионов. Здесь опять спас меня ключ по полю региона, который позволил делать эти джоины намного быстрее.
Провести нагрузочное тестирование чтобы определить на каком этапе сервер начнет тормозить не удалось еще. Максимум что я видел это чуть более 2-ух миллионов записей в таблице пермишенов.
Перемещение юнитов
Чтобы сделать перемещение юнитов пришлось тоже подумать и переписать логику несколько раз.
Первое что нам нужно, это точно отслеживать время открытия новых клеток чтобы можно было отфильтровывать клетки, юниты и города по этим данным. Сразу напрашивается использовать таблицу пермишенов на карту, но со спец-полем — означающим время когда эта запись станет активной. Так и было сделано. Клиент отправляет id юнита, и новую координату дислокации. Сервер просчитывает текущую позицию юнита, координаты клеток по которым он будет перемещаться, и в зависимости от территории этих клеток, типа юнита и других параметров высчитывает время когда этот юнит будет в каждой клетке. Затем дополнительно просчитываются таким же образом соседние клетки в зависимости от радиуса обзора юнита.
Все это вставляется в таблицу пермишенов на клетки и карта работает как часы. Юнит ходит по карте, при каждом его перемещении мы запрашиваем клетки вокруг него, стандартными методами, которые отфильтруют сущности уже по новым данным пермишенов учитывая время активации пермишена, где будут заветные открытые области.
Далее записи пермишенов, которые говорят о перемещении юнита мы помечаем еще 2 полями: id юнита и типом записи: 'обзорные клетки' или 'клетки по которым идет юнит'. Первое поле нужно чтобы при остановке юнита или смене пункта назначения можно было их удалить, второе нужно чтобы при выборке юнита записать ему времена смены дислокации.
Затем коллеги по работе мне подсказали еще один довольно очевидный момент: ввести поле означающее время выхода юнита с данной клетки. Я назвал его out_timestamp. Это позволило легко выбирать текущие позиции всех юнитов и соответственно фильтровать вражеских юнитов по видимым нами клеткам.
Уверен, что мой пример не самая удачная архитектура для подобной игры, но вроде работает =) В следующих статьях могу рассказать о клиентской архитектуре, кешировании, используемом фреймворке и о том, как мне удалось сделать демонстрационную версию игры работающую без запросов к серверу, чисто на клиенте.
Да, кстати, часто после различных постов об игре народ начинает хвалить графику, а не геймплей. Так что скажу сразу — я ее не рисовал!!! Это все наш художник-дизайнер Максим Кудрицкий.
P. S. Спасибо TheShock за помощь и поддержку в написании топика! =)