Многие из нас играли в железную дорогу в детстве и мечтали о втором-третьем наборе рельс, чтобы построить свою ветку от балкона до прихожей. Нашей команде удалось поучаствовать в виртуальном строительстве огромного транссиба с развязками поражающими воображение.
Кстати, на сайте Канобу еще неделю будет длиться конкурс с билетами на Игромир в качестве приза.
Ссылка на игру
и на внеконкурсную версию (без обертки Канобу)
Сюжет игры — полная противоположность subway surfer, надо защищать поезд от набегов идиотов. Поскольку это официальная игра, из поезда не может выйти охранник и навешать люлей. Поезд не может попасть в аварию или даже случайно кого-то задавить. В процессе было много заманчивых идей, возможно когда-то свет увидит версию с поездом, изничтожающим толпы зомби.
Эта статья не является hello-world для HTML5-игры в стиле crappy bird. Мы говорим о реальном проекте с точки зрения разработчика, о кране сжатых сроках, о написании игры с кучей хаков, о нервировании огромной цепочки заказчиков сдачей проекта в последний день, без запаса по времени. Хотите чистого кода — идите на github и мучайте наше детище сколько хотите. Код лежит под лицензией MIT, но весь арт сюда не входит.
Как РЖД печётся о безопасности мы знаем по огромным экранам на вокзалах. Лично я думаю, что если бы эти ролики не были зацензурены, и были выполнены в стиле mortal kombat, эффект был бы гораздо больший (Poezd wins. Fatality).
Так вот, где-то в РЖД решили что неплохо бы сделать небольшое промо с упором на безопасность на железнодорожных путях. Сложная цепочка посредников в итоге привела к нам
ТЗ был расписан достаточно прямо: должен быть генерируемый уровень, вид «сверху-сбоку», поезд соответствующий стилю РЖД, 6 видов препятствий и 3 супер героя, объясняющих как не надо себя вести на путях. Всё это должно работать на адекватных браузерах, на PC и планшетах. В качестве супер героев были выбраны Супермен, Человек-Паука и «тот парень из из GTA San Andreas»…
Думаете мы использовали свой супер-движок, который может рендерить всё на DIV либо на CANVAS2D либо SVG и прорисовывающем только те места где что-то менялось? Думаете мы взяли свою super-CMS для генерирования страниц и менеджмента скриптов? Нет, мы взяли самый лёгкий рендерер на свете — pixi.js, самый лёгкий на данный момент сборщик скриптов — gulp+browserify, самую простую библиотеку для single-app — angular.js, самый тупой бэкэнд — node.js, самую тупую нарезалку спрайтов — shoebox, самый простой в использовании 3d-редактор — blender, самый тупой редактор многоугольников — physicseditor, самый лучший скелетный 2d-animator spine. Инфраструктурых вопросов у нас не возникало. Мы вообще технически не выделялись.
Был составлен халявный план:
0) поезд будет проезжать целый экран, и останавливаться на станции, пока генерируется следующий экран.
1) надо чтобы был путь с фиксированной кривизной на поворотах, который можно удлинять и у которого можно получать координаты шпал.
2) путь надо уметь проверять на самопересечения. Используем простую сетку, храним в каждой клетке кто её пересекает. Мельче сетка — больше вероятность что пути начнут пересекаться.
3) путь должен заканчиваться где-то справа экрана, чтобы его можно было продолжать при генерации следующего экрана.
4) должен быть тупой алгоритм который перебирает такие пути и выбирает покрасивее и подлиннее.
5) результат можно заполнить разными объектами (деревьями и камушками).
6) поезд можно зарендерить в 32 кадра. Выглядит это не очень, но «доворот» спрайта до нужного угла прямо в игре всё решает. Это удалось сделать довольно быстро, соотношение результат/время было офигенным, на демку можно было медитировать:
первая демка
Работа сильно опережала график, и генерацию пути можно было бы всё так и оставить, но кто-то в середине цепочки решил поиграть в серьёзного заказчика, и заявил, что нужен другой вариант. С плавным скроллингом. Испорченный телефон добавил что-то про стрелки и дополнительные пути, и это всё привело к трём неделям дебага, оптимизаций и переносу тормозного генератора на вебворкер, ибо он неизбежно влиял на FPS.
вторая демка
Только представьте что получающийся при генерации граф дорог частями надо прогонять через сериализацию, чтобы передать от вебворкера на страницу. Реальный кошмар, очень легко забыть какие-нибудь важные параметры. При этом надо следить и обрубать уже пройденные экраны, чтобы не мусорить память.
От одного из промежуточных результатов всех в прямом смысле тошнило: на свой страх...
Из сложностей можно особо отметить случай, когда после уже сгенерированного экрана не удавалось найти для него продолжения, потому что какая-то рельса упиралась в соседнюю станцию, или в край экрана. Если сгенерировать новый экран за 30 проходов не удаётся, то происходит откат на 1 экран назад. Иногда это заметно графически, прямо на ходу поезда меняется будующий путь.
Ну и процесс слияния и раздвоения путей тоже добавил мороки. Мы нередко начинали ходить по кругу:
Переключение стрелок в итоге зарезали, и это нас всех очень расстроило.
Равно как и товарища, похожего на героя из GTA. Наверху решили что он слишком «быдловат»:
Дальше пошли препятствия (в прямом смысле, их надо было делать согласно ТЗ). Вот тут мы отомстили по полной программе и показали то как мы можем чинить препятствия: наш «арт-директор» ушёл в августовский отпуск и из-за длительной переделки генератора так и не успел согласовать многие моменты с заказчиком. Я регулярно подходил к компу, перечитывал пункт про препятствия, до меня не доходило что с этим делать, и в итоге тратил время на оптимизацию игры (разные 2d/webgl варианты, запихивание спрайтов в атлас), вместо того чтобы разобраться с художниками и уточнить сценарий. Недовольство по поводу отсутствия существенных апдейтов переходило по цепочке, начиная с главного заказчика, и останавливалось на kanobu.ru, которые терпели, но вели себя с нами совершенно корректно. За неделю до сдачи этот тупик совместными усилиями разрешился, был составлен подробный сценарий, нарисованы идиоты и другие элементы препятствий. В намеченный договором день всё работало как надо.
Серверная часть довольно простая, она умеет записывать результаты приходящие по ajax-запросам. Проблем с ней не было, всё было сделано в последний день, и даже хватило времени с защитой от самых очевидных хаков. Думаю, те кто сломают систему так что я это не замечу, достойны призов.
Уже после запуска проекта выяснилось что на многих устройствах генератор слишком тормозит и не успевает построить путь спереди. Выход оказался тривиальным — поменяли json-формат карты на бинарный и сгенерировали на серверной стороне 100 тысяч экранов. Это стало возможно поскольку игра разделена на node.js модули и собирается с помощью browserify. Игрок начинает на случайном экране в первой половине, и до того момента, как кешированные экраны кончатся и придётся вместо подгрузки экранов с сервера задействовать генератор пройдет очень много времени :)
Этот подход дал неожиданный эффект: на «дальнем востоке» появились очень кривые дороги.
Из-за того, что в бинарке мы писали float а не double, на экранах с огромной x-координатой (десятки миллионов) стало не хватать точности координат, рельсы стыковались с ошибками в 1-2 пикселя. Да, Дальние Земли начались у нас гораздо раньше чем в майнкрафте. Та же ошибка стала проявляться при прорисовке рельс
Кривизну дальневосточных рельс исправил переход от глобальных координат к локальным относительно экрана.
Чтобы начинать с любого места, вся дорога делится по десяткам экранов, и хранится в разных файлах. При этом конечные сегменты рельсы надо дублировать и в предыдущем и в следующем файле, иначе не получится начать езду не с первого файла. И вот эту дублированную рельсу мы забывали удалять при загрузке очередного десятки экранов, а поскольку координаты рельс расчитывались несколько извратно чтобы не заехать в дальний восток, результат выглядел просто шикарно:
Реки и мосты — одна из самых извратных частей генератора. Дело в том, что река протекает сразу через несколько экранов и сильно влияет этим на возможность продолжения пути нашим случайным генератором. Достаточно часто экран с началом реки попадает под откат, реки просто не выдерживают эволюционного отбора. Тщательный подбор констант и следюущий трюк решили эту проблему: когда какая-то рельса пересекает речку, генератор старается её удлиннить и поставить под неё мост. В процессе перебора пути, если рельсу удаляют, то мост надо удалить вместе с ней. Правда, в некоторых случаях удалялся только мост. Так появился распилочный баг.
Перспективы у игры отличные: на основе получившегося генератора можно сделать много новых проектов. Можно добавить приколов и реализма, которого все так жаждут (если поезд не задавит идиота, то хоть охрана выйдет и отомстит).
Можно расписать о том что мы сделали инновационный искусственный интеллект, который генерирует пути для РЖД, выиграть какой-нибудь приз в стартаперских тусовках, найти инвестора…
P.S. Специально для хаброжилов с сегодняшнего дня где-то около 2015-ой отметки стоит небольшая пасхалка.
P.P.S Статья случайно была опубликована изначально на ГТ, давно не постились ;)