Игра с дополненной реальностью: технический обзор

«An AR Game» — победитель в конкурсе приложений Dev Derby, который прошел в марте 2013 года. Данное приложение представляет из себя небольшую игру, опирающуюся на технологию дополненной реальности ( augmented reality, далее AR — Ред.), цель которой состоит в перемещении катящихся игровых элементов из мира с двухмерной физикой в трёхмерное пространство. Демо игры доступно на GitHub, но вы также можете посмотреть её презентацию на YouTube. В рамках этой статьи мы постараемся описать дизайнерские и инженерные решения, которые легли в основу игры.

Технически игра базируется на простом и одновременно изысканном соединении четырех технологий с открытым исходным кодом: WebRTC, JSARToolkit, ThreeJS и Box2D.js. Мы постарались дать краткое описание каждой из них и объяснить их взаимосвязь в проекте. Начиная с нуля, мы с вами постепенно будем создавать данную игру. Код, рассмотренный в этой статье, размещён на GitHub, на каждом этапе этого урока представлены ссылки на соответствующий исходник примера из проекта и его демо-страницу. Некоторые обобщённые отрывки исходного кода будут представлены в самой статье, полный процесс создания кода вы сможете отследить по ссылкам на «диффы» примеров из проекта. Также, по возможности, мы постараемся продемонстрировать с помощью видеороликов поведение приложения на том или ином этапе.

git clone https://github.com/abrie/devderby-may-2013-technical.git

По ходу урока мы сначала рассмотрим AR-плоскость (реальное пространство), затем 2D-плоскость (плоское пространство) и, в завершении, их взаимодействие.

Плоскость реального пространства

Реальное пространство в данном контексте — это то пространство, которое видит камера, дополненное виртуальными объектами.

Начнём с каркаса приложения

git checkout example_0
демо, дифф, исходник примера

Для начала, разобьем весь код на модули, используя RequireJS. Нашей отправной точкой будет служить главный модуль с двумя каркасными методами, часто используемыми в играх. Это initialize(), вызывающий запуск, и tick() для рендеринга каждого фрейма. Обратите внимание, что игровой цикл приводится в действие посредством повторяющихся обращений к requestAnimationFrame:

requirejs([], function() {

    // Инициализация компонентов и начало игрового цикла 
    function initialize() {
    }

    // Выполнение одной итерации игрового цикла 
    function tick() {
        // Запрос следующей итерации игрового цикла 
        window.requestAnimationFrame(tick);
    }

    // Запуск приложения 
    initialize();
    tick();
});

На данном этапе у нас есть приложение с пустым циклом. Продолжим разработку, используя созданный нами каркас.

Наделяем каркас зрением

git checkout example_1
демо, дифф, исходник примера

Для нашей игры с AR нам необходима потоковая видеотрансляция в реальном времени. WebRTC на основе HTML5 предоставляет возможность организации такой трансляции, обеспечивая доступ к вебкамере игрока. Из чего, кстати, следует, что наш проект с AR будет работать только в современных браузерах, вроде Firefox. Подробную документацию по WebRTC и getUserMedia вы легко можете найти самостоятельно на developer.mozilla.org, так что мы не будем дублировать ее.

Библиотека для камеры представляет собой модуль для RequireJS под названием webcam.js, который мы встроим в наш каркас.

Первоначально камеру необходимо инициализировать и авторизировать в приложении. После получения согласия пользователя модуль webcam.js выполняет callback-функцию, затем после каждого запроса tick() в игровом цикле фрейм копируется из элемента video в context для canvas. Это важно, потому что таким образом мы можем взаимодействовать с видеоданными. Мы будем использовать этот принцип в последующих разделах, а пока наше приложение представляет собой просто canvas, внутри которого обновляется видео-фрейм после каждого запроса tick().

Имитация зрительной зоны коры головного мозга

git checkout example_2
демо, дифф, исходник примера

Представляем вашему вниманию JSARToolkit — движок для дополненной реальности. Он определяет и описывает положение координатных маркеров в специальных изображениях. Каждый маркер привязан к уникальному номеру (ID) в движке. Изображения с маркерами, которые распознаются библиотекой JSARToolkit, представлены здесь в формате PNG. Имена файлов обозначают ID маркера в библиотеке. На момент написания данной статьи GitHub не поддерживал расширение PNG, и все маркеры имеют расширения txt. Если вы собираетесь их использовать, вам придется их переименовать обратно в PNG после скачивания.

Для этой игры мы будем использовать маркеры №16 и №32, объединённые на одной карточке:

Маркеры №16 и №32, объединённые на одной карточке

JSARToolkit походит на ARToolkit, который был написан в C++ в лаборатории HITLab Вашингтонского университета в Сиэтле. Оригинал был скопирован и портирован на ряд других языков, в том числе Java, затем с Java на Flash, и, наконец, с Flash на JS. Столь тернистый путь наложил свой отпечаток на использование библиотеки. В частности, вы наверняка отметите далее, что некоторые названия непоследовательны и слегка нестандартны.

Давайте взглянем на чистый функционал:

// Объект raster — это canvas, в который мы копируем кадры видео.
 var JSARRaster = NyARRgbRaster_Canvas2D(canvas);

 // Объект parameters обозначает размеры входящей видеотрансляции в пикселях.
 var JSARParameters = new FLARParam(canvas.width, canvas.height);

 // MultiMarkerDetector — движок для распознавания маркера
 var JSARDetector = new FLARMultiIdMarkerDetector(FLARParameters, 120);
 JSARDetector.setContinueMode(true);

<<<<<<< HEAD // Запуск детектора для фрейма, который возвращает количество распознанных маркеров.

 // Запуск детектора для кадра, который возвращает количество распознанных маркеров.

abef452f3552f492e2a9e20df65e35ccaa4043ad var threshold = 64; var count = JSARDetector.detectMarkerLite(JSARRaster, threshold);

После обработки кадра с помощью JSARDetector.detectMarkerLite(), объект JSARDetector будет содержать список распознанных маркеров. JSARDetector. getIdMarkerData(index) возвращает номер ID, а JSARDetector.getTransformMatrix (index) возвращает положение в пространстве. Пользоваться этими методами слегка неудобно, поэтому мы обернём их во вспомогательные методы и вызовем из цикла таким способом:

var markerCount = JSARDetector.detectMarkerLite(JSARRaster, 90); 

for( var index = 0; index < markerCount; index++ ) {
    // Получение уникального номера распознанного маркера.
    var id = getMarkerNumber(index);

    // Получение матрицы перехода для распознанного маркера.
    var matrix = getTransformMatrix(index);
}

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

Отслеживание состояния реализуется с помощью ardetector.js. Для этого мы создаем копию canvas, в который передаются кадры из видео:

// Создаем детектор маркера AR, используя canvas в качестве источника данных
var detector = ardetector.create( canvas );

С каждым запросом tick() изображение в canvas сканируется детектором и при необходимости запускается callback-функция:

// Распоряжение для детектора выполнить распознание. 
detector.detect( onMarkerCreated, onMarkerUpdated, onMarkerDestroyed );

Как видно по коду, теперь наше приложение распознаёт маркеры и выводит результаты распознавания в консоль.

Реальность как плоскость

git checkout example_3
демо, дифф, исходник примера

Как мы писали выше, изображение с дополненной реальностью состоит из реального изображения с наложенными поверх него 3D-моделями. Исходя из этого можно понять, что воспроизведение AR-изображения обычно делится на два этапа. Первый этап — воспроизведение реального изображения, захваченного камерой. В предыдущих примерах мы просто скопировали это изображение в canvas. Теперь нам необходимо дополнить наше изображение 3D-моделями, а для этого нам нужен WebGL canvas. Задача усложнена тем, что в WebGL canvas нет подходящего нам метода context, в который мы могли бы скопировать изображение. Вместо того мы будем передавать в WebGL-рендер текстурированную плоскость, используя изображения с вебкамеры в качестве текстуры. ThreeJS может воспринимать наш canvas как источник с текстурой, так что мы можем вводить в него canvas, в который передаются кадры из видео:

// Создаём текстуру texture, связанную с canvas.
var texture = new THREE.Texture(canvas);

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

// Когда текстура изменилась, нам нужно сообщить об этом ThreeJS 
function update() {
    texture.needsUpdate = true;
}

Страница с маркерами почти готова к тому, чтобы быть дополненной

В качестве результата у нас есть приложение, которое, с точки зрения пользователя, ничем не отличается от примера №2. Однако, за кадром сплошной WebGL. Следующий шаг — «дополнить» его!

Дополняем реальность

git checkout example_4
демо, дифф, исходник примера, видео

Мы готовы добавить в наш коктейль «дополнительные» ингредиенты: они должны будут принимать форму 3D-моделей, зафиксированных по маркерам, которые сняла наша камера. Для начала, нам нужно разрешить обмен данными между детектором ardetector и ThreeJS, это даст нам возможность построить модели, которыми можно дополнить координатные метки.

Шаг 1. Трансформационный перевод

Программисты, знакомые с 3D-графикой, знают, что для рендеринга необходимы две матрицы: модельная матрица (матрица трансформации) и матрица камеры ( проекционная матрица). Матрицы можно получить, используя ardetector, встроенный нами в код ранее, но их нельзя использовать «как есть», без изменений — массивы матриц, полученные ardetector, несовместимы с ThreeJS напрямую. Например, вспомогательный метод getTransformMatrix() возвращает массив Float32Array, который ThreeJS не понимает. К счастью, их можно легко конвертировать напрямую через расширение прототипа методом «monkey patching» (обезьяний патчинг):

// Разрешаем установку Matrix4 с помощью Float32Array
THREE.Matrix4.prototype.setFromArray = function(m) {
 return this.set(
  m[0], m[4], m[8],  m[12],
  m[1], m[5], m[9],  m[13],
  m[2], m[6], m[10], m[14],
  m[3], m[7], m[11], m[15]
 );
}

Эта манипуляция позволяет нам настроить матрицу трансформации в нужный формат, но на практике мы увидим, что обновление не приносит никакого эффекта. Причина этому — механизм кэширования в ThreeJS. Чтобы привести в действие изменения, мы создадим объект-контейнер и установим в значение false флаг matrixAutoUpdate. Затем при каждом обновлении в матрице устанавливаем для matrixWorldNeedsUpdate значение true.

Шаг 2. Куб, отмечающий маркер

Теперь используем наши обезьяньи патчи и объекты-контейнеры, чтобы отобразить разноцветные кубики в качестве маркеров AR. Сначала создадим сетку куба с размерами, достаточными для заполнения координатной метки:

function createMarkerMesh(color) {
    var geometry = new THREE.CubeGeometry( 100,100,100 );
    var material = new THREE.MeshPhongMaterial( {color:color, side:THREE.
    DoubleSide } );

    var mesh = new THREE.Mesh( geometry, material );                      

    //Отрицательное значение, равное половине высоты объекта, располагает объект над маркером AR.
    mesh.position.z = -50; 

    return mesh;
}

Затем поместим сетку в объект-контейнер:

function createMarkerObject(params) {
    var modelContainer = createContainer();

    var modelMesh = createMarkerMesh(params.color);
    modelContainer.add( modelMesh );

    function transform(matrix) {
        modelContainer.transformFromArray( matrix );
    }
}

Далее, генерируем маркер-объекты с привязкой к номеру ID карточки маркера:

// Создаём маркер-объекты, соответствующие желаемому номеру ID маркера.
    var markerObjects = {
        16: arobject.createMarkerObject({color:0xAA0000}), // Маркер #16, красный.
        32: arobject.createMarkerObject({color:0x00BB00}), // Маркер #32, зелёный.
    };

Callback-функции ardetector.detect() применяют матрицу трансформации к соответствующему маркеру. Например, здесь обработчик onCreate добавляет трансформированную модель в AR-отображение:

// Эта функция вызывается, когда маркер впервые распознаётся в потоке
function onMarkerCreated(marker) {
    var object = markerObjects[marker.id];

    // Установка исходной матрицы трансформации для объекта.
    object.transform( marker.matrix );

    // Добавление объекта к сцене.
    view.add( object );
}
});

Теперь наше приложение представляет собой функционирующий пример дополненнойреальности

Теперь наше приложение представляет собой функционирующий пример дополненной реальности!

Создаём отверстия

В игре «An AR Game» маркеры — это не просто разноцветные кубики. Они являются своего рода туннелями, которые визуально вогнуты внутрь карточки с маркерами. Чтобы воспроизвести такой эффект, нам придётся немного схитрить, для наглядности мы воплотим его в три шага.

Шаг 1: Открытие куба

git checkout example_5
демо, дифф, исходник примера, видео

Сначала создадим открытую коробку, удалив верхнюю стенку куба. Для этого сделаем материал стенки невидимым. Открытая коробка расположена позади, и одновременно, под карточкой с маркером благодаря тому, что координаты по оси Z равны половине высоты коробки.

![Эффект интересный, незаконченный — и, вероятно, не сразу понятно в чём дело.] незаконченный эффект

Эффект интересный, незаконченный — и, вероятно, не сразу понятно в чём дело.

Шаг 2. Покрываем куб синим цветом

git checkout example_6
демо, дифф, исходник примера, видео

Итак, что мы пропустили? Нам нужно спрятать ту часть кубика, которая должна быть сзади листа с маркером. Этого можно достичь, для начала, заключив кубик в куб побольше. Назовем этот куб «блокатором», в шаге №3 он будет играть роль плаща-невидимки. Пока оставим его видимым и закрасим синим цветом для наглядности.

Объекты-блокаторы и объекты дополненной реальности воспроизводятся в одном контексте, но отдельных сценах:

function render() {
    // Отрисовка реальной сцены 
    renderer.render(reality.scene, reality.camera);

    // Отрисовка сцены с блокатором 
    renderer.render( occluder.scene, occluder.camera);

    // Отрисовка компонентов дополненной реальности поверх реальной сцены.
    renderer.render(virtual.scene, virtual.camera);
}

Прячем ту часть коробки, которая должна быть сзади листа с маркером

Синяя обёртка пока не способствует созданию иллюзии туннеля.

Шаг 3: Части куба становятся невидимыми

git checkout example_7
демо, дифф, исходник примера, видео

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

function render() {
    // Отрисовка реальной сцены
    renderer.render(reality.scene, reality.camera);

    // Отключение цветового и альфа-буферов, оставляем включённым только 
    буфер глубины.
    renderer.context.colorMask(false,false,false,false);

    // Отрисовка сцены с блокатором
    renderer.render( occluder.scene, occluder.camera);

    // Включение цветового и альфа-буферов.
    renderer.context.colorMask(true,true,true,true);

    // Отрисовка компонентов дополненной реальности поверх реальной сцены.
    renderer.render(virtual.scene, virtual.camera);
}

Иллюзия

Этот результат уже больше похож на нужную нам иллюзию.

Выбираем отверстия

git checkout example_8
демо, дифф, исходник примера

В игре «An AR Game» пользователь может выбрать, какой туннель открыть, разместив маркер под прицельную сетку. Это ключевой аспект игры, с технической точки зрения его можно назвать выбором объекта. Благодаря ThreeJS это довольно просто реализовать. Ключевые классы — THREE.Projector() и THREE. Raycaster(), однако есть небольшая оговорка: хотя ключевой метод имеет название Raycaster.intersectObject(), он принимает THREE.Mesh в качестве параметра. Следовательно, мы добавляем сетку с названием «хитбокс» в createMarkerObject(). В нашем случае — это невидимая геометрическая плоскость. Обратите внимание, что мы не указываем напрямую расположение этой сетки, оставляя для неё значение по умолчанию — (0,0,0) относительно объекта markerContainer. Данное значение располагает хитбокс возле входного отверстия туннеля в плоскости страницы с маркерами, где ранее располагалась бы удалённая нами сторона куба.

Теперь, когда у нас есть в наличии хитбокс, который можно протестировать, мы создаём класс с названием Reticle для определения точки пересечения координат и отслеживания состояния. Его сообщения встраиваются в AR-изображение посредством подключения callback-функции при добавлении объекта через arivew.add(). Эта callback-функция выполняется при выборе объекта, например:

view.add( object, function(isSelected) {
    onMarkerSelectionChanged(marker.id, isSelected);
});

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

Рефакторинг

git checkout example_9
демо, дифф, исходник примера

С функциональностью дополненной реальности мы по сути закончили. У нас есть возможность распознать маркеры в кадрах видео с вебкамеры и привязать к ним 3D-объекты. Также можно определить, когда выбран маркер. Мы готовы перейти ко второму ключевому компоненту игры «An AR Game»: плоскому двухмерному пространству, из которого пользователь перемещает игровые объекты. Для этого потребуется приличное количество кода, предварительная реорганизация кода может помочь сохранить его изящность. Обратите внимание, что большая часть функционала AR на данный момент помещена в главном файле application.js. Давайте его оттуда вырежем и поместим в отдельный модуль с названием realspace .js, что сделает файл application.js намного чище.

Панель плоского пространства

git checkout example_10
демо, дифф, исходник примера

В игре «An AR Game» задача игрока состоит в перемещении игровых объектов из двухмерной плоскости в трёхмерное пространство. Модуль реального пространства, реализованный ранее, служит трёхмерным пространством. Наша двухмерная плоскость будет управляться модулем под названием flatspace.js, создание которого начинается из каркасного шаблона, схожего с каркасом application. js и realspace.js.

Физика

git checkout example_11
демо, дифф, исходник примера

Физика представления реального пространства досталась нам на халяву, от природы. Однако, для панели плоского пространства используется симуляция двухмерной физики, а для её построения требуется промежуточное программное обеспечение, заточенное на расчеты такой физики. Мы будем использовать JavaScript-транскомпиляцию известного движка Box2D под названием Box2D.js. Версия на JavaScript берёт начало от исходного движка на C++, пропущенного через систему LLVM, и обработанного компилятором emscripten.

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

Сперва построим оболочку для исходного движка Box2D.js и назовём её boxworld.js. Это будет то место, в котором наша программа интегрируется в плоское пространство.

Никакого зримого визуального эффекта это не принесет, но, фактически, теперь у нас есть симуляция вакуума.

Визуализация

Неплохо было бы иметь возможность видеть, что там происходит. Box2D предусмотрительно предоставляет возможность рендеринга отладки, а Box2D.js облегчает его с помощью чего-то, похожего на виртуальные методы. Методы будут отрисовывать контекст canvas, так что нам нужно создать полотно canvas, и затем предоставить для таблицы виртуальных методов поддержку отрисовки.

Шаг №1: Создаём метрический canvas

git checkout example_12
демо, дифф, исходник примера

Для преобразования данных из мира Box2D нам послужит canvas. Однако, единицей измерения для canvas служат пиксели, в то время как для описания пространства в Box2D используются метры. Нам потребуются методы для конвертации одних единиц в другие на основе коэффициента пикселя к метру. Методы конвертации используют константу для преобразования пикселей в метры и метров в пиксели. При помощи них мы также выравняем исходные координаты. Эти методы привязаны к canvas и обёрнуты в модуль boxview.js. Так его будет проще встроить в плоское пространство:

Модуль подвергается обработке при инициализации, затем его canvas добавляется в DOM:

view = boxview.create({
    width:640, 
    height:480,
    pixelsPerMeter:13,
});

document.getElementById("flatspace").appendChild( view.canvas );

Теперь на странице есть два холста canvas — плоское пространство и реальное пространство. Немного CSS в application.css располагает их рядом:

#realspace {
    overflow:hidden;
}

#flatspace {
    float:left;
}

Шаг 2: Собираем чертежный комплект

git checkout example_13
демо, дифф, исходник примера

Как уже упоминалось, Box2D.js предоставляет привязки для отрисовки отладочного эскиза мира. К ним можно получить доступ через таблицу виртуальных методов с помощью метода customizeVTable() и, впоследствии, вызвать с помощью b2World.DrawDebugData(). Мы позаимствуем методы отрисовки из описания разработчика с псевдонимом kripken и обернём их в модуль с названием boxdebugdraw.js.

Теперь можно проводить отрисовку, но отрисовывать нам нечего. Сначала придётся ещё немного попотеть!

Волокита

Мир Box2D наполнен объектами, которые называются телами. Тело, добавленное в мир Box2D, подчиняется законам физики, но также оно должно подчиняться и правилам игры. Для этого мы создаём набор управляющих структур с данными и методами для управления телами. Их применение упрощает создание тела, определение столкновения и разрушение тела. Когда эти структуры добавлены, мы можем начать работу над реализацией логики проекта, создавая систему, в которой будет происходить игра.

Создание тела

git checkout example_14
демо, дифф, исходник примера

Давайте оживим имитацию, добавив созидательный момент. Код для построения тела Box2D довольно длинный, он включает в себя описание конструкций, форм и физических параметров. Так что мы будем хранить методы для создания тела в модуле boxbody.js. Чтобы создать тело, мы передаём метод boxBody в boxworld.add(). Например:

function populate() {
    var ball = world.add(
        boxbody.ball,
        {
            x:0,
            y:8,
            radius:10
        }
    );
}

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

Регистрация

git checkout example_15
демо, дифф, исходник примера

Нам необходима возможность ведения учёта тел, которыми наполнен плоский мир. Box2D предоставляет доступ к списку тел, но он слишком общий, чтобы удовлетворить наши потребности. Вместо этого мы используем поле b2Body под названием userData. Мы присвоим ему уникальный номер, который впоследствии будет использоваться как индекс в регистре нашего проекта. Он выполняется в boxregistry.js, и является ключевым аспектом реализации плоского пространства. Данный индекс делает возможной привязку к декоративным сущностям (таким, как спрайты), и упрощает создание callback-функций для расчета столкновений, а также облегчает удаление тел из симуляции. Подробности реализации здесь описаны не будут, но заинтересованные читатели могут просмотреть репозиторий на предмет обработки регистра в boxworld.js, и возвращения методом add() тел с обёрткой, занесённых в регистр.

Столкновение

git checkout example_16
демо, дифф, исходник примера

Определение столкновения в Box2D усложнено тем, что встроенная callback-функция даёт две фиксированные величины, необработанные и неупорядоченные, и мы получаем уведомление о всех столкновениях, происходящих в мире, тем самым сталкиваясь с необходимостью проведения многочисленных условных проверок. Модуль boxregistry.js подходит для управления перегруженными данными. Он позволяет присвоить зарегистрированным объектам callback-функцию onContact. Когда запущен Box2D-обработчик столкновения, мы отправляем в регистр запрос на соответствующие объекты и проверяем наличие callback -функции. Если для объекта определена callback-функция, мы знаем что его активность представляет для нас интерес. Чтобы использовать этот алгоритм в flatspace.js, нужно просто присвоить зарегистрированному объекту callback- функцию при столкновении:

function populate() {
    var ground = world.add(
        boxbody.edge,
        {
            x:0,
            y:-15,
            width:20,
            height:0,
        }
    );

    var ball = world.add(
        boxbody.ball,
        {
            x:0,
            y:8,
            radius:10
        }
    );

    ball.onContact = function(object) {
        console.log("Шар вступил в контакт с:", object);
    };
}

Удаление

git checkout example_17
демо, дифф, исходник примера

Удаление тел осложнено тем, что Box2D не разрешает обращения к b2World.DestroyBody() из b2World.Step(). Это важно, поскольку обычно тело нужно удалить вследствие столкновения, а callback-функция столкновения встречается в симуляции именно на этом этапе, вот такая вот задачка. Одно из решений — ставить тела в очередь на удаление, а затем подвергать эту очередь обработке за пределами данного этапа симуляции. boxregistry предоставляет решение для этой проблемы посредством установки флага isMarkedForDeletion для каждого объекта. Происходит перебор списка зарегистрированных объектов, и обработчики получают извещение о запросе на удаление. Итерация происходит после этапа симуляции столкновения, так что функция удаления полностью уничтожает тела. Проницательные читатели возможно догадались, что теперь перед вызовом функции столкновения мы проверяем флаг isMarkedForDeletion.

Когда речь идёт о flatspace.js, всё происходит прозрачно, так что от нас требуется только установить флаг удаления для зарегистрированного объекта:

ball.onContact = function(object) {
    console.log("Шар вступил в контакт с:", object);
    ball.isMarkedForDeletion = true;
};

Теперь тело удаляется при контакте с поверхностью.

Распознавание

git checkout example_18
демо, дифф, исходник примера

Когда определено столкновение, игре нужно знать, с чем именно столкнулся объект. Для этого мы добавим для зарегистрированных объектов метод is(), который используется для сравнения объектов. Теперь добавим в игру условие для удаления:

ball.onContact = function(object) {
    console.log("Шар вступил в контакт с:", object);
    if( object.is( ground ) ) {
        ball.isMarkedForDeletion = true;
    }
};

Двухмерный туннель

git checkout example_19
демо, дифф, исходник примера

Мы уже обсудили туннели в реальном пространстве, теперь нужно реализовать их копии в плоском пространстве. Туннель в плоском пространстве представляет собой тело, состоящее из датчика Box2D. Шар должен пройти над закрытым туннелем, но через открытый туннель. Теперь представьте крайний случай, когда шар проходит над закрытым туннелем, который затем открывается. Проблема состоит в том, что обработчик onBeginContact ведёт себя согласно своему названию. Контакт с туннелем был определён при закрытом состоянии, после чего туннель был открыт. Следовательно траектория шара не искривляется и мы получаем ошибку. Решить это можно, используя совокупность нескольких датчиков. При использовании совокупности датчиков мы получаем цепочку событий BeginContact по мере движения шара над туннелем. Так мы можем быть уверены, что открытие туннеля, когда шар движется над ним, приведет к изменению траектории шара. Генератор совокупности датчиков называется hole и реализован в boxbody.js. Сгенерированная совокупность датчиков выглядит так:

Сгенерированная совокупность датчиков

Трубопровод

На данный момент мы подготовили к использованию модули на основе JSARToolkit и Box2D.js. Мы использовали их, чтобы создать туннель в реальном и плоском пространствах. Целью игры является перемещение игровых объектов из плоского пространства в реальное пространство, и потому нам нужно обеспечить обмен данными между этими туннелями. Мы подойдём к этому так:

git checkout example_20
демо, дифф, исходник примера

Уведомляем приложение об изменении состояния туннеля в реальном пространстве.

git checkout example_21
демо, дифф, исходник примера

Синхронизируем состояние туннеля в плоском пространстве с состоянием туннеля в реальном пространстве.

git checkout example_22
демо, дифф, исходник примера

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

git checkout example_23
демо, дифф, исходник примера

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

Заключение

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

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

Спасибо за чтение этой статьи! Надеюсь, что мы вдохновили вас на дальнейшее изучение этой темы!