Перейти к содержимому

Как сделать игру на javascript

  • автор:

Создать JS-игру с мультиплеером за 24 часа и выжить

Как всего за сутки мы с моими коллегами (шестью фронтендерами и одним бэкендером) создали настоящую мультиплеерную игру на JavaScript.

За 24 часа можно успеть очень много. Сегодня я решил рассказать, как всего за сутки мы с моими коллегами (шестью фронтендерами и одним бэкендером) создали настоящую мультиплеерную игру на JavaScript. Поехали!

Вадим Силантьев
Frontend-разработчик IRLIX

Создать JS-игру с мультиплеером за 24 часа и выжить 1

Собрались похакатонить…

Совсем недавно в компании 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)

Когда мы поняли, что движущиеся точки — это конечно круто, но недостаточно, решили пуститься «во все тяжкие» и делать мультиплеерный шутер. Первое, что мы сделали — проработали концепцию, отбросив все лишнее. После — упростили графику ( от того, что нарисовало богатое воображение в наших головах до реализуемого варианта). Затем перешли к тому, что создали персонажей и их способности к стрельбе.

Создать JS-игру с мультиплеером за 24 часа и выжить 2

Было много спорных моментов, обсуждений, далеко не всегда получалось прийти к компромиссу. И, конечно, были эпизоды, в которых спорить было не о чем, потому что мы просто не знали, как это воплотить.

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

Еще пример: мы не знали, что делать с ФПС ( наша бравая лошадка-сервер вывозила не больше 4 человек, а после грустно ложилась помирать). Происходило это по нескольким причинам: чем больше клиентов, тем больше данных нужно отправлять, а данные отправлялись 60 раз в секунду, на 8 игроках игровой цикл замедлялся до 15 циклов в секунду. Однако для оптимизации процессов было еще слишком рано, готовой игровой модели еще не было, и мы продолжили разработку.

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

Создать JS-игру с мультиплеером за 24 часа и выжить 3

// линейная интерполяция, piece = 0.0 -> 1.0 const lerp = (start, finish, piece) => < return start + (finish - start) * piece; >; const drawProjectile = (ctx, bullet, dt) =>

Отрисовка снарядов на клиенте

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

Как я написал кастомный шрифт ради одного символа. Но сначала неделю страдал

Следующим этапом стала обработка столкновений. Первая наша попытка была просто ужасной: была куча блоков if/else, но все работало, однако глядя на такой код, хотелось плакать. Поэтому мы решили сделать все области столкновений в игре круговыми и обратиться к геометрии.

Создать JS-игру с мультиплеером за 24 часа и выжить 4

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 шутер с видом сверху. Игроки сражались каждый сам за себя, количество патронов было ограничено, но на на карте постоянно появлялись случайные объекты в виде (дополнительных патронов, усиления урона, аптечки), эти объекты создавали точки интереса на карте и вынуждали игроков постоянно двигаться, чтобы выиграть бой, от этого игра становилась более динамичной и веселой. При поражении игрок выбывал с карты на некоторое время, а после опять мог вступить в бой. На следующий день после хакатона, мы рубились в эту игрой с ребятами из офиса, и, как оказалось, шутер получился веселым и залипательным, что нас очень сильно порадовало.

Создать JS-игру с мультиплеером за 24 часа и выжить 5

Простые игры на Canvas в браузере могут сделать ваше приложение более интерактивным. Например, простой игрой вы можете заменить скучные и долгие экраны загрузок. А написание полноценных браузерных игр на JS может стать отличной альтернативой разработке однотипных, скучных приложений. Вердикт: геймдеву на JS быть!

Игра на чистом JavaScript за 20 минут

Игра на чистом JavaScript за 20 минут

На JS можно создавать сложные и простые игры любых жанров. Мы расскажем как создать 2D игру на JavaScript и HTML5 всего за 20 минут.

Для создания веб игр на языке JavaScript используется технология Canvas , которая позволяет выполнять JavaScript код в HTML5 документе. Вы можете более детально ознакомиться с этой технологией посмотрев видео ниже:

На html странице прописывается лишь тег канвас, а также подключение JS файла, в котором будет происходить обработка всей функциональности. К примеру, HTML файл может выглядеть следующим образом:

      Flappy Bird!    

В JS файле необходимо найти нужный канвас по id и указать способ работы с ним.

var cvs = document.getElementById("canvas"); var ctx = cvs.getContext("2d");

Добавление изображений и аудио

Далее необходимо загрузить все изображения, а также аудио файлы, которые будут использоваться в игре. Для этого используйте класс Image и Audio соответсвенно. Ниже вы можете скачать все необходимые картинки, а также аудиофайлы к игре.

  • Скачать аудио файлы можно по этой ссылке ;

Код добавления изображений и аудио в игру:

var bird = new Image(); var bg = new Image(); // Создание объекта var fg = new Image(); // Создание объекта var pipeUp = new Image(); // Создание объекта var pipeBottom = new Image(); // Создание объекта bird.src = "img/bird.png"; // Указание нужного изображения bg.src = "img/bg.png"; // Аналогично fg.src = "img/fg.png"; // Аналогично pipeUp.src = "img/pipeUp.png"; // Аналогично pipeBottom.src = "img/pipeBottom.png"; // Аналогично // Звуковые файлы var fly = new Audio(); // Создание аудио объекта var score_audio = new Audio(); // Создание аудио объекта fly.src = "audio/fly.mp3"; // Указание нужной записи score_audio.src = "audio/score.mp3"; // Аналогично

Рисование объектов

Чтобы нарисовать объекты, а также добавить функционал к игре необходимо прописать функцию, которая будет постоянно вызываться. Такую функцию вы можете назвать как вам будет угодно. Главное, вам нужно вызвать эту функцию из вне её хотя бы один раз, а внутри неё прописать метод requestAnimationFrame , который будет вызывать функцию постоянно.

function draw() < // Какой-либо код requestAnimationFrame(draw); // Вызов функции постоянно >draw(); // Вызов функции из вне

Весь код игры стоит помещать в этот метод, ведь в нем он будет постоянно обрабатываться и игра будет выглядеть живой и анимированной.

Чтобы отследить нажатие игрока на какую-либо клавишу, необходимо использовать отслеживание событий — addEventListener . К примеру, чтобы отследить нажатие на любую клавишу на клавиатуре надо прописать следующий код:

// При нажатии на какую-либо кнопку document.addEventListener("keydown", someMethod); // Вызывается метод someMethod function someMethod() < // Изменяем что-то в коде >

Видео урок

Это были лишь небольшие азы перед созданием самой игры. Предлагаем вам ознакомиться с небольшим видео уроком, в ходе которого вы создадите небольшую 2D игру на чистом JavaScript’е.

Весь JS код игры

Ниже вы можете посмотреть на полностью весь код JavaScript файла, который был создан в ходе видео урока выше:

var cvs = document.getElementById("canvas"); var ctx = cvs.getContext("2d"); var bird = new Image(); var bg = new Image(); var fg = new Image(); var pipeUp = new Image(); var pipeBottom = new Image(); bird.src = "img/bird.png"; bg.src = "img/bg.png"; fg.src = "img/fg.png"; pipeUp.src = "img/pipeUp.png"; pipeBottom.src = "img/pipeBottom.png"; // Звуковые файлы var fly = new Audio(); var score_audio = new Audio(); fly.src = "audio/fly.mp3"; score_audio.src = "audio/score.mp3"; var gap = 90; // При нажатии на какую-либо кнопку document.addEventListener("keydown", moveUp); function moveUp() < yPos -= 25; fly.play(); >// Создание блоков var pipe = []; pipe[0] = < x : cvs.width, y : 0 >var score = 0; // Позиция птички var xPos = 10; var yPos = 150; var grav = 1.5; function draw() < ctx.drawImage(bg, 0, 0); for(var i = 0; i < pipe.length; i++) < ctx.drawImage(pipeUp, pipe[i].x, pipe[i].y); ctx.drawImage(pipeBottom, pipe[i].x, pipe[i].y + pipeUp.height + gap); pipe[i].x--; if(pipe[i].x == 125) < pipe.push(< x : cvs.width, y : Math.floor(Math.random() * pipeUp.height) - pipeUp.height >); > // Отслеживание прикосновений if(xPos + bird.width >= pipe[i].x && xPos = pipe[i].y + pipeUp.height + gap) || yPos + bird.height >= cvs.height - fg.height) < location.reload(); // Перезагрузка страницы >if(pipe[i].x == 5) < score++; score_audio.play(); >> ctx.drawImage(fg, 0, cvs.height - fg.height); ctx.drawImage(bird, xPos, yPos); yPos += grav; ctx.fillStyle = "#000"; ctx.font = "24px Verdana"; ctx.fillText("Счет: " + score, 10, cvs.height - 20); requestAnimationFrame(draw); > pipeBottom.onload = draw; 

Больше интересных новостей

6 неявных ошибок новичков в Pandas

6 неявных ошибок новичков в Pandas

17 самых популярных CSS фреймворков

17 самых популярных CSS фреймворков

Программа на Python для отслеживания курса валюты

OpenAI библиотека, что изменит мир! GPT - генерация текста, кода.

OpenAI библиотека, что изменит мир! GPT — генерация текста, кода.

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

RInat 05 мая 2023 в 17:32

Мои первые шаги в создании браузерных 2D игр на чистом JavaScript + Canvas. Часть 1. Прототип

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

Статья представляет собой перевод одного англоязычного видеоурока. Если Вы хорошо владеете английским и Вам больше нравится видеоформат подачи материала — можете посмотреть видео. В статье же я буду вставлять участки кода и стараться также подробно как и автор видео — объяснять каждый свой шаг.

От Вас требуются лишь желание, базовое представление — для чего нужны HTML, CSS и JavaScript, редактор кода и более-менее современный компьютер с браузером. На самом деле знание HTML и CSS особо не понадобится, т.к. почти весь код будет написан на JavaScript.

Поиграть в игру можно здесь, а посмотреть исходники тут.

Создание проекта

Автор видео пишет весь JavaScript код в одном единственном файле script.js. Хотя я по началу делал точно также, чтобы быстрей вносить доработки и синхронизировать с ним свои действия, в этой статье я разобью весь код на отдельные файлы и папки.

Создадим папку проекта и пустые js-файлы со следующей структурой:

Папки выделенные красным — содержат всю необходимую графику (спрайт-шиты), с помощью которой мы будем анимировать наших персонажей. Можете скачать файлы .png из репозитория и добавить их в соответствующие разделы.

В index.html, style.css и script.js добавим нижеприведенный код:

      JavaScript Game                                        
* < margin: 0; padding: 0; box-sizing: border-box; >#canvas1 < border: 5px solid black; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #4d79bc; max-width: 100%; max-height: 100%; font-family: 'Bangers', cursive; /* Подключаем шрифт Bangers */ >#layer1, #layer2, #layer3, #layer4, #player, #angler1, #angler2, #lucky, #projectile, #gears, #hivewhale, #drone, #smokeExplosion, #fireExplosion
window.addEventListener('load', function () < // canvas setup const canvas = this.document.getElementById('canvas1'); const ctx = canvas.getContext('2d'); canvas.width = 1500; canvas.height = 500; >)

В index.html мы добавили элемент canvas (далее — игровое «полотно», полотно, канвас, холст и т.п.), на котором и будет происходить все действо игры. Данный элемент является краеугольным камнем как нашей, так и многих браузерных игр. Все действия наших персонажей: столкновения, взрывы, физику мы будем программировать на JavaScript путем отрисовывания тех или иных элементов на данном полотне.

Также в html-файле в нашу игру мы «подтянем» всю необходимую графику (в тэгах img) и подключим скрипты (тэг script).

В файле style.css присвоим нашему полотну «абсолютную позицию», выровняем его по центру, добавим рамку и т.д. А также всем img-элементам установим свойство display в значение none , чтобы скрыть их отображение в окне браузера. Если не понятно для чего это нужно — можете удалить этот блок из файла style.css, либо какой-то конкретный элемент, например, #player, и посмотреть что из этого выйдет.

P.S> также мы подключили для нашей игры экзотический гугл-шрифт «Bangers», о чем говорят соответствующие строки. О том как это сделать — будет отдельный раздел.

В файле script.js напишем обработчик события load, которое срабатывает после загрузки всех ресурсов (картинок, скриптов, стилей и т.д.). Весь код игры будет находиться внутри данного обработчика. В строках 3 и 4 получим объект canvas, который ранее определили в html-файле и определим глобальную переменную контекста ctx. Переменная ctx содержит все необходимые методы для отрисовки необходимых нам элементов на игровом полотне. В строках 5 и 6 установим ширину и высоту игрового поля, соответственно.

После выполнения всех вышеописанных процедур наше игровое поле будет выглядеть следующим образом:

Рисунок 1. Создание игрового поля

Классы Game и Player. Анимационный цикл

Приступим к кодированию!

В файле Player.js создадим класс игрока:

class Player < constructor(game) < this.game = game; this.width = 120; this.height = 190; this.x = 20; this.y = 100; this.speedY = 0; >update() < this.y += this.speedY; >draw(context) < context.fillRect(this.x, this.y, this.width, this.height); >>

Про классы, пожалуй, я не буду подробно рассказывать, т.к. на эту тему написано и отснято довольно много материала. На интуитивном уровне я думаю можно понять, что это «тип данных», который представляет нашего главного персонажа (в будущем — механизированный морской конь), у которого есть свои свойства и поведение.

Все классы нашей игры должны будут иметь доступ к экземпляру класса Game, чтобы знать о ее состоянии, поэтому в конструктор класса Player передаем объект игры. Помимо этого, класс игрока включает в себя свойства width и height — ширина и высота, в пикселях, нашего персонажа, а также координаты его стартовой позиции на игровом полотне — свойства x и y (начало координат находится в левом верхнем углу). Свойство speedY будет отвечать за скорость перемещения (двигаться наш игрок сможет только по вертикали).

Метод update() нужен для обновления состояния нашего игрока, а draw() — для отрисовки персонажа на игровом полотне. Пока в теле метода update() реализуем лишь изменение скорости движения. А в метод draw() добавим код, который рисует черный прямоугольник вместо нашего игрока.

Класс Game имеет следующий вид:

class Game < constructor(width, height) < this.width = width; this.height = height; this.player = new Player(this); >update() < this.player.update(); >draw(context) < this.player.draw(context); >>

width и height представляют собой ширину и высоту игрового поля. Код new Player(this) создает для нас экземпляр (инстанс) нашего игрока, которого мы помещаем в свойство player . Отметим, что в качестве параметра в конструктор класса Player мы передаем здесь объект this — это ключевое слово говорит нам о том, что мы таким образом ссылаемся на текущий (этот) класс игры. А значит наш игрок будет также в курсе о всех изменениях состояния самой игры (игра завершена, остановлена, на паузе, время до окончания игры и т.д.).

Методы update() и draw() класса Game будут обновлять и рисовать, соответственно, все элементы игры: персонажей, взрывы, летящие «пули», разлетающиеся частицы от уничтоженных врагов и т.п. Отметим, что в методы draw во всех классах мы будем всегда передавать глобальную переменную контекста ctx, которую создали в предыдущем разделе.

В script.js добавим следующий код:

 const game = new Game(canvas.width, canvas.height); // animation loop function animate() < ctx.clearRect(0, 0, canvas.width, canvas.height); // Очищаем игровое поле перед следующей анимацией game.draw(ctx); game.update(); requestAnimationFrame(animate); >animate();

Здесь мы создаем экземпляр класса игры, передавая ему ширину и высоту канваса.

Ключевым моментом игры является создание т.н. бесконечного анимационного цикла (animation loop). Для этого используется метод requestAnimationFrame(). Простыми словами, — этот метод посылает браузеру сигнал (1 раз!), чтобы тот выполнил перерисовку всего окна. Метод requestAnimationFrame() принимает в качестве аргумента функцию, которую нужно выполнить непосредственно перед перерисовкой. Для этого мы создали функцию animate(). А для того, чтобы наш цикл получился бесконечным — необходимо поместить метод requestAnimationFrame() внутрь функции animate(), таким образом мы и создадим бесконечный анимационный цикл, который будет обновлять состояние нашей игры.

Также в теле функции animate() мы очищаем игровое поле перед следующей отрисовкой и выполняем подряд методы draw() и update().

Скорость получившейся «анимации» зависит от мощности вашего компьютера, но в среднем составляет примерно 60 обновлений в секунду. Мы еще вернемся к методу animate() и дополним его для регулирования скорости анимации и создания т.н. «периодических событий».

На данный момент «игра» должна выглядеть следующим образом:

Рисунок 2. Черный прямоугольник вместо главного игрока

Обработка нажатий клавиш

Дадим возможность двигаться нашему игроку. Как я уже говорил — двигаться наш игрок будет только по вертикали.

В файл InputHandler.js добавим следующий код:

class InputHandler < constructor(game) < this.game = game; window.addEventListener('keydown', (e) => < if (((e.key === 'ArrowUp') || (e.key === 'ArrowDown')) && this.game.keys.indexOf(e.key) === -1) < this.game.keys.push(e.key); >>); window.addEventListener('keyup', (e) => < if (this.game.keys.indexOf(e.key) >-1) < this.game.keys.splice(this.game.keys.indexOf(e.key), 1); >>); > >

а в конструкторе класса Game определим два новых свойства:

this.input = new InputHandler(this); this.keys = [];

Класс InputHandler будет отвечать за обработку событий нажатия (keydown) и отпускания (keyup) клавиш. Первый из них будет смотреть – какую клавишу нажал пользователь и, если это клавиша «вверх» или «вниз», обработчик будет добавлять эту клавишу в массив this.game.keys . Условие && this.game.keys.indexOf(e.key) === -1 запретит добавлять в массив клавишу, если та уже присутствует в нем. Обработчик события отпускания (keyup) — наоборот, сначала проверяет наличие клавиши в массиве и при ее наличии там — удаляет с помощью метода splice.

В класс Player добавим вспомогательное свойство maxSpeed , а в методе update() реализуем условия изменения скорости — с помощью метода includes() проверим наличие в массиве клавиш «вверх»/»вниз» и в зависимости от этого поменяем знак скорости, заставив «игрока» двигаться в том или ином направлении:

if (this.game.keys.includes('ArrowUp')) this.speedY = -this.maxSpeed else if (this.game.keys.includes('ArrowDown')) this.speedY = this.maxSpeed else this.speedY = 0;

Чтобы наш игрок не мог полностью «выйти» за границу игрового поля — добавим в метод update() следующее условие:

if (this.y > this.game.height - this.height * 0.5) this.y = this.game. height - this.height * 0.5; else if (this.y < -this.height * 0.5) this.y = -this.height * 0.5;

Коэффициент 0.5 позволит выходить за границу только наполовину.

Снаряды

Чтобы наш игрок мог стрелять выполним следующие действия.

Первым делом создадим класс Projectile (файл Projectile.js):

class Projectile < constructor(game, x, y) < this.game = game; this.x = x; this.y = y; this.width = 10; this.height = 3; this.speed = 3; this.markedForDeletion = false; >update() < this.x += this.speed; if (this.x >this.game.width * 0.8) this.markedForDeletion = true; > draw(context) < context.fillStyle = 'yellow'; context.fillRect(this.x, this.y, this.width, this.height); >>

x и y — это стартовая позиция снаряда на игровом поле (откуда он появляется); width и height — его ширина и высота; speed — скорость полета; markedForDeletion — флаг для "удаления" снаряда, — необходим для удаления снаряда с игрового поля, если, например, снаряд сталкивается с противником, либо вылетает за обозначенные границы.

В методе update() заставляем снаряд лететь вправо путем увеличения координаты x на величину скорости, а также заставляем исчезать снаряд как только он пролетает 80% игрового поля (чтобы появляющиеся в будущем враги могли иметь фору и не получать сразу ранения). В методе draw() окрашиваем снаряд в желтый цвет и рисуем его на поле с помощью метода fillRect(). Но это еще не все.

В класс Game добавим свойство ammo (количество боеприпасов), чтобы у игрока был лимит:

this.ammo = 20;

В классе Player определим свойство projectiles — массив снарядов:

this.projectiles = [];

В методе update() класса Player:

// handle projectiles this.projectiles.forEach(pr => < pr.update(); >); this.projectiles = this.projectiles.filter(pr => !pr.markedForDeletion);

с помощью метода forEach() пробежим по всем боеприпасам и вызовем у каждого метод update() (чтобы заставить их двигаться), а с помощью метода filter() удалим те, у которых флаг markedForDeletion = true.

в метод draw() класса Player добавим такие строки:

context.fillStyle = 'black'; this.projectiles.forEach(pr => < pr.draw(context); >);

т.к. в классе Projectile мы определили цвет заливки контекста как желтый, то теперь необходимо явно поменять цвет игрока на черный, иначе он тоже будет желтым. Ну и вызовем также с помощью метода forEach() для каждого снаряда метод draw().

Реализуем в классе игрока метод shootTop():

shootTop() < if (this.game.ammo >0)

здесь мы проверим наличие боеприпасов ( this.game.ammo > 0 ) и если снаряды есть — добавим один снаряд в массив, в то же время уменьшив на единицу свойство this.game.ammo . Заметим, что стартовую позицию снаряда мы сдвинули правее на 80 и вниз на 30 пикселей (чтобы в будущем они вылетали из носа морского коня).

Осталось добавить в обработчик события нажатия (keydown) вызов метода shootTop() при нажатии пробела:

else if (e.key === ' ') < this.game.player.shootTop();

Периодические события. FPS

Теперь наш игрок умеет стрелять и имеет целых 20 патронов. Но как Вы видите, эти 20 патронов расходуются очень быстро и много врагов ими не уничтожить. А что, если мы сделаем так, чтобы оружие у нашего игрока перезаряжалось автоматически, т.е. раз в какой-то промежуток времени боеприпасы сами пополнялись, скажем по одному патрону в секунду. Отсюда также вытекает задача иметь достоверную визуальную информацию о количестве патронов в текущий момент, но об этом чуть позже.

Немного доработаем основной скрипт нашей игры — script.js:

 let lastTime = 0; // stores a value of timestamp from the previous animation loop // animation loop function animate(currentTime) < // В currentTime будет записан момент времени следующего вызова функции animate() const deltaTime = currentTime - lastTime; // Разница, в миллисекундах, между итерациями анимационного цикла ctx.clearRect(0, 0, canvas.width, canvas.height); // Очищаем игровое поле перед следующей анимацией game.draw(ctx); game.update(deltaTime); // Теперь обновление игры будет зависеть от частоты смены кадров lastTime = currentTime; // Переприсваивание временных позиций requestAnimationFrame(animate); >animate(0); // Передаем 0 в качестве параметра (время первого вызова)

Воспользуемся еще одной фичей метода requestAnimationFrame(). Данный метод передает в аргумент коллбэк-функции (которую он вызывает) момент времени, когда он собирается ее вызывать. В нашем случае коллбэк-функция — это функция animate().

Определим переменную lastTime — она будет хранить момент времени "прошлого" вызова анимационного цикла. В функцию animate() добавим аргумент currentTime . Именно в currentTime метод requestAnimationFrame() будет записывать момент времени "текущего" вызова. Разность между "текущим" и "прошлым" временем вызова функции animate() мы запишем в переменную deltaTime , чтобы затем передать ее функции game.update() в качестве параметра. Именно на основе этой разности мы и будем строить периодические события — перезарядку снарядов и т.п.

Возможно, (я более чем уверен!), Вам, как и мне по началу, не очень понятны данные "махинации со временем". Но данная практика (с вычислением временной разности) является стандартной в разработке браузерных игр. По крайней мере так утверждает автор видео. Хотя это и логично, т.к. в зависимости от производительности Вашего компьютера — это значение может варьироваться, а значит необходимо чтобы игра могла "адаптироваться" к Вашей частоте смены кадров.

После вычисления deltaTime и ее использования — нужно перезаписать переменную lastTime, присвоив ей значение "текущего" currentTime, чтобы на следующей итерации анимационного цикла мы нашли новую deltaTime с корректными значениями.

еще немного про deltaTime

Добавим (только в целях эксперимента, а потом удалим!) в тело функции animate() вывод значения deltaTime:

console.log(deltaTime);

в консоли браузера мы увидим эти разности:

Вывод deltaTime

Если взять среднее значение — оно окажется, как и у автора видео, равным примерно 16.6 (миллисекунд). Это и есть разность между прошлой и текущей итерацией анимационного цикла. Поделив 1 секунду = 1000 мс на это значение — получим примерно 60. Т.е. как и было до этого сказано, метод requestAnimationFrame() перерисовывает окно со скоростью 60 раз в секунду
60 fps, frames per second).

При вызове функции animate() передадим ей в качестве параметра нулевое значение — время "первого" вызова.

Визуализация боеприпасов

Давайте же выведем на игровое полотно текущее количество патронов у нашего персонажа.

Создадим класс UI, главной задачей которого будет вывод игровых статусов и сообщений (файл UI.js):

class UI < constructor(game) < this.game = game; this.fontSize = 25; this.fontFamily = 'Helvetica'; this.color = 'yellow'; >draw(context) < context.fillStyle = this.color; for (let i = 0; i < this.game.ammo; i++) < context.fillRect(5 * i + 20, 50, 3, 20); >> >

Здесь я думаю пока все предельно понятно. Прокомментировать стоит лишь цикл, который рисует желтые прямоугольники, каждый из которых символизирует один патрон. Количество итераций в данном цикле равно текущему количеству боеприпасов this.game.ammo .

В класс Game добавим несколько свойств. ammoInterval — интервал перезарядки, в миллисекундах (пусть будет 500) — т.е. раз в полсекунды боеприпасы нашего игрока будет пополнять один новый патрон. Чтобы патроны не копились до бесконечности — введем свойство maxAmmo — максимальное количество патронов (сделаем равным 50); а также вспомогательное свойство ammoTimer , которое будет своего рода "временным счетчиком", меняющимся от 0 до значения ammoInterval.

В метод update() класса Game добавим следующие строки:

if (this.ammoTimer > this.ammoInterval) < if (this.ammo < this.maxAmmo) this.ammo++; this.ammoTimer = 0; >else

Теперь метод update() будет принимать в качестве параметра значение deltaTime , о котором мы подробно говорили в предыдущем разделе. Таким образом, на каждой итерации нашего анимационного цикла мы будем увеличивать значение ammoTimer на величину deltaTime . Как только ammoTimer превысит значение ammoInterval — мы сбросим ammoTimer в ноль и увеличим на единицу количество патронов (если оно меньше максимального количества maxAmmo ).

В конструкторе класса Game мы также создадим свойство — экземпляр класса UI:

this.ui = new UI(this);

А в методе draw() вызовем соответствующий метод класса UI:

this.ui.draw(context);

Создаем врагов. Наследование

Раз мы вооружили нашего персонажа — пришло время создать и врагов. Также в этом разделе поговорим о наследовании — одной из трех ключевых концепций ООП.

У нас будет несколько типов врагов, каждый со своими свойствами/поведением (количество жизней, урон наносимый главному персонажу при столкновении, либо бонусы от столкновения и т.п.). Но при этом будут и общие свойства/методы присущие всем врагам.

Давайте создадим базовый класс Enemy, который затем будут наследовать производные классы:

class Enemy < constructor(game) < this.game = game; this.x = this.game.width; this.speedX = Math.random() * -1.5 - 2.5; this.markedForDeletion = false; >update() < // Обновляем x-координату врага (уменьшаем ее на величину speedX) this.x += this.speedX; // Помечаем врага как удаленного, если он полностью пересечет левую границу игрового поля if (this.x + this.width < 0) this.markedForDeletion = true; >draw(context) < // Устанавливаем цвет врага context.fillStyle = this.color; // На данном этапе наш враг будет представлять из себя // просто прямоугольник определенного цвета context.fillRect(this.x, this.y, this.width, this.height); >>

Свойство x — начальная x-координата появления врага, которая равна правой границе игрового поля; speedX — отрицательная скорость движения врага, которая лежит в полуинтервале (-4, -2.5] (чтобы он двигался справа налево и при этом скорости каждого врага немного отличались) и стандартное уже свойство-флаг markedForDeletion для удаления врагов.

В методе update() мы обновляем x-координату нашего врага, а также помечаем его удаленным, если он полностью пересек левую границу игрового поля (это может случиться, если наш главный игрок не успел его уничтожить). В методе draw() установим цвет врага и нарисуем его (врага) в виде прямоугольника.

Как вы видите, свойств color и height в данном классе нет, но они будут определены в производных классах, т.к. для каждого типа врага они будут иметь свои уникальные значения (какие-то враги будут красными, другие зелеными, и по высоте тоже будут различаться). Поэтому вызывать метод draw() имеет смысл только на экземплярах производного класса.

Давайте создадим два типа врагов — Angler1 и Angler2:

class Angler1 extends Enemy < constructor(game) < super(game); this.width = 228 * 0.2; this.height = 169 * 0.2; this.y = Math.random() * (this.game.height * 0.95 - this.height); this.color = 'red'; >>
class Angler2 extends Enemy < constructor(game) < super(game); this.width = 213 * 0.3; this.height = 165 * 0.3; this.y = Math.random() * (this.game.height * 0.95 - this.height); this.color = 'green'; >>

Данные классы созданы с помощью ключевого слово extends (расширение), которое говорит, что эти классы являются производными (дочерними) от класса Enemy, а значит могут с легкостью использовать функционал базового класса (Enemy). Также в конструкторе этих классов используется ключевое слово super, которое в данном случае вызывает конструктор базового класса (Enemy), а затем уже определяются свойства, уникальные для данных классов.

В классах Angler1 и Angler2 мы определили ширину и высоту ( width и height ), цвет ( color ). Свойство y — y-координата врага определена таким образом, чтобы враг не мог появиться вне пределов игрового поля.

Осталось чуть-чуть доработать класс Game и мы увидим наших врагов на экране.

Добавим следующие свойства в класс Game:

this.enemies = []; this.enemyTimer = 0; this.enemyInterval = 1000; this.gameOver = false;

где enemies — массив врагов; enemyTimer и enemyInterval — переменные, которые по аналогии с переменными ammoTimer и ammoInterval из предыдущего раздела помогут нам создавать одного врага в секунду, появляющегося с правой стороны игрового полотна. Также введем свойство gameOver , чтобы иметь признак окончания игры (если игра завершена, враги появляться уже не должны).

В метод update() класса Game тоже внесем изменения:

this.enemies.forEach(enemy => enemy.update()); this.enemies = this.enemies.filter(enemy => !enemy.markedForDeletion); if (this.enemyTimer > this.enemyInterval && !this.gameOver) < this.addEnemy(); this.enemyTimer = 0; >else

как видите — эти изменения уже стандартные. Сначала в "цикле" forEach() мы вызываем метод update() у каждого врага, затем убираем "удаленных". Последний if. else. комментировать не буду, т.к. в предыдущем разделе подобное условие уже обсуждали (здесь добавил условие && !this.gameOver , чтобы враги не появлялись после окончания игры). Метод по добавлению врагов addEnemy() реализуем через пару мгновений.

В методе draw() нарисуем наших врагов:

this.enemies.forEach(enemy => enemy.draw(context));

В методе addEnemy() класса Game мы будем просто пушить в массив enemies врагов. А чтобы они появлялись с определенной вероятностью — воспользуемся псевдослучайным генератором. В будущем для корректировки сложности игры можно будет варьировать частоту появления того или иного типа противника.

addEnemy()

Вот что у нас получилось!

Теперь мы может стрелять по врагам, но. наши пули пролетают сквозь противника и не наносят им никакого урона. Нужно это исправить!

Обработка столкновений (коллизий)

Но не только пули пролетают сквозь врагов, также и враги, как вы заметили, пролетают сквозь главного персонажа.

Добавим совсем немного доработок в наш код, чтобы такого не было.

В класс Game добавим метод checkCollision():

checkCollision(rect1, rect2)

Наш главный персонаж, пули и враги — это все, по сути, прямоугольники. И у каждого из этих классов, а именно Player, Projectile и Enemy есть свойства, отвечающие за текущие координаты ( x и y — левый верхний угол прямоугольника) и свойства ширины и высоты ( width и height ). Используя только эти четыре свойства, мы и составили условие выше, которое будет говорить нам, столкнулись ли наши прямоугольники или нет.

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

Рисунок 7. Условия столкновений прямоугольников. В синих рамочках неравенства, одновременное выполнение которых свидетельствует о столкновении прямоугольников. Кружочками отмечены стороны прямоугольников, участвующих в неравенствах

В методе update() класса Game в "цикле" forEach() для массива enemies добавим следующий код:

this.enemies.forEach(enemy => < enemy.update(); // Проверим, не столкнолся ли враг с главным игроком (player) if (this.checkCollision(this.player, enemy)) < // если столкновение произошло, помечаем врага как удаленного enemy.markedForDeletion = true; >// для всех активных пуль (projectiles) также проверим условие столкновения // пули с врагом. this.player.projectiles.forEach(projectile => < if (this.checkCollision(projectile, enemy)) < // если столкновение произошло, помечаем снаряд как удаленный projectile.markedForDeletion = true; >>) >);

Сначала мы проверяем условие столкновения игрока с врагом и после этого во вложенном "цикле" forEach() для каждой летящей пули на игровом поле мы проверяем не столкнулась ли она с врагом. Если столкновение произошло — переводим свойство markedForDeletion необходимых объектов в значение true, чтобы удалить их с игрового поля.

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

Теперь при столкновениях враги и пули просто исчезают с игрового поля:

Очки. Жизни. Условие победы

Отлично! Столкновения работают. Но наши враги от столкновения со снарядами хуже себя особо не чувствуют. Давайте это исправим.

В класс Enemy внесем следующие свойства:

this.lives = 5; this.score = this.lives;

где lives — жизни врага (т.е. чтобы уничтожить его – нужно будет выпустить в него 5 патронов); score — очки, которые мы получим за уничтожение врага (пусть пока их количество равно количеству жизней);

чтобы мы видели текущие жизни каждого врага — отобразим их рядом с врагом. В метод draw() класса Enemy добавим:

// отобразим у каждого врага его жизни context.fillStyle = 'black'; context.font = '20px Helvetica'; context.fillText(this.lives, this.x, this.y - 5);

В класс Game добавим свойства:

this.score = 0; this.winningScore = 30;

score — общее количество очков, полученных за уничтожение врагов; winningScore — количество очков для победы.

Теперь в методе update() класса Game при столкновении пули и врага мы будем не просто помечать пулю как удаленную:

// Если пуля попала в врага if (this.checkCollision(projectile, enemy)) < enemy.lives--; // уменьшаем жизни врага на единицу projectile.markedForDeletion = true; // удаляем пулю // Проверяем, если у врага не осталось жизней if (enemy.lives >

Для победы нужно будет выполнить следующее условие, проверку которого реализуем в методе isWin() класса Game:

isWin() < return this.score >= this.winningScore; >

Немного приукрасим интерфейс. Доработаем метод draw() класса UI, теперь он будет выглядеть так:

Метод draw() класса UI

draw(context) < context.save(); context.fillStyle = this.color; context.shadowOffsetX = 2; context.shadowOffsetY = 2; context.shadowColor = 'black'; context.font = this.fontSize + 'px ' + this.fontFamily; // очки context.fillText('Score: ' + this.game.score, 20, 40); // сообщения о победе или проигрыше if (this.game.gameOver) < context.textAlign = 'center'; let message1; let message2; if (this.game.isWin()) < message1 = 'Победа!'; message2 = 'Отличная работа!'; >else < message1 = 'Попробуй еще раз!'; message2 = 'В следующий раз все получится!'; >context.font = '70px ' + this.fontFamily; context.fillText(message1, this.game.width * 0.5, this.game.height * 0.5 - 20); context.font = '25px ' + this.fontFamily; context.fillText(message2, this.game.width * 0.5, this.game.height * 0.5 + 20); > for (let i = 0; i < this.game.ammo; i++) < context.fillRect(5 * i + 20, 50, 3, 20); >context.restore(); >

Здесь мы воспользовались фичей элемента канвас — методами save() и restore() (эти методы всегда должны идти в паре). Простыми словами, метод save() как бы "замораживает" состояние контекста, чтобы все изменения, находящиеся между этими методами (save и restore) были применены только к элементам внутри этих методов. Таким образом, цвет текста, настройка теней, которую мы тут применили — отобразятся только для вывода общего количества очков и сообщений о победе/проигрыше. Попробуйте убрать методы save() и restore() и посмотрите что будет.

Давайте также увеличим скорость полета снарядов (класс Projectile, свойство speed):

this.speed = 10;

и поменяем цвет врага с зеленого на светло-зеленый (класс Angler2, свойство color):

this.color = 'lightgreen';

Игровой таймер

Добавим в нашу игру возможность следить за игровым временем.

В класс Game добавим два свойства:

this.gameTime = 0; this.timeLimit = 20 * 1000;

В gameTime будет храниться время, прошедшее с начала игры, а по истечении timeLimit будем определять выиграл или проиграл игрок в зависимости от набранных очков.

В самое начало метода update() класса Game добавим две строчки:

if (!this.gameOver) this.gameTime += deltaTime; if (this.gameTime > this.timeLimit) this.gameOver = true;

Первая строка будет увеличивать gameTime на значение deltaTime (при условии, что игра не завершена), а вторая — завершать игру, если мы достигли timeLimit.

В метод draw() класса UI добавим следующий код:

// таймер const formattedTime = (this.game.gameTime * 0.001).toFixed(1); context.fillText('Timer: ' + formattedTime, 20, 100);

Выведем значение текущего времени, переведя его в секунды и взяв только первую цифру после запятой с помощью метода toFixed().

А также исправим баг. В методе update() класса Game будем прибавлять очки за уничтоженных врагов только в том случае, если игра не завершена:

if (!this.gameOver) this.score += enemy.score;

Итого, для победы в игре необходимо успеть набрать 30 очков (уничтожить 6 врагов) за 20 секунд.

Заключение

Очень надеюсь Вам была полезна статья.

Не забываем ставить лайки и подписываться!

В следующей части напишу про графику: спредшиты, анимацию персонажей, эффекты взрывов и разлетающиеся частицы при столкновениях, а также немного физики (гравитация и вращение).

Ссылку на полную версию игры я дал в начале статьи. Что касается описанного в данной статье прототипа — "поиграть" в него можно здесь, а исходники лежат вот тут.

Всем добра и качественного кода!

  • разработка игр
  • игры для программистов
  • браузерные игры
  • 2d игры
  • javascript
  • canvas
  • программирование
  • 2d ресурсы для игр
  • ооп
  • ооп js

Пишем свою игру на JavaScript

JavaScript – очень гибкий язык, поскольку начиная с JavaScript ES5 каждый новый стандарт добавляет в язык все больше функциональности. За это приходится платить некоторыми старыми костылями, вокруг которых нужно «плясать», но преимущества все равно перевешивают – на чистом JavaScript можно написать практически что угодно, и гайд ниже вам это продемонстрирует, поскольку мы будем писать 2d игру «Змейку». Заметим, что выучить JavaScript с нуля на этом гайде не получится – все же нужны некоторые предварительные знания. Кроме того, предупредим, что нам понадобится чистый HTML (включая HTML5 canvas) и CSS, хотя 95% времени все же будет посвящено языку JavaScript.

Подготовка: HTML и CSS
Шаг предварительный: дизайн
Шаг 2: размещаем еду, перехватываем нажатия клавиш
Шаг 3: пишем основную функцию
Шаг 4: тестируем
Где взять гайды по разработке игр?

Подготовка: HTML и CSS

Перед тем, как размещать код, нам нужно создать стандартный HTML-файл index.html. Выглядеть он должен следующим образом:

Здесь нет никакой магии: в head мы указываем всю мета-информацию, выставляем правильную ширину и подключаем CSS и скрипт, в котором будет расположена игра. В теле создаем канву, которая и будет основой нашей игры.

CSS выглядит вот так:

body

Это позволяет разместить нашу канву по центру экрана.

Шаг предварительный: дизайн

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

У нас есть некоторое поле с клетками, представляющее собой двухмерный массив. Изначально в случайных ячейках появляется еда, игрок появляется в фиксированной ячейке. Для простоты примера не будем делать отдельную кнопку «Старт», игра начнется при первом нажатии на любую стрелочку. Змейка управляется через стрелочки, она двигается в направлении, соответствующем стрелочке, нажатой последней. Змейка двигается с определенной скоростью и не может остановиться. Если змейка сталкивается с едой – она «съедает» ее: еда удаляется с экрана, змейка «прирастает» на 1 сегмент, на экране в незанятой клетке появляется новая еда. Если голова змейки сталкивается с другим сегментом змейки, игра заканчивается. Если голова змейки сталкивается с краем экрана, игра заканчивается.

Окончание игры сопровождается надписью «Вы проиграли!».

Шаг 1: создаем глобальные переменные и функцию main

Учитывая, что у нас – небольшой проект, будет очень удобно создать все необходимые переменные в глобальной области видимости, после чего обращаться к ним напрямую (в больших проектах так делать не стоит, потому что может случиться так, что вы случайно перекроете область видимости). Что нам нужно? Нам нужны переменные для: размера блока в пикселях, количества рядов и строк, служебные переменные для доступа к канве и ее содержимому, размеры и скорость змейки, массив для частей змейки, координаты еды, флаг окончания игры.

var blockSize = 25; var rows = 20; var cols = 20; var board; var context; var snakeX = blockSize * 5; var snakeY = blockSize * 5; var velocityX = 0; var velocityY = 0; var snakeBody = []; var foodX; var foodY; var gameOver = false;

Опять же, с учетом того, что наш проект – маленький, будет неплохой идеей начать разработку с самой главной функции, в ней написать всю последовательность действий, после чего уже реализовывать функции более низкого уровня (вызываемые этой главной функцией). Начинать функцию будем по триггеру window.onload, в самой функции будем: 1) захватывать канву; 2) задавать размеры игрового поля в пикселях; 3) размещать еду; 4) подключать listener для нажатия кнопки; 5) реализовывать игровую механику с обработкой по фреймам. Код:

window.onload = function() < board = document.getElementById("board"); board.height = rows * blockSize; board.width = cols * blockSize; context = board.getContext("2d"); placeFood(); document.addEventListener("keyup", changeDirection); // update(); setInterval(update, 1000/10); //100 milliseconds >

1000/10 означает, что игра будет обновляться 10 раз в секунду, если хотите уменьшить/увеличить скорость – поиграйте с этим параметром.

Шаг 2: размещаем еду, перехватываем нажатия клавиш

У нас есть 2 небольшие функции, которые можно реализовать прямо сейчас – размещение еды и нажатие клавиши. С размещением еды все очень просто:

function placeFood() < //(0-1) * cols ->(0-19.9999) -> (0-19) * 25 foodX = Math.floor(Math.random() * cols) * blockSize; foodY = Math.floor(Math.random() * rows) * blockSize; >

Вызываем функцию генерации случайных чисел, через нее получаем номер блока, умножаем на ширину блока – получаем x и y новой еды.

С направлением все чуть сложнее:

function changeDirection(e) < if (e.code == "ArrowUp" && velocityY != 1) < velocityX = 0; velocityY = -1; >else if (e.code == "ArrowDown" && velocityY != -1) < velocityX = 0; velocityY = 1; >else if (e.code == "ArrowLeft" && velocityX != 1) < velocityX = -1; velocityY = 0; >else if (e.code == "ArrowRight" && velocityX != -1) < velocityX = 1; velocityY = 0; >>

Мы считаем ускорение как смещение относительно текущей точки на координатной оси на 1 или -1 (1 – это вправо или вниз, -1 – влево или вверх). Мы анализируем ввод и для каждой стрелочки меняем ускорение тем или иным образом. Проверка после && в условии нужна для того, чтобы исключить случаи, когда, например, змейка движется вправо, а мы нажали стрелочку влево – если позволить змейке поменять направление движения таким образом, то она тут же наткнется сама на себя.

Поэтому мы проверяем, что нажата стрелочка влево и при этом змейка не движется вправо.

Шаг 3: пишем основную функцию

В серьезных игровых движках, типа Unity и UE, функция update доступна для каждого элемента, и update исполняется каждый фрейм. Мы же хотим создать браузерную игру своими силами, поэтому используем

setInterval(update, 1000/10);

После чего пишем основной код в update(). Сначала отрисовываем игровое поле и блок еды:

context.fillStyle="black"; context.fillRect(0, 0, board.width, board.height); context.fillStyle="red"; context.fillRect(foodX, foodY, blockSize, blockSize); Теперь проверяем, находится ли голова змейки и еда на одной точке, если находится – еду нужно «съесть» и сгенерировать новую: if (snakeX == foodX && snakeY == foodY)

Теперь нам нужно решить проблему перемещения: каждый раз, когда змейка перемещается на блок куда-либо, ее тело должно перемещаться вместе с ней. Решается это просто – поскольку у нас все тело задано в массиве координатами каждого сегмента, нам нужно пройтись по всему массиву и сместить все элементы на 1, а нулевому элементу присвоить координаты головы в текущий момент:

for (let i = snakeBody.length-1; i > 0; i--) < snakeBody[i] = snakeBody[i-1]; >if (snakeBody.length)

Теперь реализовываем саму змейку:

context.fillStyle="lime"; snakeX += velocityX * blockSize; snakeY += velocityY * blockSize; context.fillRect(snakeX, snakeY, blockSize, blockSize); for (let i = 0; i

Наконец, нужно задать условия окончания игры – если змейка столкнулась с концом экрана или если она укусила себя. Первое проверяется через сравнение координат головы с краями канвы, второе проверяется через сравнение координат головы с координатами каждого сегмента тела:

if (snakeX < 0 || snakeX >cols*blockSize -1 || snakeY < 0 || snakeY >rows*blockSize - 1) < gameOver = true; alert("Game Over"); >for (let i = 0; i < snakeBody.length; i++) < if (snakeX == snakeBody[i][0] && snakeY == snakeBody[i][1]) < gameOver = true; alert("Game Over"); >>

Наконец, в самое начало функции нужно добавить проверку геймовера – если значение равно true, то нужно закончить игру и ничего больше не делать.

if (gameOver)

Все, имеем полностью работоспособную игру.

Шаг 4: тестируем

Все файлы нужно собрать в одной папке, после чего – открыть html-файл. Нажмите любую стрелочку – и увидите следующее:

Где взять гайды по разработке игр?

Какого-то универсального гайда по разработке игр нет – это все же творческий процесс, поэтому для разработки серьезных игр вам нужны сильные знания JavaScript и фантазия. Проще искать гайды по созданию конкретных игр и брать из этих гайдов идеи, после чего «пилить» свою собственную игру на основе этих идей. Вот вам парочка видеогайдов:

  • Канал ChrisCourses – огромное количество длинных и детальных гайдов по разработке самых сложных игр (вплоть до RPG про Покемонов)
  • Мультиплеерная игра на JavaScript с дополнительной библиотекой
  • Простой гайд по созданию игры на русском языке
  • Аналог Flappy Bird на JS
  • Игра, код которой умещается в 100 символов

Вывод

  • JS хорошо подходит для создания простых браузерных игр.
  • Вы можете самостоятельно по гайдам создавать такие простые игры, как змейку, гонки, Aliens Invaders, PacMan и другие.
  • Более сложные игры могут потребовать от вас пользоваться сторонними библиотеками.
  • В любом случае вам нужно хорошо знать JS, без этих знаний вы не сможете разрабатывать игры самостоятельно.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

https://kapelnicza.vyvod-iz-zapoya-na-domu-voronezh.ru/