Путеводитель по HTML5-играм без слез

Введение

Итак, вы хотите создать игру, используя 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);

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

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

Привет, мир

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

function draw() {
  canvas.fillStyle = "#000"; // Set color to black
  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);
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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("Pew pew");
  // :) Well at least adding the key binding was easy...
};

Добавление дополнительных игровых объектов

Снаряды

Давайте теперь добавим настоящие снаряды. Во-первых, нам нужна коллекция для хранения их всех:

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
  };
};

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

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. Игрок => Корабли противника

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

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();
}

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

function Enemy(I) {
  ...

  I.explode = function() {
    this.active = false;
    // Extra Credit: Add an explosion graphic
  };

  return I;
};

player.explode = function() {
  this.active = false;
  // Extra Credit: Add an explosion graphic and then end the game
};

Звук

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

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

function Enemy(I) {
  ...

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

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

Прощание

Опять же, вот полная рабочая демо-версия игры . Вы также можете скачать исходный код в формате zip .

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

Рекомендации