Phaser Quest의 클라이언트 동기화

이전 기사에서는 멀티 플레이어 온라인 게임 제작의 기본 사항을 설명했습니다. 거기에서 클라이언트 동기화, 특히 Phaser Quest와 같은보다 복잡한 게임의 경우 많은 개선이 가능하고 바람직합니다.
실시간 멀티 플레이어 온라인 게임에서 중요한 점은 모든 클라이언트를 동기화 된 상태로 유지하는 것입니다. 다시 말해, 모든 사람들이 불일치없이 게임 상태에 대한 비슷한 견해를 공유하는지 확인하십시오. 게임의 상태는 모든 플레이어 및 비 플레이어 요소의 위치, 상태, 수행중인 작업 및 게임 자체에 영향을 줄 수있는 다른 변수에 해당합니다.

동기화는 정보를 클라이언트에 적절하게 보내서 게임 상태의 표현을 업데이트 할 수 있도록하여 이루어집니다. 그렇게 하기 위해 전송되는 데이터의 양과 데이터가 전송되는 속도는 중요한 결과를 가져옵니다. 너무 많으면 사용 가능한 대역폭을 초과하여 클라이언트 측에서 지연이 발생할 수 있습니다 (서버 호스트 방법에 따라 사용자 측에서 청구 문제가 발생할 수 있음). 너무 적 으면 동기화가 중단 될 수 있고 다른 클라이언트의 게임 상태가 다른 것을 시작하는 위험이 있습니다. 이 기사에서는 Phaser Quest의 경우 클라이언트 업데이트 및 동기화를 관리하기 위해 채택한 방식을 설명합니다.

단순한 접근법

클라이언트 동기화에 대한 단순한 접근법은 서버가 반복적으로 모든 클라이언트에 전체 게임 상태를 초당 여러 번 (예 : 초당 60 회) 보내도록하는 것입니다. 예를 들어, 서버는 매 16ms마다 게임의 모든 플레이어, 몬스터 및 아이템의 위치가 포함 된 JSON 객체를 보낼 수 있습니다. 클라이언트는 이러한 모든 객체를 반복하고 필요한 경우 위치를 업데이트함으로써이를 처리합니다. 이것은 매우 무거워 질 수 있습니다. 플레이어에 대한 데이터 만 보내고, 한 플레이어에 해당하는 데이터를 10 바이트로 인코딩 할 수 있다고 가정 해 봅시다 (보수적 인 복잡한 게임인데 게임 상태와 관련된 많은 속성을 가진 플레이어는 그 이상을 필요로합니다) ). 다음은 플레이어 수가 증가함에 따라 교환되는 데이터의 양이 어떻게 변하는가입니다.

보시다시피, 이것은 매우 작은 페이로드와 매우 제한된 게임 상태에서도 빠르게 손에 닿지 않습니다. 이것은 내가 "델타 패킷"이라고 부르는 것을 사용함으로써 많이 향상 될 수 있습니다.

델타 패킷을 사용하는 클라이언트 동기화

델타 패킷의 개념은 전체 게임 상태를 전송하는 대신에 게임 상태에 적용된 변경 사항 만 전송하는 것입니다 (따라서 차이점은 "델타"이름 임). 이 패킷은 게임 개체에서 변경된 모든 속성 번들로 구성됩니다. 새로운 객체가 게임 상태에 추가되면 변경은 객체 자체의 생성이며,이 경우 모든 관련 속성을 전송해야합니다.
참고 : 대안으로 모든 플레이어에게 변경 사항을 곧 전송 할 수 있습니다. 이는 소량의 업데이트 정보를 포함하는 많은 패킷을 보내는 오버 헤드로 인해 최적이 아닙니다. 규칙적인 간격으로 전송 된 업데이트 패킷에 변경 사항을 함께 묶으면 이 오버 헤드가 완화됩니다.

또한 페이저 퀘스트가 상대적으로 느린 속도 (예 : FPS와 비교)가 주어지면이 패킷은 60 회가 아니라 초당 5 회 전송됩니다.이 업데이트 속도는 게임의 변경 사항을 나타낼만큼 빠름이 밝혀졌습니다 지연이나 끊김의 느낌없이 (예 : 플레이어가 아이템을 픽업하거나 몬스터를 죽이는 등).
참고 : 이러한 비율은 플레이어의 위치를 업데이트하는 데 적합하지 않으므로 다른 접근 방식이 사용됩니다. 관련 섹션을 참조하십시오.

이 접근법에는 두 가지 주요 이점이 있습니다. 첫째, 두 업데이트 사이에서 속성이 변경된 엔티티의 수는 일반적으로 게임의 엔티티 총 수보다 훨씬 적습니다. 또한 영향을 받는 속성의 수는 일반적으로 엔티티에있는 속성의 총 수보다 작습니다. 이로부터 클라이언트에게 게임 상태의 변화를 알리기 위해 보낼 데이터의 양은 전체 게임 상태를 보내는 데 필요한 데이터 양보다 훨씬 적습니다.

둘째, 아무 일도 일어나지 않으면, 아무런 업데이트도 보내지지 않습니다. 이점에 관해서는 위에서 언급 한 극단적 인 경우입니다. 이러한 이점은 표준 구현 (아무 일도 일어나지 않는 게임)에서 발생할 수는 없지만, 관리 시스템과 결합하면 전송을 업데이트하지 않고 여러 업데이트주기를 거칠 수 있으며 상당한 자원을 절약 할 수 있습니다.

이 섹션의 나머지 부분에서는 Phaser Quest에서 전송할 차이점을 추적하는 방법을 보여줍니다. 유사한 접근법이 상태에 추가 된 새 객체를 처리하는 데 사용됩니다. 두 가지 이유로 너무 많은 코드를 제공하지 않습니다.
- 실제 코드 중 일부는 실제로 내가 정의한 관심 관리 시스템을 구현했기 때문에 여기에서 설명하는 것보다 조금 더 복잡하고 복잡합니다.
- 관련된 모든 코드를 다루는 것은 꽤 길고 지루하고 기사의 초점에 해를 끼칠 것입니다. 나의 시도는 무엇이 행해졌는지에 대한 주요 아이디어를 전하는 것이다. 자세한 내용은 소스 코드를 확인하고 궁금한 점을 묻기 위해 저에게 연락하십시오. 동일한 측면에 대해 여러 질문을받는 경우 여기에서 다루겠습니다.

게임 상태의 변화를 추적

서버 측에서는 업데이트되기 쉬운 모든 게임 엔티티가 GameObject 객체를 상속받습니다.
js/server/GameObject.js

Class hierarchy for the game objects on the server
서버 코드의 나머지 부분에서 메소드가 게임 객체의 속성을 수정하고이 변경이 클라이언트와 관련이 있을 때마다 GameObject.setProperty () 메소드를 사용하여 변경됩니다.
// Updates a property of the object and update the update packet
GameObject.prototype.setProperty = function(property,value){
    this[property] = value;
    // category is a string property indicating if the game object is actually a 'monster', 'player' or 'item'
    if(this.id !== undefined) GameServer.updatePacket.updateProperty(this.category, this.id, property, value); // Updates the current updatePacket
};
GameServer.updatePacket은 각 업데이트에서 클라이언트가 받을 개체이며 적용 할 업데이트에 대한 정보를 포함합니다. 무엇보다도, 그것은 게임 객체의 id를 속성이 변경된 더 작은 객체 목록으로 매핑하는 연관 배열을 포함합니다. 다음은 플레이어에게 영향을 미치는 변경 사항에 대한 예입니다. 이 예에서 플레이어 2의 생명은 100으로 변경되었지만 플레이어 9의 생명은 150으로, 그의 갑옷은 3으로 변경되었습니다 (실제 갑옷 개체의 ID).
{
  players:{ // list changes made to player objects; maps the id's of the modified players to objects listing the changes
    2:{
      life: 100
    },
    9:{
      life: 150,
      armor: 3
    }
  }
}

js/server/UpdatePacket.js
updatePacket 프로토 타입에는 updatePacket 객체를 채우는 다른 여러 메소드가 포함되어 있습니다. 예를 들어, 플레이어가 게임에 연결하면 관련 메소드는 게임 상태에 추가 된 플레이어를 나열하는 배열에 추가합니다. 항목이 생성되면 비슷한 배열에 추가되지만 항목에는 추가됩니다. 기존 플레이어가 자신의 속성 중 하나를 변경하면 (예 : 새로운 갑옷이 장착 된 경우)지도 플레이어는 위의 예와 비슷한 방식으로 변경 사항을 반영하는 항목을 포함하게됩니다. 이러한 업데이트는 updatePacket.updateProperty () 메서드를 사용하여 기록됩니다.
UpdatePacket.prototype.updateProperty = function(type,id,property,value){
    var map; // Determine if the map to update is this.items, this.players or this.monsters, based on the "type" argument
    switch(type){
        case 'item':
            map = this.items;
            break;
        case 'player':
            map = this.players;
            break;
        case 'monster':
            map = this.monsters;
            break;
    }
    if(!map.hasOwnProperty(id)) map[id] = {}; // If this map doesn't have an entry corresponding to the id of the entity whose property has changed, add it
    if(map[id][property] != value) map[id][property] = value;
};
수정 된 오브젝트의 종류 따라 다른 맵이 선택되고, 키 - 값 쌍 변경된 속성을 표시하고 취한 값이 맵에 추가된다.

200ms마다 GameServer.updatePlayers () 메서드는 모든 클라이언트에 현재 updatePacket 개체를 보내고 다음 200ms 동안 게임 상태의 변경 내용을 저장할 준비가 된 새 개체를 만들기 위해 이를 삭제합니다. 클라이언트 측에서는 업데이트 패킷의 각 속성이 관련 메서드에 의해 처리됩니다. 예를 들어, updatePacket.players에 저장된 변경 사항은 Game.updatePlayerStatus () 및 Game.updatePlayerAction ()에 의해 처리됩니다.

지금까지 내가 설명한 것은 내가 "글로벌" 업데이트 패킷이라고 부르는 것에 해당합니다. 모든 클라이언트에서 동일하고 모든 사람에게 표시되는 업데이트가 포함 된 패킷 (예 : 플레이어가 갑옷을 변경 한 경우 모든 사람이 볼 수 있음). 그 외에도 비슷한 프로세스가 "로컬" 업데이트 패킷을 유지 관리하는 데 사용됩니다. 특정 플레이어 만 변경되고 해당 플레이어 만 볼 수 있습니다. 각 클라이언트의 로컬 업데이트 패킷은 글로벌 업데이트 패킷에 번들로 제공되므로 클라이언트는 오버 헤드없이 동시에 둘을받습니다.

이동 업데이트 보내기

위의 모든 것들은 모양의 변화, 몬스터의 죽어가는 것, 떨어 뜨리거나 쑤시는 것 등과 같은 이산적인 변화에 잘 맞습니다. 플레이어가 지도를 가로 질러 움직이는 것과 같이 지속적인 변화를 위해 잘 작동하려면 동일한 논리를 사용할 수 있지만 훨씬 더 빠른 속도로 업데이트 할 수 있습니다 (초당 30 회 이상). 그러나 이 게임의 맥락에서 길 찾기 알고리즘의 결정 론적 성격을 활용하여 그보다 똑똑 할 수 있습니다.

결정론적 길 찾기

Phaser Quest에서 플레이어 이동 궤적은 클라이언트 측 easystar.js 라이브러리 (플레이어가 따라갈 경로를 계산)와 서버 측 경로 찾기 npm 패키지를 사용하여 경로 찾기를 사용하여 계산됩니다. 괴물이 따를 것이다). 플레이어 A가 타일을 클릭하면 알고리즘은 해당 타일에 대한 최단 경로를 계산하고 계산 된 경로를 서버에 보냅니다. 서버는 경로가 합법적인지 (즉, 부정 행위 시도가 없다면) 체크하고, 그렇다면, 플레이어 A가 이동하고 있다는 것을 다른 모든 클라이언트들에게 통지한다.

경로 계산은 결정적입니다. 알고리즘은 항상 시작 및 종료 좌표 쌍이 주어진 경우 동일한 경로를 반환합니다. 또한 페이저 퀘스트의 장애물은 움직이지 않기 때문에 이동하는 동안 경로가 변경 될 위험이 없으므로 업데이트 할 필요가 없습니다.

부가 적으로 이것은 플레이어가 움직이기 위해 클릭 할 때 클라이언트가 서버의 유효성 검사를 기다리지 않고 즉시 이동을 시작할 수 있다는 이점이 있습니다. 진정한 부정 행위가 아닌 클릭이 이루어진 경우, 귀하는 정확합니다. 이를 클라이언트 측 예측이라고하며 플레이어에게보다 반응이 좋은 경험을 제공합니다. 제공된 경로가 잘못된 경우 (예 : 플레이어가 콘솔을 통해 가짜 전송을 시도했기 때문에 서버에서 플레이어의 위치 재설정 명령을 반환 함)

경로 찾기의 결정적 특성으로 인해 서버가 다른 클라이언트에 전체 경로를 브로드 캐스팅 할 필요가 없다는 이점이 있습니다. 플레이어 A가 좌표 (x, y)로 이동 중임을 알릴 수 있습니다. 그러면 각 클라이언트는이 정보를 받으면 A의 현재 위치와 좌표 (x, y) 사이의 경로를 계산합니다. 이 계산은 동일한 라이브러리를 사용하고 결정적이기 때문에 모든 클라이언트가 동일한 경로를 계산하도록 신뢰할 수 있습니다. 여기에는 두 가지 작은 장점이 있습니다.
- 경로의 끝점 만 전체 경로 대신 클라이언트에 전송해야하기 때문에 서버에서 데이터를 더 적게 보내야합니다.
- 경로 찾기 계산이 클라이언트에 대해 오프셋되어 서버에 더 많은 자원을 남깁니다.

더 중요한 것은이 접근법을 사용하면 클라이언트가 이미 플레이어 위치가 시간 경과에 따라 어떻게 진행되는지 알고 있기 때문에 서버는 초당 위치 업데이트를 30 번 보낼 필요가 없다는 것입니다. 이동 측면에서 서버와 클라이언트 간의 유일한 통신은 다음과 같습니다.
- 플레이어 A는 자신의 경로를 서버에 보내고 이동 사실을 알리고 이동이 합법적인지 확인합니다.
- 서버는 A의 경로의 엔드 포인트를 다른 모든 클라이언트로 전송합니다.

js/server/Route.js
대역폭 및 서버 CPU를 최적으로 사용하기 위해 초기 메시지 하나와 단일 브로드 캐스트가 있습니다. 경로에 대한 정보는 앞에서 소개 한 updatePacket에 Route 객체 형식으로 추가됩니다. 이 객체에는 움직이는 플레이어의 ID, 대상, 경로 끝에있는 플레이어의 방향 (끝에서 작업이 수행 될 때) 등의 속성이 포함됩니다.

서버에서 좌표 업데이트하기

js/server/MovingEntity.js
이 접근법은 초당 위치 업데이트를 30 번 보내야 할 필요성을 없애 주지만 서버는 여전히 자신의 이동을 통해 플레이어의 위치를 추적해야합니다. Player 및 Monster 객체는 MovingEntity 객체에서 상속됩니다.

Class hierarchy for the game objects on the server
이 객체는 이동하는 엔티티의 서버 좌표를 업데이트하기 위해 매 80ms (초당 12 회)로 호출되는 MovingEntity.updateWalk () 메소드를 가지고 있습니다. 이는 엔티티의 속도와 이동이 시작된 이후 경과 한 시간에 따라 다음과 같이 수행됩니다.
MovingEntity.prototype.updateWalk = function(){
    // Based on the speed of the entity and the time elapsed since it started moving along a path,
    // compute on which tile of the path it should be at this time. If path ended, check what should happen.
    // this.speed is the amount of time need to travel a distance of 1 tile;
    // delta = the number of tiles traveled since departure
    var delta = Math.ceil(Math.abs(Date.now() - this.route.departureTime)/this.speed); 
    var maxDelta = this.route.path.length-1;
    if(delta > maxDelta) delta = maxDelta;
    this.setAtDelta(delta); // Put at the right tile based on the computed delta
    if(delta == maxDelta){
        // ... do what has to be done when a player finishes his path
    }
};

MovingEntity.prototype.setAtDelta = function(delta){
    // Update the position of an entity by putting at the delta'th tile along its path (see updateWalk())
    if(!this.route.path) return;
    this.x = this.route.path[delta].x; // no -1 because it's done in updateWalk() already
    this.y = this.route.path[delta].y;
};

대기 시간 보정

지금까지 설명한 접근법의 주된 문제점은 클라이언트가 패킷에 도달하는 데 걸리는 시간에 따라 이동 통지를 동시에 수신하지 않는다는 것입니다. 플레이어 A가 t0에서 타일을 클릭하고 대기 시간이 20ms 인 경우 메시지는 서버에 도달하는 데 20ms가 걸립니다. 거기에서 플레이어 B에게 도달하려면 30ms가 더 걸릴 수 있지만 플레이어 C는 연결이 잘못되어 플레이어 C에 도달하는 데 150ms가 걸릴 수 있습니다. 결과적으로 플레이어 A의 캐릭터는 플레이어 A의 화면에서 시간 t1에 자신의 목적지에 도달하지만 B의 화면에서는 t1 + 50의 시간에, C의 화면에서는 t1 + 170의 시간에 도달합니다. 이러한 상황은 클라이언트 동기화를 깨뜨리기 때문에 적절하지 않습니다.Latency breaks client synchronization
이를 해결하려면 각 플레이어의 대기 시간을 추적 한 다음 적절하게 이동 간격을 조정해야합니다. 움직이는 플레이어의 대기 시간은 이동과 관련된 데이터가 포함 된 Route 개체에 추가됩니다. 업데이트를받는 플레이어의 대기 시간은 항상 각 업데이트 패킷의 일부입니다. 플레이어 A가 움직인다는 것을 알리기 위해 플레이어 B가 수신 할 작은 버전의 오브젝트가 있습니다 :
{
  players:{
    1:{ // The id of player A is 1
      route:{
        end: {x:10,y:11},
        delta: 20 // delta is the latency of the moving player
      }
    }
  },
  ...
  latency: 30 // latency of the player who receives the update
}

이 정보를 받으면 플레이어 B의 클라이언트는 플레이어 A의 위치와 종점 사이의 경로를 계산합니다 (10,11). 플레이어 A가 (0,11)에서 시작한다고 가정하면 경로 길이는 10 타일이됩니다. 플레이어는 1 타일 거리를 이동하는 데 120ms가 걸리므로 플레이어 A를 (0,11)에서 (10,10)으로 이동시키는 트윈의 지속 시간은 1200ms가됩니다.

그러나 우리가 알았 듯이, 서버는 시작한 후 20ms 후에 A의 움직임을 알게되었고, B는 보낸 후 30ms 후에 서버의 알림을 받았습니다 (이 정보는받은 업데이트 패킷의 일부로 B에 알려짐). B가 A가 움직이기 시작한 후 20 + 30 = 50ms가 통보되었다. 자연스러운 문제는 B 측에서는 트윈의 지속 시간이 50ms 단축되어 총 1200-20 (A 대기 시간) - 30 (B 대기 시간) = 1150ms입니다.

C 선수는 이전에 대기 시간이 150ms라고했습니다. 그의 경우, 1200 - 20 (A 레이턴시) - 150 (C 레이턴시) = 1030ms가 될 것입니다.

결론

이 기사에서는 Phaser Quest에서 클라이언트 동기화의 주요 아이디어를 설명했습니다. 바라기를, 이것은 당신의 자신의 게임의 디자인 과정을 위한 생각을 위한 음식일지도 모르다. 이 솔루션은 이 게임에서 작동했지만 다른 게임에서는 작동하지 않을 수도 있음을 명심하십시오. 네트워킹 솔루션은 항상 각 게임의 특정 측면에 맞춰야합니다. 이 경우 고정 된 환경, 결정 론적 길 찾기 및 상호 작용의 낮은 속도가 핵심 요인이었습니다.

델타 패킷 개념은 실제로 대부분의 게임에 적용 가능합니다. 전체 게임 상태가 아닌 업데이트의 차이를 인코딩하는 것이 항상 더 좋습니다. 더 빠른 게임의 경우 업데이트 속도가 더 높아야합니다 (초당 최대 30 또는 60 회).

역동적 인 환경을 가진 빠르게 진행되는 게임의 경우 플레이어의 좌표를 높은 속도 (초당 10 회 이상)로 전송해야합니다. 시간 간격이 너무 작아서 트윈 기간에 따라 여기에 설명 된 수정을 적용 할 수 없습니다. 대신에 움직임을 부드럽게하는 클라이언트 측 보간의 일부 형태가 필요할 수 있습니다. Gabriel GambettaGlenn Fiedler의 페이지에서 기존 솔루션에 대한 유용한 정보를 얻을 수 있습니다.

이 외에도 Phaser Quest는 이자 관리라고 하는 또 다른 최적화를 사용합니다. 그 중 하나는 대개 멀티 플레이어 온라인 게임에 필수적인 것으로 간주됩니다.

댓글 없음:

댓글 쓰기