Как написать игру на html5

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

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

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

Участвовать


October 13, 2014

javascript gamedev html5 canvas russian

На выходных нашлось немного свободного времени и я решил попрактиковаться в gamedev разработке. Давно собирался написать какую-нибудь игрушку, но все руки не доходили. Бегло пробежался по сети в поисках как это делают настоящие гуру. Мне понравилась вот эта статья. За основу своей будущей игры я взял фреймворк автора статьи.

Towers game 2D

Начало

  • sprite.js — библиотечка работы со спрайтами
  • resources.js — подгрузка ресурсов
  • input.js — библиотека ввода с клавиатуры
  • app.js — основной файл игры

Далее буду рассказывать только о файле app.js. Разберем его содержимое.

Для плавности анимации будем использовать requestAnimationFrame. Подробно о нем ознакомиться можно здесь

var requestAnimFrame = (function(){
    return window.requestAnimationFrame    ||
        window.webkitRequestAnimationFrame ||
        window.mozRequestAnimationFrame    ||
        window.oRequestAnimationFrame      ||
        window.msRequestAnimationFrame     ||
        function(callback){
            window.setTimeout(callback, 1000 / 60);
        };
})();

Разделим разработку нашей игры на несколько этапов:

  1. Создание и инициализация холста (canvas) на странице
  2. Добавление основной функции-цикла игры
  3. Инициализация и рендер объектов и ресурсов игры
  4. Обработка событий ввода пользователя
  5. Математика и расчет столкновений объектов в игре
  6. Окончание и перезагрузка игры

Этап 1. Создание и инициализация холста

Первым делом что мы должны сделать — это создать canvas элемент и добавить его к тегу body основной страницы игры.

var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
canvas.width = 1024;
canvas.height = 520;
document.body.appendChild(canvas);
  • Создаем объект canvas
  • Указываем, что мы создаем 2D игру (далее будем использовать везде в коде объект ctx)
  • Задаем размеры холста
  • Добавляем холст к тегу body на странице

Этап 2. Добавление основной функции-цикла

Основной цикл необходим для обновления и рендера игры.

var lastTime;
function main() {
    var now = Date.now();
    var dt = (now - lastTime) / 1000.0;

    update(dt);
    render();

    lastTime = now;
    requestAnimFrame(main);
}

Здесь вызываем функцию requestAnimFrame (к сожалению, поддерживается не во всех браузерах), которая генерирует 60 фреймов/секунду (как это было описано выше).

Этап 3. Инициализация и рендер объектов и ресурсов игры

Используем resource.js для загрузки ресурсов в игру. Хорошим правилом является добавить все изображения в 1 спрайт, но т.к я рисовал не сам, а брал готовые картинки, поэтому я решил с этим на заморачиваться, тем более, что в данном случае это не столь критично. Так это выглядит в коде

resources.load([
  'img/tower.png',
    'img/sprites.png',
    'img/spider.png',
  'img/hero.png',
    'img/bullet.png',
  'img/terrain.png'
]);
resources.onReady(init);

В функции init загружаем мир и добавлеем хэндлер кнопки reset, после game over.

function init() {
    terrainPattern = ctx.createPattern(resources.get('img/terrain.png'), 'repeat');

    document.getElementById('play-again').addEventListener('click', function() {
        reset();
    });
    
    reset();
    lastTime = Date.now();
    main();
}

Начальное состояние

var player = {
    pos: [0, 0],
    sprite: new Sprite('img/hero.png', [0, 0], [48, 30], 5, [0, 1, 2, 1]),
        down: new Sprite('img/hero.png', [0, 0], [48, 30], 5, [0, 1, 2, 1]),
        up: new Sprite('img/hero.png', [0, 144], [48, 30], 5, [0, 1, 2, 1]),
        left: new Sprite('img/hero.png', [0, 48], [48, 30], 5, [0, 1, 2, 1]),
        right: new Sprite('img/hero.png', [0, 96], [48, 30], 5, [0, 1, 2, 1])
};

var towers = [];
var bullets = [];
var enemies = [];
var explosions = [];

var lastTower = 0;
var gameTime = 0;
var isGameOver;
var terrainPattern;

var score = 0;
var scoreEl = document.getElementById('score');

Обновление состояния игрового процесса

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

switch (getRandomInt(0,4)) {
    case 0: //left
        enemies.push({
            pos: [0, Math.random() * (canvas.height - 30)],
            sprite: new Sprite('img/spider.png', [0, 0], [40, 30], 5, [0, 1, 2, 1])
        });
    break;
    case 1: //top
        enemies.push({
            pos: [Math.random() * canvas.width, 0],
            sprite: new Sprite('img/spider.png', [0, 0], [40, 30], 5, [0, 1, 2, 1])
        });
    break;
    case 2: //bottom
        enemies.push({
            pos: [Math.random() * canvas.width, canvas.height - 30],
            sprite: new Sprite('img/spider.png', [0, 0], [40, 30], 5, [0, 1, 2, 1])
        });
    break;
    default: //right
        enemies.push({
            pos: [canvas.width, Math.random() * (canvas.height - 30)],
            sprite: new Sprite('img/spider.png', [0, 0], [40, 30], 5, [0, 1, 2, 1])
        });
    break;
}

Здесь же используем sprite.js. Всю функцию можно посмотреть в исходниках.

Этап 4. Обработка событий ввода пользователя

Наш герой должен уметь двигаться вверх, вниз, влево, вправо. Соответственно привожу ниже реализацию данного решения

if (input.isDown('DOWN') || input.isDown('s')) {
    player.pos[1] += playerSpeed * dt;
        player.sprite = player.down;
}

if (input.isDown('UP') || input.isDown('w')) {
    player.pos[1] -= playerSpeed * dt;
        player.sprite = player.up;
}

if (input.isDown('LEFT') || input.isDown('a')) {
    player.pos[0] -= playerSpeed * dt;
        player.sprite = player.left;
}

if (input.isDown('RIGHT') || input.isDown('d')) {
    player.pos[0] += playerSpeed * dt;
        player.sprite = player.right;
}

При клике на пробел по задумке должны ставиться башни которые будут стрелять случайным образом во все стороны. Чтобы немного усложнить процесс игры башни разрешается ставить на некоторм расстоянии друг от друга. В данном случае это 50px.

if (input.isDown('SPACE') && !isGameOver) {
    var isClosest = false;
    for (var i = 0; i < towers.length; i++) {
        if (Math.abs(player.pos[0] - towers[i].pos[0]) < 50 && 
            Math.abs(player.pos[1] - towers[i].pos[1]) < 50) {
            isClosest = true;
        }
    }
    
    if (!isClosest) {
        towers[lastTower % 3] = {
            pos: [player.pos[0], player.pos[1]],
            lastFire: Date.now(),
            sprite: new Sprite('img/tower.png', [0, 0], [38, 35], 8, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])
        };
        lastTower++;
     }
}

Этап 5. Математика и расчет столкновений объектов в игре

Анимация персонажей, математика движения пуль, и логика движения NPC в игре описаны в функции updateEntities. Вот тут как раз нам и потребуются базовые знания линейной алгебры.

// Update the towers sprite animation
for(var i = 0; i < towers.length; i++) {
    var tower = towers[i];
    tower.sprite.update(dt);

    if (!isGameOver && Date.now() - tower.lastFire > 500) {
        var pi = Math.PI;
        var x = tower.pos[0] + tower.sprite.size[0] / 2;
        var y = tower.pos[1] + tower.sprite.size[1] / 2;

        bullets.push({
            pos: [x, y],
            k: getRandomArbitrary(-5 * pi, 5 * pi),
            sprite: new Sprite('img/bullet.png', [0, 0], [24, 24]) 
        });
        tower.lastFire = Date.now();
    }
}

Логика обновления анимации спрайтов башни. И создаем патроны для каждой башни в своем массиве.

Динамика пуль башни:

// Update all the bullets
for (var i = 0; i < bullets.length; i++) {
    var bullet = bullets[i];
    var c = dt * bulletSpeed;
    var sin = Math.sin(bullet.k);       
    var cos = Math.cos(bullet.k);

    bullet.pos[0] += sin * c;
    bullet.pos[1] += cos * c;       

    // Remove the bullet if it goes offscreen
    if (bullet.pos[1] < 0 || bullet.pos[1] > canvas.height ||
        bullet.pos[0] > canvas.width) {
        bullets.splice(i, 1);
        i--;
    }
}

Напомню, что нашей целью было чтобы башни стреляли случайным образом во всех направлениях.

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

// Update all the enemies
for (var i = 0; i < enemies.length; i++) {
    var x0 = enemies[i].pos[0];
    var y0 = enemies[i].pos[1];
    var x1 = player.pos[0];
    var y1 = player.pos[1];
    var c = enemySpeed * dt;
    var l = Math.sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0));
    
    enemies[i].pos[0] += (x1 - x0) * c / l;
    enemies[i].pos[1] += (y1 - y0) * c / l;
    enemies[i].sprite.update(dt);

    // Remove if offscreen
    if (enemies[i].pos[0] + enemies[i].sprite.size[0] < 0) {
        enemies.splice(i, 1);
        i--;
    }
}

Полный код функции updateEntities можно посмотреть в исходникак на GitHub.

Математика расчета столкновений хорошо описана в статье автора (раздел Collision Detection) используемого мной 2d бутстрапа.

Этап 6. Game Over и рестарт

Когда пауки доползают до нашего героя наступает конец света игры.

function gameOver() {
    document.getElementById('game-over').style.display = 'block';
    document.getElementById('game-over-overlay').style.display = 'block';
    isGameOver = true;
}

Показываем окно GAME OVER и кнопку «Начать заного». Кликаем ее и все начинается сначала :)

function reset() {
    document.getElementById('game-over').style.display = 'none';
    document.getElementById('game-over-overlay').style.display = 'none';
    isGameOver = false;
    gameTime = 0;
    lastTower = 0;
    score = 0;

    towers = [];
    enemies = [];
    bullets = [];

    player.pos = [canvas.width / 2, canvas.height / 2];
}

Заключение

В итоге, я для себя понял, что в gamedev много плюсов:

  • Весело и интересно проводишь время
  • Повторяешь курс школьной геометрии. Если игра серьезней, то и универ вспоминаешь :)
  • Практика программирования игр
  • Удовлетворение от проделанной работы

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

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

Оригинал: http://www.html5rocks.com/en/tutorials/canvas/notearsgame/

Перевод: Влад Мержевич

Итак, вы хотите сделать игру с помощью Canvas и HTML5? Следуйте этому руководству и окажетесь на пути в кратчайший срок. Руководство предполагает, что у вас, по меньшей мере, средний уровень знаний по JavaScript.

Вы можете вначале сыграть в игру или перейти непосредственно к статье и просматривать исходный код игры.

Создание холста

Чтобы начать рисовать, нам нужно создать холст. Поскольку это путеводитель без слёз, то будем использовать jQuery.

var CANVAS_WIDTH = 480;
var CANVAS_HEIGHT = 320;

var canvasElement = $("<canvas width='" + CANVAS_WIDTH + 
                      "' height='" + CANVAS_HEIGHT + "'></canvas>");
var canvas = canvasElement.get(0).getContext("2d");
canvasElement.appendTo('body');

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

var FPS = 30;
setInterval(function() {
  update();
  draw();
}, 1000/FPS);

Пока мы можем оставить методы update и draw пустыми. Главное знаем, что setInterval() периодически их вызывает.

function update() { ... }
function draw() { ... }

Здравствуй, мир

Теперь, когда мы имеем зацикленный геймплей, давайте обновим наш метод draw, нарисовав какой-нибудь текст на экране.

function draw() {
  canvas.fillStyle = "#000"; // Устанавливаем цвет чёрным
  canvas.fillText("Sup Bro!", 50, 50);
}

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

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

var textX = 50;
var textY = 50;

function update() {
  textX += 1;
  textY += 1;
}

function draw() {
  canvas.fillStyle = "#000";
  canvas.fillText("Sup Bro!", textX, textY);
}

Теперь всё должно кружиться. Если проследовать вдоль движения, то на экране остаются рисунки от предыдущих раз. Попробуйте догадаться, почему это происходит. Потому что мы не очищаем экран. Итак, давайте добавим некий код очистки экрана в метод draw.

function draw() {
  canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  canvas.fillStyle = "#000";
  canvas.fillText("Sup Bro!", textX, textY);
}

Теперь, когда у вас есть некоторый движущийся по экрану текст, вы на полпути к реальной игре. Осталось добавить управление, улучшить геймплей, подправить графику… Ладно, может быть, 1/7 пути к реальной игре.

Создание игрока

Создаем объект для хранения данных игрока ответственный за вещи вроде рисования. Здесь мы создаём объект player с помощью простого литерального объекта для хранения всей информации.

var player = {
  color: "#00A",
  x: 220,
  y: 270,
  width: 32,
  height: 32,
  draw: function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  }
};

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

function draw() {
  canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  player.draw();
}

Управление клавишами

Использование jQuery Hotkeys

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

$(document).bind("keydown", "left", function() { ... });

Это большой выигрыш, что не нужно беспокоиться о деталях, какая клавиша какой код имеет. Мы просто имеем возможность сказать вроде «когда игрок нажимает стрелку вверх что-то сделать». jQuery Hotkeys делает это хорошо.

Движения игрока

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

Вы можете спросить: «Почему бы просто не использовать механизм событий для обработки клавиш?». Да потому что величина опроса клавиатуры зависит от системы и не привязана ко времени цикла игры, так что геймплей меняется от системы к системе.

Хорошая новость в том, что я включил 16-строчный интерфейс на JavaScript, который делает доступным события запросов. Он называется key_status.js и вы можете запросить статус клавиши в любой момент проверив keydown.left и др.

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

function update() {
  if (keydown.left) {
    player.x -= 2;
  }

  if (keydown.right) {
    player.x += 2;
  }
}

Идите и подвигайте его.

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

function update() {
  if (keydown.left) {
    player.x -= 5;
  }

  if (keydown.right) {
    player.x += 5;
  }

  player.x = player.x.clamp(0, CANVAS_WIDTH - player.width);
}

Добавление большего числа клавиш довольно легко, так что добавим каких-нибудь снарядов.

function update() {
  if (keydown.space) {
    player.shoot();
  }

  if (keydown.left) {
    player.x -= 5;
  }

  if (keydown.right) {
    player.x += 5;
  }

  player.x = player.x.clamp(0, CANVAS_WIDTH - player.width);
}

player.shoot = function() {
  console.log("Пиу пиу");
  // :) По крайней мере добавить привязку клавиш было легко
};

Добавляем больше игровых объектов

Снаряды

Теперь добавим снаряды для реальности. Вначале нужна коллекция для их хранения:

var playerBullets = [];

Далее нам нужен конструктор для создания экземпляра пули.

function Bullet(I) {
  I.active = true;

  I.xVelocity = 0;
  I.yVelocity = -I.speed;
  I.width = 3;
  I.height = 3;
  I.color = "#000";

  I.inBounds = function() {
    return I.x >= 0 && I.x <= CANVAS_WIDTH &&
      I.y >= 0 && I.y <= CANVAS_HEIGHT;
  };

  I.draw = function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  };

  I.update = function() {
    I.x += I.xVelocity;
    I.y += I.yVelocity;

    I.active = I.active && I.inBounds();
  };

  return I;
}

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

player.shoot = function() {
  var bulletPosition = this.midpoint();

  playerBullets.push(Bullet({
    speed: 5,
    x: bulletPosition.x,
    y: bulletPosition.y
  }));
};

player.midpoint = function() {
  return {
    x: this.x + this.width/2,
    y: this.y + this.height/2
  };
};

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

function update() {
  ...
  playerBullets.forEach(function(bullet) {
    bullet.update();
  });

  playerBullets = playerBullets.filter(function(bullet) {
    return bullet.active;
  });
}

Финальный шаг отрисовывает пули.

function draw() {
  ...
  playerBullets.forEach(function(bullet) {
    bullet.draw();
  });
}

Враги

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

 enemies = [];

function Enemy(I) {
  I = I || {};

  I.active = true;
  I.age = Math.floor(Math.random() * 128);

  I.color = "#A2B";

  I.x = CANVAS_WIDTH / 4 + Math.random() * CANVAS_WIDTH / 2;
  I.y = 0;
  I.xVelocity = 0
  I.yVelocity = 2;

  I.width = 32;
  I.height = 32;

  I.inBounds = function() {
    return I.x >= 0 && I.x <= CANVAS_WIDTH &&
      I.y >= 0 && I.y <= CANVAS_HEIGHT;
  };

  I.draw = function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  };

  I.update = function() {
    I.x += I.xVelocity;
    I.y += I.yVelocity;

    I.xVelocity = 3 * Math.sin(I.age * Math.PI / 64);

    I.age++;

    I.active = I.active && I.inBounds();
  };

  return I;
};

function update() {
  ...

  enemies.forEach(function(enemy) {
    enemy.update();
  });

  enemies = enemies.filter(function(enemy) {
    return enemy.active;
  });

  if(Math.random() < 0.1) {
    enemies.push(Enemy());
  }
};

function draw() {
  ...

  enemies.forEach(function(enemy) {
    enemy.draw();
  });
}

Загрузка и рисование изображений

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

player.sprite = Sprite("player");

player.draw = function() {
  this.sprite.draw(canvas, this.x, this.y);
};

function Enemy(I) {
  ...

  I.sprite = Sprite("enemy");

  I.draw = function() {
    this.sprite.draw(canvas, this.x, this.y);
  };

  ...
}

Обнаружение столкновения

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

Будем использовать простой прямоугольный алгоритм определения столкновений:

function collides(a, b) {
  return a.x < b.x + b.width &&
         a.x + a.width > b.x &&
         a.y < b.y + b.height &&
         a.y + a.height > b.y;
}

Есть несколько видов столкновений, которые надо проверить:

  1. Пули игрока => вражеские корабли
  2. Игрок => вражеские корабли

Сделаем метод для обработки столкновений, который вызывается из метода update.

function handleCollisions() {
  playerBullets.forEach(function(bullet) {
    enemies.forEach(function(enemy) {
      if (collides(bullet, enemy)) {
        enemy.explode();
        bullet.active = false;
      }
    });
  });

  enemies.forEach(function(enemy) {
    if (collides(enemy, player)) {
      enemy.explode();
      player.explode();
    }
  });
}

function update() {
  ...
  handleCollisions();
}

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

function Enemy(I) {
  ...

  I.explode = function() {
    this.active = false;
    // Дополнительно: Добавляем графику для взрыва
  };

  return I;
};

player.explode = function() {
  this.active = false;
  // Дополнительно: Добавляем графику для взрыва и заканчиваем игру
};

Звук

Чтобы завершить опыт, добавим некоторые приятные звуковые эффекты. Звуки, подобно изображениям, могут вызвать боль при использовании в HTML5, но спасибо нашей магической бесслёзной формуле sound.js, звуки можно делать суперпросто.

player.shoot = function() {
  Sound.play("shoot");
  ...
}

function Enemy(I) {
  ...

  I.explode = function() {
    Sound.play("explode");
    ...
  }
}

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

На прощанье

Ещё раз. Вот рабочая версия игры. Вы можете скачать исходный код в zip.

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

Although Flash games have departed from this world, there is still a significant demand for browser-based and cross-platform games. Whether you yourself actively play them or not, more and more games from professional and indie developers alike come out each day.

This phenomenon is largely thanks to HTML5 games which have filled the gap to keep up with these eager gamers. In addition, thanks to the advancement of technology and new frameworks, HTML5 game development has reached extensive heights, allowing for multiplayer experiences, 3D graphics, and more! It is not unwise to say that gaming is now just as viable via a browser as it is on many other devices!

Assuming this sounds like an exciting prospect to you as a game developer, in this guide we’re going to delve into the topic of HTML5 game development and give you the run-down of how you can make your own games! Sit back, relax, and prepare to enhance your game development skills in entirely new ways!

What exactly is an HTML5 game?

Let’s start from total zero. What is HTML5? that’s a tricky question. There is an official definition of HTML5, which simply stands for the latest revision of HTML (the markup language used all over the world to build websites), and the more hyped definition (what most people understand when HTML5 is mentioned) which is all the “new” features of the web technologies that have come out in the last few years (JavaScript API’s like the Canvas or WebAudio, semantic HTML tags, etc).

For our purpose, we’ll use bits of the two. HTML5 is HTML in its latest version, which includes a whole bunch of cool features that make web technologies an open standard with endless possibilities combining HTML, CSS and JavaScript.

Having HTML along with all these superpowers that go beyond making a simple website allows us to make, among other things, games. These are HTML5 games.

Get 250+ coding courses for

AVAILABLE FOR A LIMITED TIME ONLY

 Building blocks

The very basic building blocks of a HTML5 game are those of the web:

  • HTML
  • CSS
  • JavaScript

Similarly to what happens with HTML5, when people talk about CSS3 they usually refer to the new things that come with CSS’s latest specifications, but in an analog manner, CSS3 is simply the latest CSS. Ignoring for a second the semantics of these definitions and thinking of the hyped versions of these terms, we also may need, in order to make HTML5 games:

  • HTML5 (JavaScript API’s)
  • CSS3

With the above you can make awesome games that will run on modern web browsers on mobile and desktop, but some games might require more features, so there are more building blocks that you can add.

For instance, you may want to make 3D games. If that is the case there is also WebGL, which is a JavaScript API to render 2D and 3D graphics on the browser, using the GPU for greater performance.

Road crossing game made with HTML5

Server side

If you want your games to saved data remotely you’ll need a server-side for your game. You can develop your own backend using any server-side language, you’ll need well a server in this case.

  • JavaScript (NodeJS)
  • PHP
  • Java
  • Python
  • Ruby

Or you can use a third-party Backend-as-a-Service provider such as Firebase or Parse. Some have free versions you can use and they’ll start charging you once you surpass certain limits. Some of these providers are particularly focused on games, some are mostly conceived for mobile apps but can be used for games too.

How to distribute a HTML5 game

The easiest way to distribute a HTML5 is to simply put it out there! By being built as a website, you can just embed it in on a page and publish it. Just like that.

If you want to distribute it through proprietary platforms you have to go through a process called wrapping. Basically, you create a native app for the platform you wanna distribute it to (iOS, Android, etc) and put your game inside so that this app acts like a web browser and “runs” your game.

For desktop platforms such as Windows, Mac or Linux there is a tool called NWjs that allows you to pack your HTML5 games for these platforms.

We can only cover the basics here, but we encourage you to read our more in-depth advice about publishing games.

Match 3 game made with HTML5

HTML5 game frameworks

Most games share some concepts, that of sprites (graphic elements that represent enemies, players, elements in your game), scenes or stages, animations, sound, loading graphic assets, etc. Since most game developers want to focus on their actual game and not in creating this whole abstraction layer, it is recommended you use a HTML5 game frameworks.

HTML5 game frameworks and libraries that contain building components you can use to create your own games. These libraries are Open Source projects created and maintained by people who want to contribute to the HTML5 gamedev environment. In many cases they created the frameworks for their own games, and after realizing that other people would want to not only use it but also contribute to it they released them as Open Source code, so everybody wins.

Picking what game engine to use is an important decision, so make sure you do proper research before making your choice. No matter what engine you pick, you will have to get familiar with its code and inner working if you want to use properly, so they shouldn’t be treated as black boxes.

What can help you make your choice:

  • Is your game for desktop, mobile or both?
  • Do they have an active community?
  • Are there many people using the framework nowadays?
  • Is it being maintained or the Github page looks like an abandoned town?

Sometimes looking at real games gives you more insight than just words. This project compares different engines by making the exact same Breakout game in all of them.

Some popular free frameworks are:

  • Phaser –> The most popular these days
  • CreateJS
  • LimeJS
  • Quintus
  • CraftyJS
  • MelonJS
  • PixieJS
  • BabylonsJS –> WebGL framework for 3D rendering
  • PlayCanvas
  • ThreeJS
  • KontraJS
  • Cannon.js
  • Stage.js
  • Matter.js

Solar system project made with Babylon.js

HTML5 game development courses

Video courses are a great way to learn new technologies. The main difference between a video course and just watching YouTube videos is that there is more structure. Good courses have a clear goal and build on to it step by step. Below a list of courses and tutorials by Zenva that can give you the tools you need to create HTML5 games.

General Game Development

  • 10 Best Ways to Learn to Code
  • How to Make a Game – Making Video Games from Scratch
  • How to Code a Game
  • How to Design a Game: Game Design Documents
  • How to Approach Game Narratives

Phaser

  • HTML5 Game Development Mini-Degree (Premium curriculum covering the creation of multiple mini-games with Phaser)
  • Phaser 4 Tutorials – Complete Guide

WebGL, 3D, and XR

  • 3D & XR JavaScript Academy (Premium curriculum covering a variety of JavaScript frameworks for 3D and XR experiences on the web)
  • WebVR for Beginners – Build VR Websites with A-Frame (Premium course on A-Frame)
  • Learn the Basics of Babylon.js (Free course on Babylon.js)

HTML5 Skills

  • Learn HTML and CSS (Free course)
  • JavaScript Programming for Beginners
  • JavaScript Mini-Projects – Language Learning Game
  • Node.js and Express for Beginners

At the GameDev Academy, as you know already we have a bunch of HTML5 game development tutorials, mostly on Phaser, LimeJs, Quintus and BabylonJS. There are other great places to find good quality HTML5 gamedev tuts:

  • Build New Games
  • Gamedevtuts+
  • HTML5 Game Development (news and links to tutorials)
  • Phaser Tutorials
  • Mozilla Game Tutorials
  • HTML5 Game Tutorial
  • HTML5 Games with JavaScript & Phaser
  • JavaScript HTML5 Game Development Tutorial
  • HTML5 Game Development Tutorials

HTML5 for Schools

Are you a teacher looking to help students get into frameworks like Phaser or teach them core and relevant web development skills? Try out Zenva Schools – an online platform offering coding-based courses for use in the classroom. The platform comes with video lessons, text summaries, quizzes, classroom management features, reporting, and more to help support learning HTML5!

HTML5 gamedev communities

You can find plenty of active communities on the Internet, some focus on gamedev in general and some others just in HTML5 gamedev.

Web:

  • HTML5GameDevs

Facebook:

  • Indie Game Developers
  • Indie App and Game Developers Group
  • Game Development
  • Video Game Developers

Other Communities:

  • Phaser Forum
  • reddit
  • GAMEDEV.JS
  • Babylonjs Froum
  • Threejs discourse

HTML5 gamedev challenges

  • The Global Game Jam® is an annual event, usually occurring in January every year. Participants work concurrently around the globe, and around a central theme, and then work together to create a game.
  • j13k competition: the competition is over but you can still read the Blog, subscribe to the Newsletter, and check the Resources for tools.

 HTML5 gamedev podcasts

I just know Lostcast, a podcast created by the guys from Lost Decade Games (whom we’ve interviewed in the past). In the podcast episodes they talk about they HTML5 games and game development in general.

FINAL DAYS: Unlock 250+ coding courses, guided learning paths, help from expert mentors, and more.

Related Posts

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

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

Но почему в настоящее время так много людей интересуются играми HTML5?

Ну, просто потому, что сегодня мы действительно можем использовать HTML5 для нацеливания на несколько платформ, используя один и тот же код: конечно, для настольных компьютеров (с IE9 / IE10, Firefox, Chrome, Opera и Safari), для планшетов и телефонов iOS и Android и телефонов и Windows 8 (и его будущее, связанное с миллионами планшетов и ПК, также тепло приветствует HTML5-игры! Затем я часто вижу некоторые игровые проекты HTML5, портированные на каждую из этих платформ практически без усилий.

Более того, производительность современных движков JavaScript в сочетании с графическими слоями с аппаратным ускорением позволяет нам уверенно создавать великолепные игры на HTML5.

Примечание: в этой статье мы поговорим только о <canvas>

Canvas и SVG: два способа рисования на экране

Canvas vs SVG

Прежде чем создавать свою первую HTML5-игру, вам нужно понять, как рисовать красивые объекты на экране. Есть два способа сделать это, и чтобы лучше понять их различия, вы должны начать с чтения этой статьи от Патрика Денглера (член рабочей группы SVG W3C): Мысли о том, когда использовать Canvas и SVG .

Вам также следует взглянуть на эти две замечательные сессии из MIX11 :

– Модернизация вашего сайта: SVG встречает HTML5
– Глубокое погружение в HTML5 <canvas>

Изучив весь этот контент, вы, вероятно, лучше поймете, почему почти во всех играх HTML5 используется Canvas, а не SVG. Canvas предлагает хорошо известную модель разработки для разработчиков игр (низкоуровневые API-интерфейсы рисования) и теперь очень хорошо аппаратно ускоряется большинством последних браузеров. Тем не менее, SVG и его технология на основе векторов, естественно, лучше вооружены для масштабирования между устройствами без потери качества. Вам просто нужно использовать магическое свойство ViewBox в SVG, которое справится с этим за вас. Тогда ваши игровые активы будут очень легко масштабироваться от 3 до 80 дюймов!

Иногда даже можно достичь одной и той же игры, используя обе технологии. Например, вот та же самая простая игра, сначала с использованием SVG, а затем Canvas: SVG Racketball и Canvas Racquetball . Они оба вышли из этой документации MSDN: программирование простых игр с использованием Canvas или SVG .

В этом случае анализ производительности каждой технологии может помочь вам решить, какую из них использовать для вашей игры. Например, вот два интересных эксперимента, которые покажут на экране тысячу маленьких шариков, используя SVG: 1000 шариков в SVG и Canvas: 1000 шариков в Canvas . Проверьте оба эксперимента в своем любимом браузере. В целом, производительность Canvas лучше подходит для рисования большого количества объектов на экране, в данном конкретном случае. Но это, как правило, также то, чего мы хотели бы достичь в наших видеоиграх. Чтобы получить лучший обзор сравнения производительности, вот интересная диаграмма:

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

So you want to use your basic knowledge of web development to create something a little cooler than a to-do app. Games are one of the best projects you can create, because they are very easily enjoyed by the end user and are all around fun to make! There are JavaScript libraries that are pre-made for game development, but I prefer creating from scratch so that I can understand everything completely.

An overview of the JavaScript game.

What better game to represent web development than the Chrome dinosaur game that you play when you lose your internet connection? It’s a fun game, and it’s easy to recreate the code. It doesn’t look exactly the same, but it functions the same. If you really want, you can style it when you’re done!

A version of a JavaScript game with a dinosaur player.

To begin coding the game, create a new folder in your documents. Use your favorite text editor to open that folder, then create three new files and name them: index.html, style.css, and script.js. It’s possible to do everything in one file with HTML5, but it’s more organized to keep everything separate.

Our index.html file is going to be very simple: once you have a basic HTML layout, create a div with the ID "game", and then two more divs inside of it with the IDs "character" and "block". The character will be the dinosaur, and the block will be the cactuses coming towards us.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Jump Game</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>

Next, go over to the CSS file and start applying styles to the two divs we just created. First, we’ll start with the game div. Select the element by its id, which is represented by the hash (#) symbol.

#game{
    width: 500px;
    height: 200px;
    border: 1px solid black;
    margin: auto;
}

Next, we’ll style our character div. We have to declare the position as relative because positional attributes like top and left only apply to positioned elements.

#character{
    width: 20px;
    height: 50px;
    background-color: red;
    position: relative;
    top: 150px; //game height - character height (200 - 50)
}

Create a keyframe animation called jump. This animation will make the top position slide up 50px and then slide back down.

@keyframes jump{
    0%{top: 150px;}
    30%{top: 100px;}
    70%{top: 100px;}
    100%{top: 150px;}
}

Next, we’ll create a new class called animate, which applies the jump animation.

.animate{
    animation: jump 300ms linear;
}

We’re going to use JavaScript to add the class "animate" to our character whenever you click your mouse.

In the script.js file, create a function called jump() that adds the "animate" class to the character div. Create an event listener that listens for the user to click, and then executes the jump function.

Create another function called removeJump() that removes the animate class. Then add a timeout function to jump() that runs removeJump() when the animation ends. The animation won’t run again unless we remove it.

var character = document.getElementById("character");
document.addEventListener("click",jump);
function jump(){
    character.classList.add("animate");
    setTimeout(removeJump,300); //300ms = length of animation
};
function removeJump(){
    character.classList.remove("animate");
}

This works, but it seems to glitch if the user clicks while it’s currently jumping. To fix that, add the line below at the beginning of jump(). It will stop the function from doing anything if the animation is running.

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

Now, we’ll go back to our CSS file and start styling the block div.

#block{
    width: 20px;
    height: 20px;
    background-color: blue;
    position: relative;
    top: 130px; //game height - character height - block height (200 - 50 - 20)
    left: 480px; //game width - block width (500 - 20)
    animation: block 1s infinite linear;
}

We haven’t created the block animation yet, so create an animation to make the block slide from the right to the left.

@keyframes block{
    0%{left: 500px} 
    100%{left: -20px}
}

Now we’re able to jump, but we have to make the game end if we hit the block. Create a function called checkDead() that gets the position of the block and character, and then evaluates if they are on top of each other. If they are, then end the game.

Create a variable called characterTop that is equal to the top value of the character div. getComputedStyle() will return all of the CSS values associated with an element, and getPropertyValue() specifies the property you want the value from.

Now, this would return a string with a value such as 100px. We only want the numerical value, so we’re going to wrap everything inside of a parseInt() function so that it returns the value as an integer.

Create an if statement that checks if blockLeft’s value is between -20px and 20px, and characterTop’s value is greater than 130px. If they are, that means they’re overlapping each other and the game is over. So w’ll set an alert “Game over”.

Create an interval function that runs the checkDead function every 10 milliseconds.

var block = document.getElementById("block");
function checkDead(){
    let characterTop = parseInt(window.getComputedStyle(character).getPropertyValue("top"));
    let blockLeft = parseInt(window.getComputedStyle(block).getPropertyValue("left"));
    if(blockLeft<20 && blockLeft>-20 && characterTop>=130){
        alert("Game over");
    }
}

setInterval(checkDead, 10);

Now you have a fully functioning game. This is a great project for sharing with non-developers, because they’ll be able to better appreciate what you’ve learned!

There is a link to my GitHub if you want to copy the code. You can also check out my YouTube video if you learn better visually!

LogRocket: Debug JavaScript errors more easily by understanding the context

Debugging code is always a tedious task. But the more you understand your errors the easier it is to fix them.

LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to find out exactly what the user did that led to an error.

LogRocket Dashboard Free Trial Banner

LogRocket records console logs, page load times, stacktraces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!

Try it for free.

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