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

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

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

Эта статья посвящена разработке стильных, модных и молодежных HTML5 приложений с помощью нового фреймворка Phaser. В ней описан процесс установки библиотеки и создание классической игры Pong.

Введение

Phaser — это движок для разработки мобильных и десктопных HTML5 игр, базирующийся на библиотеке PIXI.js. Поддерживает рендеринг в Canvas и WebGL, анимированные спрайты, частицы, аудио, разные способы ввода и физику объектов. Исходники доступны как для просмотра, так и для свободной модификации. Он создан Ричардом Дейви (Richard Davey), известному благодаря активному участию в сообществе программистов, использующих Flixel framework. Ричард не скрывает, что вдохновлялся Фликселем, поэтому некоторые вещи в Фазере будут знакомы опытным флешерам. Первая версия нового движка вышла 13 сентября этого года, сейчас ведется не только активное развитие библиотеки, но и написание документации, поэтому в данный момент уроков по ней, мягко говоря, немного. Что, по моему скромному мнению, следует исправлять, и прямо сейчас.

Установка библиотеки и локального веб-сервера

Итак, начнем. Для запуска и тестирования приложений нам необходимо установить локальный веб-сервер. Все примеры из комплекта библиотеки используют PHP, поэтому и сервер нужен соответствующий. Я использовал MAMP для MacOS, для Windows подойдет отечественный Denwer или любой другой аналог.

После установки веб-сервера необходимо скачать последнюю версию Фазера c GitHub: https://github.com/photonstorm/phaser. В данный момент (13 октября 2013 года) рекомендую качать dev ветку, так как эта версия содержит в себе ряд очень полезных изменений по сравнению с основной, в том числе и больший объем документации. Для тех, кто не использует GitHub, доступна прямая ссылка на архив: https://github.com/photonstorm/phaser/archive/dev.zip.

Чтобы убедиться, что все настроено правильно, можно запустить небольшое приложение-пример Hello Phaser. Создайте папку hellophaser в директории вашего веб-сервера, предназначенной для сайтов, и скопируйте туда три файла из папки Docs/Hello Phaser:

image

Запустите свой любимый браузер и откройте URL со скопированными файлами (в моем случае http://localhost:8888/hellophaser/). Если все хорошо, вы увидите вращающийся симпатичный логотип, такой как на скриншоте ниже:

image

Разработка игры

Подготовка необходимых файлов

Теперь можно приступать к разработке нашей первой игры. Создайте для нее папку phaser-pong на вашем веб-сервере и скопируйте туда файл phaser.js из папки build с исходниками фреймворка. Также создайте в ней папку assets, где мы будем хранить все ресурсы, относящиеся к игре, и файл index.html (собственно, здесь и будет наша игра).

Скопируйте в папку assets изображения шарика, ракетки и фона. Можно взять следующие файлы (в качестве фона я взял звездное небо из примеров Фазера), а можно нарисовать что-то свое. Главное — это убедиться, что вы загружаете в игру нужные картинки с корректными именами и подходящими размерами. Также не стоит выбирать слишком большие изображения, с их отрисовкой могут возникнуть проблемы. Поэтому перед использованием фотографии своего кота уменьшите ее до, скажем, 480х640 (разрешение нашей игры), и все будет хорошо.
image
image
image

В результате содержимое папки phaser-pong будет таким:

image

А в папке assets будет три картинки:

image

Создание главного объекта игры, загрузка ресурсов

Наконец-то все подготовительные этапы выполнены, и начинается собственно разработка. Откройте index.html и вставьте туда следующий код:

<script src="phaser.js"></script>
<script type="text/javascript">
    var game = new Phaser.Game(480, 640, Phaser.AUTO, '', { preload: preload, create: create, update: update });
    function preload() {
        game.load.image('bet', 'assets/bet.png');
        game.load.image('ball', 'assets/ball.png');
        game.load.image('background', 'assets/starfield.jpg');
    }
    function create() {
        game.add.tileSprite(0, 0, 480, 640, 'background');
    }

    function update () {
    }
</script>

Откройте в браузере адрес новой игры (у меня это http://localhost:8888/phaser-pong/) и вы увидите ее окно с нарисованным фоном

image

Что же происходит в написанном коде? Вначале мы импортируем библиотеку. Потом создаем объект типа Game, задавая разрешение нашего приложения, в данном случае ширину 480 и высоту 640 пикселей. Phaser.AUTO означает, что тип рендера будет выбираться автоматически. При желании можно задать Canvas или WebGL. Четвертый параметр задает родительский DOM-объект для игры, его мы не указываем.
В пятом параметре перечислены основные функции игры. preload() содержит код для загрузки ресурсов, create() — инициализацию игры, а update() — команды, выполняемые при обновлении приложения. На настольном компьютере update() вызывается примерно 60 раз в секунду. Именно эта функция будет содержать в себе основную игровую логику.

В функции create() мы добавляем статический спрайт с фоном нашей игры. Спрайт заполняет пространство, указанное в первых четырех параметрах tileSprite.

Игровые объекты

Сейчас перейдем к самому интересному — наполним нашу игру логикой. После объявления переменной game и перед функцией preload() объявим объекты с ракетками игрока и компьютера, мячиком, а также укажем скорости их движения:

    var playerBet;
    var computerBet;
    var ball;

    var computerBetSpeed = 190;
    var ballSpeed = 300;

Для создания ракеток напишем функцию createBet(x, y):

function createBet(x, y) {
        var bet = game.add.sprite(x, y, 'bet');
        bet.anchor.setTo(0.5, 0.5);
        bet.body.collideWorldBounds = true;
        bet.body.bounce.setTo(1, 1);
        bet.body.immovable = true;

        return bet;
    }

Метод создает спрайт с указанными координатами и добавляет его в игру. Поле anchor отвечает за точку отсчета координат спрайта, устанавливаем его по центру изображения ракетки. body содержит в себе элементы для работы с физикой. Здесь мы ограничиваем движение ракетки пределами игрового пространства, задаем силу «отскока» и указываем, что при столкновении с объектами ракетка не будет отлетать в сторону.

Добавим два вызова этой функции в create(), сразу после создания фона. Ракетки будут добавлены в игру после фонового изображения, поэтому мы будем их видеть на экране:

        playerBet = createBet(game.world.centerX, 600);
        computerBet = createBet(game.world.centerX, 20);

Аналогичным образом создадим шарик, дописав следующий код сразу после вызовов функции createBet() в create():

        ball = game.add.sprite(game.world.centerX, game.world.centerY, 'ball');
        ball.anchor.setTo(0.5, 0.5);
        ball.body.collideWorldBounds = true;
        ball.body.bounce.setTo(1, 1);

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

image

Логика

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

    var ballReleased = false;
    
    function releaseBall() {
        if (!ballReleased) {
            ball.body.velocity.x = ballSpeed;
            ball.body.velocity.y = -ballSpeed;
            ballReleased = true;
        }
    }

Функция проверяет, что шарик еще не запущен, и в таком случае задает ему скорость с помощью поля velocity.
Вызов функции повесим на нажатие кнопки мышки, написав следующую строку в create():

        game.input.onDown.add(releaseBall, this);

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

function update () {
        //Управляем ракеткой игрока
        playerBet.x = game.input.x;

        var playerBetHalfWidth = playerBet.width / 2;

        if (playerBet.x < playerBetHalfWidth) {
            playerBet.x = playerBetHalfWidth;
        }
        else if (playerBet.x > game.width - playerBetHalfWidth) {
            playerBet.x = game.width - playerBetHalfWidth;
        }

        //Управляем ракеткой компьютерного соперника
        if(computerBet.x - ball.x < -15) {
            computerBet.body.velocity.x = computerBetSpeed;
        }
        else if(computerBet.x - ball.x > 15) {
            computerBet.body.velocity.x = -computerBetSpeed;
        }
        else {
            computerBet.body.velocity.x = 0;
        }
    }

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

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

        //Проверяем и обрабатываем столкновения мячика и ракеток
        game.physics.collide(ball, playerBet, ballHitsBet, null, this);
        game.physics.collide(ball, computerBet, ballHitsBet, null, this);

Метод collide проверяет столкновение двух объектов (первые два параметра) и вызывает указанную в третьем функцию для выполнения каких-либо действий над столкнувшимися спрайтами. Эта функция выглядит так:

    function ballHitsBet (_ball, _bet) {
        var diff = 0;

        if (_ball.x < _bet.x) {
            //  Шарик находится с левой стороны ракетки
            diff = _bet.x - _ball.x;
            _ball.body.velocity.x = (-10 * diff);
        }
        else if (_ball.x > _bet.x) {
            //  Шарик находится с правой стороны ракетки
            diff = _ball.x -_bet.x;
            _ball.body.velocity.x = (10 * diff);
        }
        else {
            //  Шарик попал в центр ракетки, добавляем немножко трагической случайности его движению
            _ball.body.velocity.x = 2 + Math.random() * 8;
        }
    }

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

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

    function checkGoal() {
        if (ball.y < 15) {
            setBall();
        } else if (ball.y > 625) {
            setBall();
        }
    }

    function setBall() {
        if (ballReleased) {
            ball.x = game.world.centerX;
            ball.y = game.world.centerY;
            ball.body.velocity.x = 0;
            ball.body.velocity.y = 0;
            ballReleased = false;
        }
        
    }

checkGoal() вызывается постоянно, поэтому копируем ее в конец update():

        //Проверяем, не забил ли кто-то гол
        checkGoal();

Все! Открываем браузер, наслаждаемся фантастическим и современным геймплеем нашей игры, радуемся жизни и свежеприобретенным навыками программирования.

Заключение

Естественно, игре не хватает еще многого, как минимум подсчета очков и определения победителей. Но мне кажется, что для введения в разработку с Phaser достаточно показанных вещей. Движок поддерживает много других классных функций, которые я собираюсь показать на примере новой игры, чуть более сложной и непосредственно относящейся к Хабру, чтобы было интереснее.
Стей тьюнед.

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

Полезные ссылки:
1. Англоязычное введение в Фазер
2. Форум, посвященный движку
3. Твиттер Ричарда Дейви

Update: по совету товарища hell0w0rd закинул пример на гитхаб и создал страничку, чтобы можно было опробовать игру:
https://github.com/nononononono/phaser-pong
http://nononononono.github.io/phaser-pong/

Update от 20.10.2013: fessnecro добавил частицы при столкновении шарика с ракетками и новые уровни, за что ему спасибо. Эти изменения находятся в основном бренче. Оригинальная версия, описанная в статье, находится в ветке gh-pages.

И, на всякий случай, полный исходный код index.html:

<script src="phaser.js"></script>
<script type="text/javascript">

    var game = new Phaser.Game(480, 640, Phaser.AUTO, '', { preload: preload, create: create, update: update });

    var playerBet;
    var computerBet;
    var ball;

    var computerBetSpeed = 190;
    var ballSpeed = 300;
    var ballReleased = false;

    function preload() {

        game.load.image('bet', 'assets/bet.png');
        game.load.image('ball', 'assets/ball.png');
        game.load.image('background', 'assets/starfield.jpg');

    }


    function create() {

        game.add.tileSprite(0, 0, 480, 640, 'background');

        playerBet = createBet(game.world.centerX, 600);
        computerBet = createBet(game.world.centerX, 20);


        ball = game.add.sprite(game.world.centerX, game.world.centerY, 'ball');
        ball.anchor.setTo(0.5, 0.5);
        ball.body.collideWorldBounds = true;
        ball.body.bounce.setTo(1, 1);

        game.input.onDown.add(releaseBall, this);
    }

    function createBet(x, y) {
        var bet = game.add.sprite(x, y, 'bet');
        bet.anchor.setTo(0.5, 0.5);
        bet.body.collideWorldBounds = true;
        bet.body.bounce.setTo(1, 1);
        bet.body.immovable = true;

        return bet;
    }

    function update () {
        //Управляем ракеткой игрока
        playerBet.x = game.input.x;

        var playerBetHalfWidth = playerBet.width / 2;

        if (playerBet.x < playerBetHalfWidth) {
            playerBet.x = playerBetHalfWidth;
        }
        else if (playerBet.x > game.width - playerBetHalfWidth) {
            playerBet.x = game.width - playerBetHalfWidth;
        }

        //Управляем ракеткой компьютерного соперника
        if(computerBet.x - ball.x < -15) {
            computerBet.body.velocity.x = computerBetSpeed;
        }
        else if(computerBet.x - ball.x > 15) {
            computerBet.body.velocity.x = -computerBetSpeed;
        }
        else {
            computerBet.body.velocity.x = 0;
        }

        //Проверяем и обрабатываем столкновения мячика и ракеток
        game.physics.collide(ball, playerBet, ballHitsBet, null, this);
        game.physics.collide(ball, computerBet, ballHitsBet, null, this);

        //Проверяем, не забил ли кто-то гол
        checkGoal();
    }

    function ballHitsBet (_ball, _bet) {

        var diff = 0;

        if (_ball.x < _bet.x) {
            //  Шарик находится с левой стороны ракетки
            diff = _bet.x - _ball.x;
            _ball.body.velocity.x = (-10 * diff);
        }
        else if (_ball.x > _bet.x) {
            //  Шарик находится с правой стороны ракетки
            diff = _ball.x -_bet.x;
            _ball.body.velocity.x = (10 * diff);
        }
        else {
            //  Шарик попал в центр ракетки, добавляем немножко трагической случайности его движению
            _ball.body.velocity.x = 2 + Math.random() * 8;
        }

    }

    function checkGoal() {
        if (ball.y < 15) {
            setBall();
        } else if (ball.y > 625) {
            setBall();
        }
    }

    function setBall() {
        if (ballReleased) {
            ball.x = game.world.centerX;
            ball.y = game.world.centerY;
            ball.body.velocity.x = 0;
            ball.body.velocity.y = 0;
            ballReleased = false;
        }
        
    }

    function releaseBall() {
        if (!ballReleased) {
            ball.body.velocity.x = ballSpeed;
            ball.body.velocity.y = -ballSpeed;
            ballReleased = true;
        }
    }

</script>

В разных компаниях эта игра называется по-разному: «Мемори», «Найди пару» или «Мемо», но суть одна:

  1. Есть стопка карточек, где у каждой картинки есть такая же пара.
  2. Эти карточки раскладываются картинками вниз на игровом поле.
  3. Игроки по очереди переворачивают любые две карточки.
  4. Если картинки совпали — игрок забирает их себе и повторяет ход. 
  5. Если не совпали — переворачивает их назад на то же место и ход переходит к другому игроку.
  6. Побеждает тот, кто соберёт больше пар.

Этим мы сегодня и будем заниматься.

Собираем игру «Найди пару» на HTML и JS

Что делаем

Чтобы проект не превратился в огромный лонгрид, сделаем всё поэтапно. Сегодня будет самое начало:

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

Остальное сделаем в другой раз, этого уже будет достаточно для полноценной игры. Для реализации нам понадобятся стандартные для веба инструменты: HTML, CSS и JavaScript. Игра будет работать в браузере, на мобильниках тоже пойдёт, если высота экрана будет 500 пикселей или больше.

Готовим HTML-файл

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

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

Также нам понадобится:

  • Нормализатор CSS — он сделает так, чтобы страница выглядела одинаково во всех браузерах.
  • Библиотека jQuery — с её помощью мы получим доступ ко всем элементам на странице. Можно и без неё, но с ней проще.
  • Своя таблица стилей — её мы сделаем позже, пока просто подключим.

Создаём новый файл index.html и добавляем туда сразу весь код страницы:

<!DOCTYPE html>
<html lang="ru" >
<head>
  <meta charset="UTF-8">
  <title>Найди пару</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- подключаем нормализатор CSS -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css">
	<!-- подключаем свои стили -->
	<link rel="stylesheet" href="./style.css">

</head>
<body>
	<!-- общий блок для всего -->
	<div class="wrap">
		<!-- блок с игрой -->
		<div class="game"></div>
		<!-- модальное окно, которое появится после сбора всех пар -->
		<div class="modal-overlay">
			<div class="modal">
				<!-- поздравительная надпись -->
				<h2 class="winner">Победа!</h2>
				<!-- кнопка перезапуска игры -->
				<button class="restart">Сыграем ещё?</button>
			</div>
		</div>
  </div>

	  <!-- подключаем jQuery -->
	<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js'></script>
	<!-- и наш скрипт -->
	<script  src="./script.js"></script>

</body>
</html>

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

Настраиваем модальное окно

Раз у нас уже есть окно, его можно сразу настроить — добавить на страницу нужные стили и посмотреть, как будет выглядеть сообщение о победе.Создаём файл style.css и добавляем туда сначала общие настройки для всей страницы:

/* для всех элементов ограничиваем их размеры размерами блока */
* {
  box-sizing: border-box;
}

/* общие настройки страницы */
html, body {
  height: 100%;
}

/* ставим тёмный фон и растягиваем на всю высоту */
body {
  background: black;
  min-height: 100%;
  font-family: "Arial", sans-serif;
}

Теперь оформим модальное окно:

  1. Используем блок modal-overlay, чтобы затемнить всю страницу с её содержимым.
  2. Поверх него в блоке modal покажем наше окно с сообщением о победе.
  3. Отдельно в стилях настроим внешний вид текста и кнопки.

Добавим этот код в файл со стилями:

/* настройки затемнения при выводе модального окна */
.modal-overlay {
  /* затемняем экран */
  background: rgba(0, 0, 0, 0.8);
  /* располагаем окно по центру экрана */
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

/* настройки модального окна */
.modal {
  position: relative;
  width: 500px;
  height: 300px;
  max-height: 90%;
  max-width: 90%;
  min-height: 380px;
  margin: 0 auto;
  background: white;
  top: 50%;
  transform: translateY(-50%);
  padding: 30px 10px;
}

/* настройки шрифта сообщения о победе */
.modal .winner {
  font-size: 80px;
  text-align: center;
  color: #4d4d4d;
  text-shadow: 0px 3px 0 black;
}

/* если ширина окна маленькая, делаем шрифт поменьше */
@media (max-width: 480px) {
  .modal .winner {
    font-size: 60px;
  }
}

/* настройки кнопки перезапуска игры */
.modal .restart {
  margin: 30px auto;
  padding: 20px 30px;
  display: block;
  font-size: 30px;
  border: none;
  background: #4d4d4d;
  background: linear-gradient(#4d4d4d, #222);
  border: 1px solid #222;
  border-radius: 5px;
  color: white;
  text-shadow: 0px 1px 0 black;
  cursor: pointer;
}

/* меняем фон при наведении мышки на кнопку */
.modal .restart:hover {
  background: linear-gradient(#222, black);
}

/* выравниваем надписи на модальном окне по центру */
.modal .message {
  text-align: center;
}

Окно готово, но на старте оно не нужно, поэтому скроем его: добавим display: none; в оба первых блока. Это скроет окно, но всё оформление останется — как только окно нам понадобится, мы уберём это свойство через скрипт.

Создаём карточки

Теперь нам понадобится скрипт. Создаём файл script.js и добавляем в него пустую функцию — вся работа будет происходить внутри неё:

(function(){
})();

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

// весь скрипт — это одна большая функция
(function(){
	
	// карточки
	var cards = [
		{	
			// название
			name: "php",
			// адрес картинки
			img: "https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/PHP-logo.svg/297px-PHP-logo.svg.png",
			// порядковый номер пары
			id: 1,
		},
		{
			name: "css3",
			img: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/62/CSS3_logo.svg/160px-CSS3_logo.svg.png",
			id: 2
		},
		{
			name: "html5",
			img: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/61/HTML5_logo_and_wordmark.svg/160px-HTML5_logo_and_wordmark.svg.png",
			id: 3
		},
		{
			name: "jquery",
			img: "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fd/JQuery-Logo.svg/440px-JQuery-Logo.svg.png",
			id: 4
		}, 
		{
			name: "javascript",
			img: "https://upload.wikimedia.org/wikipedia/commons/thumb/9/99/Unofficial_JavaScript_logo_2.svg/160px-Unofficial_JavaScript_logo_2.svg.png",
			id: 5
		},
		{
			name: "node",
			img: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d9/Node.js_logo.svg/262px-Node.js_logo.svg.png",
			id: 6
		},
		{
			name: "photoshop",
			img: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/af/Adobe_Photoshop_CC_icon.svg/164px-Adobe_Photoshop_CC_icon.svg.png",
			id: 7
		},
		{
			name: "python",
			img: "https://www.python.org/static/img/python-logo@2x.png",
			id: 8
		},
		{
			name: "rails",
			img: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/62/Ruby_On_Rails_Logo.svg/425px-Ruby_On_Rails_Logo.svg.png",
			id: 9
		},
		{
			name: "sass",
			img: "https://upload.wikimedia.org/wikipedia/commons/thumb/9/96/Sass_Logo_Color.svg/213px-Sass_Logo_Color.svg.png",
			id: 10
		},
		{
			name: "sublime",
			img: "https://upload.wikimedia.org/wikipedia/commons/thumb/7/79/Breezeicons-apps-48-sublime-text.svg/160px-Breezeicons-apps-48-sublime-text.svg.png",
			id: 11
		},
		{
			name: "wordpress",
			img: "https://upload.wikimedia.org/wikipedia/commons/thumb/2/20/WordPress_logo.svg/440px-WordPress_logo.svg.png",
			id: 12
		},
	];

})();

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

Вот что будет происходить в этом объекте:

  1. Получаем доступ ко всем элементам на странице.
  2. Виртуально перемешиваем карточки.
  3. Для каждой карточки динамически создаём HTML-код — с его помощью мы увидим эту карточку на игровом поле.
  4. Получаем доступ ко всем карточкам.
  5. Устанавливаем стартовые значения разных свойств и переменных.
  6. Добавляем всем элементам свою реакцию на нажатие.
  7. Прописываем логику сравнения выбранной пары.
  8. Если пары закончились — показываем победное сообщение.
  9. Если нажали на перезапуск — запускаем игру сначала.

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

Для «рубашки» наших карточек возьмём бесплатную фотографию какого-то кода из сервиса Unsplash.

//  объявляем объект, внутри которого будет происходить основная механика игры
var Memory = {

	// создаём карточку
	init: function(cards){
		//  получаем доступ к классам
		this.$game = $(".game");
		this.$modal = $(".modal");
		this.$overlay = $(".modal-overlay");
		this.$restartButton = $("button.restart");
		// собираем из карточек массив — игровое поле
		this.cardsArray = $.merge(cards, cards);
		// перемешиваем карточки
		this.shuffleCards(this.cardsArray);
		// и раскладываем их
		this.setup();
	},

	// как перемешиваются карточки
	shuffleCards: function(cardsArray){
		// используем встроенный метод .shuffle
		this.$cards = $(this.shuffle(this.cardsArray));
	},

	// раскладываем карты
	setup: function(){
		// подготавливаем код с карточками на страницу
		this.html = this.buildHTML();
		// добавляем код в блок с игрой
		this.$game.html(this.html);
		// получаем доступ к сформированным карточкам
		this.$memoryCards = $(".card");
		// на старте мы не ждём переворота второй карточки
		this.paused = false;
		// на старте у нас нет перевёрнутой первой карточки
 		this.guess = null;
 		// добавляем элементам на странице реакции на нажатия
		this.binding();
	},

	// как элементы будут реагировать на нажатия
	binding: function(){
		// обрабатываем нажатие на карточку
		this.$memoryCards.on("click", this.cardClicked);
		// и нажатие на кнопку перезапуска игры
		this.$restartButton.on("click", $.proxy(this.reset, this));
	},

	// что происходит при нажатии на карточку
	cardClicked: function(){
		// получаем текущее состояние родительской переменной
		var _ = Memory;
		// и получаем доступ к карточке, на которую нажали
		var $card = $(this);
		// если карточка уже не перевёрнута и мы не нажимаем на ту же самую карточку второй раз подряд
		if(!_.paused && !$card.find(".inside").hasClass("matched") && !$card.find(".inside").hasClass("picked")){
			// переворачиваем её
			$card.find(".inside").addClass("picked");
			// если мы перевернули первую карточку
			if(!_.guess){
				// то пока просто запоминаем её
				_.guess = $(this).attr("data-id");
			// если мы перевернули вторую и она совпадает с первой
			} else if(_.guess == $(this).attr("data-id") && !$(this).hasClass("picked")){
				// оставляем обе на поле перевёрнутыми и показываем анимацию совпадения
				$(".picked").addClass("matched");
				// обнуляем первую карточку
				_.guess = null;
					// если вторая не совпадает с первой
					} else {
						// обнуляем первую карточку
						_.guess = null;
						// не ждём переворота второй карточки
						_.paused = true;
						// ждём полсекунды и переворачиваем всё обратно
						setTimeout(function(){
							$(".picked").removeClass("picked");
							Memory.paused = false;
						}, 600);
					}
			// если мы перевернули все карточки
			if($(".matched").length == $(".card").length){
				// показываем победное сообщение
				_.win();
			}
		}
	},

	// показываем победное сообщение
	win: function(){
		// не ждём переворота карточек
		this.paused = true;
		// плавно показываем модальное окно с предложением сыграть ещё
		setTimeout(function(){
			Memory.showModal();
			Memory.$game.fadeOut();
		}, 1000);
	},

	// показываем модальное окно
	showModal: function(){
		// плавно делаем блок с сообщением видимым
		this.$overlay.show();
		this.$modal.fadeIn("slow");
	},

	// прячем модальное окно
	hideModal: function(){
		this.$overlay.hide();
		this.$modal.hide();
	},

	// перезапуск игры
	reset: function(){
		// прячем модальное окно с поздравлением
		this.hideModal();
		// перемешиваем карточки
		this.shuffleCards(this.cardsArray);
		// раскладываем их на поле
		this.setup();
		// показываем игровое поле
		this.$game.show("slow");
	},

	// Тасование Фишера–Йетса - https://bost.ocks.org/mike/shuffle/
	shuffle: function(array){
		var counter = array.length, temp, index;
	   	while (counter > 0) {
        	index = Math.floor(Math.random() * counter);
        	counter--;
        	temp = array[counter];
        	array[counter] = array[index];
        	array[index] = temp;
	    	}
	    return array;
	},

	// код, как добавляются карточки на страницу
	buildHTML: function(){
		// сюда будем складывать HTML-код
		var frag = '';
		// перебираем все карточки подряд
		this.$cards.each(function(k, v){
			// добавляем HTML-код для очередной карточки
			frag += '<div class="card" data-id="'+ v.id +'"><div class="inside">
			<div class="front"><img src="'+ v.img +'"
			alt="'+ v.name +'" /></div>
			<div class="back"><img src="https://images.unsplash.com/photo-1576836165612-8bc9b07e7778?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1587&q=80"
			alt="Codepen" /></div></div>
			</div>';
		});
		// возвращаем собранный код
		return frag;
	}
};

Чтобы объект «ожил», добавим в самый конец нашей основной функции вызов первого метода объекта:

// запускаем игру
Memory.init(cards);

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

Настраиваем стили карточек

Наша задача сейчас — разместить картинки по карточкам, а сами карточки расположить по сетке 6 × 4. Для этого мы сначала подготовим два блока — общий и блок с карточками:

/* стили основного блока */
.wrap {
  /* устанавливаем относительное позиционирование */
  position: relative;
  /* высота элементов */
  height: 100%;
  /* минимальная высота и отступы */
  min-height: 500px;
  padding-bottom: 20px;
}

/* блок с игрой */
.game {
  /* добавляем трёхмерность для эффекта вращения */
  transform-style: preserve-3d;
  perspective: 500px;
  /* пусть элементы занимают всё доступное им пространство */
  min-height: 100%;
  height: 100%;
}

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

/* стили карточек */
.card {
  /* параметры расположения, высоты и ширины карточки */
  float: left;
  width: 16.66666%;
  height: 25%;
  /* отступы */
  padding: 5px;
  /* выравнивание по центру */
  text-align: center;
  /* подключаем блочные элементы и перспективу */
  display: block;
  perspective: 500px;
  /* добавляем относительное позиционирование */
  position: relative;
  cursor: pointer;
  z-index: 50;
}

/* настройки размера карт при максимальной ширине экрана 800 пикселей */
@media (max-width: 800px) {
  .card {
    width: 25%;
    height: 16.666%;
  }
}

Собираем игру «Найди пару» на HTML и JS

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

/* обратная сторона карточки */
.card .inside {
/* содержимое занимает весь размер карточки */
  width: 100%;
  height: 100%;
  display: block;
  /* анимация переворачивания */
  transform-style: preserve-3d;
  transition: 0.4s ease-in-out;
  /* у лицевой стороны будет белый фон */
  background: white;
}



/* общие настройки для обеих сторон карточки */
.card .front, .card .back {
  /* рисуем границу */
  border: 1px solid black;
  /* прячем обратную сторону */
  -webkit-backface-visibility: hidden;
          backface-visibility: hidden;
  /* абсолютное позиционирование */
  position: absolute;
  top: 0;
  left: 0;
  /* размеры и отступ */
  width: 100%;
  height: 100%;
  padding: 20px;
}

/* настройки изображения на лицевой и обратной стороне */
.card .front img, .card .back img {
  /* картинка занимает всю ширину */
  max-width: 100%;
  /* отображаем как блочный элемент, без отступов */
  display: block;
  margin: 0 auto;
  max-height: 100%;
}

/* настройки лицевой стороны */
.card .front {
  /* переворачиваем карточку обложкой вверх */
  transform: rotateY(-180deg);
}

/* настройки при максимальной ширине экрана 800 пикселей */
@media (max-width: 800px) {
  .card .front {
    padding: 5px;
  }

  .card .back {
    padding: 10px;
  }
}

  /* запускаем анимацию переворачивания при клике на карточку */
.card .inside.picked, .card .inside.matched {
  transform: rotateY(180deg);
}

Карточки расположились по сетке, переворачиваются и возвращаются назад, если пара не совпала. Единственное, чего нам не хватает, — анимации совпадения. Сделаем так, чтобы при переворачивании пары фон у карточек из неё на мгновение становился зелёным:

/* задаём ключевые кадры анимации совпадения */
@keyframes matchAnim {
  0% {
  /*  зелёный фон */
    background: #bcffcc;
  }
  100% {
  /*  белый фон  */
    backgroud: white;
  }
}

/* и делаем то же самое для движка WebKit */
@-webkit-keyframes matchAnim {
  0% {
    background: #bcffcc;
  }
  100% {
    background: white;
  }
}

  /* анимация совпадения пары */
.card .inside.matched {
  -webkit-animation: 1s matchAnim ease-in-out;
          animation: 1s matchAnim ease-in-out;
  -webkit-animation-delay: 0.4s;
          animation-delay: 0.4s;
}

Собираем игру «Найди пару» на HTML и JS

Поиграть на странице проекта.

<!DOCTYPE html>
<html lang="ru" >
<head>
  <meta charset="UTF-8">
  <title>Найди пару</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- подключаем нормализатор CSS -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css">
	<!-- подключаем свои стили -->
	<link rel="stylesheet" href="./style.css">

</head>
<body>
	<!-- общий блок для всего -->
	<div class="wrap">
		<!-- блок с игрой -->
		<div class="game"></div>
		<!-- модальное окно, которое появится после сбора всех пар -->
		<div class="modal-overlay">
			<div class="modal">
				<!-- поздравительная надпись -->
				<h2 class="winner">Победа!</h2>
				<!-- кнопка перезапуска игры -->
				<button class="restart">Сыграем ещё?</button>
			</div>
		</div>
  </div>

	  <!-- подключаем jQuery -->
	<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js'></script>
	<!-- и наш скрипт -->
	<script  src="./script.js"></script>

</body>
</html>
/* для всех элементов ограничиваем их размеры размерами блока */
* {
  box-sizing: border-box;
}

/* общие настройки страницы */
html, body {
  height: 100%;
}

/* ставим тёмный фон и растягиваем на всю высоту */
body {
  background: black;
  min-height: 100%;
  font-family: "Arial", sans-serif;
}

/* стили основного блока */
.wrap {
  /* устанавливаем относительное позиционирование */
  position: relative;
  /* высота элементов */
  height: 100%;
  /* минимальная высота и отступы */
  min-height: 500px;
  padding-bottom: 20px;
}

/* блок с игрой */
.game {
  /* добавляем трёхмерность для эффекта вращения */
  transform-style: preserve-3d;
  perspective: 500px;
  /* пусть элементы занимают всё доступное им пространство */
  min-height: 100%;
  height: 100%;
}



/* стили карточек */
.card {
  /* параметры расположения, высоты и ширины карточки */
  float: left;
  width: 16.66666%;
  height: 25%;
  /* отступы */
  padding: 5px;
  /* выравнивание по центру */
  text-align: center;
  /* подключаем блочные элементы и перспективу */
  display: block;
  perspective: 500px;
  /* добавляем относительное позиционирование */
  position: relative;
  cursor: pointer;
  z-index: 50;
}

/* настройки размера карт при максимальной ширине экрана 800 пикселей */
@media (max-width: 800px) {
  .card {
    width: 25%;
    height: 16.666%;
  }
}

/* обратная сторона карточки */
.card .inside {
/* содержимое занимает весь размер карточки */
  width: 100%;
  height: 100%;
  display: block;
  /* анимация переворачивания */
  transform-style: preserve-3d;
  transition: 0.4s ease-in-out;
  /* у лицевой стороны будет белый фон */
  background: white;
}



/* общие настройки для обеих сторон карточки */
.card .front, .card .back {
  /* рисуем границу */
  border: 1px solid black;
  /* прячем обратную сторону */
  -webkit-backface-visibility: hidden;
          backface-visibility: hidden;
  /* абсолютное позиционирование */
  position: absolute;
  top: 0;
  left: 0;
  /* размеры и отступ */
  width: 100%;
  height: 100%;
  padding: 20px;
}

/* настройки изображения на лицевой и обратной стороне */
.card .front img, .card .back img {
  /* картинка занимает всю ширину */
  max-width: 100%;
  /* отображаем как блочный элемент, без отступов */
  display: block;
  margin: 0 auto;
  max-height: 100%;
}

/* настройки лицевой стороны */
.card .front {
  /* переворачиваем карточку обложкой вверх */
  transform: rotateY(-180deg);
}

/* настройки при максимальной ширине экрана 800 пикселей */
@media (max-width: 800px) {
  .card .front {
    padding: 5px;
  }

  .card .back {
    padding: 10px;
  }
}

  /* запускаем анимацию переворачивания при клике на карточку */
.card .inside.picked, .card .inside.matched {
  transform: rotateY(180deg);
}

/* задаём ключевые кадры анимации совпадения */
@keyframes matchAnim {
  0% {
  /*  зелёный фон */
    background: #bcffcc;
  }
  100% {
  /*  белый фон  */
    backgroud: white;
  }
}

/* и делаем то же самое для движка WebKit */
@-webkit-keyframes matchAnim {
  0% {
    background: #bcffcc;
  }
  100% {
    background: white;
  }
}

  /* анимация совпадения пары */
.card .inside.matched {
  -webkit-animation: 1s matchAnim ease-in-out;
          animation: 1s matchAnim ease-in-out;
  -webkit-animation-delay: 0.4s;
          animation-delay: 0.4s;
}

/* настройки затемнения при выводе модального окна */
.modal-overlay {
  /* на старте его не видно */
  display: none;
  /* затемняем экран */
  background: rgba(0, 0, 0, 0.8);
  /* располагаем окно по центру экрана */
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

/* настройки модального окна */
.modal {
  display: none;
  position: relative;
  width: 500px;
  height: 300px;
  max-height: 90%;
  max-width: 90%;
  min-height: 380px;
  margin: 0 auto;
  background: white;
  top: 50%;
  transform: translateY(-50%);
  padding: 30px 10px;
}

/* настройки шрифта сообщения о победе */
.modal .winner {
  font-size: 80px;
  text-align: center;
  color: #4d4d4d;
  text-shadow: 0px 3px 0 black;
}

/* если ширина окна маленькая, делаем шрифт поменьше */
@media (max-width: 480px) {
  .modal .winner {
    font-size: 60px;
  }
}

/* настройки кнопки перезапуска игры */
.modal .restart {
  margin: 30px auto;
  padding: 20px 30px;
  display: block;
  font-size: 30px;
  border: none;
  background: #4d4d4d;
  background: linear-gradient(#4d4d4d, #222);
  border: 1px solid #222;
  border-radius: 5px;
  color: white;
  text-shadow: 0px 1px 0 black;
  cursor: pointer;
}

/* меняем фон при наведении мышки на кнопку */
.modal .restart:hover {
  background: linear-gradient(#222, black);
}

/* выравниваем надписи на модальном окне по центру */
.modal .message {
  text-align: center;
}
// Memory Game
// © 2014 Nate Wiley
// License -- MIT

// весь скрипт — это одна большая функция
(function(){
	
	//  объявляем объект, внутри которого будет происходить основная механика игры
	var Memory = {

		// создаём карточку
		init: function(cards){
			//  получаем доступ к классам
			this.$game = $(".game");
			this.$modal = $(".modal");
			this.$overlay = $(".modal-overlay");
			this.$restartButton = $("button.restart");
			// собираем из карточек массив — игровое поле
			this.cardsArray = $.merge(cards, cards);
			// перемешиваем карточки
			this.shuffleCards(this.cardsArray);
			// и раскладываем их
			this.setup();
		},

		// как перемешиваются карточки
		shuffleCards: function(cardsArray){
			// используем встроенный метод .shuffle
			this.$cards = $(this.shuffle(this.cardsArray));
		},

		// раскладываем карты
		setup: function(){
			// подготавливаем код с карточками на страницу
			this.html = this.buildHTML();
			// добавляем код в блок с игрой
			this.$game.html(this.html);
			// получаем доступ к сформированным карточкам
			this.$memoryCards = $(".card");
			// на старте мы не ждём переворота второй карточки
			this.paused = false;
			// на старте у нас нет перевёрнутой первой карточки
     		this.guess = null;
     		// добавляем элементам на странице реакции на нажатия
			this.binding();
		},

		// как элементы будут реагировать на нажатия
		binding: function(){
			// обрабатываем нажатие на карточку
			this.$memoryCards.on("click", this.cardClicked);
			// и нажатие на кнопку перезапуска игры
			this.$restartButton.on("click", $.proxy(this.reset, this));
		},

		// что происходит при нажатии на карточку
		cardClicked: function(){
			// получаем текущее состояние родительской переменной
			var _ = Memory;
			// и получаем доступ к карточке, на которую нажали
			var $card = $(this);
			// если карточка уже не перевёрнута и мы не нажимаем на ту же самую карточку второй раз подряд
			if(!_.paused && !$card.find(".inside").hasClass("matched") && !$card.find(".inside").hasClass("picked")){
				// переворачиваем её
				$card.find(".inside").addClass("picked");
				// если мы перевернули первую карточку
				if(!_.guess){
					// то пока просто запоминаем её
					_.guess = $(this).attr("data-id");
				// если мы перевернули вторую и она совпадает с первой
				} else if(_.guess == $(this).attr("data-id") && !$(this).hasClass("picked")){
					// оставляем обе на поле перевёрнутыми и показываем анимацию совпадения
					$(".picked").addClass("matched");
					// обнуляем первую карточку
					_.guess = null;
						// если вторая не совпадает с первой
						} else {
							// обнуляем первую карточку
							_.guess = null;
							// не ждём переворота второй карточки
							_.paused = true;
							// ждём полсекунды и переворачиваем всё обратно
							setTimeout(function(){
								$(".picked").removeClass("picked");
								Memory.paused = false;
							}, 600);
						}
				// если мы перевернули все карточки
				if($(".matched").length == $(".card").length){
					// показываем победное сообщение
					_.win();
				}
			}
		},

		// показываем победное сообщение
		win: function(){
			// не ждём переворота карточек
			this.paused = true;
			// плавно показываем модальное окно с предложением сыграть ещё
			setTimeout(function(){
				Memory.showModal();
				Memory.$game.fadeOut();
			}, 1000);
		},

		// показываем модальное окно
		showModal: function(){
			// плавно делаем блок с сообщением видимым
			this.$overlay.show();
			this.$modal.fadeIn("slow");
		},

		// прячем модальное окно
		hideModal: function(){
			this.$overlay.hide();
			this.$modal.hide();
		},

		// перезапуск игры
		reset: function(){
			// прячем модальное окно с поздравлением
			this.hideModal();
			// перемешиваем карточки
			this.shuffleCards(this.cardsArray);
			// раскладываем их на поле
			this.setup();
			// показываем игровое поле
			this.$game.show("slow");
		},

		// Тасование Фишера–Йетса - https://bost.ocks.org/mike/shuffle/
		shuffle: function(array){
			var counter = array.length, temp, index;
		   	while (counter > 0) {
	        	index = Math.floor(Math.random() * counter);
	        	counter--;
	        	temp = array[counter];
	        	array[counter] = array[index];
	        	array[index] = temp;
		    	}
		    return array;
		},

		// код, как добавляются карточки на страницу
		buildHTML: function(){
			// сюда будем складывать HTML-код
			var frag = '';
			// перебираем все карточки подряд
			this.$cards.each(function(k, v){
				// добавляем HTML-код для очередной карточки
				frag += '<div class="card" data-id="'+ v.id +'"><div class="inside">
				<div class="front"><img src="'+ v.img +'"
				alt="'+ v.name +'" /></div>
				<div class="back"><img src="https://images.unsplash.com/photo-1576836165612-8bc9b07e7778?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1587&q=80"
				alt="Codepen" /></div></div>
				</div>';
			});
			// возвращаем собранный код
			return frag;
		}
	};

	// карточки
	var cards = [
		{	
			// название
			name: "php",
			// адрес картинки
			img: "https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/PHP-logo.svg/297px-PHP-logo.svg.png",
			// порядковый номер пары
			id: 1,
		},
		{
			name: "css3",
			img: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/62/CSS3_logo.svg/160px-CSS3_logo.svg.png",
			id: 2
		},
		{
			name: "html5",
			img: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/61/HTML5_logo_and_wordmark.svg/160px-HTML5_logo_and_wordmark.svg.png",
			id: 3
		},
		{
			name: "jquery",
			img: "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fd/JQuery-Logo.svg/440px-JQuery-Logo.svg.png",
			id: 4
		}, 
		{
			name: "javascript",
			img: "https://upload.wikimedia.org/wikipedia/commons/thumb/9/99/Unofficial_JavaScript_logo_2.svg/160px-Unofficial_JavaScript_logo_2.svg.png",
			id: 5
		},
		{
			name: "node",
			img: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d9/Node.js_logo.svg/262px-Node.js_logo.svg.png",
			id: 6
		},
		{
			name: "photoshop",
			img: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/af/Adobe_Photoshop_CC_icon.svg/164px-Adobe_Photoshop_CC_icon.svg.png",
			id: 7
		},
		{
			name: "python",
			img: "https://www.python.org/static/img/python-logo@2x.png",
			id: 8
		},
		{
			name: "rails",
			img: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/62/Ruby_On_Rails_Logo.svg/425px-Ruby_On_Rails_Logo.svg.png",
			id: 9
		},
		{
			name: "sass",
			img: "https://upload.wikimedia.org/wikipedia/commons/thumb/9/96/Sass_Logo_Color.svg/213px-Sass_Logo_Color.svg.png",
			id: 10
		},
		{
			name: "sublime",
			img: "https://upload.wikimedia.org/wikipedia/commons/thumb/7/79/Breezeicons-apps-48-sublime-text.svg/160px-Breezeicons-apps-48-sublime-text.svg.png",
			id: 11
		},
		{
			name: "wordpress",
			img: "https://upload.wikimedia.org/wikipedia/commons/thumb/2/20/WordPress_logo.svg/440px-WordPress_logo.svg.png",
			id: 12
		},
	];
    
	// запускаем игру
	Memory.init(cards);


})();

Что дальше

Сейчас у нас всё работает, но как-то неопрятно:

  • картинки не по центру;
  • непонятно, как играть нескольким игрокам;
  • нет подсчёта пар.

Исправим это в следующей версии. Подпишитесь, чтобы не пропустить продолжени.

Вёрстка:

Кирилл Климентьев

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

HTML5-игры – незатейливый способ войти в геймдев-индустрию для тех, кто только осваивает программирование.

Преимуществ у таких игр немало:

  • Легко создавать. Достаточно знать стандартные для веба HTML, CSS и JavaScript. Код не придется писать с нуля: разработчики уже написали движки для создания игр. У пользователей хорошо заходят маленькие игры и простые динамики.
  • Кроссплатформенность. В одну игру могут играть пользователи с разных телефонов, планшетов и компьютеров. Соответственно можно делать многопользовательские игры.
  • Множество платформ распространения. Разработчики публикуют игры в AppStore и Google Play, на специальных площадках, платформах социальных сетей и мессенджеров.

В этой статье пробежимся по основным инструментам создания HTML5-игр и платформам распространения.

Движки, библиотеки и ассеты

HTML5-игру можно написать, руководствуясь стандартным набором фронтенд-технологий. Сформировать страницу на HTML, описать стили в CSS и логику работы на JavaScript. Так получится всё проконтролировать, но результат займёт много времени. Похоже на ситуацию с фронтенд-фреймворками.

Движок – ядро, на котором строятся все основные составляющие. Большинство крупных движков имеют бесплатные версии. Но это не значит, что их разработчики не зарабатывают. Так, у Phaser платные плагины, в Construct 3 платная подписка для полной версии, иные движки заставят раскошелиться ради публикации проекта в магазинах приложений.

🎮 HTML5-игры за 5 минут

Если вы не большой любитель JavaScript, но вам нравится широта распространения HTML5-игр и не хочется ни за что платить, присмотритесь к Defold. Игры в Defold пишутся на Lua, но готовый проект можно портировать на любую интересующую вас платформу, в том числе создать сборку для HTML5.

Выбирая движок самостоятельно, используйте таблицу из репозитория bebraw. В таблице указаны лицензия распространения, тип отрисовки (2D, 3D), занимаемый объем, а также ссылки на проекты и документацию.

Библиотеки. Движки заточены под разработку игр. Для тонкой настройки анимации, звуков и предзагрузки элементов есть отдельные библиотеки и пакеты модулей, такие как CreateJS. Для трехмерного геймплея – Babylon.js. Если решили заморочиться на физике взаимодействия объектов – изучите PhysicsJS.

Ассеты. Игры – это не только код, но ещё множество разных файлов: звуков, картинок моделей и текстур — их еще называют ассетами. На kenny.nl собрано 20 тыс. векторных 2D- и 3D-ассетов, звуков и элементов интерфейса. Тонны пиксель-арта есть на itch.io. Открытая библиотека звуков — freesound. 3D-модели ищите на turbosquid и sketchfab.

Распространение

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

  • Галереи игр в мобильных приложениях. Telegram, Facebook, VK, Яндекс и другие компании развивают собственные платформы для распространения HTML5-игр. Так, в Telegram можно опубликовать игру через @BotFather, а VK принимает игры на платформе Direct Games. Реальные заработки небольшой игровой студии в VK можно оценить по DTF-статье Mewton Games.
  • Специализированные платформы. Есть множество площадок, работающих преимущественно с HTML5-играми: html5games, kongregate, newgrounds.com и другие.
  • Экспорт в нативное приложение. Можно преобразовывать HTML5-игры в нативные аппы и размещать в магазинах приложений для iOS и Android. Делается это с помощью приложений-оберток, например, PhoneGap-сборок.

Актуальный турнир

Один из примеров актуальности темы: недавно Сбер открыл регистрацию на турнир SmartMarket Cup: HTML5 Games. В рамках соревнования независимые разработчики и студии могут портировать новые или уже существующие игры. Конкурс проходит на SmartMarket – платформе Сбера, на которой можно создавать, продвигать и монетизировать приложения для семейства виртуальных ассистентов Салют.

🎮 HTML5-игры за 5 минут

Если хотите поучаствовать, зарегистрируйтесь на странице турнира и платформе SmartMarket Studio, загрузите игру до 26 ноября и пройдите модерацию. Все HTML5-игры, опубликованные на SmartMarket до 30 ноября, автоматически примут участие в турнире.

Общий призовой фонд – 1,5 млн рублей, по 750 тысяч рублей в двух категориях: 1 место – 350 тысяч рублей, 2 место – 250 тысяч рублей, 3 место – 150 тысяч рублей.


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 много плюсов:

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

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

Оригинал: 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, а также быть устойчивыми перед лицом будущих изменений.

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