Как написать простую игру на javascript

#Руководства

  • 11 окт 2019

  • 14

Современные браузеры позволяют создавать игры с полноценной графикой. Рассказываем, как написать простые гонки на JavaScript и HTML5.

 vlada_maestro / shutterstock

Евгений Кучерявый

Пишет о программировании, в свободное время создаёт игры. Мечтает открыть свою студию и выпускать ламповые RPG.

Сейчас браузеры дают JavaScript-разработчикам огромное количество возможностей для создания интересных сайтов. Раньше для этого использовался Flash — он был популярен, и на нём было создано бессчётное количество игр, плееров, необычных интерфейсов и так далее. Однако они уже не запустятся ни в одном современном браузере.

Дело в том, что технология Flash тяжеловесна, а также полна уязвимостей, поэтому от неё стали отказываться. Тем более что появилась альтернатива в виде HTML5 — в этой версии появился элемент canvas.

Canvas — это холст, на котором можно рисовать с помощью JS-команд. Его можно использовать для создания анимированных фонов, различных конструкторов и, самое главное, игр.

Из этой статьи вы узнаете, как создать браузерную игру на JavaScript и HTML5. Но прежде рекомендуем ознакомиться с объектно-ориентированным программированием в JS (достаточно понимать, что такое класс, метод и объект). Оно лучше всего подходит для создания игр, потому что позволяет работать с сущностями, а не с абстрактными данными. Однако есть и недостаток: ООП не поддерживается ни в одной из версий Internet Explorer.

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

<!DOCTYPE html>
<html>
    <head>
        <title>JS Game</title>
        <link rel="stylesheet" href="style.css">
        <meta charset="utf-8">
    </head>
    <body>
        <div class="wrapper">
            <canvas width="0" height="0" class="canvas" id="canvas">Ваш браузер не поддерживает JavaScript и HTML5!</canvas>
        </div>
        <script src="game.js"></script>
    </body>
</html>

Теперь нужно добавить стили:

body, html
{
    width: 100%;
    height: 100%;
    padding: 0px;
    margin: 0px;
    overflow: hidden;
}
 
.wrapper
{
    width: 100%;
    height: 100%;
}
 
.canvas
{
    width: 100%;
    height: 100%;
    background: #000;
}

Обратите внимание, что в HTML элементу canvas были заданы нулевые ширина и высота, в то время как в CSS указано 100%. В этом плане холст ведёт себя как изображение. У него есть фактическое и видимое разрешение.

С помощью стилей меняется видимое разрешение. Однако при этом размеры картинки останутся прежними: она просто растянется или сожмётся. Поэтому фактические ширина и высота будут указаны позже — через скрипт.

Для начала добавим заготовку скрипта для игры:

var canvas = document.getElementById("canvas"); //Получение холста из DOM
var ctx = canvas.getContext("2d"); //Получение контекста — через него можно работать с холстом
 
var scale = 0.1; //Масштаб машин
 
Resize(); // При загрузке страницы задаётся размер холста
 
window.addEventListener("resize", Resize); //При изменении размеров окна будут меняться размеры холста
 
window.addEventListener("keydown", function (e) { KeyDown(e); }); //Получение нажатий с клавиатуры
 
var objects = []; //Массив игровых объектов
var roads = []; //Массив с фонами
 
var player = null; //Объект, которым управляет игрок, — тут будет указан номер объекта в массиве objects
 
function Start()
{
    timer = setInterval(Update, 1000 / 60); //Состояние игры будет обновляться 60 раз в секунду — при такой частоте обновление происходящего будет казаться очень плавным
}
 
function Stop()
{
    clearInterval(timer); //Остановка обновления
}
 
function Update() //Обновление игры
{
    Draw();
}
 
function Draw() //Работа с графикой
{
    ctx.clearRect(0, 0, canvas.width, canvas.height); //Очистка холста от предыдущего кадра
}
 
function KeyDown(e)
{
    switch(e.keyCode)
    {
        case 37: //Влево
            break;
 
        case 39: //Вправо
            break;
 
        case 38: //Вверх
            break;
 
        case 40: //Вниз
            break;
 
        case 27: //Esc
            break;
    }
}
 
function Resize()
{
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
}

В этом скрипте есть всё, что необходимо для создания игры: данные (массивы), функции обновления, прорисовки и управления. Остаётся только дополнить это основной логикой. То есть указать, как именно объекты будут себя вести и как будут выводиться на холст.

Во время вызова функции Update() будут меняться состояния игровых объектов. После этого они отрисовываются на canvas с помощью функции Draw(). То есть на самом деле мы не двигаем объекты на холсте — мы рисуем их один раз, потом меняем координаты, стираем старое изображение и выводим объекты с новыми координатами. Всё это происходит так быстро, что создаётся иллюзия движения.

Рассмотрим это на примере дороги.

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

Для этого создадим класс Road:

class Road
{
    constructor(image, y)
    {
        this.x = 0;
        this.y = y;
 
        this.image = new Image();
        
        this.image.src = image;
    }
 
    Update(road) 
    {
        this.y += speed; //При обновлении изображение смещается вниз
 
        if(this.y > window.innerHeight) //Если изображение ушло за край холста, то меняем положение
        {
            this.y = road.y - this.image.height + speed; //Новое положение указывается с учётом второго фона
        }
    }
}

В массив с фонами добавляются два объекта класса Road:

var roads = 
[
    new Road("images/road.jpg", 0),
    new Road("images/road.jpg", 626)
]; //Массив с фонами

Теперь можно изменить функцию Update(), чтобы положение изображений менялось с каждым кадром.

function Update() //Обновление игры
{
    roads[0].Update(roads[1]);
    roads[1].Update(roads[0]);
 
    Draw();
}

Остаётся только добавить вывод этих изображений:

function Draw() //Работа с графикой
{
    ctx.clearRect(0, 0, canvas.width, canvas.height); //Очистка холста от предыдущего кадра
 
    for(var i = 0; i < roads.length; i++)
    {
        ctx.drawImage
        (
            roads[i].image, //Изображение для отрисовки
            0, //Начальное положение по оси X на изображении
            0, //Начальное положение по оси Y на изображении
            roads[i].image.width, //Ширина изображения
            roads[i].image.height, //Высота изображения
            roads[i].x, //Положение по оси X на холсте
            roads[i].y, //Положение по оси Y на холсте
            canvas.width, //Ширина изображения на холсте
            canvas.width //Так как ширина и высота фона одинаковые, в качестве высоты указывается ширина
        );
    }
}

Теперь можно посмотреть, как это работает в игре:

Пора добавить игрока и NPC. Для этого нужно написать класс Car. В нём будет метод Move(), с помощью которого игрок управляет своим автомобилем. Движение NPC будет осуществляться с помощью Update(), в котором просто меняется координата Y.

class Car
{
    constructor(image, x, y)
    {
        this.x = x;
        this.y = y;
 
        this.image = new Image();
 
        this.image.src = image;
    }
 
    Update()
    {
        this.y += speed;
    }
 
    Move(v, d) 
    {
        if(v == "x") //Перемещение по оси X
        {
            this.x += d; //Смещение
 
            //Если при смещении объект выходит за края холста, то изменения откатываются
            if(this.x + this.image.width * scale > canvas.width)
            {
                this.x -= d; 
            }
    
            if(this.x < 0)
            {
                this.x = 0;
            }
        }
        else //Перемещение по оси Y
        {
            this.y += d;
 
            if(this.y + this.image.height * scale > canvas.height)
            {
                this.y -= d;
            }
 
            if(this.y < 0)
            {
                this.y = 0;
            }
        }
        
    }
}

Создадим первый объект, чтобы проверить.

var objects = 
[
    new Car("images/car.png", 15, 10)
]; //Массив игровых объектов
var player = 0; //номер объекта, которым управляет игрок

Теперь в функцию Draw() нужно добавить команду отрисовки автомобилей.

for(var i = 0; i < objects.length; i++)
{
    ctx.drawImage
    (
        objects[i].image, //Изображение для отрисовки
        0, //Начальное положение по оси X на изображении
        0, //Начальное положение по оси Y на изображении
        objects[i].image.width, //Ширина изображения
        objects[i].image.height, //Высота изображения
        objects[i].x, //Положение по оси X на холсте
        objects[i].y, //Положение по оси Y на холсте
        objects[i].image.width * scale, //Ширина изображения на холсте, умноженная на масштаб
        objects[i].image.height * scale //Высота изображения на холсте, умноженная на масштаб
    );
}

В функцию KeyDown(), которая вызывается при нажатии на клавиатуру, нужно добавить вызов метода Move().

function KeyDown(e)
{
    switch(e.keyCode)
    {
        case 37: //Влево
            objects[player].Move("x", -speed);
            break;
 
        case 39: //Вправо
            objects[player].Move("x", speed);
            break;
 
        case 38: //Вверх
            objects[player].Move("y", -speed);
            break;
 
        case 40: //Вниз
            objects[player].Move("y", speed);
            break;
 
        case 27: //Esc
            if(timer == null)
            {
                Start();
            }
            else
            {
                Stop();
            }
            break;
    }
}

Теперь можно проверить отрисовку и управление.

Следующий шаг — добавление машин. Они будут создаваться на ходу и удаляться, когда зайдут за край.

Для этого понадобится функция генерации случайных чисел:

function RandomInteger(min, max) 
{
    let rand = min - 0.5 + Math.random() * (max - min + 1);
    return Math.round(rand);
}

С её помощью в функции Update() с определённой вероятностью будет создаваться объект и добавляться в массив objects:

if(RandomInteger(0, 10000) > 9700)
{
    objects.push(new Car("images/car_red.png", RandomInteger(30, canvas.width - 50), RandomInteger(250, 400) * -1));
}

Теперь можно увидеть, как новые машины появляются вверху экрана:

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

В класс Car добавляем поле dead со значением false, а потом меняем его в методе Update():

if(this.y > canvas.height + 50)
{
    this.dead = true;
}

Теперь нужно изменить функцию обновления игры, заменив там код, связанный с объектами:

var hasDead = false;
 
for(var i = 0; i < objects.length; i++)
{
    if(i != player)
    {
        objects[i].Update();
 
        if(objects[i].dead)
        {
            hasDead = true;
        }
    }
}
 
if(hasDead)
{
    objects.shift();
}

Если не удалять объекты, то игра начнёт тормозить компьютер, когда будет сгенерировано слишком много машин.

Теперь можно приступить к реализации коллизии (англ. collision — столкновение). Для это нужно написать для класса Car метод Collide(), в котором будут проверяться координаты машин:

Collide(car)
{
    var hit = false;
 
    if(this.y < car.y + car.image.height * scale && this.y + this.image.height * scale > car.y) //Если объекты находятся на одной линии по горизонтали
    {
        if(this.x + this.image.width * scale > car.x && this.x < car.x + car.image.width * scale) //Если объекты находятся на одной линии по вертикали
        {
            hit = true;
        }
    }
 
    return hit;
}

Теперь нужно в функцию Update() добавить проверку коллизии:

var hit = false;
 
for(var i = 0; i < objects.length; i++)
{
    if(i != player)
    {
        hit = objects[player].Collide(objects[i]);
 
        if(hit)
        {
            alert("Вы врезались!");
            Stop();
            break;
        }
    }
}

Вот что будет в игре:

В момент коллизии можно добавить любую логику:

  • включение анимации;
  • добавление эффекта;
  • удаление объекта;
  • изменение здоровья и так далее.

Всё это остаётся на усмотрение разработчика.

Получилась довольно простая игра, но этого вполне достаточно, чтобы разобраться, как в JS можно работать с графикой и как в целом создаются игры. Изображения и полный код игры можно найти в репозитории на GitHub.

Использование canvas хорошо подходит для работы с графикой: он даёт большие возможности и не сильно загружает браузер. Также сейчас разработчикам доступна библиотека WebGL (примеры и использование), с помощью которой можно значительно повысить производительность и работать с 3D (canvas так не может).

Разобраться в WebGL может быть непросто — вероятно, вместо этого многим интереснее попробовать движок Unity, который умеет компилировать проекты для их запуска в браузере.

Учись бесплатно:
вебинары по программированию, маркетингу и дизайну.

Участвовать

В этой статье мы создадим простую игру с помощью HTML5, CSS3 и чистого JavaScript. Вам не понадобятся глубокие знания программирования. Если вы знаете, для чего нужны HTML, CSS и JS, то этого более чем достаточно. На работу игры вы можете посмотреть здесь.

Структура файлов

Начнём с создания нужных папок и файлов:

$ mkdir memory-game
$ cd memory-game
$ touch index.html styles.css scripts.js
$ mkdir img

HTML

Начальный шаблон, соединяющий CSS- и JS-файлы:

<!-- index.html -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">

  <title>Memory Game</title>

  <link rel="stylesheet" href="./styles.css">
</head>
<body>
  <script src="./scripts.js"></script>
</body>
</html>

В игре будет 12 карточек. Каждая карта состоит из контейнера div с классом .memory-card, внутри которого находится два элемента img. Первая отвечает за лицо (front-face) карточки, а вторая — за рубашку (back-face).

<div class="memory-card">
  <img class="front-face" src="img/react.svg" alt="React">
  <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
</div>

Необходимые изображения можно скачать из репозитория проекта.

Обернём набор карточек в контейнер section. В итоге получаем:

<!-- index.html -->

<section class="memory-game">
  <div class="memory-card">
    <img class="front-face" src="img/react.svg" alt="React">
    <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
  </div>

  <div class="memory-card">
    <img class="front-face" src="img/react.svg" alt="React">
    <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
  </div>

  <div class="memory-card">
    <img class="front-face" src="img/angular.svg" alt="Angular">
    <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
  </div>

  <div class="memory-card">
    <img class="front-face" src="img/angular.svg" alt="Angular">
    <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
  </div>

  <div class="memory-card">
    <img class="front-face" src="img/ember.svg" alt="Ember">
    <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
  </div>

  <div class="memory-card">
    <img class="front-face" src="img/ember.svg" alt="Ember">
    <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
  </div>

  <div class="memory-card">
    <img class="front-face" src="img/vue.svg" alt="Vue">
    <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
  </div>

  <div class="memory-card">
    <img class="front-face" src="img/vue.svg" alt="Vue">
    <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
  </div>

  <div class="memory-card">
    <img class="front-face" src="img/backbone.svg" alt="Backbone">
    <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
  </div>

  <div class="memory-card">
    <img class="front-face" src="img/backbone.svg" alt="Backbone">
    <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
  </div>

  <div class="memory-card">
    <img class="front-face" src="img/aurelia.svg" alt="Aurelia">
    <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
  </div>

  <div class="memory-card">
    <img class="front-face" src="img/aurelia.svg" alt="Aurelia">
    <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
  </div>
</section>

CSS

Мы используем простой, но очень полезный сброс стилей, который будет применён ко всем элементам:

/* styles.css */

* {
  padding: 0;
  margin: 0;
  box-sizing: border-box;
}i

Свойство box-sizing: border-box учитывает значения внутренних отступов и границ элемента при подсчёте общей высоты и ширины, поэтому нам не нужно заниматься математикой.

Если применить к body свойство display: flex и margin: auto к контейнеру .memory-game, то он будет выровнен вертикально и горизонтально.

.memory-game также будет flex-контейнером. По умолчанию ширина элементов уменьшается, чтобы они помещались в контейнер. Если присвоить свойству flex-wrap значение wrap, элементы будут располагаться на нескольких строках в соответствии с их размерами:

/* styles.css */

body {
  height: 100vh;
  display: flex;
  background: #060AB2;
}

.memory-game {
  width: 640px;
  height: 640px;
  margin: auto;
  display: flex;
  flex-wrap: wrap;
}

Ширина и высота каждой карточки подсчитывается с помощью CSS-функции calc(). Создадим три ряда по четыре карточки, установив значения ширины и высоты равными 25% и 33.333% соответственно минус 10px от внешнего отступа.

Чтобы разместить наследников .memory-card, добавим position: relative. Так мы сможем абсолютно расположить наследников относительно родительского элемента.

Смотрите также: Вредные советы по CSS

Свойство position: absolute, установленное для .front-face и .back-face, уберёт элементы с их исходных позиций и разместит поверх друг друга:

/* styles.css */

.memory-card {
  width: calc(25% - 10px);
  height: calc(33.333% - 10px);
  margin: 5px;
  position: relative;
  box-shadow: 1px 1px 1px rgba(0,0,0,.3);
}

.front-face,
.back-face {
  width: 100%;
  height: 100%;
  padding: 20px;
  position: absolute;
  border-radius: 5px;
  background: #1C7CCC;
}

Поле из карточек должно выглядеть примерно так:

Добавим ещё эффект при клике. Псевдокласс :active будет срабатывать при каждом нажатии на элемент. Он устанавливает длительность анимации равной 0.2 с:

.memory-card {
  width: calc(25% - 10px);
  height: calc(33.333% - 10px);
  margin: 5px;
  position: relative;
  transform-style: preserve-3d;
  box-shadow: 1px 1px 0 rgba(0, 0, 0, .3);
  transform: scale(1);
}

.memory-card:active {
  transform: scale(0.97);
  transition: transform .2s;
}

Переворачиваем карточки

Чтобы перевернуть карточку после нажатия, добавим класс flip. Для этого давайте выберем все элементы memory-card с помощью document.querySelectorAll(). Затем пройдёмся по ним в forEach-цикле и добавим обработчики событий. При каждом нажатии на карточку будет вызываться функция flipCard(). this отвечает за нажатую карточку. Функция получает доступ к списку классов элемента и активирует класс flip:

// scripts.js
const cards = document.querySelectorAll('.memory-card');

function flipCard() {
  this.classList.toggle('flip');
}

cards.forEach(card => card.addEventListener('click', flipCard));

CSS класс flip переворачивает карточку на 180 градусов:

.memory-card.flip {
  transform: rotateY(180deg);
}

Смотрите также: Детальный список инструментов для JavaScript

Чтобы создать 3D-эффект переворота, добавим свойство perspective в .memory-game. Это свойство отвечает за расстояние между объектом и пользователем в z-плоскости. Чем ниже значение, тем сильнее эффект. Установим значение 1000px для едва уловимого эффекта:

.memory-game {
  width: 640px;
  height: 640px;
  margin: auto;
  display: flex;
  flex-wrap: wrap;
  perspective: 1000px;
}

Добавим к элементам .memory-card свойство transform-style: preserve-3d, чтобы поместить их в 3D-пространство, созданное в родителе, вместо того, чтобы ограничивать их плоскостью z = 0 (transform-style):

.memory-card {
  width: calc(25% - 10px);
  height: calc(33.333% - 10px);
  margin: 5px;
  position: relative;
  box-shadow: 1px 1px 1px rgba(0,0,0,.3);
  transform: scale(1);
  transform-style: preserve-3d;
}

Теперь мы можем применить transition к свойству transform, чтобы создать эффект движения:

.memory-card {
  width: calc(25% - 10px);
  height: calc(33.333% - 10px);
  margin: 5px;
  position: relative;
  box-shadow: 1px 1px 1px rgba(0,0,0,.3);
  transform: scale(1);
  transform-style: preserve-3d;
  transition: transform .5s;
}

Отлично, теперь карточки переворачиваются в 3D! Но почему мы не видим лицо карточки? На данный момент .front-face и .back-face наложены друг на друга из-за абсолютного позиционирования. Рубашкой каждого элемента является зеркальное отражение его лица. По умолчанию значение свойства backface-visibility равно visible, поэтому вот что мы видим при перевороте карточки:

Чтобы исправить это, применим свойство backface-visibility: hidden для .front-face и .back-face:

.front-face,
.back-face {
  width: 100%;
  height: 100%;
  padding: 20px;
  position: absolute;
  border-radius: 5px;
  background: #1C7CCC;
  backface-visibility: hidden;
}

Если перезагрузить страницу и снова перевернуть карточку, она пропадёт!

Так как мы скрыли заднюю сторону обеих картинок, на обратной стороне ничего нет. Поэтому сейчас нам нужно перевернуть .front-face на 180 градусов:

.front-face {
  transform: rotateY(180deg);
}

Наконец, мы получили желаемый эффект переворота!

Ищем пару

Мы научились переворачивать карточки, теперь нужно разобраться с проверкой на совпадение.

После нажатия на первую карточку она ожидает переворота другой. Переменные hasFlippedCard и flippedCard будут отвечать за состояние переворота. Если ни одна карточка не перевёрнута, значение hasFlippedCard устанавливается равным true, а нажатой карточке присваивается flippedCard. Ещё давайте сменим метод toggle() на add():

 const cards = document.querySelectorAll('.memory-card');

 let hasFlippedCard = false;
 let firstCard, secondCard;

  function flipCard() {
   this.classList.add('flip');

   if (!hasFlippedCard) {
     hasFlippedCard = true;
     firstCard = this;
   }
  }

cards.forEach(card => card.addEventListener('click', flipCard));

Теперь при нажатии на вторую карточку мы попадаем в else-блок нашего условия. Чтобы проверить, совпадают ли карточки, нужно их всех идентифицировать.

Всякий раз, когда нам нужно добавить дополнительную информацию к HTML-элементам, мы можем использовать data-* атрибуты, где вместо «*» может быть любое слово. Добавим каждой карточке атрибут data-framework:

<section class="memory-game">
  <div class="memory-card" data-framework="react">
    <img class="front-face" src="img/react.svg" alt="React">
    <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
  </div>

  <div class="memory-card" data-framework="react">
    <img class="front-face" src="img/react.svg" alt="React">
    <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
  </div>

  <div class="memory-card" data-framework="angular">
    <img class="front-face" src="img/angular.svg" alt="Angular">
    <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
  </div>

  <div class="memory-card" data-framework="angular">
    <img class="front-face" src="img/angular.svg" alt="Angular">
    <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
  </div>

  <div class="memory-card" data-framework="ember">
    <img class="front-face" src="img/ember.svg" alt="Ember">
    <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
  </div>

  <div class="memory-card" data-framework="ember">
    <img class="front-face" src="img/ember.svg" alt="Ember">
    <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
  </div>

  <div class="memory-card" data-framework="vue">
    <img class="front-face" src="img/vue.svg" alt="Vue">
    <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
  </div>

  <div class="memory-card" data-framework="vue">
    <img class="front-face" src="img/vue.svg" alt="Vue">
    <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
  </div>

  <div class="memory-card" data-framework="backbone">
    <img class="front-face" src="img/backbone.svg" alt="Backbone">
    <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
  </div>

  <div class="memory-card" data-framework="backbone">
    <img class="front-face" src="img/backbone.svg" alt="Backbone">
    <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
  </div>

  <div class="memory-card" data-framework="aurelia">
    <img class="front-face" src="img/aurelia.svg" alt="Aurelia">
    <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
  </div>

  <div class="memory-card" data-framework="aurelia">
    <img class="front-face" src="img/aurelia.svg" alt="Aurelia">
    <img class="back-face" src="img/js-badge.svg" alt="Memory Card">
  </div>
</section>

Теперь мы можем проверить, совпадают ли карточки, с помощью свойства dataset. Поместим логику сравнения в метод checkForMatch() и снова присвоим переменной hasFlippedCard значение false. В случае совпадения будет вызван метод disableCards() и обработчики событий будут откреплены от обеих карточек, чтобы предотвратить их переворот. В противном случае метод unflipCards() перевернёт обе карточки с помощью 1500 мс тайм-аута, который удалит класс .flip:

function checkForMatch() {
    if (firstCard.dataset.framework === secondCard.dataset.framework) {
      disableCards();
      return;
    }
 
    unflipCards();
  }

Складываем всё воедино:

  const cards = document.querySelectorAll('.memory-card');

  let hasFlippedCard = false;
  let firstCard, secondCard;

  function flipCard() {
    this.classList.add('flip');

    if (!hasFlippedCard) {
      hasFlippedCard = true;
      firstCard = this;
      return;
    }
 
    secondCard = this;
    hasFlippedCard = false;
 
    checkForMatch();
  }
 
  function checkForMatch() {
    if (firstCard.dataset.framework === secondCard.dataset.framework) {
      disableCards();
      return;
    }
 
    unflipCards();
  }
 
  function disableCards() {
    firstCard.removeEventListener('click', flipCard);
    secondCard.removeEventListener('click', flipCard);
  }
 
  function unflipCards() {
    setTimeout(() => {
      firstCard.classList.remove('flip');
      secondCard.classList.remove('flip');
    }, 1500);
  }

  cards.forEach(card => card.addEventListener('click', flipCard));

Более элегантный способ написать условие совпадения — тернарный оператор. Он состоит из трёх частей. Первая часть — это условие, вторая часть выполняется, если условие возвращает true, в противном случае выполняется третья часть:

  let isMatch = firstCard.dataset.name === secondCard.dataset.name;
  isMatch ? disableCards() : unflipCards()

Блокируем поле

Мы научились проверять, совпадают ли карточки, а теперь нужно заблокировать поле. Это нужно для того, чтобы два набора карточек не могли быть перевёрнуты одновременно, в противном карточки не будут переворачиваться обратно.

Объявим переменную lockBoard. Когда игрок нажмёт на вторую карточку, lockBoard будет присвоено значение true, а условие if (lockBoard) return; предотвратит переворот других карточек до того, как эти две будут спрятаны или совпадут:

  const cards = document.querySelectorAll('.memory-card');

  let hasFlippedCard = false;
  let lockBoard = false;
  let firstCard, secondCard;

  function flipCard() {
    if (lockBoard) return;
    this.classList.add('flip');

    if (!hasFlippedCard) {
      hasFlippedCard = true;
      firstCard = this;
      return;
    }

    secondCard = this;
    hasFlippedCard = false;

    checkForMatch();
  }

  function checkForMatch() {
    let isMatch = firstCard.dataset.name === secondCard.dataset.name;
    isMatch ? disableCards() : unflipCards();
  }

  function disableCards() {
    firstCard.removeEventListener('click', flipCard);
    secondCard.removeEventListener('click', flipCard);
  }

  function unflipCards() {
      lockBoard = true;

    setTimeout(() => {
      firstCard.classList.remove('flip');
      secondCard.classList.remove('flip');

      lockBoard = false;
    }, 1500);
  }

  cards.forEach(card => card.addEventListener('click', flipCard));

Нажатие на ту же карточку

У нас всё ещё есть сценарий, при котором после нажатия на одну карточку дважды условие совпадения будет выполнено и обработчик событий будет удалён.

Чтобы избежать этого, добавим проверку на то, равняется ли нажатая карточка переменной firstCard, и вёрнемся из функции, если это так:

if (this === firstCard) return;

Переменные firstCard и secondCard нужно обнулять после каждого раунда. Реализуем эту логику в новом методе resetBoard(). Поместим в него hasFlippedCard = false и lockBoard = false. Деструктурирующее присваивание [var1, var2] = ['value1', 'value2'] из ES6 позволяет писать код меньших размеров:

function resetBoard() {
  [hasFlippedCard, lockBoard] = [false, false];
  [firstCard, secondCard] = [null, null];
}

Новый метод будет вызываться из disableCards() и unflipCards():

  const cards = document.querySelectorAll('.memory-card');
  let hasFlippedCard = false;
  let lockBoard = false;
  let firstCard, secondCard;

  function flipCard() {
    if (lockBoard) return;
    if (this === firstCard) return;

    this.classList.add('flip');

    if (!hasFlippedCard) {
      hasFlippedCard = true;
      firstCard = this;
      return;
    }

    secondCard = this;

    checkForMatch();
  }

  function checkForMatch() {
    let isMatch = firstCard.dataset.name === secondCard.dataset.name;
    isMatch ? disableCards() : unflipCards();
  }

  function disableCards() {
    firstCard.removeEventListener('click', flipCard);
    secondCard.removeEventListener('click', flipCard);

    resetBoard();
  }

  function unflipCards() {
    lockBoard = true;

    setTimeout(() => {
      firstCard.classList.remove('flip');
      secondCard.classList.remove('flip');

      resetBoard();
    }, 1500);
  }

  function resetBoard() {
    [hasFlippedCard, lockBoard] = [false, false];
    [firstCard, secondCard] = [null, null];
  }

  cards.forEach(card => card.addEventListener('click', flipCard));

Перемешивание

Наша игра выглядит довольно неплохо, но играть в неё не очень весело, если карточки всегда на одном месте. Пора это исправить.

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

В игре есть 12 карточек, поэтому мы пройдёмся по ним в цикле, сгенерируем случайное число и присвоим его свойству order. Например пусть будут сгенерированы числа в диапазоне от 0 до 12:

function shuffle() {
  cards.forEach(card => {
    let ramdomPos = Math.floor(Math.random() * 12);
    card.style.order = ramdomPos;
  });
}

Чтобы вызвать функцию shuffle(), сделаем её IIFE (Immediately Invoked Function Expression). Это значит, что она будет выполнена сразу после объявления. Скрипт должен иметь примерно такой вид:

  const cards = document.querySelectorAll('.memory-card');

  let hasFlippedCard = false;
  let lockBoard = false;
  let firstCard, secondCard;

  function flipCard() {
    if (lockBoard) return;
    if (this === firstCard) return;

    this.classList.add('flip');

    if (!hasFlippedCard) {
      hasFlippedCard = true;
      firstCard = this;
      return;
    }

    secondCard = this;
    lockBoard = true;

    checkForMatch();
  }

  function checkForMatch() {
    let isMatch = firstCard.dataset.name === secondCard.dataset.name;
    isMatch ? disableCards() : unflipCards();
  }

  function disableCards() {
    firstCard.removeEventListener('click', flipCard);
    secondCard.removeEventListener('click', flipCard);

    resetBoard();
  }

  function unflipCards() {
    setTimeout(() => {
      firstCard.classList.remove('flip');
      secondCard.classList.remove('flip');

      resetBoard();
    }, 1500);
  }

  function resetBoard() {
    [hasFlippedCard, lockBoard] = [false, false];
    [firstCard, secondCard] = [null, null];
  }

  (function shuffle() {
    cards.forEach(card => {
      let ramdomPos = Math.floor(Math.random() * 12);
      card.style.order = ramdomPos;
    });
  })();

  cards.forEach(card => card.addEventListener('click', flipCard));

Вот и всё!

Перевод статьи «Memory Game in Vanilla JavaScript»

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

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

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

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <meta http-equiv="X-UA-Compatible" content="ie=edge">
 <title>Flappy Bird!</title>
</head>
<body>

 <canvas id="canvas" width="288" height="512"></canvas>

 <script src="js/game.js"></script>
</body>
</html>

В 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].x + pipeUp.width
 && (yPos <= pipe[i].y + pipeUp.height
 || yPos + bird.height >= 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;

Современная вычислительная техника позволяет создавать классные компьютерные игры! И сейчас, достаточно популярны игры с 3d-графикой, так как, играя в них, ты окунаешься в вымышленный мир и теряешь всякую связь с реальностью. Развитие интернета и браузерных технологий сделало возможным запускать головоломки и стрелялки в любимом Хроме, Мозилле или еще в чем-то там (про Эксплорер помолчим) в онлайн-режиме, без загрузки. Так вот, здесь я расскажу о том, как создать простую трехмерную браузерную игру.

Выбор жанра, сюжета и стилистики игры является достаточно интересной задачей, и от решения этих вопросов может зависеть успех игры. Кроме этого, свои нюансы вносит и выбор технологии, на основе которой будет создаваться продукт. Моя цель – показать элементарные основы этого увлекательного процесса, поэтому я буду делать 3-мерный лабиринт с незамысловатым оформлением. Более того, я это сделаю на чистом коде без использования библиотек и движков, типа three.js (хотя большие проекты лучше делать все-таки на нем), чтобы показать, как можно создать движок для своих нужд. Полностью самописная игра может быть оригинальной, а потому интересной. В общем, оба подхода имеют свои плюсы и минусы.

Я полагаю, если вы читаете эту статью, то вам интересна тема создания игр для гугл Хром, а, значит, понимаете, как работает связка html-css-javaScript, поэтому не буду останавливаться на основах, а сразу приступлю к разработке. В html5 и css3, которые поддерживают все современные браузеры (Эксплорер не в счет), есть возможность расположения блоков в 3-мерном пространстве. Также есть элемент , в котором можно рисовать линии и графические примитивы. Большинство браузерных движков используют <сanvas>, так как на нем можно сделать больше вещей, да и производительность на нем выше. Но для простых вещей вполне можно использовать методы transform-3d, которые будут занимать меньше кода.

1. Инструменты для разработки

Я использую для проверки сайтов и игр только 2 браузера: Chrome и Mozilla. Все остальные браузеры (кроме того самого Эксплорера) построены на движке первого, поэтому использовать их я не вижу смысла, ибо результаты точно такие же, как и в Chrome. Для написания кода достаточно Notepad++.

2. Как реализуется трехмерное пространство в html?

Посмотрим на систему координат блока:

По умолчанию, дочерний блок имеет координаты (left и top) 0 пикселей по x и 0 пикселей по y. Смещение (translate), также 0 пикселей по всем трем осям. Покажем это на примере, для чего создадим новую папку. В нем создадим файлы index.html, style.css и script.js. Откроем index.html и запишем туда следующее:

<!DOCTYPE HTML>
<HTML>
<HEAD>
	<TITLE>Игра</TITLE>
	<LINK rel="stylesheet" href="style.css">
	<meta charset="utf-8">
</HEAD>
<BODY>
	<div id="container">
		<div id="world">
        </div>
	</div>
</BODY>
</HTML>
<script src="script.js"></script>

В файле style.css зададим стили для элементов “container” и “world”.

#container{
	position:absolute;
	width:1200px;
	height:800px;
	border:2px solid #000000;
}
#world{
	width:300px;
	height:300px;
        background-color:#C0FFFF;
}

Сохраним. Откроем index.html c помощью Chrome, получим:

Попробуем применить translate3d к элементу “world”:

#world{
	width:300px;
	height:300px;
        background-color:#C0FFFF;
        transform:translate3d(200px,100px,0px);
}

Как вы поняли, я перешел в полноэкранный режим. Теперь зададим смещение по оси Z:
transform:translate3d(200px,100px,-1000px);

Если вы снова откроете html-файл в браузере, то никаких изменений вы не увидите. Чтобы увидеть изменения, нужно задать перспективу для объекта “container”:

#container{
	position:absolute;
	width:1200px;
	height:800px;
	border:2px solid #000000;
	perspective:600px;
}

В результате:

Квадрат отдалился от нас. Как работает перспектива в html? Взглянем на картинку:

d – расстояние от пользователя до объекта, а z – его координата. Отрицательный z (в html это translateZ) означает, что мы отдалили объект, а положительный – наоборот. Значение perspective определяет величину d. Если же свойство perspective не задано, то значение d принимается за бесконечность, а в этом случае объект визуально не изменяется для пользователя с изменением z. В нашем случае мы задали d = 600px. По умолчанию, точка взгляда перспективы находится в центре элемента, однако ее можно изменить путем задания свойства perspective-origin: .

Теперь повернем “world” вокруг какой-нибудь оси. В сss можно использовать 2 способа вращения. Первый – вращение вокруг осей x,y и z. Для этого используются transform-свойства rotateX(), rotateY() и rotateZ(). Второй – вращение вокруг заданной оси с помощью свойства rotate3d(). Мы будем использовать первый способ, так как он больше подходит для наших задач. Обратите внимание, что оси вращения выходят из центра прямоугольника!

Точка, относительно которой происходят трансформации, может быть изменена путем задания свойства translate-origin: . Итак, зададим вращение “world” по оси x:

#world{
	width:300px;
	height:300px;
background-color:#C0FFFF;
transform:translate3d(200px,100px,0px) rotateX(45deg);
}

Получим:

Заметно смещение против часовой стрелки. Если же мы добавим rotateY(), то получим смещение уже по оси Y. Важно заметить, что при вращении блока оси вращения также поворачиваются. Вы также можете поэкспериментировать с различными значениями вращения.
Теперь внутри блока “world” создадим еще один блок, для этого добавим тег в html-файл:

<!DOCTYPE HTML>
<HTML>
<HEAD>
	<TITLE>Игра</TITLE>
	<LINK rel="stylesheet" href="style.css">
	<meta charset="utf-8">
</HEAD>
<BODY>
	<div id="container">
		<div id="world">
			<div id="square1"></div>
		</div>
	</div>
</BODY>
</HTML>
<script src="script.js"></script>

В style.css добавим стили к этому блоку:

#square1{
	position:absolute;
	width:200px;
	height:200px;
	background-color:#FF0000;
}

Получим:

То есть, элементы внутри блока “world” будут трансформироваться в составе этого блока. Попробуем повернуть “square1” по оси y, добавив к нему стиль вращения:
transform: rotateY(30deg);

В итоге:

«Где вращение?» — спросите вы? На самом деле именно так выглядит проекция блока “square1” на плоскость, образуемую элементом “world”. Но нам нужна не проекция, а настоящее вращение. Чтобы все элементы внутри “world” стали объемными, необходимо применить к нему свойство transform-style:preserve-3d. После подстановки свойства внутрь списка стилей “world” проверим изменения:

Отлично! Половина блока “square” скрылась за голубым блоком. Чтобы его полностью показать, уберем цвет блока “world”, а именно, удалим строку background-color:#C0FFFF; Если мы добавим еще прямоугольников внутрь блока “world”, то мы можем создать трехмерный мир. Сейчас же уберем смещение мира “world”, удалив строку со свойством transform в стилях для этого элемента.

3. Создаем движение в трехмерном мире

Для того, чтобы пользователь мог по этому миру передвигаться, нужно задать обработчики нажатия клавиш и перемещения мыши. Управление будет стандартным, какое присутствует в большинстве 3д-шутеров. Клавишами W, S, A, D мы будем перемещаться вперед, назад, влево, вправо, пробелом мы будем прыгать (проще говоря – перемещаться вверх), а мышью мы будем менять направление взгляда. Для этого откроем пока еще пустой файл script.js. Сначала впишем туда такие переменные:

// Нажата ли клавиша?

var PressBack = 0;
var PressForward = 0;
var PressLeft = 0;
var PressRight = 0;
var PressUp = 0;

Изначально клавиши не нажаты. Если мы нажмем клавишу, то значение определенной переменной изменится на 1. Если отпустим ее, то она снова станет 0. Реализуем это посредством добавления обработчиков нажатия и отжатия клавиш:

// Обработчик нажатия клавиш

document.addEventListener("keydown", (event) =>{
	if (event.key == "a"){
		PressLeft = 1;
	}
	if (event.key == "w"){
		PressForward = 1;
	}
	if (event.key == "d"){
		PressRight = 1;
	}
	if (event.key == "s"){
		PressBack = 1;
	}
	if (event.keyCode == 32 && onGround){
		PressUp = 1;
	}
});

// Обработчик отжатия клавиш

document.addEventListener("keyup", (event) =>{
	if (event.key == "a"){
		PressLeft = 0;
	}
	if (event.key == "w"){
		PressForward = 0;
	}
	if (event.key == "d"){
		PressRight = 0;
	}
	if (event.key == "s"){
		PressBack = 0;
	}
	if (event.keyCode == 32){
		PressUp = 0;
	}
});

Номер 32 – код пробела. Как видите, тут появилась переменная onGround, указывающая на то, находимся ли мы на земле. Пока разрешим движение вверх, добавив после переменных press… переменную onGround:

// На земле ли игрок?

var onGround = true;

Итак, мы добавили алгоритм нажатия и отжатия. Теперь необходимо добавить само передвижение. Что, собственно, мы передвигаем. Представим, что у нас есть объект, который мы двинаем. Назовем его “pawn”. Как и принято у нормальных разработчиков, для него мы создадим отдельный класс “Player”. Классы в javaScript создаются, как ни странно, с помощью функций:

function player(x,y,z,rx,ry) {
	this.x = x;
	this.y = y;
	this.z = z;
	this.rx = rx;
	this.ry = ry;
}

Вставим этот код в script.js в самом начале файла. В конце же файла создадим объект данного типа:

// Создаем новый объект

var pawn = new player(0,0,0,0,0);

Распишем, что означают эти переменные. x, y, z – это начальные координаты игрока, rx, ry – углы его поворота относительно осей x и y в градусах. Последняя записанная строка означает, что мы создаем объект “pawn” типа “player” (специально пишу тип, а не класс, так как классы в javascript означают несколько другие вещи) с нулевыми начальными координатами. Когда мы двигаем объект, координата мира изменяться не должна, а должна изменяться координата «pawn». Это с точки зрения переменных. А с точки зрения пользователя, игрок находится на одном месте, а вот мир двигается. Таким образом, нужно заставить программу изменять координаты игрока, обрабатывать эти изменения и двигать, в конце концов, мир. На деле это проще, чем кажется.

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

function update(){
	
	// Высчитываем смещения
	
	let dx = (PressRight - PressLeft);
	let dz = - (PressForward - PressBack);
	let dy = PressUp;
	
	// Прибавляем смещения к координатам
	
	pawn.x = pawn.x + dx;
	pawn.y = pawn.y + dy;
	pawn.z = pawn.z + dz;
	
	// Изменяем координаты мира (для отображения)
	
	world.style.transform = 
	"rotateX(" + (-pawn.rx) + "deg)" +
	"rotateY(" + (-pawn.ry) + "deg)" +
	"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";
	
};

В новых браузерах world будет соответствовать элементу с id=«world», однако надежнее ее присвоить перед функцией update() с помощью следующей конструкции:

var world = document.getElementById("world");

Мы будем изменять положение мира каждые 10 мс (100 обновлений в секунду), для чего запустим бесконечный цикл:

TimerGame = setInterval(update,10);

Запустим игру. Ура, теперь мы можем двигаться! Однако мир вылазит за пределы рамок элемента «container». Чтобы этого не происходило, зададим css-свойство для него в style.css. Добавим строку overflow:hidden; и посмотрим на изменения. Теперь мир остается в пределах контейнера.

Вполне возможно, что вы не всегда понимаете, куда нужно записывать те или иные строчки кода, поэтому сейчас я вам представлю файлы, которые, как я полагаю, у вас должны получиться:

index.html:

<!DOCTYPE HTML>
<HTML>
<HEAD>
	<TITLE>Игра</TITLE>
	<LINK rel="stylesheet" href="style.css">
	<meta charset="utf-8">
</HEAD>
<BODY>
	<div id="container">
		<div id="world">
			<div id="square1"></div>
		</div>
	</div>
</BODY>
</HTML>
<script src="script.js"></script>

style.css:

#container{
	position:absolute;
	width:1200px;
	height:800px;
	border:2px solid #000000;
	perspective:600px;
	overflow:hidden;
}
#world{
	position:absolute;
	width:300px;
	height:300px;
	transform-style:preserve-3d;
}
#square1{
	position:absolute;
	width:200px;
	height:200px;
	background-color:#FF0000;
	transform:rotateY(30deg);
}

script.js:

// Конструктор Pawn

function player(x,y,z,rx,ry) {
	this.x = x;
	this.y = y;
	this.z = z;
	this.rx = rx;
	this.ry = ry;
}

// Нажата ли клавиша?

var PressBack = 0;
var PressForward = 0;
var PressLeft = 0;
var PressRight = 0;
var PressUp = 0;

// На земле ли игрок?

var onGround = true;

// Обработчик нажатия клавиш

document.addEventListener("keydown", (event) =>{
	if (event.key == "a"){
		PressLeft = 1;
	}
	if (event.key == "w"){
		PressForward = 1;
	}
	if (event.key == "d"){
		PressRight = 1;
	}
	if (event.key == "s"){
		PressBack = 1;
	}
	if (event.keyCode == 32 && onGround){
		PressUp = 1;
	}
});

// Обработчик отжатия клавиш

document.addEventListener("keyup", (event) =>{
	if (event.key == "a"){
		PressLeft = 0;
	}
	if (event.key == "w"){
		PressForward = 0;
	}
	if (event.key == "d"){
		PressRight = 0;
	}
	if (event.key == "s"){
		PressBack = 0;
	}
	if (event.keyCode == 32){
		PressUp = 0;
	}
});

// Создаем новый объект

var pawn = new player(0,0,0,0,0);

// Привяжем новую переменную к world

var world = document.getElementById("world");

function update(){
	
	// Задаем локальные переменные смещения
	
	let dx = (PressRight - PressLeft);
	let dz = - (PressForward - PressBack);
	let dy = - PressUp;
	
	// Прибавляем смещения к координатам
	
	pawn.x = pawn.x + dx;
	pawn.y = pawn.y + dy;
	pawn.z = pawn.z + dz;
	
	// Изменяем координаты мира (для отображения)
	
	world.style.transform = 
	"rotateX(" + (-pawn.rx) + "deg)" +
	"rotateY(" + (-pawn.ry) + "deg)" +
	"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";
	
};

TimerGame = setInterval(update,10);

Если у вас что-то по-другому, обязательно поправьте!

Мы научились двигать персонажа, однако мы еще не умеем поворачивать его! Поворот персонажа, конечно же, будет осуществляться с помощью мыши. Для мыши к переменным состояния клавиш press… мы добавим переменные состояния движения мыши:

// Нажата ли клавиша и двигается ли мышь?

var PressBack = 0;
var PressForward = 0;
var PressLeft = 0;
var PressRight = 0;
var PressUp = 0;
var MouseX = 0;
var MouseY = 0;

А после обработчиков нажатия-отжатия вставим обработчик движения:

// Обработчик движения мыши

document.addEventListener("mousemove", (event)=>{
	MouseX = event.movementX;
	MouseY = event.movementY;
});

В функцию update добавим поворот:

	// Задаем локальные переменные смещения
	
	let dx = (PressRight - PressLeft);
	let dz = - (PressForward - PressBack);
	let dy = - PressUp;
	let drx = MouseY;
	let dry = - MouseX;
	
	// Прибавляем смещения к координатам
	
	pawn.x = pawn.x + dx;
	pawn.y = pawn.y + dy;
	pawn.z = pawn.z + dz;
	pawn.rx = pawn.rx + drx;
	pawn.ry = pawn.ry + dry;

Обратите внимание на то, что движение мыши по оси y вращает pawn по оси x и наоборот. Если мы посмотрим на результат, то ужаснемся от увиденного. Дело в том, что если смещения нет, то MouseX и MouseY остаются прежними, а не приравниваются к нулю. Значит, после каждой итерации update смещения миши должно обнуляться:

// Задаем локальные переменные смещения
	
	let dx = (PressRight - PressLeft);
	let dz = - (PressForward - PressBack);
	let dy = - PressUp;
	let drx = MouseY;
	let dry = - MouseX;

// Обнулим смещения мыши:
	
	MouseX = MouseY = 0;

// Прибавляем смещения к координатам
	
	pawn.x = pawn.x + dx;
	pawn.y = pawn.y + dy;
	pawn.z = pawn.z + dz;
	pawn.rx = pawn.rx + drx;
	pawn.ry = pawn.ry + dry;

Уже лучше, мы избавились от инерции вращения, однако вращение происходит все равно странно! Чтобы понять, что все-таки происходит, добавим div-элемент «pawn» внутрь «container»:

	<div id="container">
		<div id="world">
			<div id="square1"></div>
		</div>
		<div id="pawn"></div>
	</div>

Зададим ему стили в style.css:

#pawn{
	position:absolute;
	width:100px;
	height:100px;
	top:400px;
	left:600px;
	transform:translate(-50%,-50%);
	background-color:#0000FF;
}

Проверим результат. Теперь все ровно! Единственное — синий квадрат остается впереди, но пока оставим это. Чтобы сделать игру от первого лица, а не от третьего, нужно приблизить мир к нам на значение perspective. Сделаем это в script.js в функции update():

world.style.transform = 
	"translateZ(600px)" +
	"rotateX(" + (-pawn.rx) + "deg)" +
	"rotateY(" + (-pawn.ry) + "deg)" +
	"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";

Теперь можно делать игру от первого лица. Скроем pawn добавив строку в style.css:

#pawn{
	display:none;
	position:absolute;
	top:400px;
	left:600px;
	width:100px;
	height:100px;
	transform:translate(-50%,-50%);
	background-color:#0000FF;
}

Отлично. Сразу скажу, что ориентироваться в мире с одним квадратом крайне тяжело, поэтому создадим площадку. Добавим в «world» блок «square2»:

	<div id="world">
			<div id="square1"></div>
			<div id="square2"></div>
		</div>

А в style.css добавим стили для него:

#square2{
	position:absolute;
	width:1000px;
	height:1000px;
	top:400px;
	left:600px;
	background-color:#00FF00;
	transform:translate(-50%,-50%) rotateX(90deg) translateZ(-100px);
}

Теперь все четко. Ну… не совсем. Когда мы нажимаем по клавишам, мы движемся строго по осям X и Z. А мы хотим сделать движение по направлению взгляда. Сделаем следующее: в самом начале файла script.js добавим 2 переменные:

// Мировые константы

var pi = 3.141592;
var deg = pi/180;

Градус — это pi/180 от радиана. Нам придется применить синусы и косинусы, которые считаются от радиан. Что нужно сделать? Взгляните на рисунок:

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

// Задаем локальные переменные смещения
	
	let dx = (PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg);
	let dz = - (PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg);	
	let dy = -PressUp;
	let drx = MouseY;
	let dry = - MouseX;

Внимательно просмотрите все файлы полностью! Если у вас что-то оказалось не так, то потом обязательно буду ошибки, из-за которых вы сломаете голову!

index.html:

<!DOCTYPE HTML>
<HTML>
<HEAD>
	<TITLE>Игра</TITLE>
	<LINK rel="stylesheet" href="style.css">
	<meta charset="utf-8">
</HEAD>
<BODY>
	<div id="container">
		<div id="world">
			<div id="square1"></div>
			<div id="square2"></div>
		</div>
		<div id="pawn"></div>
	</div>
</BODY>
</HTML>
<script src="script.js"></script>

style.css:

#container{
	position:absolute;
	width:1200px;
	height:800px;
	border:2px solid #000000;
	perspective:600px;
	overflow:hidden;
}
#world{
	position:absolute;
	width:inherit;
	height:inherit;
	transform-style:preserve-3d;
}
#square1{
	position:absolute;
	width:200px;
	height:200px;
	top:400px;
	left:600px;
	background-color:#FF0000;
	transform:translate(-50%,-50%) rotateY(30deg);
}
#square2{
	position:absolute;
	width:1000px;
	height:1000px;
	top:400px;
	left:600px;
	background-color:#00FF00;
	transform:translate(-50%,-50%) rotateX(90deg) translateZ(-100px);
}
#pawn{
	display:none;
	position:absolute;
	top:400px;
	left:600px;
	transform:translate(-50%,-50%);
	width:100px;
	height:100px;
	background-color:#0000FF;
}

script.js:

// Мировые константы

var pi = 3.141592;
var deg = pi/180;

// Конструктор Pawn

function player(x,y,z,rx,ry) {
	this.x = x;
	this.y = y;
	this.z = z;
	this.rx = rx;
	this.ry = ry;
}

// Нажата ли клавиша и двигается ли мышь?

var PressBack = 0;
var PressForward = 0;
var PressLeft = 0;
var PressRight = 0;
var PressUp = 0;
var MouseX = 0;
var MouseY = 0;

// На земле ли игрок?

var onGround = true;

// Обработчик нажатия клавиш

document.addEventListener("keydown", (event) =>{
	if (event.key == "a"){
		PressLeft = 1;
	}
	if (event.key == "w"){
		PressForward = 1;
	}
	if (event.key == "d"){
		PressRight = 1;
	}
	if (event.key == "s"){
		PressBack = 1;
	}
	if (event.keyCode == 32 && onGround){
		PressUp = 1;
	}
});

// Обработчик отжатия клавиш

document.addEventListener("keyup", (event) =>{
	if (event.key == "a"){
		PressLeft = 0;
	}
	if (event.key == "w"){
		PressForward = 0;
	}
	if (event.key == "d"){
		PressRight = 0;
	}
	if (event.key == "s"){
		PressBack = 0;
	}
	if (event.keyCode == 32){
		PressUp = 0;
	}
});

// Обработчик движения мыши

document.addEventListener("mousemove", (event)=>{
	MouseX = event.movementX;
	MouseY = event.movementY;
});


// Создаем новый объект типа player

var pawn = new player(0,0,0,0,0);

// Привяжем новую переменную к world

var world = document.getElementById("world");

function update(){
	
	// Задаем локальные переменные смещения
	
	let dx = (PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg);
	let dz = - (PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg);
	let dy = - PressUp;
	let drx = MouseY;
	let dry = - MouseX;
	
	// Обнулим смещения мыши:
	
	MouseX = MouseY = 0;
	
	// Прибавляем смещения к координатам
	
	pawn.x = pawn.x + dx;
	pawn.y = pawn.y + dy;
	pawn.z = pawn.z + dz;
	pawn.rx = pawn.rx + drx;
	pawn.ry = pawn.ry + dry;

	
	// Изменяем координаты мира (для отображения)
	
	world.style.transform = 
	"translateZ(600px)" +
	"rotateX(" + (-pawn.rx) + "deg)" +
	"rotateY(" + (-pawn.ry) + "deg)" +
	"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";
	
};

TimerGame = setInterval(update,10);

С движением мы почти разобрались. Но осталось неудобство: курсор мыши может двигаться только в пределах экрана. В трехмерных шутерах можно вращать мышью сколь угодно долго и сколь угодно далеко. Сделаем также: при нажатии на экран игры (на “container”) курсор будет пропадать, и мы сможем вращать мышью без ограничений на размер экрана. Активируем захват мыши при нажатии на экран, для чего перед обработчиками нажатия клавиш поставим обработчик нажатия мыши на “container”:

// Привяжем новую переменную к container

var container = document.getElementById("container");

// Обработчик захвата курсора мыши

container.onclick = function(){
	container.requestPointerLock();
};

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

// Введен ли захват мыши?

var lock = false;

Добавим обработчик изменения состояния захвата курсора (захвачен или нет) перед обработчиком захвата курсора (извините за тавтологию):

// Обработчик изменения состояния захвата курсора

document.addEventListener("pointerlockchange", (event)=>{
	lock = !lock;
});

А в update() добавим условие вращения “pawn”:

// Если курсор захвачен, разрешаем вращение

	if (lock){
		pawn.rx = pawn.rx + drx;
		pawn.ry = pawn.ry + dry;
	};

А сам захват мыши при клике по контейнеру разрешим только тогда, когда курсор еще не захвачен:

// Обработчик захвата курсора мыши

container.onclick = function(){
	if (!lock) container.requestPointerLock();
};

С движением мы полностью разобрались. Перейдем к генерации мира

4. Загрузка карты

Мир в нашем случае удобнее всего представить в виде множества прямоугольников, имеющих разное местоположение, поворот, размеры и цвет. Вместо цвета также можно использовать текстуры. На самом деле, все современные трехмерные миры в играх – это набор треугольников и прямоугольников, которые называют полигонами. В крутых играх их количество может достигать десятков тысяч в одном только кадре. У нас же их будет около сотни, так как браузер сам по себе имеет невысокую графическую производительность. В предыдущих пунктах мы вставляли блоки “div” внутрь “world”. Но если таких блоков много (сотни), то вставлять каждый из них в контейнер очень утомительно. Да и уровней может быть много. Поэтому пусть эти прямоугольники вставляет javaScript, а не мы. Для него же мы будем создавать специальный массив.

Откроем index.html и удалим из блока “world” все внутренние блоки:

<BODY>
	<div id="container">
		<div id="world"></div>
		<div id="pawn"></div>
	</div>
</BODY>

Как видим, в “world” теперь ничего нет. В style.css удалим стили для #square1 и #square2 (вообще удалим #square1 и #square2 из этого файла), а вместо них создадим стили для класса .square, который будет общим для всех прямоугольников. Причем зададим для него только одно свойство:


.square{
	position:absolute;
}

Теперь создадим массив прямоугольников (запихнем его, примеру, между конструктором player и переменными press… в script.js):

// Массив прямоугольников

var map = [
		   [0,0,1000,0,180,0,2000,200,"#F0C0FF"],
		   [0,0,-1000,0,0,0,2000,200,"#F0C0FF"],
		   [1000,0,0,0,-90,0,2000,200,"#F0C0FF"],
		   [-1000,0,0,0,90,0,2000,200,"#F0C0FF"],
		   [0,100,0,90,0,0,2000,2000,"#666666"]
]

Можно было это сделать в виде конструктора, но пока обойдемся чисто массивом, так как запуск цикла расстановки прямоугольников проще реализовать именно через массивы, а не через конструкторы. Я же поясню, что означают цифры в нем. Массив map содержит одномерные массивы из 9 переменных: [,,,,,,,,]. Я думаю, вы понимаете, что первые три числа – это координаты центра прямоугольника, вторые три числа – углы поворота в градусах (относительно того же центра), затем два числа – его размеры и последнее число – фон. Причем фон может быть сплошным цветом, градиентом или фотографией. Последнее очень удобно использовать в качестве текстур.

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

function CreateNewWorld(){
	for (let i = 0; i < map.length; i++){
		
		// Создание прямоугольника и придание ему стилей
		
		let newElement = document.createElement("div");
		newElement.className = "square";
		newElement.id = "square" + i;
		newElement.style.width = map[i][6] + "px";
		newElement.style.height = map[i][7] + "px";
		newElement.style.background = map[i][8];
		newElement.style.transform = "translate3d(" +
                (600 - map[i][6]/2 + map[i][0]) + "px," +
		(400 - map[i][7]/2 + map[i][1]) + "px," +
		(map[i][2]) + "px)" +
		"rotateX(" + map[i][3] + "deg)" +
		"rotateY(" + map[i][4] + "deg)" +
		"rotateZ(" + map[i][5] + "deg)";
		
		// Вставка прямоугольника в world
		
		world.append(newElement);
	}
}

Поясню, что происходит: мы создаем новую переменную, которая указывает на только что созданный элемент. Ему мы присваиваем id и css-класс (именно это и имеется ввиду под словом класс в языке javaScript), задаем ширину с высотой, фон и трансформацию. Примечательно, что в трансформации помимо координат центра прямоугольника мы указываем смещение на 600 и 400 и половины размеров для того, чтобы центр прямоугольника точно оказался в точке с нужными координатами. Запустим генератор мира перед таймером:

CreateNewWorld();
TimerGame = setInterval(update,10);

Теперь мы видим площадку с розовыми стенами и серым полом. Как видите, создание карты технически несложно реализовать. А в результате ваш код в трех файлах должен получиться примерно таким:

index.html:

<!DOCTYPE HTML>
<HTML>
<HEAD>
	<TITLE>Игра</TITLE>
	<LINK rel="stylesheet" href="style.css">
	<meta charset="utf-8">
</HEAD>
<BODY>
	<div id="container">
		<div id="world"></div>
		<div id="pawn"></div>
	</div>
</BODY>
</HTML>
<script src="script.js"></script>

style.css

#container{
	position:absolute;
	width:1200px;
	height:800px;
	border:2px solid #000000;
	perspective:600px;
	overflow:hidden;
}
#world{
	position:absolute;
	width:inherit;
	height:inherit;
	transform-style:preserve-3d;
}
.square{
	position:absolute;
}
#pawn{
	display:none;
	position:absolute;
	top:400px;
	left:600px;
	transform:translate(-50%,-50%);
	width:100px;
	height:100px;
}

script.js:

// Мировые константы

var pi = 3.141592;
var deg = pi/180;

// Конструктор player

function player(x,y,z,rx,ry) {
	this.x = x;
	this.y = y;
	this.z = z;
	this.rx = rx;
	this.ry = ry;
}

// Массив прямоугольников

var map = [
		   [0,0,1000,0,180,0,2000,200,"#F0C0FF"],
		   [0,0,-1000,0,0,0,2000,200,"#F0C0FF"],
		   [1000,0,0,0,-90,0,2000,200,"#F0C0FF"],
		   [-1000,0,0,0,90,0,2000,200,"#F0C0FF"],
		   [0,100,0,90,0,0,2000,2000,"#666666"]
]

// Нажата ли клавиша и двигается ли мышь?

var PressBack = 0;
var PressForward = 0;
var PressLeft = 0;
var PressRight = 0;
var PressUp = 0;
var MouseX = 0;
var MouseY = 0;

// Введен ли захват мыши?

var lock = false;

// На земле ли игрок?

var onGround = true;

// Привяжем новую переменную к container

var container = document.getElementById("container");

// Обработчик изменения состояния захвата курсора

document.addEventListener("pointerlockchange", (event)=>{
	lock = !lock;
});

// Обработчик захвата курсора мыши

container.onclick = function(){
	if (!lock) container.requestPointerLock();
};

// Обработчик нажатия клавиш

document.addEventListener("keydown", (event) =>{
	if (event.key == "a"){
		PressLeft = 1;
	}
	if (event.key == "w"){
		PressForward = 1;
	}
	if (event.key == "d"){
		PressRight = 1;
	}
	if (event.key == "s"){
		PressBack = 1;
	}
	if (event.keyCode == 32 && onGround){
		PressUp = 1;
	}
});

// Обработчик отжатия клавиш

document.addEventListener("keyup", (event) =>{
	if (event.key == "a"){
		PressLeft = 0;
	}
	if (event.key == "w"){
		PressForward = 0;
	}
	if (event.key == "d"){
		PressRight = 0;
	}
	if (event.key == "s"){
		PressBack = 0;
	}
	if (event.keyCode == 32){
		PressUp = 0;
	}
});

// Обработчик движения мыши

document.addEventListener("mousemove", (event)=>{
	MouseX = event.movementX;
	MouseY = event.movementY;
});

// Создаем новый объект

var pawn = new player(0,0,0,0,0);

// Привяжем новую переменную к world

var world = document.getElementById("world");

function update(){
	
	// Задаем локальные переменные смещения
	
	let dx =   (PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg);
	let dz = - (PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg);
	let dy = - PressUp;
	let drx = MouseY;
	let dry = - MouseX;
	
	// Обнулим смещения мыши:
	
	MouseX = MouseY = 0;
	
	// Прибавляем смещения к координатам
	
	pawn.x = pawn.x + dx;
	pawn.y = pawn.y + dy;
	pawn.z = pawn.z + dz;
	
	// Если курсор захвачен, разрешаем вращение
	
	if (lock){
		pawn.rx = pawn.rx + drx;
		pawn.ry = pawn.ry + dry;
	};

	// Изменяем координаты мира (для отображения)
	
	world.style.transform = 
	"translateZ(" + (600 - 0) + "px)" +
	"rotateX(" + (-pawn.rx) + "deg)" +
	"rotateY(" + (-pawn.ry) + "deg)" +
	"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";
	
};

function CreateNewWorld(){
	for (let i = 0; i < map.length; i++){
		
		// Создание прямоугольника и придание ему стилей
		
		let newElement = document.createElement("div");
		newElement.className = "square";
		newElement.id = "square" + i;
		newElement.style.width = map[i][6] + "px";
		newElement.style.height = map[i][7] + "px";
		newElement.style.background = map[i][8];
		newElement.style.transform = "translate3d(" +
		(600 - map[i][6]/2 + map[i][0]) + "px," +
		(400 - map[i][7]/2 + map[i][1]) + "px," +
		                    (map[i][2]) + "px)" +
		"rotateX(" + map[i][3] + "deg)" +
		"rotateY(" + map[i][4] + "deg)" +
		"rotateZ(" + map[i][5] + "deg)";
		
		// Вставка прямоугольника в world
		
		world.append(newElement);
	}
}

CreateNewWorld();
TimerGame = setInterval(update,10);

Если все хорошо, переходим к следующему пункту.

5. Столкновения игрока с объектами мира

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

function collision(){
	
}

А вызывать ее будем в update():

// Обнулим смещения мыши:
	
	MouseX = MouseY = 0;
	
	// Проверяем коллизию с прямоугольниками
	
	collision();

Как это происходит? Представим себе, что игрок – это шар с радиусом r. И он движется в сторону прямоугольника:

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

function coorTransform(x0,y0,z0,rxc,ryc,rzc){
	let x1 =  x0;
	let y1 =  y0*Math.cos(rxc*deg) + z0*Math.sin(rxc*deg);
	let z1 = -y0*Math.sin(rxc*deg) + z0*Math.cos(rxc*deg);
	let x2 =  x1*Math.cos(ryc*deg) - z1*Math.sin(ryc*deg);
	let y2 =  y1;
	let z2 =  x1*Math.sin(ryc*deg) + z1*Math.cos(ryc*deg);
	let x3 =  x2*Math.cos(rzc*deg) + y2*Math.sin(rzc*deg);
 	let y3 = -x2*Math.sin(rzc*deg) + y2*Math.cos(rzc*deg);
	let z3 =  z2;
	return [x3,y3,z3];
}

И обратную функцию:

function coorReTransform (x3,y3,z3,rxc,ryc,rzc){
	let x2 =  x3*Math.cos(rzc*deg) - y3*Math.sin(rzc*deg);
	let y2 =  x3*Math.sin(rzc*deg) + y3*Math.cos(rzc*deg);
	let z2 =  z3
	let x1 =  x2*Math.cos(ryc*deg) + z2*Math.sin(ryc*deg);
	let y1 =  y2;
	let z1 = -x2*Math.sin(ryc*deg) + z2*Math.cos(ryc*deg);
	let x0 =  x1;
	let y0 =  y1*Math.cos(rxc*deg) - z1*Math.sin(rxc*deg);
	let z0 =  y1*Math.sin(rxc*deg) + z1*Math.cos(rxc*deg);
	return [x0,y0,z0];
}

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

В этом случае условие коллизии становится таким: если после смещения шара на величину v (v – это вектор) координата z между –r и r, а координаты x и y лежат в пределах прямоугольника или отстоят от него на величину, не большую r, то объявляется коллизия. В этом случае координата игрока по z после смещения будет составлять r или – r (в зависимости от того, с какой стороны придет игрок). В соответствии с этим, смещение игрока изменяется. Мы специально вызываем коллизию перед тем, как в update() координаты игрока будут обновлены, чтобы вовремя изменить смещение. Таким образом, шар никогда не пересечется с прямоугольником, как бывает в других алгоритмах коллизии. Хотя физически игрок будет представлять собой, скорее, случае куб, мы не будем обращать на это внимание. Итак, реализуем это в javaScript:

function collision(){
	for(let i = 0; i < map.length; i++){
		
		// рассчитываем координаты игрока в системе координат прямоугольника
		
		let x0 = (pawn.x - map[i][0]);
		let y0 = (pawn.y - map[i][1]);
		let z0 = (pawn.z - map[i][2]);
		
		let x1 = x0 + dx;
		let y1 = y0 + dy;
		let z1 = z0 + dz;
		
		let point0 = coorTransform(x0,y0,z0,map[i][3],map[i][4],map[i][5]);
		let point1 = coorTransform(x1,y1,z1,map[i][3],map[i][4],map[i][5]);
		let point2 = new Array();
		
		// Условие коллизии и действия при нем
		
		if (Math.abs(point1[0])<(map[i][6]+98)/2 && Math.abs(point1[1])<(map[i][7]+98)/2 && Math.abs(point1[2]) < 50){
			point1[2] = Math.sign(point0[2])*50;
			point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]);
			dx = point2[0] - x0;
			dy = point2[1] - y0;
			dz = point2[2] - z0;
		}
	};
}

x0,y0 и z0 – начальные координаты игрока в системе координат прямоугольника (без поворотов. x1,y1 и z1 – координаты игрока после смещения без учета коллизии. point0, point0, point1 и point2 – начальный радиус-вектор, радиус-вектор после смещения без коллизии и радиус-вектор с коллизией соответственно. map[i][3] и другие, если вы помните, это углы поворота прямоугольника. Заметим, что в условии мы к размерам прямоугольника прибавляем не 100, а 98. Это костыль, зачем, подумайте сами. Запустите игру и вы увидите довольно качественные столкновения.

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


if ((x0**2 + y0**2 + z0**2 + dx**2 + dy**2 + dz**2) < (map[i][1]**2 + map[i][2]**2)){
		
			let x1 = x0 + dx;
			let y1 = y0 + dy;
			let z1 = z0 + dz;
		
			let point0 = coorTransform(x0,y0,z0,map[i][3],map[i][4],map[i][5]);
			let point1 = coorTransform(x1,y1,z1,map[i][3],map[i][4],map[i][5]);
			let point2 = new Array();
		
			// Условие коллизии и действия при нем
		
			if (Math.abs(point1[0])<(map[i][6]+98)/2 && Math.abs(point1[1])<(map[i][7]+98)/2 && Math.abs(point1[2]) < 50){
				point1[2] = Math.sign(point0[2])*50;
				point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]);
				dx = point2[0] - x0;
				dy = point2[1] - y0;
				dz = point2[2] - z0;
			}
			
		} 

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

index.html:

<!DOCTYPE HTML>
<HTML>
<HEAD>
	<TITLE>Игра</TITLE>
	<LINK rel="stylesheet" href="style.css">
	<meta charset="utf-8">
</HEAD>
<BODY>
	<div id="container">
		<div id="world"></div>
		<div id="pawn"></div>
	</div>
</BODY>
</HTML>
<script src="script.js"></script>

style.css

#container{
	position:absolute;
	width:1200px;
	height:800px;
	border:2px solid #000000;
	perspective:600px;
	overflow:hidden;
}
#world{
	position:absolute;
	width:inherit;
	height:inherit;
	transform-style:preserve-3d;
}
.square{
	position:absolute;
}
#pawn{
	display:none;
	position:absolute;
	top:400px;
	left:600px;
	transform:translate(-50%,-50%);
	width:100px;
	height:100px;
}

script.js:

// Мировые константы

var pi = 3.141592;
var deg = pi/180;

// Конструктор player

function player(x,y,z,rx,ry) {
	this.x = x;
	this.y = y;
	this.z = z;
	this.rx = rx;
	this.ry = ry;
}

// Массив прямоугольников

var map = [
		   [0,0,1000,0,180,0,2000,200,"#F0C0FF"],
		   [0,0,-1000,0,0,0,2000,200,"#F0C0FF"],
		   [1000,0,0,0,-90,0,2000,200,"#F0C0FF"],
		   [-1000,0,0,0,90,0,2000,200,"#F0C0FF"],
		   [0,100,0,90,0,0,2000,2000,"#666666"]
];

// Нажата ли клавиша и двигается ли мышь?

var PressBack = 0;
var PressForward = 0;
var PressLeft = 0;
var PressRight = 0;
var PressUp = 0;
var MouseX = 0;
var MouseY = 0;

// Введен ли захват мыши?

var lock = false;

// На земле ли игрок?

var onGround = true;

// Привяжем новую переменную к container

var container = document.getElementById("container");

// Обработчик изменения состояния захвата курсора

document.addEventListener("pointerlockchange", (event)=>{
	lock = !lock;
});

// Обработчик захвата курсора мыши

container.onclick = function(){
	if (!lock) container.requestPointerLock();
};

// Обработчик нажатия клавиш

document.addEventListener("keydown", (event) =>{
	if (event.key == "a"){
		PressLeft = 1;
	}
	if (event.key == "w"){
		PressForward = 1;
	}
	if (event.key == "d"){
		PressRight = 1;
	}
	if (event.key == "s"){
		PressBack = 1;
	}
	if (event.keyCode == 32 && onGround){
		PressUp = 1;
	}
});

// Обработчик отжатия клавиш

document.addEventListener("keyup", (event) =>{
	if (event.key == "a"){
		PressLeft = 0;
	}
	if (event.key == "w"){
		PressForward = 0;
	}
	if (event.key == "d"){
		PressRight = 0;
	}
	if (event.key == "s"){
		PressBack = 0;
	}
	if (event.keyCode == 32){
		PressUp = 0;
	}
});

// Обработчик движения мыши

document.addEventListener("mousemove", (event)=>{
	MouseX = event.movementX;
	MouseY = event.movementY;
});

// Создаем новый объект

var pawn = new player(-900,0,-900,0,0);

// Привяжем новую переменную к world

var world = document.getElementById("world");

function update(){
	
	// Задаем локальные переменные смещения
	
	dx =   (PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg);
	dz = - (PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg);
	dy = - PressUp;
	drx = MouseY;
	dry = - MouseX;
	
	// Обнулим смещения мыши:
	
	MouseX = MouseY = 0;
	
	// Проверяем коллизию с прямоугольниками
	
	collision();
	
	// Прибавляем смещения к координатам
	
	pawn.x = pawn.x + dx;
	pawn.y = pawn.y + dy;
	pawn.z = pawn.z + dz;
	console.log(pawn.x + ":" + pawn.y + ":" + pawn.z);
	
	// Если курсор захвачен, разрешаем вращение
	
	if (lock){
		pawn.rx = pawn.rx + drx;
		pawn.ry = pawn.ry + dry;
	};

	// Изменяем координаты мира (для отображения)
	
	world.style.transform = 
	"translateZ(" + (600 - 0) + "px)" +
	"rotateX(" + (-pawn.rx) + "deg)" +
	"rotateY(" + (-pawn.ry) + "deg)" +
	"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";
	
};

function CreateNewWorld(){
	for (let i = 0; i < map.length; i++){
		
		// Создание прямоугольника и придание ему стилей
		
		let newElement = document.createElement("div");
		newElement.className = "square";
		newElement.id = "square" + i;
		newElement.style.width = map[i][6] + "px";
		newElement.style.height = map[i][7] + "px";
		newElement.style.background = map[i][8];
		newElement.style.transform = "translate3d(" +
		(600 - map[i][6]/2 + map[i][0]) + "px," +
		(400 - map[i][7]/2 + map[i][1]) + "px," +
		(map[i][2]) + "px)" +
		"rotateX(" + map[i][3] + "deg)" +
		"rotateY(" + map[i][4] + "deg)" +
		"rotateZ(" + map[i][5] + "deg)";
		
		// Вставка прямоугольника в world
		
		world.append(newElement);
	}
}

function collision(){
	for(let i = 0; i < map.length; i++){
		
		// рассчитываем координаты игрока в системе координат прямоугольника
		
		let x0 = (pawn.x - map[i][0]);
		let y0 = (pawn.y - map[i][1]);
		let z0 = (pawn.z - map[i][2]);
		
		if ((x0**2 + y0**2 + z0**2 + dx**2 + dy**2 + dz**2) < (map[i][6]**2 + map[i][7]**2)){
		
			let x1 = x0 + dx;
			let y1 = y0 + dy;
			let z1 = z0 + dz;
		
			let point0 = coorTransform(x0,y0,z0,map[i][3],map[i][4],map[i][5]);
			let point1 = coorTransform(x1,y1,z1,map[i][3],map[i][4],map[i][5]);
			let point2 = new Array();
		
			// Условие коллизии и действия при нем
		
			if (Math.abs(point1[0])<(map[i][6]+98)/2 && Math.abs(point1[1])<(map[i][7]+98)/2 && Math.abs(point1[2]) < 50){
				point1[2] = Math.sign(point0[2])*50;
				point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]);
				dx = point2[0] - x0;
				dy = point2[1] - y0;
				dz = point2[2] - z0;
			}
			
		}
	};
}

function coorTransform(x0,y0,z0,rxc,ryc,rzc){
	let x1 =  x0;
	let y1 =  y0*Math.cos(rxc*deg) + z0*Math.sin(rxc*deg);
	let z1 = -y0*Math.sin(rxc*deg) + z0*Math.cos(rxc*deg);
	let x2 =  x1*Math.cos(ryc*deg) - z1*Math.sin(ryc*deg);
	let y2 =  y1;
	let z2 =  x1*Math.sin(ryc*deg) + z1*Math.cos(ryc*deg);
	let x3 =  x2*Math.cos(rzc*deg) + y2*Math.sin(rzc*deg);
 	let y3 = -x2*Math.sin(rzc*deg) + y2*Math.cos(rzc*deg);
	let z3 =  z2;
	return [x3,y3,z3];
}

function coorReTransform(x3,y3,z3,rxc,ryc,rzc){
	let x2 =  x3*Math.cos(rzc*deg) - y3*Math.sin(rzc*deg);
	let y2 =  x3*Math.sin(rzc*deg) + y3*Math.cos(rzc*deg);
	let z2 =  z3
	let x1 =  x2*Math.cos(ryc*deg) + z2*Math.sin(ryc*deg);
	let y1 =  y2;
	let z1 = -x2*Math.sin(ryc*deg) + z2*Math.cos(ryc*deg);
	let x0 =  x1;
	let y0 =  y1*Math.cos(rxc*deg) - z1*Math.sin(rxc*deg);
	let z0 =  y1*Math.sin(rxc*deg) + z1*Math.cos(rxc*deg);
	return [x0,y0,z0];
}

CreateNewWorld();
TimerGame = setInterval(update,10);

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

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

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

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

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

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

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

<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8">

<meta name="viewport", content="width=device-width, initial-scale=1.0">

<link rel="stylesheet" href="snake.css">

<script src="snake.js"></script>

</head>

<body>

<canvas id="board"></canvas>

</body>

</html>

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

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

body {

text-align: center;

}

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

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

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

У нас есть некоторое поле с клетками, представляющее собой двухмерный массив. Изначально в случайных ячейках появляется еда, игрок появляется в фиксированной ячейке. Для простоты примера не будем делать отдельную кнопку «Старт», игра начнется при первом нажатии на любую стрелочку. Змейка управляется через стрелочки, она двигается в направлении, соответствующем стрелочке, нажатой последней. Змейка двигается с определенной скоростью и не может остановиться. Если змейка сталкивается с едой – она «съедает» ее: еда удаляется с экрана, змейка «прирастает» на 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) {

snakeBody.push([foodX, foodY]);

placeFood();

}

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

for (let i = snakeBody.length-1; i > 0; i--) {

snakeBody[i] = snakeBody[i-1];

}

if (snakeBody.length) {

snakeBody[0] = [snakeX, snakeY];

}

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

context.fillStyle="lime";

snakeX += velocityX * blockSize;

snakeY += velocityY * blockSize;

context.fillRect(snakeX, snakeY, blockSize, blockSize);

for (let i = 0; i < snakeBody.length; i++) {

context.fillRect(snakeBody[i][0], snakeBody[i][1], blockSize, blockSize);

}

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

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) {

return;

}

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

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

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

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

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

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

Вывод

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

  • Следующая статья »

В этом пошаговом руководстве мы создадим простую игру MDN Breakout, написанную на чистом JavaScript и отрендеренную на HTML5 <canvas>.

К каждому шагу прилагаются редактируемые live-примеры, с которыми можно поиграть, чтобы увидеть, как должна выглядеть игра на промежуточных этапах. Вы изучите основы использования элемента <canvas> для реализации таких фундаментальных игровых механик, как рендеринг и перемещение изображений, обнаружение столкновений, механизмы управления, а также состояния выигрыша и проигрыша.

Для извлечения максимальной пользы из этой серии статей необходимо иметь средние (или хотя бы базовые) знания языка JavaScript. После прохождения этого урока вы сможете создавать собственные простые браузерные игры.

Gameplay screen from the game MDN Breakout where you can use your paddle to bounce the ball and destroy the brick field, with keeping the score and lives.

Детали к урокам

Все уроки и версии игры MDN Breakout доступны в GitHub:

  1. Создание Canvas и рисование на нем (en-US)
  2. Движение мяча (en-US)
  3. Реакция при столкновении со стеной (en-US)
  4. Управление (en-US)
  5. Конец игры (en-US)
  6. Построение поля кирпичей (en-US)
  7. Реакция при столкновении (en-US)
  8. Счёт и выигрыш (en-US)
  9. Контроль мышью (en-US)
  10. Заключение (en-US)

Лучший способ получить надёжные знания в области разработки браузерных игр — это начать с чистого JavaScript. Затем можно выбрать любой фреймворк для использования в своих проектах. Фреймворки — это инструменты, созданные на языке JavaScript; поэтому, даже если вы планируете работать с ними, нелишним будет сначала изучить сам язык, чтобы понимать, что именно происходит внутри. Фреймворки ускоряют разработку и помогают справиться со скучными частями игры, но если что-то работает не так, как ожидалось, всегда можно попытаться отладить код или написать собственное решение на чистом JavaScript.

Примечание: Эту серию статей можно использовать как материал для практических занятий по разработке игр. Также можно воспользоваться набором инструментов Gamedev Canvas Content Kit, основанном на этом уроке, если нужно сделать доклад о разработке игр в целом.

Следующий шаг

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

Также, если вы более продвинутый разработчик, то советую вам почитать статью про то, как сделать пинг-понг на JavaScript.

Простая игра на JavaScript:

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

Игры — один из лучших проектов, которые вы можете создать, потому что они очень нравятся конечному пользователю, а создавать их очень весело!

Обзор игры на JavaScript:

Какая игра лучше представляет веб-разработку, чем игра про динозавров Chrome, в которую вы играете, когда теряете подключение к Интернету? Это забавная игра, и код легко воссоздать. Он не выглядит точно так же, но работает так же. Если вы действительно хотите, вы можете стилизовать его, когда закончите!

Версия игры на JavaScript с игроком-динозавром:

Чтобы начать кодирование игры, создайте новую папку в своих документах. Используйте свой любимый текстовый редактор, чтобы открыть эту папку, затем создайте три новых файла и назовите их: «index.html», «style.css» и «script.js». С HTML5 можно сделать все в одном файле, но он более организован, чтобы хранить все отдельно.

Наш файл «index.html» будет очень простым: как только у вас будет базовый HTML-макет, создайте div с идентификатором «game», а затем еще два внутри него с идентификаторами character и block. Персонаж будет динозавром, а блок — кактусами, приближающимися к нам.

<! DOCTYPE html>

<html lang=«ru»>

<head>

    <meta charset=«UTF-8»>

    <title> Игра в прыжки </title>

    <link rel=«stylesheet» href=«style.css»>

</head>

<body>

    <div id=«game»>

        <div id=«character»></div>

        <div id=«block»></div>

    </div>

    <script src=«script.js»> </script>

</body>

</html>

Затем перейдите к файлу CSS и начните применять стили к двум только что созданным div. Сначала мы начнем с div игры. Выберите элемент по его идентификатору, который представлен символом решетки (#).

#game{

    width: 500px;

    height: 200px;

    border: solid bleck 1px;

    margin: auto;

}

Затем мы изменим стиль нашего символа div. Мы должны объявить позицию как относительную, потому что позиционные атрибуты, такие как top и left, применяются только к позиционированным элементам.

#character{

    width: 20px;

    height: 50px;

    background-color:  red;

    position: relative;

    top: 150px; // высота игры высота персонажа (200-50)

}

Создайте анимацию по ключевым кадрам, которая называется прыжком. Эта анимация заставит верхнюю позицию сдвинуться на 50 пикселей вверх, а затем снова вниз.

@keyframes jump {

    0% {top: 150px;}

    30% {top: 100px;}

    70% {top: 100px;}

    100% {top: 150px;}

}

Затем мы создадим новый класс с именем animate, который применяет анимацию прыжка.

.animate {

    animation: jump 300ms linear;

}

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

В файле «script.js» создайте функцию с именем jump(), которая добавляет класс animate к символу div. Создайте прослушиватель событий, который прослушивает щелчок пользователя, а затем выполняет функцию перехода.

Создайте еще одну функцию с именем removeJump(), которая удаляет класс анимации. Затем добавьте функцию тайм-аута в jump(), которая запускает removeJump() по окончании анимации. Анимация не запустится снова, пока мы ее не удалим.

var character = document.getElementById(«персонаж»);

document.addEventListener («click»,  jump);

function jump () {

    character.classList.add(«animate»);

    setTimeout(removeJump, 300); // 300мс = длина анимации

};

function removeJump () {

    character.classList.remove(«animate»);

}

Это работает, но кажется, что возникает сбой, если пользователь щелкает мышью во время прыжка. Чтобы исправить это, добавьте строку ниже в начале jump(). Это остановит функцию от каких-либо действий, если анимация запущена.

if (character.classList == «animate») {return;}

Теперь вернемся к нашему файлу CSS и начнем стилизовать блок div.

#block{

    width: 20px;

    height: 20px;

    background-color: blue;

    position: relative;

    top: 130px; // высота игры высота персонажа высота блока (200-50-20)

    left: 480px; // ширина игры ширина блока (500-20)

    animation: block 1s infinite linear;

}

Мы еще не создали анимацию блока, поэтому создайте анимацию, чтобы блок скользил справа налево.

@keyframes block {

    0% {left: 500px}

    100% {left: -20px}

}

Теперь мы можем прыгать, но мы должны довести игру до конца, если попадем в блок. Создайте функцию с именем checkDead(), которая получает позицию блока и символа, а затем оценивает, находятся ли они друг над другом. Если да, то закончите игру.

Создайте переменную с именем characterTop, которая равна верхнему значению символа div. getComputedStyle() вернет все значения CSS, связанные с элементом, а getPropertyValue() указывает свойство, из которого вы хотите получить значение.

Вывод:

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

Подписываетесь на соц-сети:

Оценка:

Загрузка…

Также рекомендую:

Понравилась статья? Поделить с друзьями:
  • Как написать простую доверенность от руки образец
  • Как написать проект про рифму
  • Как написать простой сценарий
  • Как написать проект про профессию родителей
  • Как написать проект про масленицу