Как создать бесконечный раннер в виртуальной реальности (часть 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(); }

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

Часть 1 готовый продукт
Готовый продукт, часть 1 (большой предварительный просмотр)

На этом проектирование дерева шаблонов завершено.

На этом этапе мы рассмотрели и использовали примеси 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 вручную
addTreeTo вручную (большой предварительный просмотр)

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

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

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