Январь 2016

Том 31, номер 1

Разработка игр - Babylon.js: дополнительные средства для улучшения вашей первой веб-игры

Раанан Вебер | Январь 2016

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

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

Visual Studio 2015 Community Edition, Babylon.js, Oimo.js

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

  • поддержка обнаружения коллизий и гравитации;
  • использование движка физики для реалистичности игры;
  • улучшение игры за счет звуковых эффектов;
  • применение динамической текстуры для добавления табло;
  • добавление фиксированной камеры (follow camera) для создания эффекта «картинка в картинке».

В прошлом номере я начал это обучающее пособие с обзора базовых строительных блоков Babylon.js, движка трехмерных игр на основе WebGL (msdn.com/magazine/mt595753). Я приступил к разработке очень простой игры в боулинг, используя предлагаемые Babylon.js средства. На данный момент игра включает обязательные объекты: шар для боулинга, дорожку, желоба и десять кеглей.

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

В этой части учебного пособия я опираюсь на код из первой части и расширяю его. Чтобы проверсти различие между двумя частями, я создал новый JavaScript-файл, содержащий код, который будет использоваться в этой части. Даже при расширении функциональности определенного объекта (например, сцены или основной камеры) я не стану реализовать ее в функции, которая создавала объект в первой части. Единственная функция, которую мне придется просто расширять, — init, так как она содержит все переменные, необходимые для обеих частей этого пособия.

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

За то время, что я писал эту статью, была выпущена новая версия Babylon.js. Но я продолжу использовать версию 2.1. Чтобы узнать, что нового в Babylon.js 2.2, зайдите на bit.ly/1RC9k6e.

Обнаружение коллизий

Основная камера, применяемая в игре, является свободной (free camera); она позволяет игроку бродить по всей 3D-сцене с помощью мыши и клавиатуры. Однако без определенных модификаций камера могла бы пролетать сквозь сцену, проходить сквозь стены или даже через пол и дорожку. В реалистичной игре игрок должен иметь возможность перемещаться только по полу и дорожке.

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

Первым делом разрешим обнаружение коллизий и гравитацию: 

function enableCameraCollision(camera, scene) {
  // Разрешаем гравитацию в сцене.
  // Должна быть аналогична земной силе притяжения.
  scene.gravity = new BABYLON.Vector3(0, -0.98, 0);
  // Разрешаем глобальные коллизии
  scene.collisionsEnabled = true;
  // Разрешаем обнаружение коллизий
  // и гравитацию для свободной камеры
  camera.checkCollisions = true;
  camera.applyGravity = true;
  // Задаем размер игрока и эллипсоид камеры
  camera.ellipsoid = new BABYLON.Vector3(0.4, 0.8, 0.4);
}

Эта функция активирует систему обнаружения коллизий в сцене и камере игры и задает эллипсоид камеры, который можно рассматривать как размер игрока. Это параллелепипед (box) размером 0.8×1.6×0.8 единиц (в данном случае), что примерно соответствует средним размерам человека. Камере нужен этот параллелепипед, поскольку она не является мешем. Система обнаружения коллизий в Babylon.js анализирует коллизии только между мешами — вот почему для камеру нужно эмулировать меш. Эллипсоид определяет центр этого объекта, поэтому 0.4 транслируется в 0.8 по размеру. Я также разрешил гравитацию в сцене, и это будет применяться к движению камеры.

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

function enableMeshesCollision(meshes) {
  meshes.forEach(function(mesh) {
    mesh.checkCollisions = true;
  });
}

Последний шаг — добавление в функцию init двух вызовов функций:

// Функция init из первой части обучающего пособия
  ...
  enableCameraCollision(camera, scene);
  enableMeshesCollision[[floor, lane, gutters[0], gutters[1]]);
}

Единственная задача обнаружения коллизий — предотвратить слияние мешей друг с другом. Функционал гравитации был реализован, чтобы камера оставалась на уровне пола (грунта). Чтобы обеспечить реалистичное физическое взаимодействие между конкретными мешами (в данном случае — между шаром и кеглями), требуется более сложная система: движок физики (physics engine).

Бросок шара: интеграция физики в Babylon.js

Основное действие в игре — бросок шара в кегли. Требования довольно просты: игрок должен иметь возможность задать направление и силу броска. Если шар попал в конкретные кегли, они должны падать. Если какие-то кегли ударяют соседние, те тоже должны падать. Если игрок бросает шар к боковине, тот должен падать в желоб. Кегли должны падать соответственно скорости, с которой в них был брошен шар.

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

Для вычисления динамики твердого тела (следующего перемещения меша в пространстве) в реальном времени движок физики должен упрощать меш. С этой целью у каждого меша есть обертка (impostor) — простой меш, который ограничивает исходный меш (обычно сфера или параллелепипед). Это уменьшает точность вычислений, но позволяет ускорить расчет воздействия физических сил, приводящих объект в движение. Чтобы узнать больше о том, как работает движок физики, см. страницу bit.ly/1S9AIsU.

Движок физики не является частью Babylon.js. Вместо привязки к какому-то одному движку разработчики инфраструктуры решили реализовать интерфейсы для разных движков физики и позволить другим разработчикам решать, какой из них они хотят задействовать. В настоящее время в Babylon.js имеются интерфейсы для двух движков физики: Cannon.js (cannonjs.org) и Oimo.js (github.com/lo-th/Oimo.js). Оба движка просто великолепны! Лично я нахожу интеграцию Oimo чуть эффективнее и поэтому буду использовать его в своей игре в боулинг. Интерфейс для Cannon.js был полностью переписан для Babylon.js 2.3, которая сейчас находится в стадии альфа-версии и поддерживает новейшую версию Cannon.js с большим количеством исправлений ошибок и новыми обертками (impostors), включая сложные карты высот (height maps). Советую опробовать его, если вы используете Babylon.js версии 2.3 или выше.

Включение поддержки движка физики осуществляется простой строкой кода:

scene.enablePhysics(new BABYLON.Vector3(0, -9.8, 0),
  new BABYLON.OimoJSPlugin());

Она задает гравитацию сцены и определяет используемый движок физики. Замена Oimo.js на Cannon.js требует лишь изменения второй переменной на:

new BABYLON.CannonJSPlugin()

Далее мне нужно определить обертки (impostors) для всех объектов. Для этого используется функция setPhysicsState меша. Например, вот определение дорожки:

lane.setPhysicsState(BABYLON.PhysicsEngine.BoxImpostor, {
  mass: 0,
  friction: 0.5,
  restitution: 0
});

Первая переменная — это тип обертки. Поскольку дорожка является идеальным параллелепипедом (box), я использую BoxImpostor. Вторая — это определение физики тела (его масса в килограммах, трение и коэффициент упругости [restitution factor]). Масса дорожки равна 0, так как я хочу, чтобы она оставалась на своем месте. Задание обертке нулевой массы удерживает объект в текущей позиции.

Для сферы я буду использовать SphereImpostor. Шар для боулинга весит примерно 6.8 кг и обычно очень гладкий, так что учет трения не нужен:

ball.setPhysicsState(BABYLON.PhysicsEngine.SphereImpostor, {
  mass: 6.8,
  friction: 0,
  restitution: 0
});

Если вас интересует, почему я использую килограммы, а не фунты, то я делаю это по той же причине, по которой пользуюсь метрами, а не футами: в движок физики заложена метрическая система измерений. Например, определение гравитации по умолчанию (0, –9.8, 0), что приблизительно соответствует силе притяжения Земли. Эти единицы выражаются в метрах на секунду в квадрате (м/с2).

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

ball.applyImpulse(new BABYLON.Vector3(0, 0, 20),
  ball.getAbsolutePosition());

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

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

Шар для боулинга ударяет кегли
Рис. 1. Шар для боулинга ударяет кегли

Однако у меня все еще нет направления и меры силы.

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

Нахождение точки в пространстве, которой коснулся пользователь, осуществляется с помощью объекта PickingInfo, передаваемого в каждом событии PointerDown и PointerUp. Объект PickingInfo содержит информацию о точке, в которой сработало событие, включая меш, затронутый касанием, точку касания на этом меше, расстояние до этой точки и др. Если не было касания ни одного меша, поле hit объекта PickingInfo будет false. Сцена в Babylon.js имеет две полезные функции обратного вызова, которые помогут мне получить собранную информацию: onPointerUp и onPointerDown. Эти два обратных вызова инициируются при срабатывании событий указателя (pointer events), и их сигнатура выглядит так:

function(evt: PointerEvent, pickInfo: PickingInfo) => void

Переменная evt — это сгенерированное исходное JavaScript-событие. Вторая переменная содержит информацию, генерируемую инфраструктурой при каждом срабатывании события.

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

scene.onPointerUp = function(evt, pickInfo) {
  if (pickInfo.hit) {
    // Вычисляем направление, используя
    // точку касания и позицию шара
    var direction = pickInfo.pickedPoint.subtract(
      ball.position);
    // Для корректного масштабирования требуется нормализация
    direction = direction.normalize();
    // Придаем чуть больше силы
    // (масштабируем нормализованное направление)
    var impulse = direction.scale(20);
    // Применяем импульс (и бросаем шар)
    ball.applyImpulse(impulse, new BABYLON.Vector3(0, 0, 0));
  }
}

Теперь я могу бросить шар в определенном направлении. Единственное, чего не хватает, — силы броска. Чтобы добавить его, я буду вычислять разницу кадров между событиями PointerDown и PointerUp. На рис. 2 показана функция, используемая для броска шара с определенной силой.

Рис. 2. Бросок шара с определенной силой и в определенном направлении

var strengthCounter = 5;

var counterUp = function() {
  strengthCounter += 0.5;
}

// Эта функция будет вызываться при событиях PointerDown
scene.onPointerDown = function(evt, pickInfo) {
  // Начинаем приращение счетчика силы (strength counter)
  scene.registerBeforeRender(counterUp);
}

// Эта функция будет вызываться при событиях PointerUp
scene.onPointerUp = function(evt, pickInfo) {
  // Прекращаем приращение счетчика силы
  scene.unregisterBeforeRender(counterUp);
  // Вычисляем направление броска
  var direction = pickInfo.pickedPoint.subtract(
    ball.position).normalize();
  // Impulse умножается на счетчик силы
  // с максимальным значением 25
  var impulse = direction.scale(Math.min(strengthCounter, 25));
  // Применяем этот импульс
  ball.applyImpulse(impulse, ball.getAbsolutePosition());
  // Регистрируем функцию, которая будет выполняться
  // до каждого вызова рендера
  scene.registerBeforeRender(function ballCheck() {
    if (ball.intersectsMesh(floor, false)) {
      // Шар пересекает пол, прекращаем проверять его позицию
      scene.unregisterBeforeRender(ballCheck);
      // Пусть шар катится в течение 1.5 секунд до его сброса
      setTimeout(function() {
        var newPosition =
          scene.activeCameras[0].position.clone();
        newPosition.y /= 2;
        resetBall(ball, newPosition);
      }, 1500);
    }
  });
  strengthCounter = 5;
}

Одно замечание по поводу событий указателя: я умышленно не употреблял термин «щелчок». Babylon.js использует систему Pointer Events, которая расширяет средства захвата щелчка мыши в браузере поддержкой сенсорного ввода и других устройств ввода, способных принимать щелчки, касания и указания направления (pointing). Благодаря этому касание на смартфоне и щелчок на настольном ПК дадут одно и то же событие. Чтобы имитировать это в браузерах, которые не поддерживают такой функционал, Babylon.js использует hand.js — полизаполнение (polyfill) для событий указателя, которое также включается в index.html игры. Узнать больше о hand.js можно на странице GitHub по ссылке bit.ly/1S4taHF. Проект спецификации Pointer Events можно найти на bit.ly/1PAdo9J. Заметьте, что hand.js заменят в будущих выпусках библиотекой jQuery PEP (bit.ly/1NDMyYa).

Вот и все о физике! Игра в боулинг только что стала намного лучше.

Добавление звуковых эффектов

Добавление звуковых эффектов в игру резко усиливает впечатление от нее. Звуки могут создавать правильную атмосферу и сделать игру в боулинг немного реалистичнее. К счастью, Babylon.js включает звуковой движок (audio engine), появившийся в версии 2.0. Звуковой движок основан на Web Audio API (bit.ly/1YgBWWQ), который поддерживается всеми основными браузерами, кроме Internet Explorer. Допустимые файловые форматы зависят от самого браузера.

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

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

var atmosphere = new BABYLON.Sound(
  "Ambient", "ambient.mp3", scene, null, {
  loop: true,
  autoplay: true
});

Этот звук будет воспроизводиться постоянно с момента его загрузки.

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

Для начала создадим звук катящегося шара:

var rollingSound = new BABYLON.Sound(
  "rolling", "rolling.mp3", scene, null, {
  loop: true,
  autoplay: false
});

Звук загрузится, но воспроизводиться не будет, пока я не выполню его функцию play, что произойдет при броске шара. Я расширил функцию с рис. 2 и добавил следующее:

...ball.applyImpulse(impulse, new BABYLON.Vector3(0, 0, 0));
// Начало воспроизведения звука
rollingSound.play();
...

Звук прекращается, когда шар скатывается с дорожки:

...
If(ball.intersectsMesh(floor, false)) {
  // Конец воспроизведения звука
  rollingSound.stop();
  ...
}
...

Babylon.js позволяет подключать звук к конкретному мешу. Тем самым он автоматически вычисляет громкость звука и панорамирование (panning), используя позицию меша, что создает более реалистичное впечатление. Для этого я просто добавил следующую строку после создания звука:

rollingSound.attachToMesh(ball);

Теперь звук всегда будет проигрываться от позиции шара.

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

var hitSound = new BABYLON.Sound("hit", "hit.mp3", scene);
hitSound.attachToMesh(pins[0]);

Это звук не зациклен и не воспроизводится автоматически.

Я буду проигрывать этот звук всякий раз, когда шар ударит один из кеглей. Для этого я добавлю функцию, которая после броска шара будет постоянно проверять, не пересекся ли шар с любым из кеглей. Если да, я отменяю регистрацию функции и проигрываю звук. С этой целью я включаю следующие строки в функцию scene.onPointerUp с рис. 2:

scene.registerBeforeRender(function ballIntersectsPins() {
  // Часть функций вернет true при ударе шара
  // о соответствующие кегли
  var intersects = pins.some(function (pin) {
    return ball.intersectsMesh(pin, false);
  });
  if (intersects) {
    // Отменяем регистрацию этой функции –
    || прекращаем проверять взаимные пересечения
    scene.unregisterBeforeRender(ballIntersectsPins);
    // Проигрываем звук удара
    hit.play();
  }
});

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

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

Добавление табло

Когда игроку удается сбить кегли, было бы неплохо видеть, сколько из них устояло и сколько упало. Для этого я добавлю табло.

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

Создать плоскость и динамическую текстуру весьма легко:

var scoreTexture = new BABYLON.DynamicTexture(
  "scoreTexture", 512, scene, true);
var scoreboard = BABYLON.Mesh.CreatePlane(
  "scoreboard", 5, scene);
// Позиционируем табло за дорожкой
scoreboard.position.z = 40;
// Создаем материал для табло
scoreboard.material = new BABYLON.StandardMaterial(
  "scoradboardMat", scene);
// Задаем диффузную текстуру (diffuse texture)
// в качестве динамической
scoreboard.material.diffuseTexture = scoreTexture;

Динамическая текстура позволяет напрямую рисовать на нижележащем холсте, используя его функцию getContext, которая возвращает CanvasRenderingContext2D (mzl.la/1M2mz01). Объект динамической текстуры также предоставляет несколько вспомогательных функций, полезных, если вы не хотите иметь дело непосредственно с контекстом холста. Одна из таких функций — drawText, которая дает возможность рисовать строку с указанным шрифтом поверх этого холста. Я буду обновлять холст при каждом изменении количества упавших кеглей:

var score = 0;
scene.registerBeforeRender(function() {
  var newScore = 10 - checkPins(pins, lane);
  if (newScore != score) {
    score = newScore;
    // Очищаем холст
    scoreTexture.clear();
    // Рисуем текст белым шрифтом на черном фоне
    scoreTexture.drawText(score + " pins down", 40, 100,
      "bold 72px Arial", "white", "black");
  }
});

Проверка на падение кеглей тривиальна: я лишь проверяю, равна ли позиция всех кеглей на оси Y их исходной позиции по той же оси Y (предопределенная переменная pinYPosition):

function checkPins(pins) {
  var pinsStanding = 0;
  pins.forEach(function(pin, idx) {
    // Устоял ли данный кегль на дорожке?
    if (BABYLON.Tools.WithinEpsilon(
      pinYPosition, pin.position.y, 0.01)) {
      pinsStanding++;
    }
  });
  return pinsStanding;
}

Динамическую текстуру можно увидеть на рис. 3..

Табло игры
Рис. 3. Табло игры

Теперь мне не хватает только функции для сброса дорожки и табло. Я добавлю триггер действия, который будет срабатывать, когда на клавиатуре нажимается клавиша R (рис. 4).

Рис. 4. Сброс дорожки и табло

function clear() {
  // Сброс счета
  score = 0;
  // Инициализация кеглей
  initPins(scene, pins);
  // Очистка динамической текстуры
  // и рисование строки приветствия
  scoreTexture.clear();
  scoreTexture.drawText("welcome!", 120, 100,
    "bold 72px Arial", "white", "black");
}

scene.actionManager.registerAction(
  new BABYLON.ExecuteCodeAction({
  trigger: BABYLON.ActionManager.OnKeyUpTrigger,
  parameter: "r"
},

Нажатие клавиши R будет сбрасывать/инициализировать сцену.

Добавление фиксированной камеры

Симпатичный эффект, который я хочу добавить в игру, — камера, следующая за шаром для боулинга после броска. Я хочу, чтобы камера «катилась» вслед за шаром в направлении кеглей и останавливалась, когда шар возвращается на свою исходную позицию. Для этого можно задействовать функциональность нескольких представлений на одной странице (multi-view feature) в Babylon.js.

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

scene.activeCamera = camera

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

Первый шаг — добавить в этот массив исходную свободную камеру. Это легко сделать в функции init. Замените scene.activeCamera = camera на:

scene.activeCameras.push(camera);

Следующий шаг — создать фиксированную камеру (follow camera) при броске шара:

var followCamera = new BABYLON.FollowCamera(
  "followCamera", ball.position, scene);
// Насколько далеко от объекта должна находиться камера
followCamera.radius = 1.5;
// Насколько высоко она должна быть над объектом
followCamera.heightOffset = 0.8;
// Ракурс камеры (camera angle); здесь - сзади
followCamera.rotationOffset = 180;
// Ускорение камеры
followCamera.cameraAcceleration = 0.5
// Максимальная скорость камеры
followCamera.maxCameraSpeed = 20;

Это создает камеру и настраивает ее так, чтобы она отстояла от объекта на 1.5 единицы сзади и располагалась на 0.8 единиц выше объекта, за которым она следует. Сопровождаемым объектом должен быть шар, но здесь есть проблема — шар должен катиться вращаясь, и камера тоже будет вращаться вместе с ним. А мне надо добиться ее полета позади объекта. Для этого я создам фиксированный объект (follow object), который будет получать позицию шара, но не его вращение:

// Создаем очень малый простой меш
var followObject = BABYLON.Mesh.CreateBox(
  "followObject", 0.001, scene);
// Указываем, что его позиция всегда равна позиции шара
followObject.position = ball.position;
После этого я задаю, что цель камеры — followObject:
followCamera.target = followObject;

После этого я задаю, что цель камеры — followObject:

followCamera.target = followObject;

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

Последняя настройка камеры требует указать ее окно просмотра (viewport). Для каждой камеры можно определить область, занимаемую ею на экране. Это делается с помощью переменной viewport, которая определяется с применением следующих переменных:

var viewport = new BABYLON.Viewport(
  xPosition, yPosition, width, height);

Все значения находятся между 0 и 1 по аналогии с процентной долей (где 1 соответствует 100%) относительно высоты и ширины экрана. Первые два значения определяют начальную точку прямоугольника камеры на экране, а width и height — ширину и высоту прямоугольника относительно реальному размеру экрана. По умолчанию окно просмотра задается равным (0.0, 0.0, 1.0, 1.0), что охватывает весь экран. Для фиксированной камеры я задам высоту и ширину в 30% от соответствующих размеров экрана:

followCamera.viewport =new BABYLON.Viewport(0.0, 0.0, 0.3, 0.3);

На рис. 5 показано, как выглядит ракурс камеры после броска шара. Обратите внимание на окно в нижнем левом углу экрана.

Эффект картинки в картинке с использованием фиксированной камеры
Рис. 5. Эффект картинки в картинке с использованием фиксированной камеры

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

Как заниматься дальнейшей разработкой игры

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

Первым делом стоило бы добавить GUI, который запрашивал бы имя пользователя и, например, показывал бы параметры конфигурации игры. (Может, мы играем в боулинг на Марсе! Просто задайте другую силу притяжения.)

В Babylon.js отсутствует встроенный способ создания GUI, но члены сообщества разработали несколько расширений, которые можно использовать для создания чудесных GUI, в том числе CastorGUI (bit.ly/1M2xEhD), bGUi (bit.ly/1LCR6jk) и расширение диалогов (bit.ly/1MUKpXH). Первое использует HTML и CSS для добавления слоев поверх 3D-холста. Остальные добавляют трехмерные диалоги в саму сцену, используя обычные меши и динамические текстуры. Советую опробовать их, прежде чем писать собственное решение. Все они очень просты в использовании и сильно облегчают процесс создания GUI.

Другим улучшением было бы использование более качественных мешей. Все меши в моей игре созданы с помощью внутренних функций Babylon.js и очевидно, что это ограничивает их. Существует много сайтов, предлагающих бесплатные и платные 3D-объекты. Лучшим из них, по моему мнению, является TurboSquid (bit.ly/1jSrTZy). Для большей производительности ищите только низко полигональные меши.

А после добавления более качественных мешей почему бы не добавить объект человечка, который и будет бросать шар? Для этого вам понадобится функциональность анимации костей, интегрированная в Babylon.js, и меш, который поддерживает эту функциональность. Вы найдете демонстрацию, показывающую, как это выглядит, на странице babylonjs.com/?BONES.

Как последний штрих попробуйте сделать игру дружественной к виртуальной реальности. В этом случае единственное, что понадобится изменить, — используемую камеру. Замените FreeCamera на WebVRFreeCamera и посмотрите, насколько легко можно будет ориентировать игру на Google Cardboard.

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

Заключение

Надеюсь, что вы получили не меньше удовольствия от чтения этого пособия, чем я от его написания, что это подтолкнет вас в правильном направлении и заставит экспериментировать с Babylon.js. Это по-настоящему замечательная инфраструктура, созданная с большой любовью одними разработчиками к другим. Чтобы увидеть больше демонстраций этой инфраструктуры, посетите сайт babylonjs.com. И опять же, без колебаний присоединяйтесь к форуму поддержки и задавайте там любые вопросы, которые у вас есть. Глупых вопросов не бывает, бывают глупые ответы!


Раанан Вебер (Raanan Weber) — IT-консультант, специалист по комплексной разработке, муж и отец. В свободное время старается внести свой вклад в совершенствование Babylon.js и других проектов с открытым исходным кодом. Читайте его блог blog.raananweber.com.

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