Как создать бесконечный раннер в виртуальной реальности (часть 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
Теперь вы напишете алгоритм, который генерирует деревья случайным образом:
- Выберите дорожку случайным образом (которая еще не выбрана для этого временного шага);
- Спаун дерева с некоторой вероятностью;
- Если для этого временного шага было создано максимальное количество деревьев, остановитесь. В противном случае повторите шаг 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, в дополнение к другим альтернативам для взаимодействия.
В следующей части мы добавим несколько завершающих штрихов и синхронизируем игровые состояния , которые еще на шаг приблизит нас к многопользовательским играм.