5 незнакомых людей за три месяца запилили ремейк культовой игры 90-х «Танчики». В этой статье они расскажут: Как построить командную работу, чтобы всё успеть и не поубивать друг друга? Как создать движок игры, который потянул бы даже старый iPhone? Как втащить в проект самописный Server Side Rendering так, чтобы игра и окружающее его React-приложение не сыпали ошибками? Как сделать браузерную игру, работающую даже без интернета? Эти и другие проблемы ребята решили в рамках курса «Мидл фронтенд-разработчик» в Яндекс Практикуме. Подготовка состояла из двух модулей по три месяца: сначала был индивидуальный трек, а затем — командный, в рамках которого надо было впятером разработать совместный проект. «В первой части курса мы делали индивидуальные проекты, и мне запомнился Станислав, который в чате задавал интересные глубокие вопросы, больше интересовался архитектурой, а не какими-то мелкими вопросами. Мы начали переписываться и обсуждать, как лучше делать задания. К тому моменту, когда понадобилось формировать команду, я уже знал, что хочу работать со Станиславом.  Получилось забавно, что Станислав первым написал мне с идеей объединиться в команду в тот момент, когда я сам собирался написать ему :) Оказалось, что под капотом предложения Станислава уже был Дмитрий Грамотеев, ещё один активный участник чата. Так мы собрались втроём. Дальше обратили внимание на кандидатуру Алексея. Я посмотрел его проект с первого модуля, и он показался мне очень необычным, даже непонятным. У нас троих были относительно похожие проекты, а код Алексея как будто был с другой планеты :) Поначалу это смущало, но в итоге я рад, что мы стали работать вместе. Алексей внёс мощный вклад в разработку игры, по сути реализовав движок. Практика показала, что получилось очень хорошо. Думаю, если бы я сам писал движок игры, он получился бы не таким удачным. Потом куратор добавил к нам Андрея. Скажу честно, сначала его уровень показался ниже, чем у остальных. Но за время нашей общей работы он очень вырос как специалист, и я доволен, что работал вместе с ним». Дмитрий Назимов, тимлид команды При знакомстве выяснилось, что у всех ребят уже имеется значительный опыт кодинга, но знаниям не хватало системности, кому-то, например, нужно было поглубже изучить CI/CD, кому-то подтянуть TypeScript или лучше понять командные процессы.  Идею выбирали по нескольким критериям: интерес для команды; сложность реализации; используемые технологии; возможность усовершенствовать или масштабировать игру; потенциал для портфолио. «Мы с командой поначалу не могли определиться с вариантом. Я предложил градостроительную стратегию типа Anno Online, но возникли сомнения насчёт того, удастся ли сделать достойный продукт в отведённые три месяца. Кинул клич среди близких и друзей, в какую браузерную игру им хотелось бы поиграть. Рейтинг сразу же возглавили «Танчики» — моя жена оказалась фанаткой этой аркады. Команда с ходу одобрила эту идею. По прикидкам сложность реализации была в самый раз на пятерых с учётом целого пула других задач по проекту». Алексей Савенков, поддался супруге «Все вспомнили эту игру нашего детства и долгие часы, проведённые в ней. Захотелось самостоятельно реализовать что-то подобное. Выбрать интересный проект — очень важно. В противном случае энтузиазма было бы меньше. Здесь сработала ностальгия, мы приняли это решение единогласно. Другие варианты были, но их сейчас даже трудно вспомнить». Станислав Сальников, поддался ностальгии Команда приняла и технический вызов: аркады весьма непросты в проектировании и разработке, т.к. надо обеспечить взаимодействие множества игровых объектов в реальном времени. А в «Танчиках» вообще оказался полный набор соответствующих фишек: нужно было проработать физику движений бронемашин и снарядов на разных скоростях, расчёт их столкновений, сделать разрушаемый ландшафт, отрисовывать анимации, проигрывать звуковые эффекты, запрограммировать ИИ врагов и много чего ещё. Широкий фронт работ: от алгоритмов до самых разных Web API со спецификой отдельных браузеров и устройств (привет, Safari на старом айфоне!). «Поскольку у меня уже был опыт руководителя, то тимлидом быть вызвался я. Т.к. свою инициативу я подкрепил рядом предложений по организации работы — никто не был против».Дмитрий Назимов, уверен, что никто не был против Ребята собрались из разных часовых поясов и с разной занятостью. Нужно было определить время, в которое всем было бы удобно проводить дейлики — решили, что это будет три раза в неделю в 21:00 по московскому времени. Созвоны редко удавалось уложить менее чем в полтора часа — время пролетало незаметно, так как всем было очень интересно. «Работу над проектом можно обсуждать часами, что мы и делали на созвонах три раза в неделю :) Много внимания уделяли планированию». Станислав Сальников, научился называть планированием болтовню на созвонах Поскольку команда была небольшая, то внедрять полноценный Scrum со всеми церемониями смысла не имело, а качество кода обеспечивали такие вещи, как: Typescript со строгими настройками; линтеры (eslint, stylelint); форматтер (prettier); прекоммит-хук (lefthook) для единого стиля сообщений коммитов; документация, в том числе единый code style, на который можно было сослаться во время code-review; перекрёстное code-review. В команде было правило: merge ветки возможен, только если Pull Request набрал три и более аппрува. Иногда процесс ревью был простым и приятным, а иногда доходило до сложных комментариев со схемами. Но никогда не обходилось без обсуждений; тесты (jest, testing-library): практически каждый React-компонент и модуль игрового движка имеет покрытие. И разумеется, всё, что можно было автоматически запускать из вышеперечисленного, — автоматически запускалось на этапе CI.  Задачи распределяли по интересам — каждый брал на себя то, чем хотел заняться. «Мне понравилась задача по бэкенду для форума. Довелось изучить PostgreSQL: использовать базу данных и запросы, писать endpoint’ы, то есть то, что я не делал раньше». Дмитрий Грамотеев, изучил основы бэкенда на курсе по фронтенду «Если какая-то задача простаивала, я её подхватывал. Как тимлид, принимал спорные моменты на себя, чтобы команда могла сосредоточиться на разработке. Организационные задачи поначалу занимали большую часть времени, так что даже не успевал писать код. Нужно было наладить единый кодстайл, чтобы мы говорили на одном языке».Дмитрий Назимов, развивал бурную деятельность вместо написания кода Итогом удачно построенных командных процессов можно считать два обстоятельства: Код лился рекой: за три месяца закрыли 104 Pull Request и написали 21 тысячу строк кода. Ребята тратили на проект всё свободное время. «Я был так замотивирован, что занимался проектом в среднем 8 часов в день». Дмитрий Грамотеев, проникся проектом «Приходилось жертвовать личными планами, потому что времени было очень мало. Но так хотелось проявить себя, что это не пугало. Сам проект настолько интересный, что процесс приносил удовольствие. Мы изучили и внедрили много непростых технологий». Алексей Савенков, страдал и получал удовольствие Команде пришлось попотеть над архитектурой игрового движка, который был реализован без использования сторонних библиотек на чистом TypeScript. Главная проблема — избежать спагетти-кода, который бы причинял боль при масштабировании и внедрении новых модулей. На помощь пришли принципы SOLID. Весь код разбит на классы-сервисы (рисовальщик анимаций, проигрыватель аудио, определитель местоположения сущностей на карте и т.д.) и классы-объекты (танки, снаряды и т.д.). Последние не обращаются к первым напрямую, а генерируют события, которые отслеживаются в сервисах, — за счёт этого снижается связность кода и соблюдается принцип единственной ответственности. Например, когда танк делает выстрел — в его классе не вызываются напрямую методы звукового сервиса, чтобы был воспроизведён соответствующий звук, а вместо этого эмитится событие выстрела, которое подхватывается в нужных местах. Подробнее со спецификой работы движка можно ознакомиться в документации проекта. Интересный кейс получился с отрисовкой анимаций. Изначально игровой движок обновлял содержимое экрана через циклический вызов соответствующих функций с помощью setInterval() каждые 20 мс. Если в Хроме всё двигалось быстро, то в Фаерфоксе выполнение кода занимало больше времени, что выглядело так, как будто игра подтормаживала. Эту проблему удалось решить, заменив setInterval() на другой Web API — requestAnimationFrame(), который во всех браузерах работает предсказуемо и консистентно с обновлением кадра на экране.  Однако у некоторых устройств в разных режимах частота может отличаться от стандартных 60 кадров в секунду. Под 120-герцовые яблочные экраны пришлось сделать тротлинг, чтобы requestAnimationFrame() не гонял анимации в два раза быстрее. А вот задача победить тормоза при переводе мобильных устройств в энергосберегающий режим, когда герцовка падает до 30 кадров, осталась в бэклоге. Ещё один интересный лайфхак, чтобы всё работало быстро, даже на старых айфонах: игровой движок использует несколько наложенных друг на друга Canvas’ов. Это позволяет избежать траты ресурсов на лишние перерисовки игровых объектов, когда, например, танк заезжает под дерево или снаряд пролетает над водой. На определённом слое затирается только один отдельно взятый элемент, который потом появляется на новом месте. Дело вот в чём: когда что-то попадает на Canvas, оно превращается в набор пикселей, который смешивается с уже присутствующим там содержимым. Если нужно что-то удалить, то только вместе с остальным. Нельзя обойти стороной работу с памятью. В JavaScript есть сборщик мусора, который автоматически подчищает за разработчиками отработанные объекты. Однако он не всесилен, и при большом количестве порождаемых объектов могут образовываться утечки памяти. Не обошла эта участь и «Танчики». При большом количестве пройденных уровней расходование памяти росло в геометрической прогрессии. Пришлось провести поиск memory leaks и устранить их. С чем повезло, так это с реализацией ИИ врагов. Тут не пришлось прикручивать нейросеть или внедрять алгоритм волновой трассировки. Повороты и стрельба варяжских танков банально сделаны через рандом с setTimeout(). Даже с таким тривиальным решением игра получилась трудной для прохождения, пришлось потом сделать выбор более лёгкого уровня сложности. Встретилось много других таких задач, где не очевидно, как всё будет работать. Команда собралась сильная, поэтому участники помогали друг другу, подкидывали идеи. В итоге практически все сложности удалось решить. Основной вклад в разработку движка внес Алексей Савенков. Не обошлось без жертв. Вот как он это описывает: «Я взял на себя разработку прототипа игрового движка, поскольку идея с игрой была предложена мной. Было очень интересно попробовать собрать воедино этот нетривиальный технический пазл. Ну и в целом у меня уже был опыт с React и другими вещами, которые выкатывались на проекте, а тут принципиально новая для меня тема. Параллельно с курсом я работал, иногда по 12 часов в день. Для проекта приходилось концентрированно кодить по вечерам и в выходные. Всё свободное время было забито разработкой и созвонами с командой. Когда чувствуешь драйв, делая крутую вещь, всё нипочём. Жену попросил не отвлекать меня эти месяцы, если она хочет, чтобы игра вышла в свет :)». «Эта штука прилично попортила нам нервы. В Практикуме задача описывалась как лёгкая, быстрая и весёлая. Но по сути, чтобы внедрить SSR, нужно написать полноценный фреймворк. Наверное, стоит указывать это сразу. Тогда у разработчиков будет возможность сразу заложить что-то более сложное». Дмитрий Назимов, не оценил веселье Пока все адепты React релаксировали на Next.js с его богатым инструментарием, включая пререндеринг страниц, команда пошла по экстремальному пути и внедрила на проекте SSR собственного разлива. Многие уже обкатанные реализации этой технологии базировались на старом добром Webpack, тогда как на «Танчиках», развёрнутых на более шустром Vite с монорепой на Lerna (с двумя пакетами — для бэкенда и для фронтенда), всё получилось куда интереснее и изощрённее. Много сюрпризов возникло с использованием React Router, который к тому времени только-только получил большое обновление. Один из трудно отлавливаемых моментов — редиректы между страницами при серверном рендеринге. Некоторые баги с SSR удавалось закрывать вливанием последующих хотфиксов от разработчиков React Router буквально день в день. В какой-то момент даже пришлось откатываться до проверенной временем пятой версии, однако это породило новый круг проблем (многие вещи, заточенные под RR6, пришлось бы переписывать), и решено было вернуться обратно. Особенно потрепала нервы задача с проверкой авторизации пользователя, решение которой заняло больше двух недель. Из-за использования стороннего API для реализации этого функционала (эндпоинты Яндекса, в т.ч. для OAuth) в браузере возникали ошибки гидрации, когда код, сгенерированный на сервере, не совпадал с тем, что получилось на клиенте (в одном месте пользователь как бы авторизован, а в другом — нет). Проблему удалось решить проксированием запросов через собственный бэкенд с применением библиотеки http-proxy-middleware, чтобы отлавливать нужные куки и подменять в них домен из-за политик CORS (Cross-Origin Resource Sharing).  Далее команду ждал еще один неприятный сюрприз, выявленный во время ревью, и связан он был с безопасностью. Из-за специфики работы Express-сервера на NodeJS авторизационный токен одного пользователя мог быть передан другому человеку. Выяснилось, что используемый для сетевых запросов обработчик Axios без использования должных ухищрений хранит настройки в окружении NodeJS не отдельно под каждый запрос, из-за чего обратившийся к серверу клиент может быть идентифицирован сервером как предыдущий пользователь. «Мы медленно продвигались вперед с SSR, но на каждом шагу нас ждали как небольшие, так и достаточно крупные проблемы, которые в конечном итоге были решены. Иногда хотелось бросить эту задачу, ведь можно было сделать “костыль”, но азарт взял верх и в итоге все было сделано. На наш взгляд, достаточно качественно». Станислав Сальников, отвечал за внедрение SSR и испытал 7 стадий принятия SSR было нелегко сдружить с другой примечательной штукой на проекте, которая редко где встречается, — Service Worker. Благодаря ему приложение кешируется в памяти устройства при первом использовании (через caches Web API), и в «Танчики» можно играть даже при отключённом интернете.  Разумеется, всплыла проблема с тем, что сохранялись страницы с неактуальным предзагруженным состоянием (preloaded_state) у Redux, сгенерированным SSR, и пришлось сделать замысловатую логику, определяющую отдачу статики из кеша или сети в зависимости от типа данных и их доступности. Но и тут не обошлось без неожиданностей: например, некоторые браузеры не помечали mp3-файлы как аудиоконтент, хотя jpg и png шли как изображения.  В итоге все нюансы были учтены, Service Worker сдобрили директивой webmanifest, и получилось Progressive Web Application (PWA): при открытии с телефона/планшета игру можно сохранить на домашний экран и потом запускать, не заходя в браузер — как будто она установлена из магазина приложений. Ничто не расскажет про проект лучше, чем видео с финальной защиты. Посмотреть его можно здесь.  Поиграть в «Танчики» без регистрации и СМС можно здесь.А с кодом проекта и документацией можно ознакомиться на GitHub. Если вкратце, то ребята реализовали за три месяца: Полный клон оригинальной игры Battle City — все 35 уровней. Мобильную версию игры. Два вида дизайна игры — классический ретро 90-х и более современный.  Возможность играть офлайн благодаря технологии Service Worker. React-приложение вокруг игры, включающее в себя: Server Side Rendering. Регистрацию и авторизацию, в том числе посредством oAuth. Полноценный форум. Возможность смены темы оформления сайта. Рейтинг игроков с разными сортировками, фильтрами и графиком. Спойлер: да!  Если в начале интервью показать интересный личный проект, то это задаёт тон всему собеседованию. Бывает, что всё время интервью занимает обсуждение деталей реализации проекта, и времени на теоретические вопросы или лайвкодинг уже не остается — максимально комфортный вариант собеседования для кандидата :)  Дмитрий Грамотеев сейчас работает в Промсвязьбанке. Устроиться сюда ему помогла в том числе работа над проектом. «Благодаря проекту мы научились взаимодействовать в команде. Приходилось много общаться, вникать в суть проблемы. Старались помочь друг другу, обсудить, у кого какие задачи, чтобы двигаться максимально слаженно. Это помогло мне на собеседованиях достаточно глубоко обсуждать предложенные вопросы и подмечать нюансы проекта, а во время работы сразу же пригодился навык проработки задач: обсуждение бизнес-требований и декомпозиция.  В коммерческой разработке я открываю код любой сложности и могу понять, что в нём происходит, просто потратив нужное количество времени. До Практикума рассуждал по-другому. Если не справлялся с какой-то задачей — искал того, кто может её сделать. Или отказывался от определённых инструментов, когда их освоение казалось слишком сложным. Сейчас понимаю, что возьмусь за любую задачу или инструмент: хорошо разберусь, внимательно посмотрю — и вопросов не останется» Дмитрий Грамотеев, преисполнился и теперь понимает любой код Станислав тоже устроился фронтенд-разработчиком. Проект помог найти новую работу, потому что научил хорошо структурировать процессы с точки зрения проектирования системы. «У меня всегда были сложности с оценкой, какая задача занимает сколько времени. Теперь умею планировать наперёд, разбивать проект на части. На курсе не глубоко проработан вопрос построения архитектуры, но мы получили нужный опыт во время практической работы. Стало ясно, что нужно заранее потратить больше времени на проектирование, потом это окупится. Когда проект разрастётся и его понадобится дорабатывать, возникнет меньше сложностей». Станислав Сальников, теперь тратит время на планирование, а не написание кода Дмитрий Назимов удалённо работает фронтенд-разработчиком в компании, которая базируется на Кипре, в одном из крупнейших видеосервисов в мире.   «На курсе, как бы странно это ни звучало, — я научился учиться. Для компании, куда я устроился, нужно было выполнить тестовое задание на редком фреймворке Svelte, с которым я никогда не работал. Для меня это не стало проблемой, т.к. за время курса я выучил много новых технологий, и после этого разобраться со Svelte было совсем не сложно.  Должен отметить, что на собеседованиях я показывал наш проект «Танчики», и это всегда производило впечатление». Дмитрий Назимов, испытывает непреодолимое желание показать всем свои танки Алексей и Андрей тоже без проблем нашли новую работу с интересными задачами. Игра «Танчики» стала козырем в поиске.   «Многие работодатели смотрят на профиль GitHub соискателя — как он оформлен, какие там есть пет-проекты. Наши «Танчики» выделяются на фоне остального — проект сложный, многогранный, с код-ревью и прочим. Он действительно производит впечатление на собеседованиях. Некоторые интервьюеры не ленились обстоятельно покопаться в нашем коде перед звонком и с интересом спрашивали, почему у нас что-то реализовано так, а не иначе. Это очень вдохновляет, когда ты можешь рассказать что-то новое для инженеров из крупных компаний, а не просто отвечать на дежурные вопросы». Алексей Савенков, на собеседованиях дает мастер-классы инженерам из крупных компаний «Я работаю в организации «Открытые решения». Это аутстафф-компания со своими внутренними проектами. Попал туда почти сразу — для меня это было второе собеседование. Возможно, сыграло на руку то, что мой наставник в этой компании тоже проходил курс «Мидл-разработчик». Думаю, это расположило его ко мне». Андрей, попал на работу по блату :) Пройдя путь командной работы над игрой, ребята составили рекомендации для тех, кто сейчас работает над проектами для портфолио: Подобрать хорошую команду. Не оставляйте это дело на волю случая. Нужно искать сокомандников, с которыми вы на одной волне, а в процессе работы помнить, что это осознанный выбор. Выбрать интересную, цепляющую тему для проекта. Настроиться на тяжёлую работу. Будьте готовы выложиться на все 100%. Сохранять оптимизм и спокойствие. В программировании любые трудности решаются усердием и общением с другими разработчиками.  Презентовать проект на собеседованиях. Когда на собеседовании просят рассказать о себе, вспомните о проекте, которым гордитесь. Дайте ссылку, и, возможно, интервьюера заинтересует ваш опыт.  Возможно, кому-то эти советы покажутся банальными, но именно они помогли закончить проект успешно, а не так, как на скриншоте ниже :)   Контент-продюсер