실시간 멀티플레이어 가상 현실 게임을 만드는 방법(2부)
게시 됨: 2022-03-10이 튜토리얼 시리즈에서는 플레이어가 퍼즐을 풀기 위해 협력해야 하는 웹 기반 멀티플레이어 가상 현실 게임을 구축할 것입니다. 이 시리즈의 첫 번째 부분에서 우리는 게임에 등장하는 오브를 디자인했습니다. 시리즈의 이 부분에서는 게임 메커니즘을 추가하고 플레이어 쌍 간의 통신 프로토콜을 설정합니다.
여기에 있는 게임 설명은 시리즈의 첫 번째 부분에서 발췌한 것입니다. 각 플레이어 쌍에는 오브 링이 제공됩니다. 목표는 모든 구를 "켜는" 것인데, 구가 높고 밝으면 "켜져 있는" 것입니다. 구체가 더 낮고 희미하면 "꺼져" 있습니다. 그러나 특정 "지배적인" 구는 이웃에 영향을 미칩니다. 상태를 전환하면 이웃도 상태를 전환합니다. 2번 플레이어는 짝수 번째 구슬을 제어할 수 있고 1번 플레이어는 홀수번 구슬을 제어할 수 있습니다. 이를 통해 두 플레이어는 퍼즐을 풀기 위해 협력해야 합니다.
이 자습서의 8단계는 3개의 섹션으로 그룹화되어 있습니다.
- 사용자 인터페이스 채우기(1단계 및 2단계)
- 게임 역학 추가(3~5단계)
- 통신 설정(6~8단계)
이 부분은 누구나 플레이할 수 있는 완전한 기능의 온라인 데모로 마무리됩니다. A-Frame VR 및 여러 A-Frame 확장을 사용합니다.
완성된 소스 코드는 여기에서 찾을 수 있습니다.

1. 시각적 지표 추가
시작하려면 오브의 ID에 대한 시각적 표시기를 추가합니다. 새로운 a-text
VR 요소를 L36에 #container-orb0
의 첫 번째 자식으로 삽입합니다.
<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-esque 사양에 따라 오브를 추가할 것입니다. 이를 통해 새 레벨을 쉽게 지정하고 생성할 수 있습니다. 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) {}
다음으로, 선택기가 주어지면 VR 장면에서 항목을 단순히 제거하는 remove
기능을 채웁니다. 다행히 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
함수에서 모든 오브 및 해당 컨테이너의 요소에서 오브 ID를 올바르게 설정하십시오.
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
동작을 설정하고, 각 오브가 시각적으로 다르도록 임의의 시드를 설정하고, 마지막으로 ID를 기반으로 오브의 회전 위치를 설정합니다.
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); });
이것으로 자바스크립트 수정을 마칩니다. 다음으로 템플릿의 기본 설정을 'off' 구로 변경합니다. #container-orb0
의 위치와 크기를 다음과 같이 변경합니다.
position="8 0.5 0" scale="0.5 0.5 0.5"
그런 다음 #light-orb0
강도를 0으로 변경합니다.
intensity="0"
소스 코드가 2단계의 소스 코드와 일치하는지 확인합니다.
이제 VR 장면에 동적으로 채워진 5개의 구가 있어야 합니다. 또한 오브 중 하나에는 아래와 같은 종속성에 대한 시각적 표시기가 있어야 합니다.

이것으로 오브를 동적으로 추가하는 첫 번째 섹션을 마칩니다. 다음 섹션에서는 게임 역학을 추가하는 세 단계를 사용합니다. 특히, 플레이어는 플레이어 ID에 따라 특정 구만 토글할 수 있습니다.
3. 터미널 상태 추가
이 단계에서는 터미널 상태를 추가합니다. 모든 구체가 성공적으로 켜지면 플레이어는 "승리" 페이지를 보게 됩니다. 이렇게 하려면 모든 구의 상태를 추적해야 합니다. 구를 켜거나 끌 때마다 내부 상태를 업데이트해야 합니다. 도우미 함수 toggleOrb
가 상태를 업데이트한다고 가정해 보겠습니다. 오브의 상태가 변경될 때마다 toggleOrb
함수를 호출합니다. (1) onload 핸들러에 클릭 리스너를 추가하고 (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
를 사용하여 i번째 구의 on
상태를 전환합니다. toggleOrb
는 모든 오브가 켜져 있는 경우 터미널 상태를 추가로 트리거할 수 있습니다.

function toggleOrb(i) { orbs[i].on = !orbs[i].on; if (orbs.every(orb => orb.on)) console.log('Victory!'); }
코드가 3단계의 소스 코드와 일치하는지 다시 확인하세요.
이것으로 게임의 "싱글 플레이어" 모드가 종료됩니다. 이 시점에서 완전한 기능의 가상 현실 게임이 있습니다. 그러나 이제 멀티플레이어 구성 요소를 작성하고 게임 메커니즘을 통해 협업을 장려해야 합니다.
4. 플레이어 개체 만들기
이 단계에서는 플레이어 ID가 있는 플레이어에 대한 추상화를 만듭니다. 이 플레이어 ID는 나중에 서버에서 할당됩니다.
지금은 단순히 전역 변수일 것입니다. orbs
를 정의한 직후 플레이어 ID를 정의하십시오.
var orbs = ... var current_player_id = 1;
코드가 4단계의 소스 코드와 일치하는지 다시 확인하십시오. 다음 단계에서 이 플레이어 ID는 플레이어가 제어할 수 있는 오브를 결정하는 데 사용됩니다.
5. 조건부로 오브 토글
이 단계에서는 구 토글 동작을 수정합니다. 구체적으로, 플레이어 1은 홀수 번째 오브를 제어할 수 있고 플레이어 2는 짝수 번째 오브를 제어할 수 있습니다. 먼저 오브가 상태를 변경하는 두 위치 모두에서 이 로직을 구현합니다.
$('.orb').on('click', function() { var id = ... if (!allowedToToggle(id)) return false; ... } ... function clickOrb(i) { if (!allowedToToggle(id)) return; ... }
둘째, clickOrb
allowedToToggle
를 정의합니다. 현재 플레이어가 플레이어 1인 경우 홀수 번호의 id는 truth-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) 플레이어 ID를 추적하고 (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); });
위의 내용은 기본 익스프레스 애플리케이션을 위해 포트 3000에 socket.io를 설정합니다. 다음으로 두 개의 전역 변수를 정의합니다. 하나는 활성 플레이어 목록을 유지하기 위한 것이고 다른 하나는 할당되지 않은 가장 작은 플레이어 ID를 유지하기 위한 것입니다.
/** * Maintain player IDs */ var playerIds = []; var smallestPlayerId = 1;
다음으로, 새로운 플레이어 ID를 생성하고 이를 playerIds
배열에 추가하여 새 플레이어 ID를 "가져가는" 것으로 표시하는 getPlayerId
함수를 정의합니다. 특히, 이 함수는 단순히 minimumPlayerId를 표시한 다음 취하지 않은 다음으로 가장 smallestPlayerId
정수를 검색하여 smallestPlayerId
를 업데이트합니다.
function getPlayerId() { var playerId = smallestPlayerId; playerIds.push(playerId); while (playerIds.includes(smallestPlayerId)) { smallestPlayerId++; } return playerId; }
그에 따라 playerId
smallestPlayerId
해제하는 removePlayer
함수를 정의합니다.
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단계의 소스 코드와 일치하는지 다시 확인하십시오. 이것으로 기본 플레이어 등록 및 등록 취소가 완료됩니다. 이제 각 클라이언트는 서버 생성 플레이어 ID를 사용할 수 있습니다.
다음 단계에서는 서버에서 내보낸 플레이어 ID를 수신하고 사용하도록 클라이언트를 수정합니다.
7. 플레이어 ID 적용
다음 두 단계에서는 멀티플레이어 경험의 기본 버전을 완료합니다. 시작하려면 클라이언트 측에서 플레이어 ID 할당을 통합하십시오. 특히, 각 클라이언트는 서버에 플레이어 ID를 요청할 것입니다. 4단계 및 이전에서 작업했던 클라이언트 측 index.html
로 다시 이동합니다.
L7의 head
에서 socket.io
가져오기:
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.js"></script>
문서 로드 핸들러 후에 소켓을 인스턴스화하고 newPlayer
이벤트를 내보냅니다. 이에 대한 응답으로 서버 측에서는 playerId
이벤트를 사용하여 새 플레이어 ID를 생성합니다. 아래에서 lightful.glitch.me
대신 Glitch 프로젝트 미리보기 URL을 사용하세요. 아래 데모 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에도 켜져 있어야 합니다. 클라이언트 측에서는 orb 토글을 발표하고 수신 대기합니다. 발표하기 위해 토글된 구의 ID를 전달하기만 하면 됩니다.
두 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단계의 소스 코드와 일치하는지 다시 확인하세요.
이제 서버 측에서 토글된 구 ID를 수신하고 브로드캐스트해야 합니다. 서버 측 index.js
에서 다음 리스너를 추가합니다. 이 수신기는 소켓 disconnect
수신기 바로 아래에 배치되어야 합니다.
socket.on('toggleOrb', function(i) { socket.broadcast.emit('toggleOrb', i); });
코드가 8단계의 소스 코드와 일치하는지 다시 확인하십시오. 이제 한 창에 로드된 플레이어 1과 두 번째 창에 로드된 플레이어 2 모두 동일한 게임 상태를 볼 수 있습니다. 그것으로, 당신은 멀티플레이어 가상 현실 게임을 완성했습니다. 또한 두 플레이어는 목표를 완료하기 위해 협력해야 합니다. 최종 제품은 다음과 일치합니다.

결론
이것으로 멀티플레이어 가상 현실 게임을 만드는 방법에 대한 자습서를 마칩니다. 그 과정에서 A-Frame VR의 3D 모델링과 WebSocket을 사용한 실시간 멀티플레이어 경험을 포함하여 많은 주제를 다루셨습니다.
지금까지 살펴본 개념을 바탕으로 두 플레이어가 더 부드러운 경험을 할 수 있도록 하려면 어떻게 해야 합니까? 여기에는 게임 상태가 동기화되었는지 확인하고 그렇지 않은 경우 사용자에게 경고하는 것이 포함될 수 있습니다. 터미널 상태 및 플레이어 연결 상태에 대한 간단한 시각적 표시기를 만들 수도 있습니다.
우리가 구축한 프레임워크와 도입한 개념을 감안할 때 이제 이러한 질문에 답하고 훨씬 더 많은 것을 구축할 수 있는 도구를 갖게 되었습니다.
완성된 소스 코드는 여기에서 찾을 수 있습니다.