Кейс-стади – Натиск! Арена

Введение

В июне 2010 года мы узнали, что местное издательство Boing Boing проводит конкурс на разработку игр . Мы увидели в этом отличный повод создать быструю и простую игру на JavaScript и <canvas> , поэтому приступили к работе. После конкурса у нас еще было много идей и хотелось закончить начатое. Вот пример результата: маленькая игра под названием Onslaught! Арена .

Пиксельный ретро-стиль

Было важно, чтобы наша игра выглядела и ощущалась как ретро-игра Nintendo Entertainment System , учитывая предпосылку конкурса на разработку игры на основе чиптюна . В большинстве игр этого требования нет, но это по-прежнему распространенный художественный стиль (особенно среди инди-разработчиков) из-за простоты создания ресурсов и естественной привлекательности для ностальгирующих геймеров.

Натиск! Размеры арены в пикселях
Увеличение размера пикселей может уменьшить объем работы по графическому дизайну.

Учитывая, насколько малы эти спрайты, мы решили удвоить количество пикселей, а это означает, что спрайт 16x16 теперь будет иметь размер 32x32 пикселя и так далее. С самого начала мы удваивали работу по созданию ресурсов вместо того, чтобы заставлять браузер выполнять тяжелую работу. Это было просто проще реализовать, но при этом имело некоторые определенные преимущества во внешнем виде.

Вот сценарий, который мы рассмотрели:

<style>
canvas {
  width: 640px;
  height: 320px;
}
</style>
<canvas width="320" height="240">
  Sorry, your browser is not supported.
</canvas>

Этот метод будет состоять из спрайтов 1x1 вместо их удвоения на стороне создания ресурса. После этого CSS возьмет на себя управление и изменит размер самого холста. Наши тесты показали, что этот метод может быть примерно в два раза быстрее, чем рендеринг более крупных (удвоенных) изображений, но, к сожалению, изменение размера CSS включает в себя сглаживание, и мы не смогли найти способ предотвратить это.

Параметры изменения размера холста
Слева: идеальные по пикселям ресурсы, удвоенные в Photoshop. Справа: изменение размера CSS добавило эффект размытия.

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

Забавные трюки с холстом

Мы все знаем, что <canvas> — это новая мода, но иногда разработчики все же рекомендуют использовать DOM . Если вы сомневаетесь, что использовать, вот пример того, как <canvas> сэкономил нам много времени и энергии.

Когда враг получает удар в режиме «Натиск!» Arena , он мигает красным и на короткое время отображает анимацию «боли». Чтобы ограничить количество графики, которую нам пришлось создать, мы показываем врагов, страдающих от боли, только в направлении вниз. В игре это выглядит приемлемо и позволяет сэкономить кучу времени на создании спрайтов. Однако монстров-боссов было неприятно видеть, как большой спрайт (размером 64x64 пикселей или больше) внезапно переключался с направления влево или вверх на внезапное положение лицом вниз для кадра боли.

Очевидным решением было бы нарисовать болевые рамки для каждого босса в каждом из восьми направлений, но это заняло бы очень много времени. Благодаря <canvas> мы смогли решить эту проблему в коде:

Бехолдер получает урон в Натиске! Арена
Интересные эффекты можно создать с помощью context.globalCompositeOperation.

Сначала мы рисуем монстра в скрытом «буфере» <canvas> , накладываем на него красный цвет, а затем отображаем результат обратно на экран. Код выглядит примерно так:

// Get the "buffer" canvas (that isn't visible to the user)
var bufferCanvas = document.getElementById("buffer");
var buffer = bufferCanvas.getContext("2d");

// Draw your image on the buffer
buffer.drawImage(image, 0, 0);

// Draw a rectangle over the image using a nice translucent overlay
buffer.save();
buffer.globalCompositeOperation = "source-in";
buffer.fillStyle = "rgba(186, 51, 35, 0.6)"; // red
buffer.fillRect(0, 0, image.width, image.height);
buffer.restore();

// Copy the buffer onto the visible canvas
document.getElementById("stage").getContext("2d").drawImage(bufferCanvas, x, y);

Игровой цикл

Разработка игр имеет некоторые заметные отличия от веб-разработки. В веб-стеке принято реагировать на происходящие события через прослушиватели событий. Таким образом, код инициализации не может делать ничего, кроме прослушивания входных событий. Логика игры другая, поскольку необходимо постоянно обновляться. Если, например, игрок не пошевелился, это не должно помешать гоблинам схватить его!

Вот пример игрового цикла:

function main () {
  handleInput();
  update();
  render();
};

setInterval(main, 1);

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

window.addEventListener("mousedown", function(e) {
  // A mouse click means the players wants to attack.
  // We don't actually do that yet, but instead tell the rest
  // of the program about the request.
  buttonStates[e.button] = true;
}, false);

function handleInput() {
  // Here is where we respond to the click
  if (buttonStates[LEFT_BUTTON]) {
    player.attacking = true;
    delete buttonStates[LEFT_BUTTON];
  }
};

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

function update() {
  // Check for collisions, states, whatever else is needed

  // If after that the player can still attack, do it!
  if (player.attacking && player.canAttack()) {
    player.attack();
  }
};

Наконец, когда все вычислено, пришло время перерисовать экран! В DOM-стране эту тяжелую работу берет на себя браузер. Но при использовании <canvas> необходимо вручную перерисовывать каждый раз, когда что-то происходит (обычно это каждый отдельный кадр!).

function render() {
  // First erase everything, something like:
  context.clearRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);

  // Draw the player (and whatever else you need)
  context.drawImage(
    player.getImage(),
    player.x, player.y
  );
};

Временное моделирование

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

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

function update() {

  // NOTE: You'll need to initially seed this.lastUpdate
  // with the current time when your game loop starts
  // this.lastUpdate = Date.now();

  // Calculate elapsed time since last frame
  var now = Date.now();
  var elapsed = (now - this.lastUpdate);
  this.lastUpdate = now;

  // Do stuff with elapsed

};

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

var Sprite = function() {

  // The sprite's position relative to the top left of the game world
  this.position = {x: 0, y: 0};

  // The sprite's direction. A positive x value indicates moving to the right
  this.direction = {x: 1, y: 0};

  // How many pixels the sprite moves per second
  this.speed = 50;
};

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

// Determine how far this sprite will move this frame
var distance = (sprite.speed / 1000) * elapsed;

// Apply the movement distance to the sprite's current position
// taking into account its direction
sprite.position.x += (distance * sprite.direction.x);
sprite.position.y += (distance * sprite.direction.y);

Обратите внимание, что значения direction.x и direction.y должны быть нормализованы, что означает, что они всегда должны находиться в диапазоне от -1 до 1 .

Элементы управления

Возможно, управление было самым большим камнем преткновения при разработке Onslaught! Арена . Самая первая демоверсия поддерживала только клавиатуру; игроки перемещали главного героя по экрану с помощью клавиш со стрелками и стреляли в том направлении, куда он смотрел, с помощью клавиши пробела. Хотя игра была несколько интуитивно понятной и простой для понимания, это делало игру практически неиграбельной на более сложных уровнях. Поскольку в любой момент времени в игрока летят десятки врагов и снарядов, крайне важно иметь возможность лавировать между плохими парнями , стреляя в любом направлении.

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

Натиск! Модальное управление ареной (устарело)
Старое окно управления или модальное окно «Как играть» в Onslaught! Арена.

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

Натиск! Учебное пособие по управлению ареной
Игроки в основном игнорируют наложение обучения; они предпочитают играть и веселиться!

Нам также повезло, что у нас есть несколько европейских фанатов, но мы слышали от них разочарование по поводу того, что у них нет типичной QWERTY-клавиатуры и они не могут использовать клавиши WASD для перемещения по направлению. Аналогичные жалобы высказали и левши.

С этой сложной схемой управления, которую мы реализовали, также возникает проблема с игрой на мобильных устройствах. Действительно, одна из самых частых наших просьб — сделать «Натиск»! Арена доступна на Android, iPad и других сенсорных устройствах (где нет клавиатуры). Одной из сильных сторон HTML5 является его портативность, поэтому установка игры на эти устройства определенно осуществима, нам просто нужно решить множество проблем (в первую очередь, управление и производительность).

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

// Find the nearest hostile target (if any) to the player
var player = this.getPlayerObject();
var hostile = this.getNearestHostile(player);
if (hostile !== null) {
  // Found one! Shoot in its direction
  var shoot = hostile.boundingBox().center().subtract(
    player.boundingBox().center()
  ).normalize();
}

// Move towards where the player clicked/touched
var move = this.targetReticle.position.clone().subtract(
  player.boundingBox().center()
).normalize();
var distance = this.targetReticle.position.clone().subtract(
  player.boundingBox().center()
).magnitude();

// Prevent jittering if the character is close enough
if (distance < 3) {
  move.zero();
}

// Move the player
if ((move.x !== 0) || (move.y !== 0)) {
  player.setDirection(move);
}

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

Аудио

Среди элементов управления и производительности — одна из самых больших проблем при разработке Onslaught! Арена была тегом <audio> HTML5. Вероятно, худшим аспектом является задержка: почти во всех браузерах существует задержка между вызовом .play() и фактическим воспроизведением звука. Это может испортить впечатления геймера, особенно при игре в такую ​​динамичную игру, как наша.

Другие проблемы включают в себя сбой события «прогресса» , что может привести к зависанию процесса загрузки игры на неопределенный срок. По этим причинам мы приняли так называемый метод «перехода вперед», при котором, если Flash не загружается, мы переключаемся на HTML5 Audio. Код выглядит примерно так:

/*
This example uses the SoundManager 2 library by Scott Schiller:
http://www.schillmania.com/projects/soundmanager2/
*/

// Default to sm2 (Flash)
var api = "sm2";

function initAudio (callback) {
  switch (api) {
    case "sm2":
      soundManager.onerror = (function (init) {
        return function () {
          api = "html5";
          init(callback);
        };
      }(arguments.callee));
      break;
    case "html5":
      var audio = document.createElement("audio");

      if (
        audio
        && audio.canPlayType
        && audio.canPlayType("audio/mpeg;")
      ) {
        callback();
      } else {
        // No audio support :(
      }
      break;
  }
};

Также может быть важно, чтобы игра поддерживала браузеры, которые не воспроизводят файлы MP3 (например, Mozilla Firefox). Если это так, поддержку можно обнаружить и переключить на что-то вроде Ogg Vorbis с помощью такого кода:

/*
Note: you could instead use "new Audio()" here,
but the client will throw an error if it doesn't support Audio,
which makes using "document.createElement" a safer approach.
*/

var audio = document.createElement("audio");

if (audio && audio.canPlayType) {
  if (!audio.canPlayType("audio/mpeg;")) {
    // Here you know you CANNOT use .mp3 files
    if (audio.canPlayType("audio/ogg; codecs=vorbis")) {
      // Here you know you CAN use .ogg files
    }
  }
}

Сохранение данных

Невозможно провести аркадную перестрелку без высоких результатов! Мы знали, что нам понадобится сохранение некоторых игровых данных, и, хотя мы могли бы использовать что-то старое, например файлы cookie, мы хотели изучить новые интересные технологии HTML5. Конечно, нет недостатка в опциях, включая локальное хранилище, хранилище сеансов и базы данных Web SQL.

ALT_TEXT_ЗДЕСЬ
Сохраняются высокие баллы, а также ваше место в игре после победы над каждым боссом.

Мы решили использовать localStorage , поскольку он новый, замечательный и простой в использовании. Он поддерживает сохранение базовых пар ключ/значение, а это все, что нужно нашей простой игре. Вот простой пример того, как его использовать:

if (typeof localStorage == "object") {
  localStorage.setItem("foo", "bar");
  localStorage.getItem("foo"); // Value is "bar"
  localStorage.removeItem("foo");
  localStorage.getItem("foo"); // Value is now null
}

Есть некоторые «подводные камни», о которых следует знать. Независимо от того, что вы передаете, значения сохраняются в виде строк, что может привести к неожиданным результатам:

localStorage.setItem("foo", false);
typeof localStorage.getItem("foo"); // Value is "false" (a string literal)
if (localStorage.getItem("foo")) {
  // It's true!
}

// Don't pass objects into setItem
localStorage.setItem("bar", {"key": "value"});
localStorage.getItem("bar"); // Value is "[object Object]" (a string literal)

// JSON stringify and parse when dealing with localStorage
localStorage.setItem("json", JSON.stringify({"key": "value"}));
typeof localStorage.getItem("json"); // string
JSON.parse(localStorage.getItem("json")); // {"key": "value"}

Краткое содержание

С HTML5 приятно работать. Большинство реализаций обрабатывают все, что нужно разработчику игр: от графики до сохранения состояния игры. Хотя есть некоторые проблемы роста (например, проблемы с тегом <audio> ), разработчики браузеров движутся быстро, и, несмотря на то, что дела уже идут так хорошо, будущее выглядит светлым для игр, созданных на HTML5.

Натиск! Арена со скрытым логотипом HTML5
Вы можете получить щит HTML5, набрав «html5» во время игры в Onslaught! Арена.