Как написать игру на джава скрипт

На 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;

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

  • 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, который умеет компилировать проекты для их запуска в браузере.

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

Участвовать

Масса людей думает, что
все крутые игры (God Of War, Assassin’s Creed, Skyrim, добавь по вкусу) созданы на C++. Это отчасти так. В проекте принимают участие
сотни специалистов из разных отраслей, в том числе и разработчики, юзающие
другой язык – обычная распространенная практика.

Некоторые классные игры
написаны на “непопулярных” языках программирования, и это нормально. Если ты
работаешь с JavaScript, то не нужно
после этой статьи бросаться изучать “плюсы”, оставайся с JavaScript.

Существуют Unity, Unreal Engine, CryEngine и прочие
классные решения для создания игрушек, и если тебе удобно развлекаться с ними –
пожалуйста. Поэтому нет никакой разницы, на чем ты будешь кодить, но в нашем
случае речь пойдет о JS-фреймворках.

Основы

Прежде чем мы перейдем
к рассмотрению фреймворков для создания игр, следует изучить существующие технологии.
Один из вариантов – HTML5. Начиная с 5-й версии спецификации, HTML возымел тег <canvas>,
который позволяет создавать контекст для рисования на веб-странице.

Не нужно забывать о
творении команды Khronos Group. WebGL – это веб-версия спецификации OpenGL ES,
позволяющая разработчикам общаться с видеокартой через браузер (поверь, лучше
не знать, как это работает).

Таким образом, можно
создавать 2D и 3D сцены на GPU (что эффективнее, чем на CPU). Супер! Но если
взглянуть на код JavaScript,
использующий эти технологии, тебе поплохеет.

Поэтому давай
разбираться с фреймворками, оберегающими нас от canvas и абстрагирующими от WebGL.

2D Frameworks

Разработка игр на JavaScript: реально и безболезненно

PixiJS

Этот инструмент можно
назвать 2D-рендером WebGL. Это означает, что данная библиотека включает в себя
множество функций, предназначенных для эффективной отрисовки 2D-сцен и
объектов. Так проще сосредоточиться на создании программного кода, а хардкорные
“низкоуровневые” вещи оставить разработчикам PixiJS.

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

Если ты планируешь
создать что-то большее, чем анимация, то поищи дополнительные библиотеки для
других частей игровой разработки (физика, масштабирование, tilemaps и т. д.).

ExcaliburJS

Здесь у нас полноценный
игровой фреймворк, написанный на Typescript. Полная система сцен и камер,
спрайты и анимации, звуки, физика и т. д. – все, что пожелаешь. Многим очень
нравится API, предоставляемый ExcaliburJS, т. к. с ним уютнее.

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

ImpactJS

ImpactJS начал свой путь
со звания “Первый фреймворк для веб-игр”. Большинство фреймворков, рассмотренных
ранее, были просто экспериментами, а не коммерческим продуктом. Этот
опенсорсный претендент распространяется бесплатно и поставляется с хорошим
редактором уровней.

Фреймворк не является
самым понятным или документированным, но его надежность уже доказана. Например,
разрабы из CrossCode
взяли за основу форкнутую версию Impact для своего движка за его производительность
и способность масштабироваться под конкретную задачу.

CreateJS

CreateJS – это набор модульных
библиотек и HTML5-инструментов, работающих асинхронно или параллельно в зависимости от ситуации.

Инструмент
предоставляет все, что нужно для создания игры с нуля, с помощью отдельного
модуля языка JavaScript. Например, для
рендеринга можно взять PixiJS, а для работы со звуковыми материалами SoundJS и
т. д.

PhaserJS

И напоследок самый
популярный – PhaserJS. Это мощный набор инструментов для создания веб и
мобильных игр. Этот фреймворк имеет огромное и активное сообщество – каждую
неделю эти ребята выкладывают много новых статей, демо и туториалов, основанных
на PhaserJS. Это обеспечивает отличное подспорье для людей, делающих свои
первые шаги в геймдеве и нуждающихся в наставлениях. А еще, начиная с 3-й
версии, это один из самых производительных игровых фреймворков.

Разработка игр на JavaScript: реально и безболезненно

3D Frameworks

ThreeJS

ThreeJs – самая популярная
3D-библиотека. Она предлагает наборы функций для выполнения общих операций,
которые должны происходить в 3D-сцене. Все мероприятия происходят на более высоком
уровне, чем raw WebGL, и не надо заморачиваться с горой низкоуровневых действий.

BabylonJS

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

  • API меняется каждые
    3 месяца, что помогает при поиске старых решений в интернете;
  • активное и полезное сообщество;
  • продуктивные и отзывчивые разработчики (у Three.js самый старый баг на GitHub датируется 2013 годом, в Babylon.js отмечен два дня назад);
  • The playground – это отличный инструмент для быстрого “опробования” кода, объяснения проблемы и оказания помощи.

Литература

Как в любом уважающем
себя мануале, далее идет подборочка книг по теме.

Кстати, у нас есть
очень крутая статья по книгам для геймдэва – рекомендуем!

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

  • 3D Math Primer for Graphics and Game Development –
    Dunn Fletcher
  • Unity in Action – Джозеф Хокинг
  • Game Programming Patterns – Robert Nystrom
  • Архитектура игровых движков – Джейсон Грегори
  • Language Implementation Patterns – Terence Parr
  • Introduction to 3D Game Programming with DirectX 12 – Frank Luna
  • Искусственный интеллект. Современный подход – Стюарт Рассел
  • Системное программирование – Роберт Лав

Разрабатываешь
игрушки? Поделись опытом в комментариях

Время на прочтение
13 мин

Количество просмотров 69K


Здравствуйте! Я предлагаю вам со мной создать небольшую казуальную игру на нескольких человек за одним компьютером на Javascript Canvas.
В статье я пошагово разобрала процесс создания такой игры при помощи MooTools и LibCanvas, останавливаясь на каждом мелком действии, объясняя причины и логику добавления нового и рефакторинга существующего кода.

p.s. К сожалению, Хабр обрезает большие раскрашенные статьи где-то на шестидесятитысячном символе, потому я была вынуждена вынести пару участков кода из статьи на pastebin. Если вы хотите прочитать статью, не бегая по ссылках в поисках кода — можно воспользоваться зеркалом.

Правила

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

HTML файл

Создаем начальный файл, на котором будет наше канвас-приложение. Я воспользовалась файлами, размещенными на сайте libcanvas, но это — не обязательно. JavaScript-файлы добавлялись в процессе создания игры, но я не хочу больше возвращаться к этому файлу, потому объявлю их сразу.

[[ Код ./index.html на pastebin ]]

Создаем проект

Для начала создадим сам проект. Нам нужно сделать это только тогда, когда документ будет готов — воспользуемся предоставленным mootools событием «domready«
Также создадим объект LibCanvas.Canvas2D, который поможет нам с анимацией.

./js/start.js

window.addEvent('domready', function () {
    
// с помощью Мутулз выберем первый елемент канвас на странице
    
var elem = $$('canvas')[0];
    
// На его основе создадим елемент LibCanvas
    
var libcanvas = new LibCanvas.Canvas2D(elem);
    
// Перерисовка будет осуществлятся каждый кадр, несмотря на наличие или отсутствие изменений
    
libcanvas.autoUpdate true;
    
// Будем стремится к 60 fps
    
libcanvas.fps        60;// Стартуем наше приложение
    
libcanvas.start();
});

Добавляем пользователя

Добавим новый объект — Player, который будет управлятся мышью — его координаты будут равны координатам курсора мыши.
Этот объект будет выглядеть как кружочек определенного цвета и размера (указанные в свойстве)

[[ Код ./js/Player.js на pastebin ]]

Добавим в ./js/start.js перед libcanvas.start();:

libcanvas.listenMouse();

var 

player = new Player().setZIndex(30);
libcanvas.addElement(player);

= Шаг 1 =

Можно заметить, что результат не совсем такой, как мы ожидали, потому-что после каждого из кадров холст не очищается автоматически.
Необходимо добавить очищающий и заливающий черным препроцессор в ./js/start.js

libcanvas.addProcessor('pre',
    new 
LibCanvas.Processors.Clearer('#000')
);

= Шаг 2 =

Добавляем наживку

[[ Код ./js/Bait.js на pastebin ]]

Добавим в ./js/start.js:

// Возьмем индекс поменьше, чтобы наживка рисовалась под игроком
var bait = new Bait().setZIndex(20);
libcanvas.addElement(bait);

Рефакторинг — создаем родительский класс

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

Для начала вынесем createPosition из конструктора класса Player:
./js/Player.js

var Player = new Class({
    
// ...
    
initialize : function () {
        
// ..
            
this.shape = new LibCanvas.Shapes.Circle({
                
center this.createPosition()
        
// ...
    
},createPosition : function () {
        return 
this.libcanvas.mouse.point;
    },

Теперь создадим класс GameObject

[[ Код ./js/GameObject.js на pastebin ]]

После этого можно облегчить другие классы:

./js/Bait.js

var Bait = new Class({
    Extends : 
GameObject,radius 15,
    
color '#f0f'
});

./js/Player.js

var Player = new Class({
    Extends : 
GameObject,radius 15,
    
color '#080',createPosition : function () {
        return 
this.libcanvas.mouse.point;
    },
draw : function () {
        if (
this.libcanvas.mouse.inCanvas) {
            
this.parent();
        }
    }
});

Смотрим, ничего ли не сломалось:

= Шаг 3 =

Ура! Все везде работает, а код стал значительно легче.

Дружим игрока с наживкой

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

Пишем в конец ./js/start.js:

(function(){
    
bait.isCatched(player);
}.
periodical(30));

Теперь надо реализовать метод isCatched в ./js/Bait.js:

isCatched : function (player) {
    if (
player.shape.intersect(this.shape)) {
        
this.move();
        return 
true;
    }
    return 
false;
},
move : function () {
    
// перемещаем в случайное место
    
this.shape.center this.createPosition();
}

= Шаг 4 =

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

Теперь надо реализовать метод isCatched в ./js/Bait.js:

var Bait = new Class({
    Extends : 
GameObject,
    Implements : [
LibCanvas.Behaviors.Moveable],// ...move : function () {
        
// быстро (800), но плавно перемещаем в случайное место
        
this.moveTo(this.createPosition(), 800);
    }
});

Очень просто, правда? А результат мне нравится намного больше:

= Шаг 5 =

Добавим хищников

./js/Barrier.js:

var Barrier = new Class({
    Extends : 
GameObject,full null,
    
speed null,
    
radius 8,
    
color '#0ff',initialize : function () {
        
this.parent();
        
this.speed = new LibCanvas.Point(
            
$random(2,5), $random(2,5)
        );
        
// Через раз летим влево, а не вправо
        
$random(0,1) && (this.speed.*= -1);
        
// Через раз летим вверх, а не вниз
        
$random(0,1) && (this.speed.*= -1);
    },
    
move : function () {
        
this.shape.center.move(this.speed);
        return 
this;
    },
    
intersect : function (player) {
        return (
player.shape.intersect(this.shape));
    }
});

Также чуть изменим ./js/start.js, чтобы при ловле наживки появлялись хищники:

bait.isCatched(player);
// меняем на
if (bait.isCatched(player)) {
    
player.createBarrier();
}
player.checkBarriers();

Реализуем добавление барьеров для игрока, ./js/Player.js и двигаем их все каждую проверку:

barriers : [],createBarrier : function () {
    var 
barrier = new Barrier().setZIndex(10);
    
this.barriers.push(barrier);
    
// Надо не забыть добавить его в наш объект libcanvas, чтобы хищник рендерился
    
this.libcanvas.addElement(barrier);
    return 
barrier;
},
checkBarriers : function () {
    for (var 
this.barriers.lengthi--;) {
        if (
this.barriers[i].move().intersect(this)) {
            
this.die();
            return 
true;
        }
    }
    return 
false;
},

die : function () { },;

= Шаг 6 =

Отлично, появилось движение в игре. Но мы видим три проблемы:
1. Хищники улетают за игровое поле — надо сделать «отбивание от стен».
2. Иногда наживка успевает схватиться дважды, пока улетает — надо сделать небольшой таймаут «неуязвимости».
3. Не обработан случай смерти.

Хищники отбиваются от стен, наживка получает небольшое время «неуязвимости»

Реализовать отбивание от стен проще простого. Слегка меняем метод move класса Barrier, в файле ./js/Barrier.js:

[[ Код Barrier.move на pastebin ]]

Исправить проблему с наживкой тоже не очень сложно — вносим изменения в класс Bait, в файле ./js/Bait.js

[[ Код Bait.makeInvulnerable на pastebin ]]

= Шаг 7 =

Реализуем смерть и подсчёт очков

Т.к. очки — это сколько раз поймана наживка и она равная количеству хищников на экране — сделать подсчёт очков очень легко:
Чуть расширим метод draw в классе Player, файл ./js/Player.js:

draw : function () {
    
// ...
    
this.libcanvas.ctx.text({
        
text 'Score : ' this.barriers.length,
        
to : [201020040],
        
color this.color
    
});
},
// Т.к. очки - это всего-лишь количество хищников - при смерти достаточно удалить всех хищников
die : function () {
    for (var 
this.barriers.lengthi--;) {
        
this.libcanvas.rmElement(this.barriers[i]);
    }
    
this.barriers = [];
}

Одиночная игра — закончена!

= Шаг 8 — одиночная игра =

Реализуем многопользовательскую игру за одним компьютером

Движение с клавиатуры

Для начала — перенесем управление с мышки на клавиатуру. В ./js/start.js меняем libcanvas.listenMouse() на libcanvas.listenKeyboard()
В нем же в таймаут добавляем player.checkMovement();.
В ./js/Player.js удаляем переопределение createPosition, в методе draw удаляем проверку мыши и реализуем движение с помощью стрелочек:

speed 8,
checkMovement : function () {
    var 
pos  this.shape.center;
    if (
this.libcanvas.getKey('left'))  pos.-= this.speed;
    if (
this.libcanvas.getKey('right')) pos.+= this.speed;
    if (
this.libcanvas.getKey('up'))    pos.-= this.speed;
    if (
this.libcanvas.getKey('down'))  pos.+= this.speed;
},

= Шаг 9 =

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

isMoveTo : function (dir) {
    return 
this.libcanvas.getKey(dir);
},
checkMovement : function () {
    var 
pos  this.shape.center;
    var 
full this.getFull();
    if (
this.isMoveTo('left')  && pos.0          pos.-= this.speed;
    if (
this.isMoveTo('right') && pos.full.width pos.+= this.speed;
    if (
this.isMoveTo('up')    && pos.0          pos.-= this.speed;
    if (
this.isMoveTo('down')  && pos.full.heightpos.+= this.speed;
},

Также слегка изменим метод isMoveTo — чтобы можно было с легкостью изменять клавиши для управления игроком:

control : {
    
up    'up',
    
down  'down',
    
left  'left',
    
right 'right'
},
isMoveTo : function (dir) {
    return 
this.libcanvas.getKey(this.control[dir]);
},

= Шаг 10 =

Вводим второго игрока

Изменяем файл ./js/start.js:

var player = new Player().setZIndex(30);
libcanvas.addElement(player);// =>var players = [];
(
2).times(function (i) {
    var 
player = new Player().setZIndex(30 i);
    
libcanvas.addElement(player);
    
players.push(player);
});
// Меняем стиль и управление второго игрока
players[1].color '#ff0';
players[1].control = {
    
up    'w',
    
down  's',
    
left  'a',
    
right 'd'
};

Содержимое таймера оборачиваем в players.each(function (player) { /* * */ });

= Шаг 11 =

Осталось сделать небольшие поправки:
1. Сдвинуть счёт второго игрока ниже счёта первого игрока.
2. Раскрасить хищников разных игроков в разные цвета.
3. Ради статистики ввести «Рекорд» — какой максимальный счёт каким игроком был достигнут.

Вносим соответствующие изменения в ./js/Player.js:

var Player = new Class({// ...

    // Красим хищников в соответствующий игроку цвет:

createBarrier : function () {
        
// ...
        
barrier.color this.barrierColor || this.color;
        
// ...
    
},// Реализуем подсчет максимального рекорда
    
maxScore 0,
    die : function () {
        
this.maxScore Math.max(this.maxScorethis.barriers.length);
        
// ...
    
},index 0,
    
draw : function () {
        
this.parent();
        
this.libcanvas.ctx.text({
            
// Выводим максимальный рекорд:
            
text 'Score : ' this.barriers.length ' (' this.maxScore ')',
            
// Смещаем очки игрока на 20 пикселей вниз зависимо от его индекса:
            
to : [2010 20*this.index20040],
            
color this.color
        
});
    }
});

Вносим коррективы в ./js/start.js:

(2).times(function (i) {
    var 
player = new Player().setZIndex(30 i);
    
player.index i;
    
// ...
});players[0].color '#09f';
players[0].barrierColor '#069';// Меняем стиль и управление второго игрока
players[1].color '#ff0';
players[1].barrierColor '#960';
players[1].control = {
    
up    'w',
    
down  's',
    
left  'a',
    
right 'd'
};

Поздравляем, игра сделана!

= Шаг 12 — игра на двоих =

Добавляем третьего и четвертого игрока

При желании очень просто добавить третьего и четвертого игрока:

players[2].color '#f30';
players[2].barrierColor '#900';
players[2].control = {
    
up    'i',
    
down  'k',
    
left  'j',
    
right 'l'
};// players[0] uses numpad
// players[3] uses home end delete & pagedown
players[3].color '#3f0';
players[3].barrierColor '#090';
players[3].control = {
    
up    '$',
    
down  '#',
    
left  'delete',
    
right '"'
};

= Шаг 13 — игра на четверых =

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, без этих знаний вы не сможете разрабатывать игры самостоятельно.

В этой статье мы создадим простую игру с помощью 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»

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

В этом пошаговом руководстве мы создадим простую игру 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, основанном на этом уроке, если нужно сделать доклад о разработке игр в целом.

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

Понравилась статья? Поделить с друзьями:
  • Как написать игру на source
  • Как написать игру на python для андроид
  • Как написать игру на python 2048
  • Как написать игру на pygame
  • Как написать игру на lazarus