Search
Duplicate
🕣

멀티플레이 이동게임 만들기

간단소개
React 와 Nest 를 이용한 멀티플레이 이동게임
팔만코딩경 컨트리뷰터
ContributorNotionAccount
주제 / 분류
React
Nest
Scrap
태그
저빈도 업데이트
멀티플레이 게임 동기화
React
Nest
9 more properties

서론

진행하고 있는 프로젝트에서, 간단히 유저들 끼리 서로 캐릭터들의 위치(움직임) 이나 상태 등을 실시간으로 확인해야 하는 기능이 있어, 난생 처음으로 게임 서버 비슷한 무언가를 구현 해보았다.
우선 처음에는 굉장히 안일하게 생각했었다.
플레이어가 느낄 때 부드러워 보이려면 1초에 60번의 화면을 새로 그려내야 한다. (60프레임)
따라서 각각의 클라이언트가 1초에 60번씩 자신의 위치를 서버로 전송하고, 서버에서 1초에 60번씩 모든 클라이언트들 에게 모든 클라이언트의 위치를 담은 정보를 전송하면 되지 않을까? 라는 생각을 하였다.
하지만 아주 소수만 사용하는 서비스면 몰라도, 1초에 60번이나 n 명의 클라이언트들 에게 n 명의 데이터가 담긴 전체 데이터를 전송하려고 하면 엄청난 서버 부하를 일으킬 것이다.
해서 보통 1초에 10회 내외로 클라이언트 ︎ 서버 간의 통신이 이루어 진다고 한다. (저빈도 업데이트)
그렇다면 각각의 클라이언트들은 1초에 60회를 그려내야 캐릭터의 움직임이 자연스럽다고 느끼는데, 1초 동안 60개의 위치 데이터가 필요할 텐데, 겨우 10개의 데이터를 가지고 어떻게 스무스하게 캐릭터의 움직임을 표현할 수 있을까? 라는 의문이 생긴다. 그 의문을 추측 항법을 이용하여 해결해 보았다.

실행 화면

위 시뮬레이션을 보면 좌 / 우 화면이 약간의 딜레이를 두고 서로의 움직임을 그려낸다. 이것이 추측항법의 열쇠이다.
→ 각각의 클라이언트는 자신의 상태(현재 위치한 좌표) 를 1초에 10번 서버로 보낸다.
→ 서버는 모든 클라이언트의 상태(현재 위치한 좌표) 를 1초에 10번 broadcast 한다.
→ 클라이언트는 1초에 60회(모니터 프레임) 화면을 다시 그려낸다.
클라이언트 동작 순서
1.
접속하면 서버와 100회의 핑을 주고받아 서버의 시간과 클라이언트 머신의 시간(javascript 에서는 Date.now()) 을 동기화 한다.
2.
이후 서버로부터 1초에 10회 다른 모든 클라이언트들의 움직임을 담은 정보를 수신하고, 또한 1초에 10회 자기 캐릭터의 위치를 담은 정보를 서버로 송신한다.
3.
매 초마다 60회씩 자신을 포함한 모든 클라이언트들의 움직임을 그려낸다. 이 때, 추측항법을 사용하여 비어있는 시간은 가장 가까운 타임스템프가 찍힌 두 점을 이은 곳으로 진행한다.
1번을 조금 더 설명하자면, A클라이언트가 자신의 캐릭터의 위치를 서버로 보낼때 (어떤 데이터든) 그 데이터에 timeStamp 가 적혀있어야 한다. 그래야 언제 어느 위치에 있었는지 알 수있으니까. 하지만 A클라이언트에서 무턱대고 서버로 보내는 timeStamp를 Date.now() 로 만들어 내게 되면 문제가 생긴다. 왜냐면 프로그램이 실행되는 환경마다 Date.now() 의 값은 조금씩 다를 수 있기 때문이다.
해서 모든 클라이언트와, 서버간의 timeStamp 를 동기화 하는 작업이 필요한데, 당연히 모든 클라이언트들이 서버시간을 기준으로 자신의 타임스템프를 동기화하는 것이 맞다.
위와같은 방법으로, Server 로 요청을 보내는데 이 요청에 현재 시간을 찍어 보낸다. 그리고 Server 는 이 요청을 받자마자 응답을 보내는데, 이 응답에 Server 의 시간을 적어보낸다.
그리고 client 가 마지막으로 자신이 보낸 요청에 대한 응답을 받았을 때, Latency(지연시간) = TEc - TSc 가 되고, 서버가 Ts 라는 타임스템프를 찍었을 때의 클라이언트 시간(Tc = TSc + Latency / 2) 가 된다.
즉, 클라이언트에서의 Date.now() 가 Tc 일때 서버에서의 Date.now() 는 Ts 라는 소리이다.
따라서 Tdiff = Tc - Ts 로 클라이언트와 서버간의 시간 차이(Tdiff) 를 알아낼 있고, 이 이후부터 클라이언트는 데이터를 서버로 전송할 때 타임스템프로 Date.now() - Tdiff 를 사용하면 된다.
3번을 조금 더 설명하자면, A클라이언트 입장에서 Tx시간에 B클라이언트 객체를 그려야 한다면, A는 이미 서버로부터 받은 B클라이언트의 정보를 가지고 있을 것이다. 하지만 1초에 10번이기 때문에 A가 프레임을 그려내야 할때의 딱 알맞는 시간은 존재하지 않을 가능성이 매우매우 높다. 해서 아래와 같이 대략적으로 추측을 한다 (T1, T2 의 데이터는 서버로 부터 받은 데이터)
하지만 여기서 이런 의문이 생긴다. B클라이언트도 1초에 10회 서버로 전송하고, 서버가 또 1초에 10회 모든 클라이언트에게 전송하기 때문에 A클라이언트 입장에서 Tx (현재)의 시간에, B 클라이언트의 T2시간의 위치 를 가지고 있는 것은 불가능하지 않나?
맞는 생각이다. 사실 A클라이언트 입장에서는 서버가 저빈도업데이트를 다루기도 하고, B클라이언트가 보낸 패킷이 서버를 통해 내려오는 레이턴시도 생각해야 하기 때문에 웬만해서는 현재의 위치 비슷한 것도 가지고 있기 힘들다.
이때 사용 하는 것이 객체 삽입이다. 왜 객체삽입 이라는 말로 표현하는 진 모르겠는데, 요지는 A클라이언트는 일정시간 과거의 B클라이언트를 렌더링 하는 것을 약속하는 것이다.
즉 A클라이언트의 실제 하드웨어 Date.now() 는 Tx 지만, B클라이언트로 부터 받은 B객체의 정보를 탐색할 때는 Tx -= delay 로 delay 만큼의 과거를 렌더링 하는 것이다.
해서 위의 동영상에서 A클라이언트에서 움직이면, B클라이언트에서 스무스하게 움직이긴하지만, 어느정도 딜레이를 가지고 따라 움직이는 것이다.
소스는 각각 여기에 있다. (크게 상관은 없지만 백엔드 서버부터 켜도록 하자).
nest 는
npm install → npm run start:dev
react 는
npm install → npm run start
각각 실행후 브라우저로 localhost:3000 (react default port) 로 접속하면 된다.
서비스로 실행되고 있는 것을 경험해보려면 under5.site 로 방문하면 된다.