현재 Node.js가 본격적으로 개발되면서 우리는 이미 Node.js를 사용하여 다양한 작업을 수행할 수 있습니다. 얼마 전 UP 소유자가 Geek Song 이벤트에 참여했습니다. 이번 이벤트에서는 '낮은 사람들'이 더 많이 소통할 수 있는 게임을 만드는 것을 목표로 삼았습니다. 랜파티 컨셉. Geek Pine 대회는 가련할 정도로 짧은 36시간 동안 진행되며 모든 것이 빠르고 빨라야 합니다. 그런 전제 하에 초기 준비는 다소 '당연'해 보였다. 크로스 플랫폼 애플리케이션을 위한 솔루션으로 우리는 충분히 간단하고 우리의 요구 사항을 충족하는 node-webkit을 선택했습니다.
필요에 따라 모듈별로 별도로 개발을 진행할 수 있습니다. 이 글에서는 Node.js와 WebKit 플랫폼 자체의 몇 가지 한계점을 해결하고 솔루션을 제안하는 것뿐만 아니라 일련의 탐색과 시도를 포함하여 Spaceroom(실시간 멀티플레이어 게임 프레임워크)을 개발하는 과정을 자세히 설명합니다.
시작하기
우주공간을 한눈에
Spaceroom의 디자인은 처음부터 확실히 요구 중심으로 이루어졌습니다. 우리는 이 프레임워크가 다음과 같은 기본 기능을 제공할 수 있기를 바랍니다:
방(또는 채널) 단위로 사용자 그룹을 구분할 수 있습니다
컬렉션 그룹의 사용자로부터 지시를 받을 수 있습니다
각 클라이언트 간 시간 동기화를 통해 지정된 간격에 따라 게임 데이터를 정확하게 방송할 수 있습니다
네트워크 지연으로 인한 영향을 최소화할 수 있습니다
물론, 코딩 후반부에서는 게임 일시 중지, 다양한 클라이언트 간에 일관된 난수 생성 등을 포함하여 Spaceroom에 더 많은 기능을 제공했습니다. (물론 이러한 기능은 필요에 따라 게임 로직 프레임워크에서 구현할 수 있지만, 반드시 그런 것은 아닙니다. 통신 수준에서 더 잘 작동하는 프레임워크인 Spaceroom을 사용해야 합니다.
API
Spaceroom은 프런트엔드와 백엔드의 두 부분으로 나뉩니다. 서버 측에서 요구되는 작업에는 방 목록 관리, 방 생성 및 방 참여 기능 제공 등이 포함됩니다. 클라이언트 API는 다음과 같습니다.
spaceroom.connect(address, callback) – 서버에 연결
spaceroom.createRoom(콜백) – 방 만들기
spaceroom.joinRoom(roomId) – 룸 참여
spaceroom.on(event, callback) – 이벤트 수신
…
클라이언트가 서버에 연결되면 다양한 이벤트를 수신하게 됩니다. 예를 들어 방에 있는 사용자는 새 플레이어가 참가했거나 게임이 시작되었다는 이벤트를 받을 수 있습니다. 우리는 클라이언트에게 "라이프 사이클"을 제공했으며 언제든지 다음 상태 중 하나에 있게 됩니다.
spaceroom.state를 통해 클라이언트의 현재 상태를 확인할 수 있습니다.
서버측 프레임워크를 사용하는 방법은 비교적 간단합니다. 기본 구성 파일을 사용하면 서버측 프레임워크를 직접 실행할 수 있습니다. 기본 요구 사항이 있습니다. 별도의 서버가 필요 없이 서버 코드가 클라이언트에서 직접 실행될 수 있다는 것입니다. PS나 PSP를 플레이해본 분들은 제가 무슨 말을 하는지 정확히 아실 겁니다. 물론 전용서버에서도 구동이 가능하니 당연히 우수합니다.
여기서는 논리 코드 구현이 단순화되었습니다. 1세대 Spaceroom은 방의 상태를 포함한 방의 목록과 각 방에 해당하는 게임 시간 통신(명령 수집, 버킷 방송 등)을 유지하는 기능을 완료했습니다. 구체적인 구현 방법은 소스 코드를 참조하세요.
동기화 알고리즘
그렇다면 각 클라이언트 간에 표시되는 내용을 실시간으로 일관되게 만들 수 있는 방법은 무엇일까요?
이거 재미있을 것 같네요. 잘 생각해 보세요. 통과하는 데 서버가 필요한 것은 무엇입니까? 다양한 클라이언트 사이에 논리적 불일치를 일으킬 수 있는 요소, 즉 사용자 지침을 생각하는 것은 자연스러운 일입니다. 게임 로직을 처리하는 코드는 모두 동일하므로 동일한 조건에서 코드의 결과도 동일할 것입니다. 유일한 차이점은 게임 중에 받는 다양한 플레이어 지시입니다. 물론 이러한 명령을 동기화하는 방법이 필요합니다. 모든 클라이언트가 동일한 지침을 얻을 수 있다면 이론적으로 모든 클라이언트는 동일한 실행 결과를 가질 수 있습니다.
온라인 게임의 동기화 알고리즘은 종류가 다양하고 적용 가능한 시나리오도 다릅니다. Spaceroom에서 사용하는 동기화 알고리즘은 프레임 잠금 개념과 유사합니다. 타임라인을 간격으로 나누고 각 간격을 버킷이라고 합니다. 버킷은 명령어를 로드하는 데 사용되며 서버에서 유지 관리됩니다. 각 버킷 기간이 끝나면 서버는 버킷을 모든 클라이언트에 브로드캐스트합니다. 클라이언트가 버킷을 얻은 후 버킷에서 명령을 가져와 확인 후 실행합니다.
네트워크 지연의 영향을 줄이기 위해 서버가 클라이언트로부터 받은 각 명령은 특정 알고리즘에 따라 해당 버킷으로 전달됩니다.
order_start가 명령어가 전달하는 명령어 발생 시간이고 t가 order_start가 위치한 버킷의 시작 시간이라고 가정합니다
t Delay_time
t Delay_time
에 해당하는 버킷에 명령을 전달합니다.
그 중 Delay_time은 합의된 서버 지연 시간으로, 클라이언트 간 평균 지연 시간으로 간주할 수 있습니다. Spaceroom의 기본값은 80이고, 버킷 길이의 기본값은 48입니다. 각 버킷 기간이 끝나면 서버는 이 버킷을 모든 클라이언트에 브로드캐스트하여 다음 버킷에 대한 지침을 받기 시작합니다. 클라이언트는 수신된 버킷 간격을 기반으로 로직에서 자동으로 시간 조정을 수행하여 허용 가능한 범위 내에서 시간 오류를 제어합니다.
이는 일반적인 상황에서 클라이언트가 48ms마다 서버로부터 버킷을 수신한다는 의미이며, 버킷 처리 시간에 도달하면 그에 따라 클라이언트가 이를 처리합니다. 클라이언트 FPS=60이라고 가정하면 3프레임 정도마다 버킷이 수신되고 이 버킷을 기반으로 로직이 업데이트됩니다. 네트워크 변동으로 인해 시간이 초과된 후에도 버킷이 수신되지 않으면 클라이언트는 게임 로직을 일시 중단하고 기다립니다. 버킷 내에서 논리적 업데이트는 lerp 메서드를 사용할 수 있습니다.
delay_time = 80, bucket_size = 48의 경우 모든 명령은 최소 96ms 동안 지연됩니다. 예를 들어, Delay_time = 60, bucket_size = 32의 경우 이 두 매개변수를 변경하면 모든 명령이 최소 64ms 동안 지연됩니다.
타이머로 인한 살인
전체적으로 볼 때 우리 프레임워크는 실행 시 정확한 타이머를 가져야 합니다. 고정된 간격으로 버킷 브로드캐스트를 수행합니다. 물론 처음에는 setInterval()을 사용하려고 생각했지만 다음 순간 이 아이디어가 얼마나 신뢰할 수 없는지 깨달았습니다. 장난스러운 setInterval()에 매우 심각한 오류가 있는 것 같았습니다. 그리고 끔찍한 점은 모든 오류가 누적되어 점점 더 심각한 결과를 초래한다는 것입니다.
그래서 우리는 지정된 간격 주위에서 논리를 대략적으로 안정적으로 유지하기 위해 다음 도착 시간을 동적으로 수정하기 위해 setTimeout()을 사용하는 방법을 즉시 생각했습니다. 예를 들어 이번 setTimeout()은 예상보다 5ms 적습니다. 다음 번에는 5ms 더 일찍 만들겠습니다. 그러나 테스트 결과가 만족스럽지 않으며 이는 충분히 우아하지 않습니다.
그래서 우리는 다시 생각을 바꿔야 합니다. setTimeout()을 최대한 빨리 만료시킨 다음 현재 시간이 목표 시간에 도달했는지 확인하는 것이 가능합니까? 예를 들어, 루프에서 setTimeout(callback, 1)을 사용하여 시간을 지속적으로 확인하는 것은 좋은 생각인 것 같습니다.
실망스러운 타이머
우리는 아이디어를 테스트하기 위해 즉시 코드를 작성했지만 결과는 실망스러웠습니다. 최신 안정 버전의 node.js(v0.10.32) 및 Windows 플랫폼에서 다음 코드를 실행하세요:
잠깐, 이 숫자가 왜 그렇게 친숙해 보일까요? 15.625ms라는 숫자는 Windows의 최대 타이머 간격과 너무 유사합니까? 테스트를 위해 즉시 ClockRes를 다운로드했고, 콘솔에서 실행해 보니 다음과 같은 결과가 나왔습니다.
역시! node.js 매뉴얼을 보면 setTimeout에 대한 다음 설명을 볼 수 있습니다.
실제 지연은 OS 타이머 세분성 및 시스템 로드와 같은 외부 요인에 따라 달라집니다.
그러나 테스트 결과에 따르면 이 실제 지연은 최대 타이머 간격(현재 시스템의 타이머 간격은 1.001ms에 불과함)이며 이는 어쨌든 용납할 수 없는 것으로 나타났습니다. node.js를 자세히 살펴보세요.
Node.js의 버그
여러분과 저는 Node.js의 짝수 루프 메커니즘에 대해 어느 정도 이해하고 있다고 믿습니다. 타이머 구현의 소스 코드를 보면 타이머의 구현 원리를 대략적으로 이해할 수 있습니다. 이벤트 루프의 루프:
https://github.com/joyent/libuv/blob/v0.10/src/win/timer.c))
이 함수의 내부 구현에서는 Windows GetTickCount() 함수를 사용하여 현재 시간을 설정합니다. 간단히 말해서, setTimeout 함수를 호출한 후 일련의 어려움을 겪은 후 내부 타이머->due가 현재 루프 시간 제한으로 설정됩니다. 이벤트 루프에서는 먼저 uv_update_time을 통해 현재 루프 시간을 업데이트한 다음 uv_process_timers에서 타이머가 만료되었는지 확인합니다. 그렇다면 JavaScript의 세계로 들어갑니다. 전체 기사를 읽은 후 이벤트 루프에는 아마도 다음 프로세스가 있을 것입니다.
글로벌 시간 업데이트
타이머를 확인하고, 타이머가 만료되면 콜백을 실행하세요
요청 대기열을 확인하고 보류 중인 요청을 실행하세요
IO 이벤트를 수집하는 폴링 함수를 입력합니다. IO 이벤트가 도착하면 다음 이벤트 루프에서 실행할 수 있도록 해당 처리 함수를 reqs 큐에 추가합니다. 폴링 함수 내에서는 IO 이벤트를 수집하기 위해 시스템 메서드가 호출됩니다. 이 메서드는 IO 이벤트가 도착하거나 설정된 시간 초과에 도달할 때까지 프로세스를 차단합니다. 이 메서드가 호출되면 시간 제한은 가장 최근의 타이머 만료 시간으로 설정됩니다. 이는 IO 이벤트 수집이 차단된다는 의미이며, 최대 차단 시간은 다음 타이머의 종료 시간이 됩니다.
Windows의 설문 조사 기능 중 하나의 소스 코드:
위 단계에 따라 타이머를 제한 시간 = 1ms로 설정했다고 가정하면 폴링 기능은 최대 1ms 동안 차단된 다음 다시 시작됩니다(해당 기간 동안 IO 이벤트가 없는 경우). 이벤트 루프에 계속 들어가면 uv_update_time은 시간을 업데이트하고 uv_process_timers는 타이머가 만료되었음을 확인하고 콜백을 실행합니다. 그래서 예비 분석은 uv_update_time에 문제가 있거나(현재 시간이 정확하게 업데이트되지 않음), 아니면 poll 기능이 1ms를 기다린 후 복구되는데, 이 1ms의 대기에 뭔가 문제가 있다는 것입니다.
MSDN을 검색하다가 놀랍게도 GetTickCount 함수에 대한 설명을 발견했습니다.
GetTickCount 함수의 해상도는 시스템 타이머의 해상도로 제한되며 일반적으로 10밀리초~16밀리초 범위입니다.
GetTickCount의 정확도가 너무 낮습니다! 폴링 기능이 1ms 동안 올바르게 차단되었지만 다음에 uv_update_time이 실행될 때 현재 루프 시간이 올바르게 업데이트되지 않는다고 가정합니다! 그래서 우리의 타이머는 만료된 것으로 판단되지 않았기 때문에 poll은 1ms를 더 기다렸다가 다음 이벤트 루프에 들어갔습니다. GetTickCount가 마침내 올바르게 업데이트되고(소위 15.625ms마다 업데이트됨) 루프의 현재 시간이 업데이트되어 타이머가 uv_process_timers에서 만료된 것으로 판단될 때까지는 그렇지 않았습니다.
WebKit에 도움 요청
Node.js의 이 소스 코드는 매우 무력합니다. 아무런 처리도 하지 않고 정밀도가 낮은 시간 함수를 사용합니다. 하지만 우리는 Node-WebKit을 사용하기 때문에 Node.js의 setTimeout 외에도 Chromium의 setTimeout도 있다는 것을 즉시 생각했습니다. 테스트 코드를 작성하고 브라우저나 Node-WebKit으로 실행하세요: http://marks.lrednight.com/test.html#1 (# 뒤의 숫자는 측정해야 하는 간격을 나타냅니다) , 결과는 아래와 같습니다.
HTML5 사양에 따르면 이론적인 결과는 처음 5회에는 1ms, 이후 결과에는 4ms가 되어야 합니다. 테스트 케이스에 표시되는 결과는 세 번째부터 시작됩니다. 즉, 테이블의 데이터는 이론적으로 처음 세 번은 1ms여야 하고 이후 결과는 모두 4ms입니다. 결과에는 특정 오류가 있으며 규정에 따르면 우리가 얻을 수 있는 가장 작은 이론적 결과는 4ms입니다. 만족스럽지는 않지만 node.js의 결과보다는 분명히 훨씬 더 만족스럽습니다. 강력한 호기심 트렌드 Chromium의 소스 코드를 살펴보고 어떻게 구현되는지 살펴보겠습니다. (https://chromium.googlesource.com/chromium/src.git/ /38.0.2125.101/base/time/time_win.cc)
먼저 루프의 현재 시간을 확인하기 위해 Chromium은 timeGetTime() 함수를 사용합니다. MSDN을 참조하면 이 기능의 정확성이 시스템의 현재 타이머 간격에 영향을 받는다는 것을 알 수 있습니다. 테스트 머신에서는 이론적으로 위에서 언급한 1.001ms입니다. 그러나 기본적으로 Windows 시스템에서는 응용 프로그램이 전역 타이머 간격을 수정하지 않는 한 타이머 간격은 최대값(테스트 컴퓨터에서는 15.625ms)입니다.
IT업계 뉴스를 접한다면 이런 뉴스를 본 적이 있을 것입니다. 우리 Chromium에서는 타이머 간격을 매우 작게 설정한 것 같습니다! 더 이상 시스템 타이머 간격에 대해 걱정할 필요가 없는 것 같나요? 너무 빨리 기뻐하지 마십시오. 이 수정 사항은 우리에게 큰 타격을 줄 것입니다. 실제로 이 문제는 Chrome 38에서 해결되었습니다. 이전 Node-WebKit을 복구해야 합니까? 이는 분명히 우아하지 않으며 더 성능이 뛰어난 Chromium 버전을 사용하는 것을 방해합니다.
Chromium 소스 코드를 자세히 살펴보면 타이머가 있고 타이머의 시간 초과가
其中,kMinTimerIntervalLowResMs = 4,kMinTimerIntervalHighResMs = 1。timeBeginPeriod 以及timeEndPeriod 是 Windows에서는 타이머 간격을 사용하여 타이머 간격을 설정합니다.源时,我们能拿到的最작은 타이머 간격 是1ms,而使用电池时,是4ms。由于我们的循环不断地调用了 setTimeout,根据 W3C 规范,最以松气,这个个对我们的影响不大。
又一个精島问题
回到开头 回到开头, 我们发现测试结果显示, settimeout 的间隔并不是稳定在 4ms 的, http://marks.lrednight.com/test.html#48测试结果也显示, 间隔에서 48ms 및 49ms의 시간이 걸립니다.当前系统的计时器影响。游戏逻辑的实现需要用到 requestAnimationFrame 函数(不停更新画布),这个函数可以帮我们将计时器间隔至少设置为 kMinTimerIntervalLowResMs (因为他需要一个16ms의 시간이 16ms로 빨라졌습니다.使用电源는 系统의 타이머 간격이 1ms이고 所以测试结果는 ±1ms입니다. 화면은 #48의 최대 속도, 최대 값은 16=64ms입니다. Chromium의 setTimeout을 사용하면 setTimeout(fn, 1)이 4ms로 설정되고 setTimeout(fn, 48)이 1ms로 설정됩니다. ,저의 마음에는 새로운 것이 있습니다的蓝图,它让我们的代码看起来image是这样:
위의 이론에 따르면 최대 오류가 46ms의 지연으로 발생하더라도 위 코드를 사용하면 오류가 bucket_size(bucket_size – 편차)보다 작은 시간을 기다릴 수 있습니다. 실제 간격은 48ms 미만입니다. 나머지 시간에는 gameLoop가 충분히 정확한 간격으로 실행되도록 하기 위해 바쁜 대기 방법을 사용합니다.
Chromium을 사용하여 어느 정도 문제를 해결했지만 분명히 충분히 우아하지는 않습니다.
원래 요청을 기억하시나요? 우리의 서버 측 코드는 Node-Webkit 클라이언트와 독립적으로 실행될 수 있어야 하며 Node.js 환경이 있는 컴퓨터에서 직접 실행될 수 있어야 합니다. 위 코드를 직접 실행하면 편차 값이 최소 16ms가 되는데, 이는 48ms마다 16ms를 기다려야 한다는 뜻입니다. CPU 사용량이 계속해서 증가하고 있습니다.
예상치 못한 놀라움
Node.js에 이렇게 큰 버그가 있는데 아무도 눈치채지 못해서 정말 짜증나요. 그 대답은 우리를 정말 놀라게 했습니다. 이 버그는 v.0.11.3에서 수정되었습니다. libuv 코드의 마스터 브랜치를 직접 보면 수정된 결과를 확인할 수도 있습니다. 구체적인 방법은 폴링 기능이 완료될 때까지 기다린 후 루프의 현재 시간에 시간 제한을 추가하는 것입니다. 이런 식으로 GetTickCount가 응답하지 않더라도 폴링을 기다린 후에도 이 대기 시간을 추가합니다. 그러면 타이머가 원활하게 만료될 수 있습니다.
즉, 작업하는데 오랜 시간이 걸렸던 문제가 v.0.11.3에서 해결되었습니다. 그러나 우리의 노력은 헛되지 않습니다. 왜냐하면 GetTickCount 함수의 영향이 없어지더라도 poll 함수 자체도 시스템 타이머의 영향을 받기 때문입니다. 한 가지 해결책은 Node.js 플러그인을 작성하여 시스템 타이머의 간격을 변경하는 것입니다.
그런데 이번 게임의 경우 초기 설정은 서버가 없다는 것입니다. 클라이언트가 룸을 생성하면 서버가 됩니다. 서버 코드는 Node-WebKit 환경에서 실행될 수 있으므로 Windows 시스템의 타이머 문제는 최우선 순위가 아닙니다. 위에서 제시한 솔루션에 따르면 결과는 우리를 만족시키기에 충분합니다.
종료
타이머 문제를 해결한 후에는 기본적으로 프레임워크 구현에 장애물이 없습니다. 우리는 WebSocket 지원(순수한 HTML5 환경에서)을 제공하고 통신 프로토콜을 사용자 정의하여 더 높은 성능의 소켓 지원을 구현합니다(Node-WebKit 환경에서). 물론 스페이스룸의 기능은 처음에는 상대적으로 초보적이었지만, 요구 사항이 늘어나고 시간이 늘어남에 따라 점차적으로 프레임워크를 개선해 나가고 있었습니다.
예를 들어, 게임에서 일관된 난수를 생성해야 한다는 사실을 발견했을 때 Spaceroom에 이러한 기능을 추가했습니다. Spaceroom은 게임이 시작될 때 난수 시드를 배포합니다. 클라이언트의 Spaceroom은 난수 시드의 도움으로 md5의 무작위성을 사용하여 난수를 생성하는 방법을 제공합니다.
꽤 만족스러워 보이네요. 이런 프레임워크를 작성하는 과정에서 나 역시 많은 것을 배웠다. Spaceroom에 관심이 있으신 분들도 참여하실 수 있습니다. 스페이스룸은 더 많은 곳에서 그 위력을 발휘할 것이라고 믿습니다.