Как создать многопользовательскую игру виртуальной реальности в реальном времени (часть 2)
Опубликовано: 2022-03-10В этой серии руководств мы создадим многопользовательскую онлайн-игру виртуальной реальности, в которой игрокам нужно будет сотрудничать, чтобы решить головоломку. В первой части этой серии мы разработали сферы, представленные в игре. В этой части серии мы добавим игровую механику и настроим протоколы связи между парами игроков.
Описание игры здесь взято из первой части серии: Каждой паре игроков дается кольцо сфер. Цель состоит в том, чтобы «включить» все сферы, где «включена» сфера, если она приподнята и ярка. Сфера «выключена», если она ниже и тусклая. Однако некоторые «доминирующие» шары влияют на своих соседей: если он меняет состояние, его соседи также меняют состояние. Игрок 2 может управлять сферами с четными номерами, а игрок 1 может контролировать сферы с нечетными номерами. Это заставляет обоих игроков сотрудничать, чтобы решить головоломку.
8 шагов в этом руководстве сгруппированы в 3 раздела:
- Заполнение пользовательского интерфейса (шаги 1 и 2)
- Добавьте игровую механику (шаги 3–5)
- Настройка связи (шаги 6–8)
Эта часть завершится полнофункциональной демо-версией онлайн, в которую может сыграть каждый. Вы будете использовать A-Frame VR и несколько расширений A-Frame.
Вы можете найти готовый исходный код здесь.

1. Добавьте визуальные индикаторы
Для начала добавим визуальные индикаторы ID шара. Вставьте новый элемент VR a-text
в качестве первого дочернего элемента #container-orb0
на L36.
<a-entity ...> <a-text class="orb-id" opacity="0.25" rotation="0 -90 0" value="4" color="#FFF" scale="3 3 3" position="0 -2 -0.25" material="side:double"></a-text> ... <a-entity position...> ... </a-entity> </a-entity>
«Зависимости» сферы — это сферы, которые она будет переключать при переключении: например, скажем, сфера 1 имеет в качестве зависимостей сферы 2 и 3. Это означает, что если сфера 1 переключается, сферы 2 и 3 также будут переключаться. Мы добавим визуальные индикаторы зависимостей, как показано ниже, сразу после .animation-position
.
<a-animation class="animation-position" ... /> <a-text class="dep-right" opacity="0.25" rotation="0 -90 0" value="4" color="#FFF" scale="10 10 10" position="0 0 1" material="side:double" ></a-text> <a-text class="dep-left" opacity="0.25"rotation="0 -90 0" value="1" color="#FFF" scale="10 10 10" position="0 0 -3" material="side:double" ></a-text>
Убедитесь, что ваш код соответствует нашему исходному коду для шага 1. Теперь ваша сфера должна соответствовать следующему:

На этом дополнительные визуальные индикаторы, которые нам понадобятся, заканчиваются. Далее мы будем динамически добавлять шары в сцену VR, используя этот шаблон шара.
2. Динамически добавлять сферы
На этом этапе мы добавим сферы в соответствии со спецификацией уровня в стиле JSON. Это позволяет нам легко определять и генерировать новые уровни. Мы будем использовать сферу из последнего шага в части 1 в качестве шаблона.
Для начала импортируйте jQuery, так как это упростит модификацию DOM и, следовательно, модификацию VR-сцены. Сразу после импорта A-Frame добавьте в L8 следующее:
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
Задайте уровень с помощью массива. Массив будет содержать литералы объектов, которые кодируют «зависимости» каждого шара. Внутри <head>
добавьте следующую конфигурацию уровня:
<script> var orbs = [ {left: 1, right: 4}, {}, {on: true}, {}, {on: true} ]; </script>
На данный момент у каждого шара может быть только одна зависимость «справа» от него и одна «слева» от него. Сразу после объявления orbs
выше добавьте обработчик, который будет запускаться при загрузке страницы. Этот обработчик (1) дублирует сферу шаблона и (2) удаляет сферу шаблона, используя предоставленную конфигурацию уровня:
$(document).ready(function() { function populateTemplate(orb, template, i, total) {} function remove(selector) {} for (var i=0; i < orbs.length; i++) { var orb = orbs[i]; var template = $('#template').clone(); template = populateTemplate(orb, template, i, orbs.length); $('#carousel').append(template); } remove('#template'); } function clickOrb(i) {}
Затем заполните функцию remove
, которая просто удаляет элемент из VR-сцены с помощью селектора. К счастью, A-Frame отслеживает изменения в DOM, и поэтому удаления элемента из DOM достаточно, чтобы удалить его из сцены VR. Заполните функцию remove
следующим образом.
function remove(selector) { var el = document.querySelector(selector); el.parentNode.removeChild(el); }
Заполните функцию clickOrb
, которая просто запускает действие щелчка на сфере.
function clickOrb(i) { document.querySelector("#container-orb" + i).click(); }
Затем начните писать функцию populateTemplate
. В этой функции начните с получения .container
. Этот контейнер для шара дополнительно содержит визуальные индикаторы, которые мы добавили на предыдущем шаге. Кроме того, нам нужно будет изменить поведение шара при onclick
на основе его зависимостей. Если существует левая зависимость, измените как визуальный индикатор, так и поведение при onclick
, чтобы отразить это; то же верно и для правой зависимости:
function populateTemplate(orb, template, i, total) { var container = template.find('.container'); var onclick = 'document.querySelector("#light-orb' + i + '").emit("switch");'; if (orb.left || orb.right) { if (orb.left) { onclick += 'clickOrb(' + orb.left + ');'; container.find('.dep-left').attr('value', orb.left); } if (orb.right) { onclick += 'clickOrb(' + orb.right + ');'; container.find('.dep-right').attr('value', orb.right); } } else { container.find('.dep-left').remove(); container.find('.dep-right').remove(); } }
По-прежнему в функции populateTemplate
правильно установите идентификатор шара во всех элементах шара и его контейнера.
container.find('.orb-id').attr('value', i); container.attr('id', 'container-orb' + i); template.find('.orb').attr('id', 'orb' + i); template.find('.light-orb').attr('id', 'light-orb' + i); template.find('.clickable').attr('data-id', i);
По-прежнему в функции populateTemplate
задайте поведение при onclick
, задайте случайное начальное число, чтобы каждый шар визуально различался, и, наконец, задайте положение вращения шара на основе его идентификатора.
container.attr('onclick', onclick); container.find('lp-sphere').attr('seed', i); template.attr('rotation', '0 ' + (360 / total * i) + ' 0');
По завершении функции верните template
со всеми указанными выше конфигурациями.
return template;
Внутри обработчика загрузки документа и после удаления шаблона с помощью remove('#template')
включите сферы, которые изначально были настроены на включение.
$(document).ready(function() { ... setTimeout(function() { for (var i=0; i < orbs.length; i++) { var orb = orbs[i]; if (orb.on) { document.querySelector("#container-orb" + i).click(); } } }, 1000); });
На этом изменения Javascript завершены. Далее мы изменим настройки шаблона по умолчанию на «выключенную» сферу. Измените положение и масштаб #container-orb0
на следующие:
position="8 0.5 0" scale="0.5 0.5 0.5"
Затем измените интенсивность #light-orb0
на 0.
intensity="0"
Убедитесь, что ваш исходный код соответствует нашему исходному коду для шага 2.
Теперь ваша VR-сцена должна содержать 5 динамически заполняемых сфер. Кроме того, одна из сфер должна иметь визуальные индикаторы зависимостей, как показано ниже:

На этом первый раздел о динамическом добавлении сфер заканчивается. В следующем разделе мы потратим три шага на добавление игровой механики. В частности, игрок сможет переключать только определенные сферы в зависимости от идентификатора игрока.
3. Добавить состояние терминала
На этом шаге мы добавим терминальное состояние. Если все шары включены успешно, игрок видит страницу «победы». Для этого вам нужно будет отслеживать состояние всех орбов. Каждый раз, когда сфера включается или выключается, нам нужно будет обновлять наше внутреннее состояние. Скажем, вспомогательная функция toggleOrb
обновляет для нас состояние. toggleOrb
функцию toggleOrb каждый раз, когда сфера меняет состояние: (1) добавьте прослушиватель кликов в обработчик загрузки и (2) добавьте toggleOrb(i);
вызов clickOrb
. Наконец, (3) определите пустой toggleOrb
.
$(document).ready(function() { ... $('.orb').on('click', function() { var id = $(this).attr('data-id') toggleOrb(id); }); }); function toggleOrb(i) {} function clickOrb(i) { ... toggleOrb(i); }
Для простоты мы будем использовать нашу конфигурацию уровня для обозначения состояния игры. Используйте toggleOrb
для on
состояния i-й сферы. toggleOrb
может дополнительно вызвать терминальное состояние, если все сферы включены.
function toggleOrb(i) { orbs[i].on = !orbs[i].on; if (orbs.every(orb => orb.on)) console.log('Victory!'); }
Дважды проверьте, соответствует ли ваш код нашему исходному коду для шага 3.
На этом режим одиночной игры заканчивается. На данный момент у вас есть полнофункциональная игра виртуальной реальности. Однако теперь вам нужно будет написать многопользовательский компонент и поощрять совместную работу с помощью игровой механики.

4. Создайте объект игрока
На этом шаге мы создадим абстракцию для игрока с идентификатором игрока. Этот идентификатор игрока будет назначен сервером позже.
Пока это будет просто глобальная переменная. Сразу после определения orbs
определите идентификатор игрока:
var orbs = ... var current_player_id = 1;
Дважды проверьте, соответствует ли ваш код нашему исходному коду для шага 4. На следующем шаге этот идентификатор игрока будет использоваться для определения того, какими сферами игрок может управлять.
5. Условное переключение сфер
На этом шаге мы изменим поведение переключения сферы. В частности, игрок 1 может управлять сферами с нечетными номерами, а игрок 2 может контролировать сферы с четными номерами. Во-первых, реализуйте эту логику в обоих местах, где сферы меняют состояние:
$('.orb').on('click', function() { var id = ... if (!allowedToToggle(id)) return false; ... } ... function clickOrb(i) { if (!allowedToToggle(id)) return; ... }
Во-вторых, определите функцию allowedToToggle
сразу после clickOrb
. Если текущим игроком является игрок 1, идентификаторы с нечетными номерами вернут значение истинности y, и, таким образом, игроку 1 будет разрешено управлять сферами с нечетными номерами. Обратное верно для игрока 2. Все остальные игроки не могут управлять сферами.
function allowedToToggle(id) { if (current_player_id == 1) { return id % 2; } else if (current_player_id == 2) { return !(id % 2); } return false; }
Дважды проверьте, соответствует ли ваш код нашему исходному коду для шага 5. По умолчанию игроком является игрок 1. Это означает, что вы, как игрок 1, можете управлять только сферами с нечетными номерами в предварительном просмотре. На этом мы завершаем раздел об игровой механике.
В следующем разделе мы облегчим общение между обоими игроками через сервер.
6. Настройте сервер с помощью WebSocket
На этом этапе вы настроите простой сервер для (1) отслеживания идентификаторов игроков и (2) ретрансляции сообщений. Эти сообщения будут включать состояние игры, чтобы игроки могли быть уверены, что каждый видит то, что видит другой.
Мы будем ссылаться на ваш предыдущий index.html
как на исходный код на стороне клиента. Мы будем называть код на этом шаге исходным кодом на стороне сервера. Перейдите на glitch.com, нажмите «новый проект» в правом верхнем углу и в раскрывающемся списке нажмите «hello-express».
На левой панели выберите «package.json» и добавьте socket-io
в dependencies
. Теперь ваш словарь dependencies
должен соответствовать следующему.
"dependencies": { "express": "^4.16.4", "socketio": "^1.0.0" },
На левой панели выберите «index.js» и замените содержимое этого файла следующим минимальным сокетом socket.io Hello World:
const express = require("express"); const app = express(); var http = require('http').Server(app); var io = require('socket.io')(http); /** * Run application on port 3000 */ var port = process.env.PORT || 3000; http.listen(port, function(){ console.log('listening on *:', port); });
Вышеприведенное устанавливает socket.io на порт 3000 для базового экспресс-приложения. Затем определите две глобальные переменные, одну для ведения списка активных игроков, а другую для ведения наименьшего неназначенного идентификатора игрока.
/** * Maintain player IDs */ var playerIds = []; var smallestPlayerId = 1;
Затем определите функцию getPlayerId
, которая генерирует новый идентификатор игрока и помечает новый идентификатор игрока как «занятый», добавляя его в массив playerIds
. В частности, функция просто отмечает smallestPlayerId
идентификатор игрока, а затем обновляет smallestPlayerId
идентификатор игрока, выполняя поиск следующего наименьшего невыбранного целого числа.
function getPlayerId() { var playerId = smallestPlayerId; playerIds.push(playerId); while (playerIds.includes(smallestPlayerId)) { smallestPlayerId++; } return playerId; }
Определите функцию removePlayer
, которая соответствующим образом обновляет smallestPlayerId
идентификатор игрока и освобождает указанный playerId
, чтобы другой игрок мог использовать этот идентификатор.
function removePlayer(playerId) { if (playerId < smallestPlayerId) { smallestPlayerId = playerId; } var index = playerIds.indexOf(playerId); playerIds.splice(index, 1); }
Наконец, определите пару обработчиков событий сокета, которые регистрируют новых игроков и отменяют регистрацию отключенных игроков, используя описанную выше пару методов.
/** * Handle socket interactions */ io.on('connection', function(socket) { socket.on('newPlayer', function() { socket.playerId = getPlayerId(); console.log("new player: ", socket.playerId); socket.emit('playerId', socket.playerId); }); socket.on('disconnect', function() { if (socket.playerId === undefined) return; console.log("disconnected player: ", socket.playerId); removePlayer(socket.playerId); }); });
Дважды проверьте, соответствует ли ваш код нашему исходному коду для шага 6. На этом базовая регистрация и отмена регистрации игрока завершены. Теперь каждый клиент может использовать сгенерированный сервером идентификатор игрока.
На следующем шаге мы изменим клиент, чтобы он получал и использовал идентификатор игрока, выдаваемый сервером.
7. Применить идентификатор игрока
В следующих двух шагах мы создадим рудиментарную версию многопользовательской игры. Для начала интегрируйте назначение идентификатора игрока на стороне клиента. В частности, каждый клиент будет запрашивать у сервера идентификатор игрока. Вернитесь к index.html
на стороне клиента, с которым мы работали на шаге 4 и ранее.
Импортируем socket.io
в head
на L7:
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.js"></script>
После обработчика загрузки документа создайте экземпляр сокета и создайте событие newPlayer
. В ответ серверная сторона сгенерирует новый идентификатор игрока, используя событие playerId
. Ниже используйте URL-адрес для предварительного просмотра вашего проекта Glitch вместо lightful.glitch.me
. Вы можете использовать демонстрационный URL-адрес ниже, но любые изменения кода, конечно, не будут отражены.
$(document).ready(function() { ... }); socket = io("https://lightful.glitch.me"); socket.emit('newPlayer'); socket.on('playerId', function(player_id) { current_player_id = player_id; console.log(" * You are now player", current_player_id); });
Убедитесь, что ваш код соответствует нашему исходному коду для шага 7. Теперь вы можете загрузить свою игру в двух разных браузерах или вкладках, чтобы сыграть две стороны в многопользовательской игре. Игрок 1 сможет управлять сферами с нечетными номерами, а игрок 2 сможет управлять сферами с четными номерами.
Однако обратите внимание, что переключение сфер для игрока 1 не повлияет на состояние сферы для игрока 2. Далее нам нужно синхронизировать игровые состояния.
8. Синхронизируйте состояние игры
На этом этапе мы синхронизируем игровые состояния, чтобы игроки 1 и 2 видели одинаковые состояния сфер. Если сфера 1 включена для игрока 1, она должна быть включена и для игрока 2. На стороне клиента мы будем объявлять и прослушивать переключения сфер. Чтобы объявить, мы просто передадим идентификатор переключаемой сферы.
Перед обоими toggleOrb
добавьте следующий вызов socket.emit
.
$(document).ready(function() { ... $('.orb').on('click', function() { ... socket.emit('toggleOrb', id); toggleOrb(id); }); }); ... function clickOrb(i) { ... socket.emit('toggleOrb', i); toggleOrb(i); }
Затем слушайте переключатели сфер и переключайте соответствующие сферы. Непосредственно под прослушивателем событий сокета playerId
добавьте еще один прослушиватель для события toggleOrb
.
socket.on('toggleOrb', function(i) { document.querySelector("#container-orb" + i).click(); toggleOrb(i); });
На этом внесение изменений в клиентский код завершено. Дважды проверьте, соответствует ли ваш код исходному коду для шага 8.
Серверная сторона теперь должна получать и транслировать идентификатор переключенного шара. В index.js
на стороне сервера добавьте следующий прослушиватель. Этот прослушиватель должен быть размещен непосредственно под прослушивателем disconnect
от сокета.
socket.on('toggleOrb', function(i) { socket.broadcast.emit('toggleOrb', i); });
Дважды проверьте, соответствует ли ваш код исходному коду для шага 8. Теперь игрок 1, загруженный в одном окне, и игрок 2, загруженный во втором окне, увидят одно и то же состояние игры. На этом вы завершили многопользовательскую игру в виртуальной реальности. Кроме того, два игрока должны сотрудничать, чтобы выполнить задачу. Конечный продукт будет соответствовать следующему.

Заключение
На этом мы завершаем наше руководство по созданию многопользовательской игры в виртуальной реальности. В процессе вы затронули ряд тем, в том числе 3D-моделирование в A-Frame VR и многопользовательские игры в реальном времени с использованием WebSockets.
Опираясь на концепции, которые мы затронули, как бы вы обеспечили более плавный игровой процесс для двух игроков? Это может включать проверку того, что состояние игры синхронизировано, и предупреждение пользователя, если это не так. Вы также можете сделать простые визуальные индикаторы для состояния терминала и состояния подключения к плееру.
Учитывая структуру, которую мы установили, и концепции, которые мы представили, теперь у вас есть инструменты, чтобы ответить на эти вопросы и построить гораздо больше.
Вы можете найти готовый исходный код здесь.