Создать JS-игру с мультиплеером за 24 часа и выжить
Как всего за сутки мы с моими коллегами (шестью фронтендерами и одним бэкендером) создали настоящую мультиплеерную игру на JavaScript.
За 24 часа можно успеть очень много. Сегодня я решил рассказать, как всего за сутки мы с моими коллегами (шестью фронтендерами и одним бэкендером) создали настоящую мультиплеерную игру на JavaScript. Поехали!
Вадим Силантьев
Frontend-разработчик IRLIX
Собрались похакатонить…
Совсем недавно в компании IRLIX, где я работаю frontend-разработчиком, прошло знакомое для многих мероприятие — хакатон. На мой взгляд, это очень крутой формат, который независимо от масштаба позволяет изучить новые грани разработки. В том числе для frontend направления. Главное — объективно оценивать, на что вы способны и найти баланс между полётом фантазии и реальностью при выполнении задач.
Целью данного хакатона было создать ровно за 24 часа игру с нуля, используя только JavaScript, Node.js, Soket.io. и два ящика энергетиков.
Я очень легко соглашаюсь на разную движуху. И мне совершенно не обязательно знать, что из этого выйдет. Уже в процессе меня одергивает разум: «Чувак, зачем ты в это вписался?». Но раз пути назад нет — погнали. Этот раз тоже не стал исключением. Не успел я оглянуться — и вот уже полностью погружен в процесс разработки игры.
Почему JavaScript?
Сегодня игрушку можно сделать практически на любом языке программирования, но мы решили остановиться на JS. Выбор стека был обусловлен в основном нашими знаниями по определенным технологиям. Мы решили использовать библиотеку Soket.io, так как она поддерживает постоянную связь клиента с сервером и имеет возможность создавать кастомные эвенты между ними, что необходимо при построении мультиплеерной игры. На мой взгляд, это было идеальным решением в рамках времени, которым мы располагали.
Процесс разработки или сутки, которые я не забуду никогда
Мне казалось нереально сложным иметь только общее представление о том, что должно получиться в итоге, и ни малейшего представления о том, как это сделать. Спустя сутки я понял, что в данном случае главная фича для крутого результата — это именно свобода действий. Ведь тебя практически ничего не ограничивает (ну, кроме времени, само собой) — фантазируй, придумывай, воплощай.
Начали мы с того, что командой накидали простейший код связи клиента с сервером, визуализировали себя на экранах в виде красных шаров и безумно радовались этому. На первичную логику ушел примерно час. Но далее мы столкнулись с интересной задачей игрового цикла, мы разделили отрисовку данных на клиенте и игровой цикл на сервере.
const gameLoop = (game, io) => < io.sockets.emit('state', game); >; setInterval(() => < if (game.players && io) < gameLoop(game, io); >>, engineTimeConnect);
Серверный движок
socket.on('state', (game) => < GAME = game; PLAYER = game.players[socket.id]; dt = 0; >); const drawLoop = () => < if (GAME) < now = performance.now(); dt = dt + (now - last); const dtPercent = dt / engineTimeConnect; last = now; context.clearRect(0, 0, WINDOW_WIDTH, WINDOW_HIGHT); // Рисуем игроков for (const id in GAME.players) < const player = GAME.players[id]; if (player.state !== 'DEATH') < drawPlayer(context, player, dtPercent); >> > window.requestAnimationFrame(drawLoop); >; window.requestAnimationFrame(drawLoop);
Клиентский движок
Движение игроков было реализовано через изменение стейта игрока на сервере. Мы слушали состояние нажатых кнопок на клиенте, изменяли объект и отправляли его на сервер, а он, в свою очередь, менял позицию игрока.
const movement = < up: false, down: false, left: false, right: false, >document.addEventListener("keydown", (event) => < switch (event.keyCode) < case 65: // A movement.left = true; socket.emit("movement", movement); break; case 87: // W movement.up = true; socket.emit("movement", movement); break; case 68: // D movement.right = true; socket.emit("movement", movement); break; case 83: // S movement.down = true; socket.emit("movement", movement); break; >>); document.addEventListener("keyup", (event) => < switch (event.keyCode) < case 65: // A movement.left = false; socket.emit("movement", movement); break; case 87: // W movement.up = false; socket.emit("movement", movement); break; case 68: // D movement.right = false; socket.emit("movement", movement); break; case 83: // S movement.down = false; socket.emit("movement", movement); break; >>);
Клиентский код
const movement = < up: false, down: false, left: false, right: false, >; socket.on('movement', (move) => < movement.up = move.up; movement.down = move.down; movement.left = move.left; movement.right = move.right; >); setInterval(() => < if (movement.down||movement.left||movement.right||movement.up) < const player = players[socket.id] || <>; if (movement.left && player.positionX > 0) < player.moveTo('left'); >if (movement.up && player.positionY > 0) < player.moveTo('up'); >if (movement.right && player.positionX < map.size.x) < player.moveTo('right'); >if (movement.down && player.positionY < map.size.y) < player.moveTo('down'); >> else < const player = players[socket.id] || <>; player.moveTo && player.moveTo('stop'); > >, engineTimeConnect);
Серверный код
Это весь цикл перемещения игрока. Кроме того, для удобства в дальнейших кейсах мы решили задавать игроку состояние. Ниже пример метода перемещения игрока.
moveTo(direction)
Когда мы поняли, что движущиеся точки — это конечно круто, но недостаточно, решили пуститься «во все тяжкие» и делать мультиплеерный шутер. Первое, что мы сделали — проработали концепцию, отбросив все лишнее. После — упростили графику ( от того, что нарисовало богатое воображение в наших головах до реализуемого варианта). Затем перешли к тому, что создали персонажей и их способности к стрельбе.
Было много спорных моментов, обсуждений, далеко не всегда получалось прийти к компромиссу. И, конечно, были эпизоды, в которых спорить было не о чем, потому что мы просто не знали, как это воплотить.
Например, как грамотно доставить информацию на оба экрана, чтобы на каждом кадре отрисовки летевшая пуля находилась у обоих клиентов. Но выбранная архитектура решила проблему на корню: клиент просто рисовал данные приходящие с сервера и отправлял события об изменении данных, почти все расчеты происходили на серверном движке.
Еще пример: мы не знали, что делать с ФПС ( наша бравая лошадка-сервер вывозила не больше 4 человек, а после грустно ложилась помирать). Происходило это по нескольким причинам: чем больше клиентов, тем больше данных нужно отправлять, а данные отправлялись 60 раз в секунду, на 8 игроках игровой цикл замедлялся до 15 циклов в секунду. Однако для оптимизации процессов было еще слишком рано, готовой игровой модели еще не было, и мы продолжили разработку.
Мы круто упростили модель передачи данных для выстрелов с помощью линейной интерполяции и сделали, в целом, много всего интересного (в первую очередь, для улучшения игрового процесса). Вот первый этап оптимизации для нашего серверного движка.
// линейная интерполяция, piece = 0.0 -> 1.0 const lerp = (start, finish, piece) => < return start + (finish - start) * piece; >; const drawProjectile = (ctx, bullet, dt) =>
Отрисовка снарядов на клиенте
Также мы решили использовать пулинг для снарядов на сервере, хранили готовые инстансы снарядов в массиве и добавляли их при каждом выстреле, а после возвращали в пул для хранения. Это положительным образом влияло на нагрузку сервера.
Следующим этапом стала обработка столкновений. Первая наша попытка была просто ужасной: была куча блоков if/else, но все работало, однако глядя на такой код, хотелось плакать. Поэтому мы решили сделать все области столкновений в игре круговыми и обратиться к геометрии.
const getCollision = (obj1, obj2) => < const r = obj1.r + obj2.r; const dX = obj1.x - obj2.x; const dY = obj1.y - obj2.y; const xSq = dX * dX; const ySq = dY * dY; const dSq = xSq + ySq; const d = Math.sqrt(dSq); return r >d; >;
Реализация с помощью кода
Сделав лишь одну функцию для расчета столкновений между двумя объектами, мы решили все трудности с коллизией, но вычислений все равно было очень много: помимо снарядов у нас еще были стены на карте, и их тоже необходимо было обрабатывать.
Вот тут мы и столкнулись с задачей на оптимизацию. Мы решили считать столкновения игрока со стенами на клиенте. И это сильно облегчило работу серверному движку.
Самым эмоциональным для меня стал момент записи звуков для игры. За неимением других источников нам пришлось имитировать их самим. Мы бегали вокруг стола, стучали кружками и делали что-то еще. К тому моменту прошло уже много часов хакатона, мозг немного был затуманен, что в некоторых моментах даже сыграло нам на руку. Особенно смешным и немного нелепым получился звук попадания снаряда — представьте, как рыба пытается сказать букву «П».
А что в результате?
В нашем случае результатом стал мультиплеерный 2D шутер с видом сверху. Игроки сражались каждый сам за себя, количество патронов было ограничено, но на на карте постоянно появлялись случайные объекты в виде (дополнительных патронов, усиления урона, аптечки), эти объекты создавали точки интереса на карте и вынуждали игроков постоянно двигаться, чтобы выиграть бой, от этого игра становилась более динамичной и веселой. При поражении игрок выбывал с карты на некоторое время, а после опять мог вступить в бой. На следующий день после хакатона, мы рубились в эту игрой с ребятами из офиса, и, как оказалось, шутер получился веселым и залипательным, что нас очень сильно порадовало.
Простые игры на Canvas в браузере могут сделать ваше приложение более интерактивным. Например, простой игрой вы можете заменить скучные и долгие экраны загрузок. А написание полноценных браузерных игр на JS может стать отличной альтернативой разработке однотипных, скучных приложений. Вердикт: геймдеву на JS быть!
2D игра на чистом JavaScript
В этом пошаговом руководстве мы создадим простую игру MDN Breakout, написанную на чистом JavaScript и отрендеренную на HTML5 .
К каждому шагу прилагаются редактируемые live-примеры, с которыми можно поиграть, чтобы увидеть, как должна выглядеть игра на промежуточных этапах. Вы изучите основы использования элемента для реализации таких фундаментальных игровых механик, как рендеринг и перемещение изображений, обнаружение столкновений, механизмы управления, а также состояния выигрыша и проигрыша.
Для извлечения максимальной пользы из этой серии статей необходимо иметь средние (или хотя бы базовые) знания языка JavaScript. После прохождения этого урока вы сможете создавать собственные простые браузерные игры.

Детали к урокам
Все уроки и версии игры MDN Breakout доступны в GitHub:
Лучший способ получить надёжные знания в области разработки браузерных игр — это начать с чистого JavaScript. Затем можно выбрать любой фреймворк для использования в своих проектах. Фреймворки — это инструменты, созданные на языке JavaScript; поэтому, даже если вы планируете работать с ними, нелишним будет сначала изучить сам язык, чтобы понимать, что именно происходит внутри. Фреймворки ускоряют разработку и помогают справиться со скучными частями игры, но если что-то работает не так, как ожидалось, всегда можно попытаться отладить код или написать собственное решение на чистом JavaScript.
Примечание: Если вам интересно узнать о разработке двухмерных игр с помощью игровой библиотеки, ознакомьтесь с альтернативной серией статей 2D игра Breakout с использованием Phaser (en-US) .
Примечание: Эту серию статей можно использовать как материал для практических занятий по разработке игр. Также можно воспользоваться набором инструментов Gamedev Canvas Content Kit, основанном на этом уроке, если нужно сделать доклад о разработке игр в целом.
Крестики-нолики — разработка игры: как создать игру
Вместо того, чтобы долго изучать программирование по статьям и лекциям, которые скучно описывают какие-то непонятные действия с числами и строками, лучше изучать программирование на красочных примерах, таких, как создание игр!
Естественно, вам потребуется хотя бы теоретическое знание о том, что такое: язык программирования, переменная, цикл, массив. Так же, для чтения этой статьи и изучения программы, желательно иметь представление о: JavaScript, HTML, и CSS (CSS не обязательно — требуется только для украшения).
Структура игры
Эта игра состоит из нескольких частей, которые написаны на HTML, CSS и JavaScript.
Общий принцип такой: на веб-страничке (HTML) делаются ссылки на файл с JavaScript-ом и CSS файл со стилями. Так же, в HTML размечаются те места, где будут элементы игры отображаться. А так же вставлена одна функция на JavaScript, которая запускается сразу после загрузки страницы, и задаёт параметры языка для игры, и даёт команду на создание нового игрового поля.
В CSS файле содержатся стили элементов игры, такие, как обводка клеток, и тип курсора мыши, при наведении на клетку.
В JS файле содержится основной код, который и управляет игрой.
Раз мы изучаем на примере, то практически каждая строчка будет содержать комментарий – подробное описание того, чего делает данная команда.
Исходный код CSS файла
CSS (каскадные таблицы стилей) – описывает стиль для отображения игрового поля на HTML страницы. Исходный код CSS приведу без подробных объяснений, только с комментариями, которые начинаются символами /* и заканчиваются символами */.
#gameinfo < font-size: 110%; >/* размер шрифта в текстовом поле, с */
#game table < /* описывает стиль таблицы с крестиками и ноликами */
border:3px groove blue; /* окаёмка таблицы */
background-image:url(«fon.jpg»); /* фоновая картинка для игры */ >
#game table tr td < /* стиль для одной ячейки в таблице – где будет располагаться крестик, либо нолик, или просто будет пустовать */
border:1px dotted #eeb; /* опишем обводку клетки */
cursor:pointer; /* установим вид курсора мыши, чтобы показать, что можно кликать на клетку */ >
#game table tr td:hover < border:1px solid #ffa; >/* подсвечивание клетки, при наведении на неё курсора мыши */
Код на HTML странице
HTML. Следующий код необходимо вставить на веб-страницу. Каждый фрагмент кода подписан.
внутрь тега HEAD:
Так же, нужно прописать параметр onload=»initGame();» в тег BODY.
внутрь BODY – туда, где будет управление игрой:
| Кликните по клетке, чтобы открыть её |
внутрь BODY – туда, где будет игровое поле:
Код JavaScript игры
Самый большой код. И самый главный. В нём есть и ИИ соперника, и функции отображения и построения поля, и проверка победы. Но, так как разбор программы сходу, думаю, будет немного сложноват, то сначала я опишу вкратце, какой фрагмент и функция программы за что отвечает.
- gameImgDir — содержит сведения об расположении относительного адреса графических изображений.
- gTexts — содержит текстовые строки, полезны для поддержки многоязычности.
- gField — двумерный массив — содержит информацию о клетках.
- createField(w,h) — создание нового поля, с размером w на h.
- setCell(x,y,t) — ставит в клетку с координатами [x,y] значение t (крестик, или нолик). А так же заменяет картинки на поле.
- isWin() — проверяет, кто победил.
- CompGame() — вызывает компьютерного игрока, который сначало методом перебора проверяет все варианты, запуская isWin(), а затем делает ход, вызывая setCell(x,y,t).
- onCellClk(x,y) — вызывается по клику, передаются координаты клетки. Вызывает isWin(), setCell(x,y,t) и CompGame(). Сообщает информацию о победе.
После того, как мы изучили основные принципы игры, теперь можно смотреть весь исходный код файла gamescript.js. Все основные моменты кода даны с комментариями (комментарии в JS идут после двойного слеша //).
// (C) www.AlexeyK.com, 2012.
var gameImgDir=»data/»;
var gTexts=[]; // Для многоязычности.
gTexts[‘win1’]=’Win ‘; gTexts[‘win2′]=’!’;
gTexts[‘playing’]=’. ‘
gTexts[‘start’]=’Begin game!’
var gField=[]; // игровое поле
var hT=»
| «; hT+=» «; hT+=» |
«; // вставка таблицы на страницу.
document.getElementById(‘gameinfo’).innerText=gTexts[‘start’]; // отобразить сообщение
>
function setCell(x,y,t) < // Поставить крестик или нолик
gField[x][y]=t; // Запомнить t в массиве
var imgsrc=gameImgDir+’c_null.gif’; // изображение по умолчанию
if (t==’x’) imgsrc=gameImgDir+’c_x.gif’; // картинка для крестика
if (t==’o’) imgsrc=gameImgDir+’c_o.gif’; // картинка для нолика
var oName=»c»+x+»_»+y; // составление имени картинки
document.getElementById(oName).src = imgsrc; // замена изображения
if (t!=null) document.getElementById(oName).alt = t; // если картинки выключены, то игра будет в текстовом режиме 🙂
>
for ( i=0 ; i lC=gField[stX+i][stY];
if (lC!=null) for ( j=0 ; j if (lC!=null) return lC;
>
for ( j=0 ; j lC=gField[stX][stY+j];
if (lC!=null) for ( i=0 ; i if (lC!=null) return lC;
>
>
// TO DO — если все клетки заняты, но никто не победил, то ничья, но пока это условие не сделано. Попробуйте сами доделать эту проверку 😉
return false; // если никто не победил
>
Прочие файлы
- c_null.gif — изображение пустой ячейки
- c_x.gif — изображение ячейки с x
- c_o.gif — изображение ячейки с o
- fon.jpg — фоновое изображение
Вот и всё. Попробуйте попрактиковаться в создании html-страничек, и попробуйте запустить эту игру, собрав её самостоятельно. Если получилось, то я специально недоделал сообщение о нечьей. Попробуйте самостоятельно сделать проверку на нечью, и добавить её в игру.
Обучаемся через игры! 5 игр для изучения JavaScript
В этой статье вы найдете пятерку увлекательных игр для изучения JavaScript.
10K показов
2.5K открытий
В мире программирования JavaScriptявляется одним из наиболее широко используемых языков, и познание его основных принципов и концепций является неотъемлемой частью пути к становлению опытным веб-разработчиком.
Хотите ли вы освоить этот язык программирования или улучшить свои навыки в нем, игры могут стать увлекательным и эффективным способом изучения. В этой статье вы найдете пятерку увлекательных игр для изучения JavaScript.
Поддерживает все востребованные языки программирования. Платформу можно использовать как мультиплеерную игру и соревноваться с друзьями!

«;