Май 2015

Том 30 выпуск 5


Game Development - Движки для двухмерных веб-игр

Michael Oneppo | Май 2015

Продукты и технологии:

Visual Studio Pro 2013

В статье рассматриваются:

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

Исходный код можно скачать по ссылке

Вообразите, что некий разработчик хочет написать игру. Он смотрит, какие инструменты для этого доступны, и разочаровывается предлагаемыми вариантами. Поэтому он решает создать собственный движок (engine), который будет отвечать всем требованиям концепции текущей игры. Он также будет соответствовать всем будущим требованиям разработки игр, какие только могут понадобиться. И в этом случае разработчик никогда не напишет ни одной игры.

И такая трагедия разыгрывается весьма нередко. К счастью, это привело к появлению десятков игровых движков для всех возможных платформ. Они охватывают широкий спектр методологий, инструментов и моделей лицензирования. Например, на JavaScripting.com в настоящее время перечислено 14 игровых библиотек с открытым исходным кодом. Сюда даже не входят библиотеки, моделирующие игровую физику, звуковые библиотеки и специализированные системы ввода для игр. При таком широком выборе обязательно найдется какой-то игровой движок, который будет хорошо работать в любом игровом проекте, какой вы только сумеете придумать.

В этой статье мы рассмотрим три популярных движка с открытым исходным кодом для двухмерных веб-игр: Crafty, Pixi и Phaser. Чтобы сравнить и выделить различия этих библиотек, я перенес игру Ping из своей первой статьи (msdn.microsoft.com/magazine/dn913185) под каждую из них и изучил работу с ними. Учтите, что, сузив свой обзор двухмерным играми, я оставил за бортом ряд движков для трехмерных веб-игр. О них мы поговорим в одной из будущих статей. А пока я сосредоточусь на двухмерной графике и возможностях в этой области.

Crafty

Crafty (craftyjs.com) предназначена в основном для игр на основе плиток (tile-based games), хотя она хорошо подходит для самых разнообразных двухмерных игр, включая мою игру Ping. В основе Crafty лежит система объектной модели/зависимостей, называемых компонентами.

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

Crafty.sprite("sprites.png", {
  PlayerSprite: [0, 128, 128, 128],
  OpponentSprite: [0, 0, 128, 128],
  BallSprite: [128, 128, 64, 64],
  UpSprite: [128,0,64,64],
  DownSprite: [128,64,64,64],
  LeftSprite: [192,0,64,64],
  RightSprite: [192,64,64,64]});

Теперь, если вы хотите создать сущность Ball, которая использует изображение мяча из таблицы спрайтов, включите компонент BallSprite при создании сущности с помощью функции «e»:

Crafty.e("Ball, BallSprite");

Это пока не приведет к ее рисованию на экране. Вы должны сообщить Crafty, что эта сущность должна существовать в двухмерном пространстве (поэтому у нее будет позиции, определяемые координатами x и y) и что вы хотите рисовать ее, используя DOM-элементы:

Crafty.e("Ball, 2D, DOM, BallSprite");

Если вам нужно рисовать свои элементы на холсте, достаточно заменить компонент Document Object Model (DOM) на Canvas.

Сущности ведут себя как независимые объекты на экране, реагируя на события. Как и во многих JavaScript-библиотеках, сущности Crafty имеют функцию bind для реагирования на события. Важно помнить, что эти события специфичны для Crafty. Например, если вы хотите, чтобы сущность что-то делала в каждом кадре, заставьте ее реагировать на событие EnterFrame. Это как раз то, что требуется для перемещения мяча по экрану в зависимости от того, кто им владеет, и при необходимости с поддержкой соответствующей физики. На рис. 1 показана инициализация сущности Ball функцией события EnterFrame, обеспечивающей движение мяча.

Рис. 1. Определение сущности Ball

Crafty.e("Ball, 2D, DOM, BallSprite, Collision")
  .attr({ x: width/2, y: height/2, velocity: [0,0],
  owner: 'User' })
  .bind('EnterFrame', function () {
    // Если есть владелец, пусть мяч следует за владельцем.
    // Слегка подталкиваем мяч слева или справа от игрока,
    // чтобы не было перекрытия.
    if (this.owner) {
      var owner = Crafty(this.owner);
      switch(this.owner) {
        case 'User':
          this.x = owner.x + 128;
          break;
        case 'Opponent':
          this.x = owner.x - 64;
            }
            this.y = owner.y + 32;
            return;
    }

    // Мяч отскакивает от потолка или пола
    if (this.y <= 0 || this.y >= (height - 64))
      this.velocity[1] = -this.velocity[1];

    // Перемещаем мяч с учетом его скорости, которая
    // определяется в пикселях в миллисекунду
    var millis = 1000 / Crafty.timer.FPS();
    this.x += this.velocity[0] * millis;
    this.y += this.velocity[1] * millis;
  })

Crafty предназначена в основном для игр на основе плиток, хотя она хорошо подходит для самых разнообразных двухмерных игр.

Если вы внимательно изучите листинг, то заметите this, часто используемое для ссылки на свойства сущности. Встроенные в Crafty this.x и this.y определяют позицию мяча. Эта функциональность добавляется, когда вы создаете мяч с компонентом 2D. Выражение this.velocity является пользовательским в моем коде. Оно определено как свойство, используя функцию attr. То же относится к this.owner, с помощью которого я узнаю, владеет ли любой из игроков мячом.

В Crafty есть ряд встроенных компонентов, но в примере Ping я использую всего несколько из них.

  • Гравитация В Crafty есть простая поддержка гравитации, которая автоматически тянет сущность вниз. Вы можете определить любое количество типов элементов, чтобы остановить ее движение.
  • ДвижениеTwoWay,FourWay и MultiWay С помощью этих компонентов вы получаете разнообразные методы ввода для перемещения в аркадном стиле. TwoWay предназначен для платформеров, где движение влево, вправо и прыжок можно связать с любыми клавишами. FourWay обеспечивает движение вверх-вниз в направлениях по сторонам света. MultiWay поддерживает пользовательский ввод, каждый из которых может иметь произвольное направление.
  • Частицы Crafty включает быструю систему частиц, которую можно использовать только при рисовании на холсте (Canvas). Частицы особенно хорошо подходят для изображения дыма, огня и взрывов.

Код для Ping, реализованный в Crafty, доступен в пакете исходного кода, сопутствующем этой статье. Изучите его, чтобы получить полную картину того, как я перенес те или иные вещи, особенно искусственный интеллект для оппонента.

Pixi.js

Pixi.js (pixijs.com) — это, по сути, просто низкоуровневый двухмерный веб-рендер. Pixi.js может обходить двухмерный холст и работать напрямую с WebGL, что дает значительный выигрыш в производительности. Игра на основе Pixi.js имеет две ключевые части: сцену (stage), которая описывает структуру вашей игры в графе сцены, и рендер (renderer), который реально рисует элементы сцены на экране, используя холст или WebGL. По умолчанию Pixi.js будет использовать WebGL, если он доступен:

$(document).ready(function() {
  // Сцена для вашей игры
  stage = new PIXI.Stage(0xFFFFFFFF);
  lastUpdate = 0;

  // autoDetectRenderer() находит самый быстрый рендер
  // из числа имеющихся у вас
  renderer = PIXI.autoDetectRenderer(innerWidth, innerHeight);
  document.body.appendChild(renderer.view);
});

В Pixi.js есть граф сцены (scene graph) и преобразования, но важно отметить, что вы должны сами обеспечивать рендеринг сцены. Поэтому в своей игре Ping я сохраняю цикл рендеринга, но используя requestAnimationFrame:

function update(time) {
  var t = time - lastUpdate;
  lastUpdate = time;
  ball.update(t);
  ai.update();

  // Новое: вы сами обеспечиваете рендеринг сцены
  renderer.render(stage);
  requestAnimationFrame(update);
}

Pixi.js в отличие от Crafty особо не диктует вам, как нужно структурировать вашу игру, поэтому я могу использовать довольно много кода из исходной игры Ping, внеся лишь небольшие изменения.

Большая разница в том, что Pixi.js позволяет загружать спрайты либо как изображения из файлов одно за другим, либо как мозаичное изображение (tiled image), используя специальную утилиту TexturePacker. Эта утилита автоматически объединяет группу индивидуальных изображений в один оптимизированный файл с сопутствующим JSON-файлом, который описывает адреса индивидуальных изображений. Я воспользовался TexturePacker для создания таблицы спрайтов. На рис. 2 показана обновленная функция ready, загружающая изображения спрайтов.

Рис. 2. Загрузка таблицы спрайтов в Pixi.js

$(document).ready(function() {
  // Создаем новый экземпляр сцены Pixi
  stage = new PIXI.Stage(0xFFFFFFFF);
  lastUpdate = 0;

  // Создаем экземпляр рендера
  renderer = PIXI.autoDetectRenderer(innerWidth, innerHeight);
  document.body.appendChild(renderer.view);

  var images = ["sprites.json"];
  var loader = new PIXI.AssetLoader(images);
  loader.onComplete = startGame;
  loader.load();
});

Загрузчик является асинхронным, и он требует передачи функции, которая будет вызвана по окончании загрузки. Остальные операции инициализации игры вроде создания игрока, оппонента и мяча выполняются в новой функции startGame. Чтобы использовать эти только что инициализированные спрайты, вызывайте метод PIXI.Sprite.fromFrame, который принимает индекс в массиве спрайтов в JSON-файле.

Еще одно отличие: Pixi.js предоставляет систему сенсорного ввода и ввода мышью. ЧТобы сделать любой спрайт готовым к реакции на ввод, я установил свойство interactive в true и напрямую добавил обработчики некоторых событий в спрайт. Например, для кнопок вверх/вниз я связываю события, которые перемещают игрока вверх и вниз (рис. 3).

Рис. 3. Связывание событий для перемещения игрока вверх и вниз

up = new PIXI.Sprite.fromFrame(3);
up.interactive = true;
stage.addChild(up);
up.touchstart = up.mousedown = function() {
  player.move(-distance);
}

down = new PIXI.Sprite.fromFrame(4);
down.interactive = true;
stage.addChild(down);
down.touchstart = down.mousedown = function() {
  player.move(distance);
}

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

Phaser

Phaser (phaser.io) — полнофункциональный игровой движок, который на внутреннем уровне использует Pixi для выполнения всех задач рендеринга. Phaser имеет длинный список средств, поддерживая, в том числе, покадровую анимацию спрайтов, музыку и звуки, систему состояния игры и физику с помощью трех разных библиотек.

Поскольку я уже перенес Ping на Pixi.js, дальнейший перенос на Phaser достаточно прост. В Phaser объекты создаются на основе Pixi.js:

ball = game.add.sprite(x, y, 'sprites');

Хотя код под Phaser может быть более лаконичным и ясным, существуют места, где некоторые вещи сложнее. Скажем, последний параметр в предыдущей строке кода, sprite, ссылается на изображение, загружаемое при запуске:

game.load.image('sprites', 'sprites.png');

В Phaser другая система таблицы спрайтов для изображений из Pixi.js. Вы можете разбить изображение на плитки. Для его использования выдайте примерно такой вызов:

game.load.spritesheet('sprites', 'mySprites.png',
  spriteWidth, spriteHeight, totalSprites);

В своем примере я не пользуюсь этой функцией. В только что показанном коде предполагается, что все спрайты имеют одинаковый размер, но в таблице спрайтов Ping есть спрайты размером 64 и 128 пикселей. Поэтому мне пришлось вручную обрезать каждое изображение спрайта. Чтобы обрезать изображение для мяча, я задал отсекающий прямоугольник (cropping rectangle) в спрайте:

ball.cropRect = new Phaser.Rectangle(0, 64, 64, 64);
ball.updateCrop();

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

game.input.keyboard.onDownCallback = function(key) {
  console.log(key + " was pressed.");
}

или проверять, была ли нажата клавиша в цикле обновления:

function update() {
  if (game.input.keyboard.isDown('a'.charCodeAt(0)) {
    console.log("'A' key was pressed");
  }
}

Это применимо к большинству событий в Phaser при использовании последовательной или асинхронной обработки в зависимости от ваших предпочтений или потребностей. Иногда Phaser предоставляет лишь вторую форму обработки событий. Хороший пример — система аркадной физики, где вы должны явным образом вызывать какую-то функцию при каждом обновлении, чтобы проверять, не произошла ли коллизия между объектами:

game.physics.arcade.collide(player, ball, function() {
  ballOwner = player;
});

Этот код определяет, не вошел ли мяч в контакт со спрайтом игрока, и, если да, передает управление мячом игроку. Изучите мою реализацию Ping под Phaser в пакете кода, сопутствующего этой статье.

Заключение

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

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

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

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


Michael Oneppoкреативный технолог и бывший менеджер программ в группе Microsoft Direct3D. В последнее время работает в качестве главного технического директора в технологической некоммерческой компании Library For All и ведет исследования по программе NYU Interactive Telecommunications Program для получения степени магистра​.

Выражаю благодарность за рецензирование статьи эксперту Microsoft Джастину Гарретту (Justin Garrett).