HTML5 Canvas - простая игра

129

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

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

Интерактивная игра в лабиринт на основе HTML5 Canvas с анимацией

С точки зрения посетителя веб-страницы это забавная игра, а с точки зрения разработчика это эффективное использование возможностей холста HTML5 и искусного программирования на JavaScript. Посмотреть этот пример вживую вы можете на странице - "HTML5 Canvas - лабиринт". Оттуда же можно скачать необходимые файлы изображений для этого примера - face.png, maze.png и easy_maze.png.

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

Подготовительные работы

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

Для этого подхода почти наверняка потребуется инструмент для автоматического создания кода рисования. Например, лабиринт можно будет нарисовать в Adobe Illustrator, а потом преобразовать его в код для холста с помощью какого-либо модуля расширения.

Другой вариант — взять готовую графику лабиринта и прорисовать ее на холсте. Этот подход будет особенно легким, т.к. Интернет изобилует бесплатными страницами для создания лабиринтов. Найти такие страницы очень легко — просто выполните поиск в Google по словам "maze generator", и вы получите буквально тысячи ссылок. Выбрав понравившийся вам генератор, укажите несколько параметров (например, размер, форму, цвета, плотность и сложность лабиринта), нажмите кнопку Создать, и в считанные секунды вы получите рисунок лабиринта, который можно сохранить на своем компьютере.

Исходная разметка примера выглядит следующим образом:

<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>HTML5 Canvas - Лабиринт</title>
<style>
  canvas {
    border: 6px double black;
    background: white;
  }

  img {
    display: none;
  }

  button {
    padding: 8px;
  }
</style>
<script src="maze.js"></script>
</head>
<body>
   <canvas id="canvas"></canvas>
   <div>
      <button onclick="loadHard()">Сложный лабиринт</button>
      <button onclick="loadEasy()">Простой лабиринт</button>
   </div>

   <img id="face" src="face.png">
</body>
</html>

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

// Определяем глобальные переменные для холста и контекста 
var canvas;
var context;

window.onload = function() {
  // Подготавливаем холст
  canvas = document.getElementById("canvas");
  context = canvas.getContext("2d");

  // Рисуем фон лабиринта
  drawMaze("maze.png", 268, 5);

  // При нажатии клавиши вызываем функцию processKey()
  window.onkeydown = processKey;
};

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

// Отслеживаем текущую позицию значка
var x = 0;
var y = 0;

// Таймер, включающий и отключающий новый лабиринт в любое время
var timer;

function drawMaze(mazeFile, startingX, startingY) {
  // Остановить таймер (если запущен)
  clearTimeout(timer);

  // Остановить перемещение значка
  dx = 0;
  dy = 0;

  // Загружаем изображение лабиринта
  var imgMaze = new Image();
  imgMaze.onload = function() {
    // Изменяем размер холста в соответствии 
	// с размером изображения лабиринта
    canvas.width = imgMaze.width;
    canvas.height = imgMaze.height;

    // Рисуем лабиринт
    context.drawImage(imgMaze, 0,0);

    // Рисуем значок
    x = startingX;
    y = startingY;

    var imgFace = document.getElementById("face");
    context.drawImage(imgFace, x, y);
    context.stroke();

    // Рисуем следующий кадр через 10 миллисекунд
    timer = setTimeout("drawFrame()", 10);
  };
  imgMaze.src = mazeFile;
}

В коде используется двухэтапный метод рисования изображения на холсте. Сначала определяется функция для обработки события изображения onload и последующего отображения загруженного изображения на холсте. Потом устанавливается атрибут src объекта изображения, что загружает изображение и активирует код. Этот двухэтапный процесс немного посложнее, чем просто получение изображения из скрытого элемента <img> на странице, но он необходим для создания функции, позволяющей загружать любое изображение лабиринта.

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

Анимация значка

Процесс прохождения лабиринта начинается, когда пользователь нажмет одну из клавиш со стрелками. Например, при нажатии клавиши <↓> значок начинает двигаться вниз, пока не натолкнется на препятствие или не будет нажата другая клавиша.

Для этого в коде используются две глобальные переменные для отслеживания скорости значка, иными словами, количества пикселов, на которое он смещается по оси x или y в каждом кадре. Эти переменные называются dx и dy.

Когда пользователь нажимает какую-либо клавишу, холст вызывает функцию processKey(). Эта функция проверяет, не была ли нажата одна из клавиш со стрелкой, и если была, изменяет направление движения значка. Чтобы определить, какая именно клавиша со стрелкой была нажата, проверяется код нажатой клавиши. Например, код 38 соответствует клавише <↑>. Функция processKey() игнорирует все клавиши, за исключением клавиш со стрелками:

// Скорость перемещения значка
var dx = 0;
var dy = 0;

function processKey(e) {
  // Если значок находится в движении, останавливаем его
  dx = 0;
  dy = 0;

  // Если нажата стрелка вверх, начинаем двигаться вверх
  if (e.keyCode == 38) {
    dy = -1;
  }

  // Если нажата стрелка вниз, начинаем двигаться вниз
  if (e.keyCode == 40) {
    dy = 1;
  }

  // Если нажата стрелка влево, начинаем двигаться влево
  if (e.keyCode == 37) {
    dx = -1;
  }

  // Если нажата стрелка вправо, начинаем двигаться вправо
  if (e.keyCode == 39) {
    dx = 1;
  }
}

Функция processKey() не меняет текущую позицию значка и не пытается обновить ее отображение на холсте. Эта задача осуществляется функцией drawFrame(), которая вызывается каждые 10 мс:

function drawFrame() {
  // Обновляем кадр только если значок движется
  if (dx != 0 || dy != 0) {
    // Закрашиваем перемещение значка желтым цветом
    context.beginPath();
    context.fillStyle = "rgb(254,244,207)";
    context.rect(x, y, 15, 15);
    context.fill()

    // Обновляем координаты значка, создавая перемещение
    x += dx;
    y += dy;

    // Проверка столкновения со стенками лабиринта
	// (вызывается доп. функция)
    if (checkForCollision()) {
      x -= dx;
      y -= dy;
      dx = 0;
      dy = 0;
    }

    // Перерисовываем значок
    var imgFace = document.getElementById("face");
    context.drawImage(imgFace, x, y);

    // Проверяем дошел ли пользователь до финиша.
	// Если дошел, то выводим сообщение
    if (y > (canvas.height - 17)) {
      alert("Ты победил!");
      return;
    }
  }

  // Рисуем следующий кадр через 10 миллисекунд
  timer = setTimeout("drawFrame()", 10);
}

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

Если же значок движется, то функция drawFrame() закрашивает желтым цветом текущую позицию значка, создавая, таким образом, след после продвижения. Потом значок перемещается в новую позицию. Затем код вызывает функцию checkForCollision(), чтобы проверить новую позицию. (Код этой функции проверки попадания рассматривается в следующем разделе.) Если новая позиция не верна, это означает, что значок столкнулся с преградой, и его нужно возвратить назад в старую позицию и прекратить движение.

Потом проверяется, не вышел ли значок за пределы лабиринта, т.е. прошел его. Если вышел, то выводится соответствующее сообщение. В противном случае код устанавливает время ожидания для вызова метода drawFrame() опять 10 мс.

На данном этапе мы рассмотрели весь код для игры в лабиринт, за исключением логики функции checkForCollision(), которая выполняет проверку столкновения значка с преградой.

Проверка попадания с использованием цвета пикселов

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

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

Метод проверки на столкновения посредством анализа цвета пикселов возможен благодаря предоставляемой холстом возможности манипулировать отдельными пикселами, из которых состоит любое изображение. Контекст рисования имеет три метода для манипулирования пикселами: getImageData(), putImageData() и createImageData().

Метод getImageData() применяется для захвата пикселов прямоугольной области холста. Захваченные пикселы можно изменить и вставить обратно в холст с помощью метода putImageData(). А метод createImageData() позволяет создать в памяти новый, пустой блок пикселов, которые можно изменить, а потом вставить в холст посредством метода putImageData().

Если вы ожидаете, что каждый пиксел представляется одним числом, то ошибаетесь. Каждый пиксел представляется четырьмя числами — тремя для красной, зеленой и синей цветовых составляющих и одним для прозрачности. Поэтому, чтобы исследовать каждый пиксел, нам требуется цикл, который проходит через массив с шагом в четыре элемента. Ниже показана структура метода checkForCollision(), выполняющего проверку столкновения с преградой:

function checkForCollision() {
  // Перебираем все пикселы и инвертируем их цвет
  var imgData = context.getImageData(x-1, y-1, 15+2, 15+2);
  var pixels = imgData.data;

  // Получаем данные для одного пиксела
  for (var i = 0; n = pixels.length, i < n; i += 4) {
    var red = pixels[i];
    var green = pixels[i+1];
    var blue = pixels[i+2];
    var alpha = pixels[i+3];

    // Смотрим на наличие черного цвета стены, что указывает на столкновение
    if (red == 0 && green == 0 && blue == 0) {
      return true;
    }
    // Смотрим на наличие серого цвета краев, что указывает на столкновение
    if (red == 169 && green == 169 && blue == 169) {
      return true;
    }
  }
  // Столкновения не было
  return false;
}

Итак, обсуждение программы "Лабиринт" завершено.

Примеры игр на HTML5 Canvas

Диапазон творческих возможностей холста практически неограничен. Интернет содержит огромное множество еще более амбициозных примеров работы с холстом, которые демонстрируют высшее мастерство кодирования на HTML5. Далее приводится список некоторых веб-сайтов, демонстрирующих потрясающие примеры:

Образцы игр на основе Canvas

Этот сайт содержит столько увлекательных примеров разработок на основе холста, что вы не сможете оторваться от экрана. Можно порекомендовать начать знакомство с этим сайтом с игры Mutant Zombie Monsters или инструмента для построения графиков биржевых котировок TickerPlot.

Карта знаний Википедии

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

Трехмерный лабиринт

В этой игре вы ходите с автоматом наперевес по простому трехмерному лабиринту, наподобие древней 3D-игры Wolfenstein, которая открыла повальное увлечение стрелялками в далеком 1992 г.

Пройди тесты
Лучший чат для C# программистов