Всем привет!

Время от времени меня сильно тянет на разработку игр и я даже публиковал на любимом хабре пару статей (здесь и тут) о том, что из этого получается. И вот в очередной раз, когда желание пересилило все остальное я почему-то решил, что могу сделать "простую" игру "длинные нарды" за несколько дней. Если вам интересно что из этого вышло, то усаживайтесь поудобнее, сегодня я расскажу о том, как все начиналось.

Длинные нарды - игра в которую я играю время от времени еще с самого детства, разнообразие комбинаций, случайность выпадения костей и динамически меняющаяся тактика противника делают эту игру для меня бесконечно интересной, да и вообще люди играют в нарды уже более 3000 лет.

После того как игра была выбрана, я решил составить список того, что я хочу видеть в "своих" нардах. Мне точно хочется играть с друзьями и я люблю поговорить во время игры на общие темы, по этому это точно будет мультиплеер с голосовой связью. Подумав о том, что не каждый раз получится найти "свободного" для игры друга решил, что точно необходим режим игры с ботом. На этом и был определен минимальный функционал приложения.

В своей профессиональной деятельности я занимаюсь разработкой архитектур различных приложений, управлением процессом разработки и собственно самим программированием, как на стороне клиента, так и на сервере. В силу сложившейся традиции я использую один язык программирования и для клиента и для сервера, и если раньше это был JavaScript, то последние несколько лет я использую TypeScript. В мой "стандартный набор" входят базы PostgreSQL, Redis, файловое хранилище S3 в связке с CDN и Node.js, как среда исполнения кода на стороне сервера. Как показала практика, на этом наборе удается эффективно решать задачи в подавляющем большинстве случаев с которыми я сталкиваюсь. Со стеком используемых технологий определились, идем дальше.

С чего обычно начинается любое приложение, пользователи которого должны быть идентифицируемыми - верно, с авторизации. В настоящий момент я считаю, что наиболее безопасный способ, из известных мне, это использование связки accessToken (JWT) и refreshToken, без явного хранения обоих ключей на стороне клиента. В моем случае это достижимо за счет использования механизма хранения refreshToken в http-only cookie, прочитать которую со стороны браузера не представляется возможным в связи с политикой безопасности, а следовательно никакой внедренный на страницу код не получит доступа к ключу. Для того чтобы можно было использовать один и тот же аккаунт на различных устройствах или браузерах refreshToken используется совместно с идентификатором устройства, так называемым "fingerprint". Данный механизм позволяет привязывать сессии к устройствам и иметь возможность "разлогинить" то или иное устройство удалив сессию и соответственно ее refreshToken. В сети можно найти предостаточно информации о том, как это все реализуется на стороне клиента и сервера, по этому я опущу подробности этого процесса, однако если тема будет интересна, можно пообщаться в комментариях или даже написать отдельную статью. В итоге с тем какая будет авторизация закончили, идем дальше.

Учитывая необходимость двухсторонней связи между клиентом и сервером в режиме онлайн игры необходимо определиться с тем, как мы будем передавать информацию на сервер и с него на клиентов. Раньше я был ярым сторонником HTTP транспорта, который в подавляющем большинстве случаев выигрывает по производительности и простоте использования, однако в нашем случае у нас игра, в которой информация поступает к клиентам в событийном режиме, да и вообще не хочется постоянно дергать сервер с запросами типа кто из моих друзей сейчас онлайн, да и вообще запрашивать у сервера периодически "жив" ли мой оппонент по игре, а то что-то долго он не ходит. Конечно можно было бы реализовать отправку всех данных на сервер с использованием HTTP, а данные на клиент через подключение к server-sent events (SSE), тем более что сейчас у нас есть HTTP/2 который значительно экономит время каждого запроса на сервер, используя существующее соединение многократно, что позволяет исключить "длительные" процессы рукопожатия и проверки сертификатов, обмен ключами шифрования и прочие невидимые процессы, которые делают нашу жизнь безопаснее в сети. Однако, несмотря на все это, последние несколько лет я активно использую в различных проектах двустороннюю связь на базе WebSocket, открыв в свое время замечательную библиотеку uWebSockets.js, которая на мой взгляд является эталоном того, как можно использовать сокеты в веб-разработке. Мало того, что она очень эффективно использует ресурсы сервера, в ее арсенал входит обработка и HTTP запросов, что в совокупности позволяет собрать на ее базе универсальный back-end для практически любого проекта, от самого простого, до большого и сложного. К тому же WebSocket, в отличии от SSE, позволяет нам передавать бинарные данные, что повышает эффективность за счет заметного снижения объема передаваемого трафика. Можно конечно сетовать на то, что во многих корпоративных сетях WebSocket трафик блокируется, а SSE при этом работает, но мы делаем игру и это не для работы. В общем с передачей данных тоже определились, но отдельно хочется еще обратить внимание на то, как я подкрепляю типизацию данных на транспортном уровне, тем более что TypeScript у нас используется с обоих сторон. Можно долго дискутировать о том, что лучше использовать для сериализации передаваемых данных - бинарный предкомпилируемый protobuf или например просто json, тут у каждого свои предпочтения, но для меня этот выбор был сделан еще несколько лет назад, когда я написал для одного своего проекта библиотеку, позволяющую описывать любые типизированные структуры данных и эффективно сериализовывать/десериализовывать их в бинарный вид без предварительной компиляции. Эта библиотека используется и на клиенте и на сервере, протокол описывается на стороне сервера и при подключении клиент получает все необходимые схемы данных, которыми может впоследствии оперировать с сервером. Данный механизм позволяет забыть об этапе валидации данных как на клиенте, так и на сервере, мы просто не сможем собрать пакет для отправки на сервер или клиент с неверными типами данных, так же как и декодировать его. Опять-таки, если тема будет интересной то можно собрать материал на отдельную историю по этой библиотеке. Отмечайте это в комментариях.

С используемыми технологиями и стеком на стороне сервера вроде разобрались, пришла пора подумать о клиентской части приложения. Так как одно из условий работа на всех платформах, а писать нативные приложения под каждую из них чрезмерно долго, тут безальтернативно был выбран старый знакомый TypeScript, шаблоны на HTML и дизайн на CSS. Я сразу скажу, что бесконечно благодарен людям которые делают Next.js, это реально сумасшедший по своим возможностям фреймворк для создания front-end, мы с командой много раз с его помощью собирали как весьма сложные и большие проекты, так и простые одностраничники - универсально, быстро, красиво и куча всего "из коробки". Есть конечно вещи которые очень сложно решаются при использовании этого инструмента, но в подавляющем большинстве случаев все отлично. Это отступление я сделал потому что тут стоит отметить тот факт, что я люблю порой сделать что-то не так, как сделал бы в случае реализации коммерческого проекта, ведь где еще как не в своей песочнице можно порезвиться как тебе вздумается. Вот и в этот раз я решил написать свой небольшой движок для управления всем UI и игровым процессом, использовать, так сказать, нестандартный подход, но в соответствии со стандартами W3C.

Для разработки фронта я решил использовать esbuild, он быстро компилирует TypeScript, имеет встроенный наблюдатель (watcher) изменений исходного кода и web server для раздачи скомпилированного JavaScript кода, строит source maps и имеет кучу других интересных возможностей. Я также использую его для сборки и минификации production версии приложения. Кстати прошлый абзац я не зря закончил словами о W3C, за последние несколько лет эта организация сделала очень много для развития технологий и индустрии в целом, особенно это заметно по сравнению с тем, что было на заре веб-разработки, а я отлично помню начало 2000-х, когда не было не только fetch, но и вообще о подгрузке данных с сервера можно было думать только в формате использования iframe. Так вот, весь движок игры построен вокруг простых функций доступных во всех современных браузерах. В настоящее время в проекте используется библиотека howler.js для загрузки и воспроизведения звука, все остальное это чистой воды эксперимент по созданию приложения на TypeScript без использования привычного Redux, i18n и прочих прелестей современной веб разработки.

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

Посмотреть текущее состояние проекта и поиграть можно перейдя по ссылке.

Комментарии (0)