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

Начните с добавления объекта в index.html . Добавьте text
миксин, который будет повторно использоваться для других текстовых элементов:
<a-assets> ... <a-mixin text=" font:exo2bold; anchor:center; align:center;"></a-mixin> ... </a-assets>
Затем добавьте text
элемент на платформу прямо перед игроком:
<!-- Score --> <a-text value="" mixin="text" height="40" width="40" position="0 1.2 -3" opacity="0.75"></a-text> <!-- Player --> ...
Это добавляет текстовый объект к сцене виртуальной реальности. Текст в настоящее время не виден, так как его значение установлено пустым. Однако теперь вы будете заполнять текстовый объект динамически, используя JavaScript. Перейдите к assets/ergo.js . После раздела collisions
добавьте раздел score
и определите ряд глобальных переменных:
-
score
: текущий счет игры. -
countedTrees
: идентификаторы всех деревьев, включенных в оценку. (Это связано с тем, что тесты на коллизии могут запускаться несколько раз для одного и того же дерева.) -
scoreDisplay
: ссылка на объект DOM, соответствующий текстовому объекту в мире виртуальной реальности.
/********* * SCORE * *********/ var score; var countedTrees; var scoreDisplay;
Затем определите функцию настройки для инициализации наших глобальных переменных. В том же духе определите функцию teardown
.
... var scoreDisplay; function setupScore() { score = 0; countedTrees = new Set(); scoreDisplay = document.getElementById('score'); } function teardownScore() { scoreDisplay.setAttribute('value', ''); }
В разделе « Game
» обновите gameOver
, startGame
и window.onload
, чтобы включить настройку счета и удаление.
/******** * GAME * ********/ function gameOver() { ... teardownScore(); } function startGame() { ... setupScore(); addTreesRandomlyLoop(); } window.onload = function() { setupScore(); ... }
Определите функцию, которая увеличивает оценку для определенного дерева. Эта функция проверит countedTrees
, чтобы убедиться, что дерево не подсчитано дважды.
function addScoreForTree(tree_id) { if (countedTrees.has(tree_id)) return; score += 1; countedTrees.add(tree_id); }
Кроме того, добавьте утилиту для обновления отображения результатов с помощью глобальной переменной.
function updateScoreDisplay() { scoreDisplay.setAttribute('value', score); }
Обновите тестирование столкновений соответствующим образом, чтобы вызывать эту функцию увеличения счета всякий раз, когда препятствие проходит мимо игрока. По-прежнему в assets/ergo.js
перейдите в раздел collisions
. Добавьте следующую проверку и обновление.
AFRAME.registerComponent('player', { tick: function() { document.querySelectorAll('.tree').forEach(function(tree) { ... if (position.z > POSITION_Z_LINE_END) { addScoreForTree(tree_id); updateScoreDisplay(); } }) } })
Наконец, обновите отображение счета, как только игра начнется. Перейдите в раздел « Game
» и добавьте updateScoreDisplay();
для startGame
:
function startGame() { ... setupScore(); updateScoreDisplay(); ... }
Убедитесь, что assets/ergo.js и index.html соответствуют соответствующим файлам исходного кода. Затем перейдите к предварительному просмотру. Вы должны увидеть следующее:

На этом показ очков заканчивается. Затем мы добавим правильное меню запуска и окончания игры , чтобы игрок мог переиграть игру по своему желанию.
Шаг 2: Добавьте меню «Пуск»
Теперь, когда пользователь может отслеживать прогресс, вы добавите последние штрихи, чтобы завершить игру. На этом шаге вы добавите меню « Пуск » и меню « Игра окончена », чтобы пользователь мог запускать и перезапускать игры.
Начнем с меню « Пуск », где игрок нажимает кнопку «Пуск», чтобы начать игру. Во второй половине этого шага вы добавите меню Game Over с кнопкой «Перезапустить»:

Перейдите к index.html в вашем редакторе. Затем найдите раздел Mixins
. Здесь добавьте миксин title
, который определяет стили для особо крупного текста. Мы используем тот же шрифт, что и раньше, выравниваем текст по центру и определяем размер, соответствующий типу текста. (Обратите внимание, что anchor
— это место, где текстовый объект привязан к своей позиции.)
<a-assets> ... <a-mixin text=" font:exo2bold; height:40; width:40; opacity:0.75; anchor:center; align:center;"></a-mixin> </a-assets>
Затем добавьте второй миксин для второстепенных заголовков. Этот текст немного меньше, но в остальном идентичен заголовку.
<a-assets> ... <a-mixin text=" font:exo2bold; height:10; width:10; opacity:0.75; anchor:center; align:center;"></a-mixin> </a-assets>
Для третьего и последнего миксина определите свойства описательного текста — даже меньшего размера, чем второстепенные заголовки.
<a-assets> ... <a-mixin text=" font:exo2bold; height:5; width:5; opacity:0.75; anchor:center; align:center;"></a-mixin> </a-assets>
Определив все стили текста, вы теперь определите текстовые объекты в мире. Добавьте новый раздел « Menus
» под разделом « Score
» с пустым контейнером для меню « Пуск »:
<!-- Score --> ... <!-- Menus --> <a-entity> <a-entity position="0 1.1 -3"> </a-entity> </a-entity>
Внутри контейнера меню «Пуск» определите заголовок и контейнер для всего текста, не являющегося заголовком:
... <a-entity ...> <a-entity position="0 1 0"> </a-entity> <a-text value="ERGO" mixin="title"></a-text> </a-entity> </a-entity>
Внутри контейнера для текста без заголовка добавьте инструкции по игре:
<a-entity...> <a-text value="Turn left and right to move your player, and avoid the trees!" mixin="copy"></a-text> </a-entity>
Чтобы завершить меню « Пуск », добавьте кнопку с надписью «Пуск»:
<a-entity...> ... <a-text value="Start" position="0 0.75 0" mixin="heading"></a-text> <a-box position="0 0.65 -0.05" width="1.5" height="0.6" depth="0.1"></a-box> </a-entity>
Дважды проверьте, соответствует ли HTML-код меню « Пуск » следующему:
<!-- Menus --> <a-entity> <a-entity position="0 1.1 -3"> <a-entity position="0 1 0"> <a-text value="Turn left and right to move your player, and avoid the trees!" mixin="copy"></a-text> <a-text value="Start" position="0 0.75 0" mixin="heading"></a-text> <a-box position="0 0.65 -0.05" width="1.5" height="0.6" depth="0.1"></a-box> </a-entity> <a-text value="ERGO" mixin="title"></a-text> </a-entity> </a-entity>
Перейдите к предварительному просмотру, и вы увидите следующее меню «Пуск»:

По-прежнему в разделе « Menus
» (прямо под меню « start
») добавьте меню окончания game-over
используя те же миксины:
<!-- Menus --> <a-entity> ... <a-entity position="0 1.1 -3"> <a-text value="?" mixin="heading" position="0 1.7 0"></a-text> <a-text value="Score" mixin="copy" position="0 1.2 0"></a-text> <a-entity> <a-text value="Restart" mixin="heading" position="0 0.7 0"></a-text> <a-box position="0 0.6 -0.05" width="2" height="0.6" depth="0.1"></a-box> </a-entity> <a-text value="Game Over" mixin="title"></a-text> </a-entity> </a-entity>
Перейдите к вашему файлу JavaScript, assets/ergo.js . Создайте новый раздел « Menus
» перед разделом « Game
». Кроме того, определите три пустые функции: setupAllMenus
, hideAllMenus
и showGameOverMenu
.
/******** * MENU * ********/ function setupAllMenus() { } function hideAllMenus() { } function showGameOverMenu() { } /******** * GAME * ********/
Далее обновите раздел Game
в трех местах. В gameOver
покажите меню Game Over :

function gameOver() { ... showGameOverMenu(); } ``` In `startGame`, hide all menus: ``` function startGame() { ... hideAllMenus(); }
Затем в window.onload
удалите прямой вызов startGame
и вместо этого вызовите setupAllMenus
. Обновите слушателя, чтобы он соответствовал следующему:
window.onload = function() { setupAllMenus(); setupScore(); setupTrees(); }
Вернитесь в раздел Menu
. Сохраняйте ссылки на различные объекты DOM:
/******** * MENU * ********/ var menuStart; var menuGameOver; var menuContainer; var isGameRunning = false; var startButton; var restartButton; function setupAllMenus() { menuStart = document.getElementById('start-menu'); menuGameOver = document.getElementById('game-over'); menuContainer = document.getElementById('menu-container'); startButton = document.getElementById('start-button'); restartButton = document.getElementById('restart-button'); }
Затем привяжите обе кнопки «Start» и «Restart» к startGame
:
function setupAllMenus() { ... startButton.addEventListener('click', startGame); restartButton.addEventListener('click', startGame); }
Определите showStartMenu
и вызовите его из setupAllMenus
:
function setupAllMenus() { ... showStartMenu(); } function hideAllMenus() { } function showGameOverMenu() { } function showStartMenu() { }
Чтобы заполнить три пустые функции, вам понадобится несколько вспомогательных функций. Определите следующие две функции, которые принимают элемент DOM, представляющий объект A-Frame VR, и отображают или скрывают его. Определите обе функции выше showAllMenus
:
... var restartButton; function hideEntity(el) { el.setAttribute('visible', false); } function showEntity(el) { el.setAttribute('visible', true); } function showAllMenus() { ...
Сначала заполните hideAllMenus
. Вы удалите объекты из поля зрения, а затем удалите прослушиватели кликов для обоих меню:
function hideAllMenus() { hideEntity(menuContainer); startButton.classList.remove('clickable'); restartButton.classList.remove('clickable'); }
Во-вторых, заполните showGameOverMenu
. Здесь восстановите контейнер для обоих меню, а также меню Game Over и прослушиватель кликов кнопки «Перезапустить». Однако удалите прослушиватель щелчков кнопки «Пуск» и скройте меню «Пуск».
function showGameOverMenu() { showEntity(menuContainer); hideEntity(menuStart); showEntity(menuGameOver); startButton.classList.remove('clickable'); restartButton.classList.add('clickable'); }
В-третьих, заполните showStartMenu
. Здесь отмените все изменения, showGameOverMenu
.
function showStartMenu() { showEntity(menuContainer); hideEntity(menuGameOver); showEntity(menuStart); startButton.classList.add('clickable'); restartButton.classList.remove('clickable'); }
Дважды проверьте, соответствует ли ваш код соответствующим исходным файлам. Затем перейдите к предварительному просмотру, и вы увидите следующее поведение:

На этом меню « Пуск » и « Игра окончена» закрыты.
Поздравляем! Теперь у вас есть полностью функционирующая игра с правильным началом и правильным концом. Однако в этом руководстве нам остался еще один шаг: нам нужно синхронизировать состояние игры между различными устройствами игроков. Это приблизит нас на один шаг к многопользовательским играм.
Шаг 3: Синхронизация состояния игры с MirrorVR
В предыдущем руководстве вы узнали, как отправлять информацию в режиме реального времени через сокеты, чтобы упростить одностороннюю связь между сервером и клиентом. На этом шаге вы создадите полноценный продукт из этого руководства, MirrorVR, который управляет сервером-посредником в обмене данными между клиентами.
Примечание . Вы можете узнать больше о MirrorVR здесь.
Перейдите к index.html . Здесь мы загрузим MirrorVR и добавим к камере компонент, указывающий, что он должен отражать вид мобильного устройства, где это применимо. Импортируйте зависимость socket.io и MirrorVR 0.2.3.
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.js"></script> <script src="https://cdn.jsdelivr.net/gh/alvinwan/[email protected]/dist/mirrorvr.min.js"></script>
Затем добавьте к камере компонент camera-listener
:
<a-camera camera-listener ...>
Перейдите к assets/ergo.js . На этом этапе мобильное устройство будет отправлять команды, а настольное устройство будет только зеркально отображать мобильное устройство.
Чтобы облегчить это, вам нужна утилита для различения настольных и мобильных устройств. В конце вашего файла добавьте функцию mobileCheck
после shuffle
:
/** * Checks for mobile and tablet platforms. */ function mobileCheck() { var check = false; (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[aw])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera); return check; };
Во-первых, мы синхронизируем запуск игры. В startGame
раздела Game добавьте в конце уведомление mirrorVR
.
function startGame() { ... if (mobileCheck()) { mirrorVR.notify('startGame', {}) } }
Мобильный клиент теперь отправляет уведомления о запуске игры. Теперь вы реализуете ответ рабочего стола.
В прослушивателе загрузки окна вызовите функцию setupMirrorVR
:
window.onload = function() { ... setupMirrorVR(); }
Определите новый раздел над разделом « Game
» для настройки MirrorVR:
/************ * MirrorVR * ************/ function setupMirrorVR() { mirrorVR.init(); }
Затем добавьте аргументы ключевого слова в функцию инициализации для mirrorVR. В частности, мы определим обработчик уведомлений о начале игры. Дополнительно укажем ID комнаты; это гарантирует, что любой, кто загружает ваше приложение, будет немедленно синхронизирован.
function setupMirrorVR() { mirrorVR.init({ roomId: 'ergo', state: { startGame: { onNotify: function(data) { hideAllMenus(); setupScore(); updateScoreDisplay(); } }, } }); }
Повторите тот же процесс синхронизации для Game Over . В gameOver
в разделе Game
добавить проверку для мобильных устройств и соответственно отправить уведомление:
function gameOver() { ... if (mobileCheck()) { mirrorVR.notify('gameOver', {}); } }
Перейдите в раздел MirrorVR
и обновите аргументы ключевого слова с помощью прослушивателя gameOver
:
function setupMirrorVR() { mirrorVR.init({ state: { startGame: {... }, gameOver: { onNotify: function(data) { gameOver(); } }, } }) }
Затем повторите тот же процесс синхронизации для добавления деревьев. Перейдите к addTreesRandomly
в разделе Trees
. Следите за тем, какие дорожки получают новые деревья. Затем, непосредственно перед директивой return
, и отправьте соответствующее уведомление:
function addTreesRandomly(...) { ... var numberOfTreesAdded ... var position_indices = []; trees.forEach(function (tree) { if (...) { ... position_indices.push(tree.position_index); } }); if (mobileCheck()) { mirrorVR.notify('addTrees', position_indices); } return ... }
Перейдите в раздел MirrorVR
и обновите аргументы ключевого слова до mirrorVR.init
с новым прослушивателем для деревьев:
function setupMirrorVR() { mirrorVR.init({ state: { ... gameOver: {... }, addTrees: { onNotify: function(position_indices) { position_indices.forEach(addTreeTo) } }, } }) }
Наконец, мы синхронизируем счет игры. В updateScoreDisplay
из раздела Score
отправьте уведомление, когда это применимо:
function updateScoreDisplay() { ... if (mobileCheck()) { mirrorVR.notify('score', score); } }
Обновите инициализацию mirrorVR
в последний раз, используя прослушиватель для изменений оценки:
function setupMirrorVR() { mirrorVR.init({ state: { addTrees: { }, score: { onNotify: function(data) { score = data; updateScoreDisplay(); } } } }); }
Дважды проверьте, соответствует ли ваш код соответствующим файлам исходного кода для этого шага. Затем перейдите к предварительному просмотру рабочего стола. Кроме того, откройте тот же URL-адрес на своем мобильном устройстве. Как только ваше мобильное устройство загрузит веб-страницу, ваш рабочий стол должен немедленно начать отражать игру мобильного устройства.
Вот демо. Обратите внимание, что курсор на рабочем столе не перемещается, указывая на то, что мобильное устройство управляет предварительным просмотром на рабочем столе.

На этом ваш расширенный проект с помощью mirrorVR завершен.
Этот третий шаг представил несколько основных шагов синхронизации состояния игры; чтобы сделать это более надежным, вы можете добавить больше проверок работоспособности и больше точек синхронизации.
Заключение
В этом руководстве вы добавили последние штрихи к своей бесконечной игре-раннере и реализовали синхронизацию настольного клиента с мобильным клиентом в реальном времени, эффективно отражая экран мобильного устройства на вашем рабочем столе. На этом мы завершаем серию о создании бесконечного раннера в виртуальной реальности. Наряду с методами A-Frame VR вы изучили 3D-моделирование, общение между клиентами и другие широко применимые концепции.
Следующие шаги могут включать:
- Более продвинутое моделирование
Это означает более реалистичные 3D-модели, потенциально созданные в стороннем программном обеспечении и импортированные. Например, (MagicaVoxel) упрощает создание воксельной графики, а (Blender) представляет собой комплексное решение для 3D-моделирования. - Больше сложности
Более сложные игры, такие как стратегия в реальном времени, могут использовать сторонний движок для повышения эффективности. Это может означать полный отказ от A-Frame и webVR вместо публикации скомпилированной (Unity3d) игры.
Другие возможности включают поддержку многопользовательской игры и более богатую графику. После завершения этой серии руководств у вас теперь есть основа для дальнейшего изучения.