실시간 멀티플레이어 가상 현실 게임을 만드는 방법(2부)

게시 됨: 2022-03-10
빠른 요약 ↬ 이 튜토리얼에서는 게임의 실시간 멀티플레이어 요소와 밀접하게 결합된 가상 현실 게임의 게임 메커니즘을 작성합니다.

이 튜토리얼 시리즈에서는 플레이어가 퍼즐을 풀기 위해 협력해야 하는 웹 기반 멀티플레이어 가상 현실 게임을 구축할 것입니다. 이 시리즈의 첫 번째 부분에서 우리는 게임에 등장하는 오브를 디자인했습니다. 시리즈의 이 부분에서는 게임 메커니즘을 추가하고 플레이어 쌍 간의 통신 프로토콜을 설정합니다.

여기에 있는 게임 설명은 시리즈의 첫 번째 부분에서 발췌한 것입니다. 각 플레이어 쌍에는 오브 링이 제공됩니다. 목표는 모든 구를 "켜는" 것인데, 구가 높고 밝으면 "켜져 있는" 것입니다. 구체가 더 낮고 희미하면 "꺼져" 있습니다. 그러나 특정 "지배적인" 구는 이웃에 영향을 미칩니다. 상태를 전환하면 이웃도 상태를 전환합니다. 2번 플레이어는 짝수 번째 구슬을 제어할 수 있고 1번 플레이어는 홀수번 구슬을 제어할 수 있습니다. 이를 통해 두 플레이어는 퍼즐을 풀기 위해 협력해야 합니다.

이 자습서의 8단계는 3개의 섹션으로 그룹화되어 있습니다.

  1. 사용자 인터페이스 채우기(1단계 및 2단계)
  2. 게임 역학 추가(3~5단계)
  3. 통신 설정(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단계의 소스 코드와 일치하는지 확인합니다. 이제 오브는 다음과 일치해야 합니다.

오브의 ID 및 트리거할 오브의 ID에 대한 시각적 표시기가 있는 오브
오브의 ID 및 트리거할 오브의 ID에 대한 시각적 표시기가 있는 오브(큰 미리보기)

이것으로 우리가 필요로 하는 추가 시각적 지표를 마칩니다. 다음으로 이 템플릿 구를 사용하여 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-iodependencies 에 추가합니다. 이제 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을 사용한 실시간 멀티플레이어 경험을 포함하여 많은 주제를 다루셨습니다.

지금까지 살펴본 개념을 바탕으로 두 플레이어가 더 부드러운 경험을 할 수 있도록 하려면 어떻게 해야 합니까? 여기에는 게임 상태가 동기화되었는지 확인하고 그렇지 않은 경우 사용자에게 경고하는 것이 포함될 수 있습니다. 터미널 상태 및 플레이어 연결 상태에 대한 간단한 시각적 표시기를 만들 수도 있습니다.

우리가 구축한 프레임워크와 도입한 개념을 감안할 때 이제 이러한 질문에 답하고 훨씬 더 많은 것을 구축할 수 있는 도구를 갖게 되었습니다.

완성된 소스 코드는 여기에서 찾을 수 있습니다.

점프 후 더! 아래에서 계속 읽기 ↓