Как создать бесконечный раннер в виртуальной реальности (часть 2)
Опубликовано: 2022-03-10В первой части этой серии мы увидели, как можно создать модель виртуальной реальности со световыми и анимационными эффектами. В этой части мы реализуем основную логику игры и используем более сложные манипуляции со средой A-Frame для создания «игровой» части этого приложения. В конце у вас будет работающая игра в виртуальной реальности с реальной задачей.
Это руководство включает в себя ряд шагов, включая (но не ограничиваясь) обнаружение столкновений и другие концепции A-Frame, такие как примеси.
- Демонстрация конечного продукта
 
Предпосылки
Как и в предыдущем уроке, вам понадобится следующее:
- Доступ в Интернет (конкретно на сайт glitch.com);
 - Проект Glitch, завершенный из части 1. (Вы можете продолжить с готового продукта, перейдя на https://glitch.com/edit/#!/ergo-1 и нажав «Remix to edit»;
 -  Гарнитура виртуальной реальности (необязательно, рекомендуется). (Я использую Google Cardboard, который предлагается по цене 15 долларов за штуку.) 
 
Шаг 1: Проектирование препятствий
На этом этапе вы проектируете деревья, которые мы будем использовать в качестве препятствий. Затем вы добавите простую анимацию, которая перемещает деревья по направлению к игроку, как показано ниже:

Эти деревья будут служить шаблонами для препятствий, которые вы будете создавать во время игры. В заключительной части этого шага мы удалим эти «деревья шаблонов».
Для начала добавьте несколько различных миксинов A-Frame . Миксины — это часто используемые наборы свойств компонентов. В нашем случае все наши деревья будут иметь одинаковый цвет, высоту, ширину, глубину и т. д. Другими словами, все ваши деревья будут выглядеть одинаково и, следовательно, будут использовать несколько общих миксинов.
Примечание . В нашем туториале вашими единственными активами будут миксины. Посетите страницу A-Frame Mixins, чтобы узнать больше.
В редакторе перейдите к index.html . Сразу после вашего неба и перед вашими огнями добавьте новый объект A-Frame для хранения ваших активов:
 <a-sky...></a-sky> <!-- Mixins --> <a-assets> </a-assets> <!-- Lights --> ... В вашей новой сущности a-assets начните с добавления миксина для вашей листвы. Этот миксин определяет общие свойства листвы дерева шаблона. Короче говоря, это белая плоская пирамида для низкополигонального эффекта.
 <a-assets> <a-mixin geometry=" primitive: cone; segments-height: 1; segments-radial:4; radius-bottom:0.3;" material="color:white;flat-shading: true;"></a-mixin> </a-assets>Сразу под миксином листвы добавьте миксин для ствола. Этот ствол будет маленькой белой прямоугольной призмой.
 <a-assets> ... <a-mixin geometry=" primitive: box; height:0.5; width:0.1; depth:0.1;" material="color:white;"></a-mixin> </a-assets>Затем добавьте объекты дерева шаблонов, которые будут использовать эти примеси. Находясь в index.html , прокрутите вниз до раздела платформ. Прямо перед разделом игрока добавьте новый раздел дерева с тремя пустыми объектами дерева:
 <a-entity ...> <!-- Trees --> <a-entity></a-entity> <a-entity></a-entity> <a-entity></a-entity> <!-- Player --> ...Затем измените положение, масштаб и добавьте тени к объектам дерева.
 <!-- Trees --> <a-entity shadow scale="0.3 0.3 0.3" position="0 0.6 0"></a-entity> <a-entity shadow scale="0.3 0.3 0.3" position="0 0.6 0"></a-entity> <a-entity shadow scale="0.3 0.3 0.3" position="0 0.6 0"></a-entity>Теперь заполните объекты дерева стволом и листвой, используя примеси, которые мы определили ранее.
 <!-- Trees --> <a-entity ...> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> </a-entity> <a-entity ...> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> </a-entity> <a-entity ...> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> </a-entity>Перейдите к предварительному просмотру, и теперь вы должны увидеть следующие деревья шаблонов.

 Теперь анимируйте деревья из удаленного места на платформе по направлению к пользователю. Как и прежде, используйте тег a-animation :
 <!-- Trees --> <a-entity ...> ... <a-animation attribute="position" ease="linear" from="0 0.6 -7" to="0 0.6 1.5" dur="5000"></a-animation> </a-entity> <a-entity ...> ... <a-animation attribute="position" ease="linear" from="-0.5 0.55 -7" to="-0.5 0.55 1.5" dur="5000"></a-animation> </a-entity> <a-entity ...> ... <a-animation attribute="position" ease="linear" from="0.5 0.55 -7" to="0.5 0.55 1.5" dur="5000"></a-animation> </a-entity>Убедитесь, что ваш код соответствует следующему.
 <a-entity...> <!-- Trees --> <a-entity shadow scale="0.3 0.3 0.3" position="0 0.6 0"> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> <a-animation attribute="position" ease="linear" from="0 0.6 -7" to="0 0.6 1.5" dur="5000"></a-animation> </a-entity> <a-entity shadow scale="0.3 0.3 0.3" position="-0.5 0.55 0"> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> <a-animation attribute="position" ease="linear" from="-0.5 0.55 -7" to="-0.5 0.55 1.5" dur="5000"></a-animation> </a-entity> <a-entity shadow scale="0.3 0.3 0.3" position="0.5 0.55 0"> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> <a-animation attribute="position" ease="linear" from="0.5 0.55 -7" to="0.5 0.55 1.5" dur="5000"></a-animation> </a-entity> <!-- Player --> ...Перейдите к предварительному просмотру, и теперь вы увидите, как деревья движутся к вам.

Вернитесь к своему редактору. На этот раз выберите assets/ergo.js . В разделе игры настройте деревья после загрузки окна.
 /******** * GAME * ********/ ... window.onload = function() { setupTrees(); } Под элементами управления, но перед разделом «Игра», добавьте новый раздел « TREES ». В этом разделе определите новую функцию setupTrees .
 /************ * CONTROLS * ************/ ... /********* * TREES * *********/ function setupTrees() { } /******** * GAME * ********/ ... В новой функции setupTrees получите ссылки на объекты DOM дерева шаблонов и сделайте ссылки доступными глобально.
 /********* * TREES * *********/ var templateTreeLeft; var templateTreeCenter; var templateTreeRight; function setupTrees() { templateTreeLeft = document.getElementById('template-tree-left'); templateTreeCenter = document.getElementById('template-tree-center'); templateTreeRight = document.getElementById('template-tree-right'); } Затем определите новую утилиту removeTree . С помощью этой утилиты вы можете затем удалить деревья шаблонов со сцены. Под функцией setupTrees определите новую утилиту.
 function setupTrees() { ... } function removeTree(tree) { tree.parentNode.removeChild(tree); } Вернитесь в setupTrees и используйте новую утилиту для удаления деревьев шаблонов.
 function setupTrees() { ... removeTree(templateTreeLeft); removeTree(templateTreeRight); removeTree(templateTreeCenter); }Убедитесь, что разделы вашего дерева и игры соответствуют следующему:
 /********* * TREES * *********/ var templateTreeLeft; var templateTreeCenter; var templateTreeRight; function setupTrees() { templateTreeLeft = document.getElementById('template-tree-left'); templateTreeCenter = document.getElementById('template-tree-center'); templateTreeRight = document.getElementById('template-tree-right'); removeTree(templateTreeLeft); removeTree(templateTreeRight); removeTree(templateTreeCenter); } function removeTree(tree) { tree.parentNode.removeChild(tree); } /******** * GAME * ********/ setupControls(); // TODO: AFRAME.registerComponent has to occur before window.onload? window.onload = function() { setupTrees(); }Снова откройте предварительный просмотр, и теперь ваши деревья должны отсутствовать. Предварительный просмотр должен соответствовать нашей игре в начале этого урока.

На этом проектирование дерева шаблонов завершено.
На этом этапе мы рассмотрели и использовали примеси A-Frame, которые позволяют нам упростить код за счет определения общих свойств. Кроме того, мы использовали интеграцию A-Frame с DOM для удаления объектов из сцены A-Frame VR.
На следующем этапе мы создадим несколько препятствий и разработаем простой алгоритм для распределения деревьев по разным дорожкам.
Шаг 2: Создание препятствий
В бесконечном раннере наша цель состоит в том, чтобы избегать летящих к нам препятствий. В этой конкретной реализации игры мы используем три дорожки, как это обычно бывает.
В отличие от большинства бесконечных бегунов, эта игра поддерживает движение только влево и вправо . Это накладывает ограничение на наш алгоритм создания препятствий: мы не можем иметь три препятствия на всех трех дорожках одновременно, летящих к нам. Если это произойдет, у игрока будет нулевой шанс выжить. В результате наш алгоритм порождения должен учитывать это ограничение.
 На этом этапе все правки кода будут внесены в assets/ergo.js . HTML-файл останется прежним. Перейдите в раздел TREES файла assets/ergo.js .
Для начала добавим утилиты для спавна деревьев. Каждому дереву потребуется уникальный идентификатор, который мы наивно определим как количество деревьев, существующих на момент создания дерева. Начните с отслеживания количества деревьев в глобальной переменной.
 /********* * TREES * *********/ ... var numberOfTrees = 0; function setupTrees() { ... Далее мы инициализируем ссылку на элемент DOM контейнера дерева, к которому наша функция порождения будет добавлять деревья. По-прежнему в разделе TREES добавьте глобальную переменную, а затем создайте ссылку.
 ... var treeContainer; var numberOfTrees ... function setupTrees() { ... templateTreeRight = ... treeContainer = document.getElementById('tree-container'); removeTree(...); ... }Используя как количество деревьев, так и контейнер дерева, напишите новую функцию, которая порождает деревья.

 function removeTree(tree) { ... } function addTree(el) { numberOfTrees += 1; el.id = 'tree-' + numberOfTrees; treeContainer.appendChild(el); } ... Для простоты использования позже вы создадите вторую функцию, которая добавляет правильное дерево к правильной полосе. Для начала определите новый массив templates в разделе TREES .
 var templates; var treeContainer; ... function setupTrees() { ... templates = [templateTreeLeft, templateTreeCenter, templateTreeRight]; removeTree(...); ... }Используя этот массив шаблонов, добавьте утилиту, которая порождает деревья на определенной полосе с заданным идентификатором, представляющим левый, средний или правый.
 function function addTree(el) { ... } function addTreeTo(position_index) { var template = templates[position_index]; addTree(template.cloneNode(true)); } Перейдите к предварительному просмотру и откройте консоль разработчика. В консоли разработчика вызовите глобальную функцию addTreeTo .
 > addTreeTo(0); # spawns tree in left lane 
addTreeTo вручную (большой предварительный просмотр)Теперь вы напишете алгоритм, который генерирует деревья случайным образом:
- Выберите дорожку случайным образом (которая еще не выбрана для этого временного шага);
 - Спаун дерева с некоторой вероятностью;
 - Если для этого временного шага было создано максимальное количество деревьев, остановитесь. В противном случае повторите шаг 1.
 
 Чтобы реализовать этот алгоритм, мы вместо этого будем перемешивать список шаблонов и обрабатывать их по одному. Начните с определения новой функции addTreesRandomly , которая принимает несколько различных аргументов ключевого слова.
 function addTreeTo(position_index) { ... } /** * Add any number of trees across different lanes, randomly. **/ function addTreesRandomly( { probTreeLeft = 0.5, probTreeCenter = 0.5, probTreeRight = 0.5, maxNumberTrees = 2 } = {}) { } В вашей новой функции addTreesRandomly определите список деревьев шаблонов и перемешайте список.
 function addTreesRandomly( ... ) { var trees = [ {probability: probTreeLeft, position_index: 0}, {probability: probTreeCenter, position_index: 1}, {probability: probTreeRight, position_index: 2}, ] shuffle(trees); } Прокрутите вниз до конца файла и создайте новый раздел утилит вместе с новой утилитой shuffle . Эта утилита перемешает массив на месте.
 /******** * GAME * ********/ ... /************* * UTILITIES * *************/ /** * Shuffles array in place. * @param {Array} a items An array containing the items. */ function shuffle(a) { var j, x, i; for (i = a.length - 1; i > 0; i--) { j = Math.floor(Math.random() * (i + 1)); x = a[i]; a[i] = a[j]; a[j] = x; } return a; } Вернитесь к функции addTreesRandomly в разделе «Деревья». Добавьте новую переменную numberOfTreesAdded и выполните итерацию по списку деревьев, определенному выше.
 function addTreesRandomly( ... ) { ... var numberOfTreesAdded = 0; trees.forEach(function (tree) { }); } В итерации по деревьям создайте дерево только с некоторой вероятностью и только в том случае, если количество добавленных деревьев не превышает 2 . Обновите цикл for следующим образом.
 function addTreesRandomly( ... ) { ... trees.forEach(function (tree) { if (Math.random() < tree.probability && numberOfTreesAdded < maxNumberTrees) { addTreeTo(tree.position_index); numberOfTreesAdded += 1; } }); }Чтобы завершить функцию, верните количество добавленных деревьев.
 function addTreesRandomly( ... ) { ... return numberOfTreesAdded; } Дважды проверьте, что ваша функция addTreesRandomly соответствует следующему.
 /** * Add any number of trees across different lanes, randomly. **/ function addTreesRandomly( { probTreeLeft = 0.5, probTreeCenter = 0.5, probTreeRight = 0.5, maxNumberTrees = 2 } = {}) { var trees = [ {probability: probTreeLeft, position_index: 0}, {probability: probTreeCenter, position_index: 1}, {probability: probTreeRight, position_index: 2}, ] shuffle(trees); var numberOfTreesAdded = 0; trees.forEach(function (tree) { if (Math.random() < tree.probability && numberOfTreesAdded < maxNumberTrees) { addTreeTo(tree.position_index); numberOfTreesAdded += 1; } }); return numberOfTreesAdded; }Наконец, для автоматического создания деревьев настройте таймер, который запускает создание деревьев через регулярные промежутки времени. Определите таймер глобально и добавьте новую функцию разрыва для этого таймера.
 /********* * TREES * *********/ ... var treeTimer; function setupTrees() { ... } function teardownTrees() { clearInterval(treeTimer); }Затем определите новую функцию, которая инициализирует таймер и сохраняет его в ранее определенной глобальной переменной. Приведенный ниже таймер запускается каждые полсекунды.
 function addTreesRandomlyLoop({intervalLength = 500} = {}) { treeTimer = setInterval(addTreesRandomly, intervalLength); }Наконец, запустите таймер после загрузки окна из раздела «Игра».
 /******** * GAME * ********/ ... window.onload = function() { ... addTreesRandomlyLoop(); }Перейдите к предварительному просмотру, и вы увидите, что деревья появляются случайным образом. Обратите внимание, что никогда не бывает трех деревьев одновременно.

На этом шаг с препятствиями заканчивается. Мы успешно взяли ряд деревьев шаблонов и создали бесконечное количество препятствий из шаблонов. Наш алгоритм нереста также учитывает естественные ограничения в игре, чтобы сделать ее играбельной.
На следующем шаге добавим проверку на столкновение.
Шаг 3: Проверка на столкновение
В этом разделе мы реализуем тесты на столкновение между препятствиями и игроком. Эти тесты на столкновение проще, чем тесты на столкновение в большинстве других игр; однако игрок движется только вдоль оси x, поэтому всякий раз, когда дерево пересекает ось x, проверьте, совпадает ли полоса движения дерева с полосой движения игрока. Мы реализуем эту простую проверку для этой игры.
 Перейдите к index.html , вниз к разделу TREES . Здесь мы добавим информацию о переулке к каждому из деревьев. Для каждого дерева добавьте data-tree-position-index= следующим образом. Дополнительно добавьте class="tree" , чтобы мы могли легко выбирать все деревья в строке:
 <a-entity data-tree-position-index="1" class="tree" ...> </a-entity> <a-entity data-tree-position-index="0" class="tree" ...> </a-entity> <a-entity data-tree-position-index="2" class="tree" ...> </a-entity> Перейдите к assets/ergo.js и вызовите новую функцию setupCollisions в разделе GAME . Кроме того, определите новую глобальную переменную isGameRunning , которая указывает, запущена ли уже существующая игра.
 /******** * GAME * ********/ var isGameRunning = false; setupControls(); setupCollision(); window.onload = function() { ... Определите новый раздел COLLISIONS сразу после раздела TREES , но перед разделом Game. В этом разделе определите функцию setupCollisions.
 /********* * TREES * *********/ ... /************** * COLLISIONS * **************/ const POSITION_Z_OUT_OF_SIGHT = 1; const POSITION_Z_LINE_START = 0.6; const POSITION_Z_LINE_END = 0.7; function setupCollision() { } /******** * GAME * ********/ Как и прежде, мы зарегистрируем компонент AFRAME и будем использовать прослушиватель событий tick для запуска кода на каждом временном шаге. В этом случае мы зарегистрируем компонент с player и запустим проверку всех деревьев в этом слушателе:
 function setupCollisions() { AFRAME.registerComponent('player', { tick: function() { document.querySelectorAll('.tree').forEach(function(tree) { } } } } В цикле for начните с получения соответствующей информации о дереве:
 document.querySelectorAll('.tree').forEach(function(tree) { position = tree.getAttribute('position'); tree_position_index = tree.getAttribute('data-tree-position-index'); tree_id = tree.getAttribute('id'); } Затем, все еще в цикле for , удалите дерево, если оно находится вне поля зрения, сразу после извлечения свойств дерева:
 document.querySelectorAll('.tree').forEach(function(tree) { ... if (position.z > POSITION_Z_OUT_OF_SIGHT) { removeTree(tree); } }Далее, если игра не запущена, не проверяйте, есть ли коллизия.
 document.querySelectorAll('.tree').forEach(function(tree) { if (!isGameRunning) return; } Наконец (все еще в цикле for ), проверьте, находится ли дерево в той же позиции одновременно с игроком. Если это так, вызовите еще не определенную функцию gameOver :
 document.querySelectorAll('.tree').forEach(function(tree) { ... if (POSITION_Z_LINE_START < position.z && position.z < POSITION_Z_LINE_END && tree_position_index == player_position_index) { gameOver(); } } Убедитесь, что ваша функция setupCollisions соответствует следующему:
 function setupCollisions() { AFRAME.registerComponent('player', { tick: function() { document.querySelectorAll('.tree').forEach(function(tree) { position = tree.getAttribute('position'); tree_position_index = tree.getAttribute('data-tree-position-index'); tree_id = tree.getAttribute('id'); if (position.z > POSITION_Z_OUT_OF_SIGHT) { removeTree(tree); } if (!isGameRunning) return; if (POSITION_Z_LINE_START < position.z && position.z < POSITION_Z_LINE_END && tree_position_index == player_position_index) { gameOver(); } }) } }) } На этом настройка столкновения завершена. Теперь мы добавим несколько тонкостей, чтобы абстрагироваться от последовательностей startGame и gameOver . Перейдите в раздел GAME . Обновите блок window.onload , чтобы он соответствовал следующему, заменив addTreesRandomlyLoop еще не определенной функцией startGame .
 window.onload = function() { setupTrees(); startGame(); } Под вызовами функции настройки создайте новую функцию startGame . Эта функция соответствующим образом инициализирует переменную isGameRunning и предотвратит избыточные вызовы.
 window.onload = function() { ... } function startGame() { if (isGameRunning) return; isGameRunning = true; addTreesRandomlyLoop(); } Наконец, определите gameOver , который будет предупреждать «Игра окончена!» сообщение пока.
 function startGame() { ... } function gameOver() { isGameRunning = false; alert('Game Over!'); teardownTrees(); }На этом завершается раздел проверки столкновений в бесконечной игре-раннере.
На этом этапе мы снова использовали компоненты A-Frame и ряд других утилит, которые мы добавили ранее. Мы дополнительно реорганизовали и правильно абстрагировали игровые функции; впоследствии мы расширим эти игровые функции, чтобы добиться более полного игрового опыта.
Заключение
В части 1 мы добавили удобные элементы управления для VR-гарнитуры: смотрите влево, чтобы двигаться влево, и вправо, чтобы двигаться вправо. Во второй части серии я показал вам, как легко можно создать базовую функционирующую игру виртуальной реальности. Мы добавили игровую логику, чтобы бесконечный раннер соответствовал вашим ожиданиям: бегите вечно и на игрока летит бесконечная череда опасных препятствий. На данный момент вы создали работающую игру с поддержкой гарнитур виртуальной реальности без клавиатуры.
Вот дополнительные ресурсы для различных элементов управления и гарнитур виртуальной реальности:
-  A-Frame для VR-гарнитур
Обзор браузеров и гарнитур, которые поддерживает A-Frame VR. -  A-Frame для контроллеров VR
Как A-Frame не поддерживает контроллеры без контроллеров, контроллеры 3DoF и контроллеры 6DoF, в дополнение к другим альтернативам для взаимодействия. 
В следующей части мы добавим несколько завершающих штрихов и синхронизируем игровые состояния , которые еще на шаг приблизит нас к многопользовательским играм.
