Эта статья написана Майклом Кареном, автором Образовательного курса "Разработка игр с помощью JavaScript: создание тетриса".

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

С детскими шагами.

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

«Анимация — это не искусство движущихся рисунков, а искусство рисуемых движений». — Норман Макларен

Вот что мы сегодня рассмотрим:

  • История холста
  • Что такое элемент холста?
  • Что такое контекст холста?
  • Рисование вашего объекта
  • Анимация вашего объекта
  • Анимация нескольких объектов
  • Оптимизируйте свои анимации
  • Подведение итогов

История холста

Apple представила Canvas в 2004 году для поддержки приложений и браузера Safari. Несколько лет спустя он был стандартизирован WHATWG. Он поставляется с более точным контролем над рендерингом, но с затратами на управление каждой деталью вручную. Другими словами, он может обрабатывать множество объектов, но нам нужно кодировать все в деталях.

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

Canvas использует немедленный рендеринг: когда мы рисуем, он сразу же отображается на экране. Но это система «выстрелил-забыл». После того, как мы что-то нарисовали, холст забывает об объекте и знает его только как пиксели. Итак, нет объекта, который мы можем переместить. Вместо этого мы должны нарисовать его снова.

Создание анимации на Canvas похоже на создание покадрового фильма. Каждый кадр должен немного двигать объекты, чтобы оживить их.

Что такое элемент холста?

Элемент HTML <canvas> предоставляет пустой контейнер, на котором мы можем рисовать графику. Мы можем рисовать на нем фигуры и линии с помощью Canvas API, который позволяет рисовать графику с помощью JavaScript.

Холст — это прямоугольная область на HTML-странице, которая по умолчанию не имеет границ или содержимого. Размер холста по умолчанию составляет 300 пикселей × 150 пикселей (ширина × высота). Однако пользовательские размеры можно определить с помощью свойств HTML height и width:

<canvas id="canvas" width="600" height="300"></canvas>

Укажите атрибут id, чтобы иметь возможность ссылаться на него из скрипта. Чтобы добавить границу, используйте атрибут style или используйте CSS с атрибутом class:

<canvas id="canvas" width="600" height="300" style="border: 2px solid"></canvas>
<button onclick="animate()">Play</button>

Теперь, когда мы добавили границу, мы видим размер нашего пустого холста на экране.
У нас также есть кнопка с событием onclick для запуска нашей функции animate() при нажатии на нее.

Мы можем разместить наш код JavaScript в <script> элементах, которые мы поместим в документ <body> после элемента <canvas>:

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

Мы получаем ссылку на HTML-элемент <canvas> в DOM (Document Object Model) с помощью метода getElementById():

const canvas = document.getElementById('canvas');

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

Что такое контекст холста?

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

Метод HTMLCanvasElement.getContext() возвращает контекст рисования, в котором мы визуализируем графику. Передав '2d' в качестве аргумента, мы получаем контекст 2D-рендеринга холста:

const ctx = canvas.getContext('2d');

Существуют и другие доступные контексты, например webgl для контекста трехмерной визуализации, которые выходят за рамки этой статьи.

CanvasRenderingContext2D имеет множество методов рисования линий и форм на холсте. Для установки цвета линии мы используем strokeStyle, а для установки толщины используем lineWidth:

ctx.strokeStyle = 'black';
ctx.lineWidth = 5;

Теперь мы готовы нарисовать нашу первую линию на холсте. Но прежде чем мы это сделаем, нам нужно понять, как мы сообщаем холсту, где рисовать. Холст HTML представляет собой двумерную сетку. Верхний левый угол холста имеет координаты (0, 0).

X →
Y [(0,0), (1,0), (2,0), (3,0), (4,0), (5,0)]
↓ [(0,1), (1,1), (2,1), (3,1), (4,1), (5,1)]
  [(0,2), (1,2), (2,2), (3,2), (4,2), (5,2)]

Итак, когда мы говорим, что хотим moveTo(4, 1) на холсте, это означает, что мы начинаем с верхнего левого угла (0,0) и перемещаем четыре столбца вправо и одну строку вниз.

Рисуем свой объект 🔵

Получив контекст холста, мы можем рисовать на нем с помощью API контекста холста. Метод lineTo() добавляет прямую линию к текущему подпути, соединяя его последнюю точку с указанными координатами (x, y).

Как и другие методы, которые изменяют текущий путь, этот метод ничего не отображает напрямую. Чтобы нарисовать путь на холсте, вы можете использовать методы fill() или stroke().

ctx.beginPath();      // Start a new path
ctx.moveTo(100, 50);  // Move the pen to x=100, y=50.
ctx.lineTo(300, 150); // Draw a line to x=300, y=150.
ctx.stroke();         // Render the path

Мы можем использовать fillRect() для рисования заполненного прямоугольника. Установка fillStyle определяет цвет, используемый при заливке нарисованных фигур:

ctx.fillStyle = 'blue';
ctx.fillRect(100, 100, 30, 30); // (x, y, width, height);

Это рисует спрайт заполненного синего прямоугольника:

Анимация вашего объекта 🎥

Теперь давайте посмотрим, сможем ли мы заставить наш блок двигаться по холсту. Мы начинаем с установки size квадрата на 30. Затем мы можем перемещать значение x вправо с шагом size и рисовать объект снова и снова. Следующий код перемещает блок вправо, пока он не достигнет конца холста:

const size = 30;
ctx.fillStyle = 'blue';
for (let x = 0; x < canvas.width; x += size) {
  ctx.fillRect(x, 50, size, size);
}

Хорошо, мы смогли нарисовать квадрат так, как хотели. Но у нас есть две проблемы:

  1. Мы не убираем за собой.
  2. Слишком быстро, чтобы увидеть анимацию.

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

for (let x = 0; x < canvas.width; x += size) {
  ctx.clearRect(0, 0, canvas.width, canvas.height); // Clean up
  ctx.fillRect(x, 50, size, size);
}

Большой! Мы исправили первую проблему. Теперь давайте попробуем замедлить рисование, чтобы увидеть анимацию.

Возможно, вы знакомы с setInterval(function, delay). Он начинает повторное выполнение указанного function каждые delay миллисекунды. Я установил интервал 200 мс, что означает, что код выполняется пять раз в секунду.

let x = 0;
const id = setInterval(() => {
  ctx.clearRect(0, 0, canvas.width, canvas.height);      
  ctx.fillRect(x, 50, size, size);
  x += size;
  if (x >= canvas.width) {
    clearInterval(id);
  }
}, 200);

Чтобы остановить таймер, созданный setInterval(), нам нужно вызвать clearInterval() и дать ему идентификатор интервала для отмены. Используемый идентификатор — это тот, который возвращается setInterval(), и поэтому нам нужно его сохранить.

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

Каждый квадрат получает свой интервал, который очищает доску и закрашивает квадрат.

Это повсюду! Давайте посмотрим, как мы можем это исправить.

Анимировать несколько объектов

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

Мы можем добавить переменную started, чтобы запускать setInterval() только при первом нажатии кнопки. Каждый раз, когда мы нажимаем кнопку воспроизведения, мы добавляем новое значение 0 в массив squares. Этого достаточно для этой простой анимации, но для чего-то более сложного мы могли бы создать объект Square с координатами и возможными другими свойствами, такими как цвет.

let squares = [];
let started = false;
function play() {
  // Add 0 as x value for object to start from the left.
  squares.push(0);
  if (!started) {
      started = true;
      setInterval(() => {
        tick();
      }, 200)
  }
}
function tick() {
  // Clear canvas
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  // Paint objects
  squares.forEach(x => ctx.fillRect(x, 50, size, size));
  squares = squares.map(x => x += size) // move x to right
      .filter(x => x < canvas.width);  // remove when at end
}

Функция tick() очищает экран и рисует все объекты в массиве каждые 200 мс. И имея только один интервал, мы избегаем мерцания, которое было раньше. И теперь мы получаем более качественную анимацию:

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

Оптимизируйте свои анимации 🏃

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

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

function play() {
  // Add 0 as x value for object to start from the left.
  squares.push(0);
  if (!started) {
      animate();
  }
}
function animate() {
  tick();
  requestAnimationFrame(animate);  
}

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

Метод requestAnimationFrame() возвращает id, который мы используем для отмены запланированного кадра анимации. Чтобы отменить запланированный кадр анимации, вы можете использовать метод cancelAnimationFrame(id).

Чтобы замедлить анимацию, нам нужен таймер для проверки elapsed времени с момента последнего вызова функции tick(). Чтобы помочь нам, функции обратного вызова передается аргумент DOMHighResTimeStamp, указывающий момент времени, когда requestAnimationFrame() начинает выполнять функции обратного вызова.

let start = 0;
function animate(timestamp) {    
  const elapsed  = timestamp - start;
  if (elapsed > 200) {
    start = timestamp;
    tick();
  }
  requestAnimationFrame(animate);  
}

При этом у нас есть та же функциональность, что и раньше с setInterval().

Итак, в заключение, почему мы должны использовать requestAnimationFrame() вместо setInterval()?

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

Подведение итогов

В этой статье мы создали холст HTML5 и использовали JavaScript и его контекст 2D-рендеринга для рисования на холсте. Мы познакомились с некоторыми методами, доступными в контексте холста, и использовали их для визуализации различных фигур.

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

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

  • Создавайте циклы геймплея
  • Реализуйте интерактивные элементы управления с помощью addEventListener
  • Обнаружить столкновение с объектом
  • Отслеживание результатов

Чтобы помочь вам, я создал Разработка игр с помощью JavaScript: создание тетрис. Этот курс научит вас применять свои навыки работы с JavaScript для создания своей первой игры. Создав Tetris, вы изучите основы разработки игр и создадите свое профессиональное портфолио, на которое можно будет ссылаться во время следующего собеседования.

Удачного обучения!

Продолжить чтение о JavaScript и разработке игр на Educative

Начать обсуждение

Какие игры вы надеетесь разработать? Была ли эта статья полезна? Дайте нам знать в комментариях ниже!