레이블이 PhaserJS인 게시물을 표시합니다. 모든 게시물 표시
레이블이 PhaserJS인 게시물을 표시합니다. 모든 게시물 표시

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는 이자 관리라고 하는 또 다른 최적화를 사용합니다. 그 중 하나는 대개 멀티 플레이어 온라인 게임에 필수적인 것으로 간주됩니다.

페이저 퀘스트의 지연 시간 추정

이 기사에서는 Phaser Quest의 서버가 게임 내내 연결된 모든 플레이어의 대기 시간을 추적하는 방법을 설명합니다.

대기 시간이란 무엇이며 왜 측정해야합니까?

대기 시간은 게임 서버와 클라이언트간에 데이터 패킷이 이동하는 데 걸리는 시간입니다. 서버에 연결된 각 클라이언트는 여러 가지 요인에 따라 다른 대기 시간을 가질 수 있습니다. 서버 컴퓨터에서 클라이언트 컴퓨터로 또는 그 반대로 이동하려면 물리적 와이어를 통해 정보를 이동해야합니다. 인터넷상의 정보가 빛의 속도에 가까운 속도로 여행 할 수 있지만, 여전히 시간이 걸립니다. 너무 지리적으로 멀지 않은 기계와 상호 작용할 때 일반적이고 일반적인 대기 시간은 약 20ms입니다. 하지만 플레이어 중 하나가 연결 상태가 좋지 않거나 혼잡하면 대기 시간이 크게 늘어나 익숙한 지연 현상이 발생할 수 있습니다.

플레이어의 대기 시간을 잘 예측하면 여러 가지 이유로 (Phaser Quest에서 설명한 것처럼) 보상을 시도하고 게임 통계의 일부로 기록하고 문제를 발견하는 등 여러 가지 이유로 유용 할 수 있습니다.

대기 시간을 계산하는 방법

Phaser Quest에서 서버가 클라이언트에 업데이트를 보낼 때마다 수행되는 탁구 방식을 사용하여 지연 시간을 예측합니다. 다음은 프로세스입니다.
- 서버가 모든 클라이언트에 업데이트 패킷을 보냅니다. stamp라는 추가 필드가 패킷에 추가됩니다. 이 값은 업데이트를 전송할 당시 서버의 타임 스탬프입니다. 이것은 "ping"단계입니다.
- 각 클라이언트는 각 클라이언트의 대기 시간에 따라 다른 클라이언트보다 빨리 업데이트를받습니다. 업데이트를 처리하기 전에 각 클라이언트는 작은 패킷 인 "pong"을 되돌려 보냅니다. 수정되지 않은 서버가 보낸 스탬프 필드를 포함합니다.
- 서버가 클라이언트의 pong 패킷을 수신 할 때마다 패킷의 스탬프 (갱신이 전송 된 순간의 서버 시간 소인)와 현재 서버 시간 소인을 비교합니다. 차이점은 업데이트가 클라이언트에 도달하고 클라이언트의 응답이 서버에 도달하는 데 걸리는 시간 (밀리 초)입니다. 이 값을 2로 나누면 클라이언트의 대기 시간을 예측할 수 있습니다.
-이 상호 작용은 서버가 클라이언트에 업데이트를 보낼 때마다 반복되므로 전체 게임에서 지연 시간을 샘플링합니다. 서버는 각 클라이언트에 대해 마지막 20 개의 대기 시간 예상치 목록을 보관합니다. 클라이언트의 대기 시간에 대한 실행 예상치는이 20 개의 마지막 추정치의 중간 값에 해당합니다 (중간 값은 평균과는 달리 특이 치에 강하기 때문에 선호됩니다).

다음은 pong 응답 처리를위한 코드입니다. server.js
ocket.on('ponq',function(sentStamp){ // sentStamp is the stamp sent back by the client
        // Compute a running estimate of the latency of a client each time an interaction takes place between client and server
        // The running estimate is the median of the last 20 sampled values
        var ss = server.getStamp();
        var delta = (ss - sentStamp)/2;
        if(delta < 0) delta = 0;
        socket.pings.push(delta); // socket.pings is the list of the 20 last latencies
        if(socket.pings.length > 20) socket.pings.shift(); // keep the size down to 20
        socket.latency = server.quickMedian(socket.pings.slice(0)); // quickMedian used the quickselect algorithm to compute the median of a list of values
    });

이 구성표를 사용하면 서버가 언제든지 게임의 각 클라이언트의 대기 시간을 대략적이지만 견고하게 추정 할 수 있습니다. 탁구 상호 작용과 업데이트를 결합하는 것은 의도적 인 일임에 유의하십시오. 이러한 상호 작용은 대개 트래픽이 적을수록 여분의 데이터없이 이루어집니다. 그러나 데이터가 클라이언트와 적극적으로 교환 될 때 "실제"대기 시간이 무엇인지 평가하는 것이 더 흥미로운 것 같습니다. 조금 더 높을 수도 있지만 클라이언트가 경험하는 업데이트 지연을보다 잘 나타냅니다. 또한 업데이트와 함께 번들링하면 대기 시간을 샘플링하기 위해 별도의 패킷을 별도로 전송하는 오버 헤드가 발생하지 않습니다.

결론

이 기사에서는 고객의 대기 시간을 예측하는 매우 간단하면서도 강력한 방법에 대해 설명했습니다. 모든 의견은 환영합니다. 과거 프로젝트에서 다른 접근법을 구현했거나 사용했는지 여부를 알려 주시면 언제든지 알려 주시기 바랍니다.

멀티 플레이어 해적 슈터 게임 만들기

멀티 플레이어 게임을 제작하는 것은 여러 가지 이유로 어려움을 겪습니다. 호스트하는 데 많은 비용이 들고 설계가 까다 롭고 구현하기가 어려울 수 있습니다. 이 자습서를 통해 마지막 장벽을 해결할 수 있기를 바랍니다.

이것은 게임을 만드는 방법을 알고 자바 스크립트에 익숙하지만 온라인 멀티 플레이어 게임을 만든 적이없는 개발자를 대상으로합니다. 일단 끝나면 기본적인 네트워킹 구성 요소를 모든 게임에 구현하고 거기에서 구축 할 수 있어야합니다.
Screenshot of the Final Game - Two Ships Attacking Each Other

여기 게임의 라이브 버전을 사용해 볼 수 있습니다! W 또는 위로 이동하여 마우스를 향해 이동하고 클릭하여 촬영하십시오. 온라인에 다른 사람이 없다면 동일한 컴퓨터에서 두 개의 브라우저 창을 열거 나 휴대 전화에서 두 개의 브라우저 창을 열어 멀티 플레이어 작동 방식을보십시오. 로컬에서 실행하는 데 관심이 있다면 완전한 소스 코드를 GitHub에서도 사용할 수 있습니다.

저는 Kenney 's Pirate Pack 아트 자산과 Phaser 게임 프레임 워크를 사용하여이 게임을 한데 모았습니다. 이 자습서에서는 네트워크 프로그래머 역할을 맡을 것입니다. 출발점은 이 게임의 모든 기능을 갖춘 싱글 플레이어 버전이 될 것이며, 네트워킹 부분에 Socket.io를 사용하여 Node.js에 서버를 작성하는 것은 여러분의 일이 될 것입니다. 이 튜토리얼을 관리하기 쉽게하기 위해 멀티 플레이어 부분에 초점을 맞추고 Phaser 및 Node.js의 특정 개념을 살펴 보겠습니다.

1. 설정

Glitch.com에 스타터 키트를 설치했습니다.

몇 가지 빠른 인터페이스 도움말 : 언제든지 Show 버튼 (왼쪽 상단)을 클릭하여 앱의 실시간 미리보기를 볼 수 있습니다.
The show button is at the top left on the Glitch interface

왼쪽의 수직 사이드 바에는 앱의 모든 파일이 포함됩니다. 이 응용 프로그램을 편집하려면 "리믹스"해야합니다. 이렇게하면 귀하의 계정에 사본이 생성됩니다 (또는 git lingo에서 포크). Remix this button을 클릭하십시오.
The remix button is at the top of the code editor
이 시점에서 익명 계정으로 앱을 수정하게됩니다. 로그인하여 (오른쪽 상단) 작업 내용을 저장할 수 있습니다.

이제 더 나아 가기 전에 멀티 플레이어를 추가하려는 게임의 코드에 익숙해지는 것이 중요합니다. index.html을 살펴보십시오. 사전 객체 (line 99), 생성 (line 115), GameLoop (line 142), 플레이어 객체 (line 35) 외에 세 가지 중요한 기능을 알고 있어야합니다.

게임을 통해 배우는 방법을 배우려면 다음과 같은 도전 과제를 시도하여 게임 작동 원리를 확인하십시오.

- 세계를 더 크게 만든다. (29 행) - 게임 내 세계에 대해 별도의 세계 크기가 있고 페이지의 실제 캔버스에 창 크기가 있음을 알린다.
- 스페이스 바를 앞으로 돌리십시오 (53 행).
- 플레이어 선종을 변경하십시오 (129 행).
- 총알이 느리게 움직 이도록하십시오 (줄 155).

Socket.io 설치하기

Socket.io는 웹 소켓을 사용하여 브라우저에서 실시간 통신을 관리하는 라이브러리 입니다 (멀티 플레이어 데스크톱 게임을 제작하는 경우 UDP와 같은 프로토콜을 사용하는 것과 반대입니다). 또한 WebSocket이 지원되지 않는 경우에도 여전히 작동하는지 확인해야합니다. 따라서 메시징 프로토콜을 처리하고 사용자가 사용할 수있는 훌륭한 이벤트 기반 메시지 시스템을 제공합니다.

가장 먼저해야 할 일은 Socket.io 모듈을 설치하는 것입니다. Glitch에서는 package.json 파일로 이동하여 종속성에서 원하는 모듈을 입력하거나 패키지 추가를 클릭하고 "socket.io"를 입력하여이 작업을 수행 할 수 있습니다.
The add package menu can be found at the top of the code editor when selecting the file packagejson

이것은 서버 로그를 지적하는 좋은 시간입니다. 왼쪽에있는 Logs 버튼을 클릭하여 서버 로그를 불러 오십시오. Socket.io는 모든 종속 항목과 함께 설치되어야합니다. 여기서 서버 코드의 오류나 출력을 보러 갈 것입니다.

The Logs button is on the left side of the screen
이제 server.js로 이동하십시오. 이것이 서버 코드가있는 곳입니다. 지금 당장은 HTML을 제공하기위한 몇 가지 기본적인 상용구가 있습니다. Socket.io를 포함하도록 맨 위에이 행을 추가하십시오.
var io = require('socket.io')(http); // Make sure to put this after http has been defined

이제 클라이언트에 Socket.io를 포함시켜야 하므로 index.html로 돌아가서 <head> 태그의 맨 위에 추가하십시오.
<!-- Load the Socket.io networking library -->
<script src="/socket.io/socket.io.js"></script>

참고 : Socket.io는 자동으로 해당 경로에서 클라이언트 라이브러리 제공을 처리하므로 폴더에 /socket.io/ 디렉토리가 없는데도이 행이 작동하는 이유입니다.

이제 Socket.io가 포함되어 가고 준비가 되었습니다!

2. 플레이어 탐지 및 산포

첫 번째 단계는 서버에서 연결을 수락하고 클라이언트에서 새로운 플레이어를 생성하는 것입니다.

서버에서 연결 수락
server.js의 하단에 다음 코드를 추가하십시오.
// Tell Socket.io to start accepting connections
io.on('connection', function(socket){
    console.log("New client has connected with id:",socket.id);
})
이것은 Socket.io에게 클라이언트가 연결될 때 자동으로 트리거되는 모든 연결 이벤트를 수신하도록 지시합니다. 각 클라이언트에 대해 새로운 소켓 객체를 생성합니다. 여기서 socket.id는 해당 클라이언트의 고유 식별자입니다.

이 작업이 제대로 작동하는지 확인하려면 클라이언트 (index.html)로 돌아가서이 함수를 create 함수의 어딘가에 추가하십시오.
var socket = io(); // This triggers the 'connection' event on the server

게임을 시작한 다음 서버 로그를 보면 로그 버튼을 클릭하면 해당 연결 이벤트가 기록됩니다.

이제 새로운 플레이어가 연결될 때, 우리는 그들이 우리에게 그들의 상태에 관한 정보를 보내길 기대합니다. 이 경우 올바른 위치에 올바르게 생성하려면 x, y 및 각도를 알아야합니다.

이벤트 연결은 Socket.io가 우리를 위해 시작하는 내장 이벤트였습니다. 우리는 원하는대로 정의 된 이벤트를 들을 수 있습니다. 저는 new-player에게 콜 할 것이고, 클라이언트가 자신의 위치에 관한 정보에 연결하자 마자 그것을 보내 줄 것으로 기대합니다. 이것은 다음과 같습니다.
// Tell Socket.io to start accepting connections
io.on('connection', function(socket){
    console.log("New client has connected with id:",socket.id);
    socket.on('new-player',function(state_data){ // Listen for new-player event on this client
      console.log("New player has state:",state_data);
    })
})

이 프로그램을 실행하면 서버 로그에 아무 것도 표시되지 않습니다. 이것은 우리가 고객에게이 새로운 플레이어 이벤트를 아직 내 보내지 않았기 때문입니다. 그러나 잠시 돌보고 서버를 계속 사용한다고 가정 해 봅시다. 가입 한 새 플레이어의 위치를받은 후에는 어떻게 해야합니까?

우리는 연결된 모든 다른 플레이어에게 새로운 플레이어가 가입되었음을 알리는 메시지를 보낼 수 있습니다. Socket.io는 이렇게 하기위한 편리한 함수를 제공합니다 :
socket.broadcast.emit('create-player',state_data);

socket.emit을 호출하면 해당 클라이언트로 메시지가 다시 전송됩니다. socket.broadcast.emit을 호출하면 호출 된 하나의 소켓을 제외하고 서버에 연결된 모든 클라이언트로 소켓을 보냅니다.

io.emit을 사용하면 예외없이 서버에 연결된 모든 클라이언트에 메시지를 보냅니다. 게임을 시작할 때 이미 자신의 플레이어의 배를 만들었 기 때문에 자신의 배를 만들 것을 요청하는 서버에서 메시지를 받으면 중복 된 스프라이트가 생기기 때문에 현재 설정에서는 이 작업을 수행하고 싶지 않습니다. 여기 이 튜토리얼에서 사용하는 다양한 종류의 메시징 기능에 대한 간단한 설명 서를 제공합니다.

서버 코드는 이제 다음과 같이 보입니다.
// Tell Socket.io to start accepting connections
io.on('connection', function(socket){
    console.log("New client has connected with id:",socket.id);
    socket.on('new-player',function(state_data){ // Listen for new-player event on this client
      console.log("New player has state:",state_data);
      socket.broadcast.emit('create-player',state_data);
    })
})

따라서 플레이어가 연결할 때마다 위치 데이터가 포함 된 메시지를 보내고 다른 모든 플레이어에게 해당 데이터를 보내서 해당 스프라이트를 생성 할 수있게합니다.

클라이언트에서 생성
이제 완료하기 위해 클라이언트에서 두 가지 작업을 수행해야합니다.
- 우리가 연결되면 우리의 위치 데이터로 메시지를 내 보냅니다.
- 플레이어 생성 이벤트를 듣고 그 위치에있는 플레이어를 스폰합니다.

첫 번째 작업의 경우 create 함수 (135 행 주변)에서 플레이어를 만든 후 다음과 같이 보내려는 위치 데이터가 포함 된 메시지를 내보낼 수 있습니다.
socket.emit('new-player',{x:player.sprite.x,y:player.sprite.y,angle:player.sprite.rotation})

보내는 데이터를 직렬화하는 것에 대해 걱정할 필요가 없습니다. 어떤 종류의 객체라도 전달할 수 있고 Socket.io가 처리 할 수 있습니다.

앞으로 나아 가기 전에 이것이 작동하는지 테스트 하십시오. 서버 로그에 다음과 같은 내용의 메시지가 표시됩니다.
New player has state: { x: 728.8180247836519, y: 261.9979387913289, angle: 0 }

이제 테스트 해보세요. 게임의 두 창을 열고 작동하는지 확인하십시오.

두 개의 클라이언트를 연 후 첫 번째 클라이언트에는 두 개의 스폰된 선박이 있고 두 번째 클라이언트에는 두 개의 클라이언트 만 표시됩니다.

문제점 : 왜 이런 일이 발생하는지 파악할 수 있습니까? 아니면 어떻게 고칠 수 있을까요? 우리가 작성한 클라이언트 / 서버 로직을 단계별로 실행하고 디버깅을 시도하십시오.

첫 번째 플레이어가 연결되었을 때 서버가 모든 다른 플레이어에게 플레이어 생성 이벤트를 보냈지 만 서버를 받는 플레이어가 없었습니다. 두 번째 플레이어가 연결되면 서버는 다시 브로드 캐스트를 전송하고 플레이어 1은 이를 수신하여 올바르게 스프라이트를 생성하지만 플레이어 2는 플레이어 1의 초기 연결 브로드 캐스트를 놓쳤습니다.

따라서 플레이어 2가 게임의 후반에 참가하여 게임의 상태를 알아야 하기 때문에 문제가 발생합니다. 우리는 어떤 선수가 이미 존재하는지 (또는 이미 세계에서 일어난 일)를 연결하는 새로운 플레이어에게 그들이 따라 잡을 수 있도록 말할 필요가 있습니다. 이 문제를 해결하기 전에 간단한 경고가 있습니다.

게임 상태 동기화에 대한 경고
모든 플레이어의 게임을 동기화 상태로 유지하는 데는 두 가지 방법이 있습니다. 첫 번째는 네트워크를 통해 변경된 사항에 대한 최소한의 정보 만 전송하는 것입니다. 그래서 새로운 플레이어가 연결될 때마다, 당신은 그 새로운 플레이어에 대한 정보를 모든 다른 플레이어들에게 보내고 (그리고 그 새로운 플레이어에게 세계의 모든 다른 플레이어들의리스트를 보냅니다), 연결을 끊을 때, 당신은 다른 플레이어들에게 이 개별 클라이언트가 연결이 끊어졌습니다.

두 번째 방법은 전체 게임 상태를 보내는 것입니다. 이 경우 연결 또는 연결 끊기가 발생할 때마다 모든 플레이어의 전체 목록을 모든 사람에게 보냅니다.

첫 번째는 네트워크를 통해 전송되는 정보를 최소화한다는 점에서 더 좋지만, 매우 까다로울 수 있으며 플레이어가 동기화되지 않을 위험이 있습니다. 두 번째 옵션은 플레이어가 항상 동기화되지만 각 메시지와 함께 더 많은 데이터를 보내는 것을 보장합니다.

우리의 경우, 새 플레이어가 연결되어 있을 때 메시지를 보내려고 하지 않고, 연결을 끊어서 메시지를 삭제하고, 자신의 위치를 업데이트하기 위해 이동 한 경우,이를 모두 하나의 업데이트 이벤트로 통합 할 수 있습니다 . 이 업데이트 이벤트는 모든 이용 가능한 플레이어의 위치를 항상 모든 클라이언트에게 보냅니다. 그것이 서버의 전부입니다. 클라이언트는 받은 상태로 최신 정보를 유지해야합니다.

이를 구현하기 위해 다음과 같은 작업을 수행합니다.

- 키가 자신의 ID이고 값이 위치 데이터 인 플레이어 사전을 보관하십시오.
- 플레이어가 연결되어 업데이트 이벤트를 보낼 때 플레이어를 이 사전에 추가하십시오.
- 플레이어가 연결을 끊고 업데이트 이벤트를 보낼 때 이 사전에서 플레이어를 제거합니다.

이 단계는 매우 간단하기 때문에 스스로 구현할 수 있습니다 (치트 시트가 유용 할 수 있습니다). 전체 구현은 다음과 같습니다.
// Tell Socket.io to start accepting connections
// 1 - Keep a dictionary of all the players as key/value
var players = {};
io.on('connection', function(socket){
    console.log("New client has connected with id:",socket.id);
    socket.on('new-player',function(state_data){ // Listen for new-player event on this client
      console.log("New player has state:",state_data);
      // 2 - Add the new player to the dict
      players[socket.id] = state_data;
      // Send an update event
      io.emit('update-players',players);
    })
    socket.on('disconnect',function(){
      // 3- Delete from dict on disconnect
      delete players[socket.id];
      // Send an update event
    })
})

클라이언트 측은 조금 까다 롭습니다. 한편으로는 업데이트 플레이어 이벤트에 대해서만 걱정할 필요가 있습니다. 그러나 서버가 우리에게 알려진 것보다 많은 배를 보내거나 너무 많은 배를 보내면 더 많은 배를 만들어야 합니다. .

다음은 클라이언트에서이 이벤트를 처리 한 방법입니다.
// Listen for other players connecting
// NOTE: You must have other_players = {} defined somewhere
socket.on('update-players',function(players_data){
    var players_found = {};
    // Loop over all the player data received
    for(var id in players_data){
        // If the player hasn't been created yet
        if(other_players[id] == undefined && id != socket.id){ // Make sure you don't create yourself
            var data = players_data[id];
            var p = CreateShip(1,data.x,data.y,data.angle);
            other_players[id] = p;
            console.log("Created new player at (" + data.x + ", " + data.y + ")");
        }
        players_found[id] = true;
         
        // Update positions of other players
        if(id != socket.id){
          other_players[id].x  = players_data[id].x; // Update target, not actual position, so we can interpolate
          other_players[id].y  = players_data[id].y;
          other_players[id].rotation  = players_data[id].angle;
        }
         
         
    }
    // Check if a player is missing and delete them
    for(var id in other_players){
        if(!players_found[id]){
            other_players[id].destroy();
            delete other_players[id];
        }
    }
    
})

나는 스크립트의 맨 위에 정의 된 other_players라는 사전에서 클라이언트의 배송 정보를 추적합니다 (여기에 표시되지 않음). 서버가 모든 플레이어에게 플레이어 데이터를 전송하기 때문에 클라이언트가 별도의 스프라이트를 생성하지 않도록 체크를 추가해야합니다. (구조화에 어려움이있는 경우 여기에서 index.html에있는 전체 코드를 살펴보십시오.)

이제 이것을 시험해보십시오. 여러 고객을 만들고 닫을 수 있어야하며 올바른 위치에 산란하는 선박의 수를 파악할 수 있어야합니다.

3. 선박 위치 동기화

여기가 정말 재미있는 부분입니다. 실제로 모든 고객들에게 배송 위치를 동기화시키고 싶습니다. 이것은 우리가 지금까지 구축 한 구조의 단순성이 실제로 보여주는 곳입니다. 모든 사용자의 위치를 동기화 할 수있는 업데이트 이벤트가 이미 있습니다. 우리가 해야 할 일은 다음과 같습니다.

- 클라이언트가 새 위치로 이동할 때마다 클라이언트를 내보내도록하십시오.
- 서버가 해당 이동 메시지를 수신 대기하게하고 플레이어 사전에서 해당 플레이어의 항목을 업데이트하십시오.
- 모든 클라이언트에 업데이트 이벤트를 내 보냅니다.

힌트가 필요한 경우 최종 완성 프로젝트를 참조 할 수 있습니다.

네트워크 데이터 최소화에 대한 참고 사항
이를 구현하는 가장 간단한 방법은 모든 플레이어가 이동 메시지를 받을 때마다 모든 플레이어를 새 위치로 업데이트 하는 것입니다. 이것은 플레이어가 가능한 한 빨리 최신 정보를 수신한다는 점에서 훌륭하지만 네트워크를 통해 전송되는 메시지의 수는 프레임 당 수백 개까지 쉽게 증가 할 수 있습니다. 10 명의 플레이어가 있고, 각 플레이어가 매 프레임마다 이동 메시지를 보내는 경우 서버가 모든 10 명의 플레이어에게 다시 릴레이해야한다고 가정 해보십시오. 그것은 이미 프레임 당 100 개의 메시지입니다!

더 좋은 방법은 모든 정보를 포함하는 큰 업데이트를 모든 플레이어에게 보내기 전에 서버가 플레이어로부터 모든 메시지를 수신 할 때까지 기다리는 것입니다. 그런 식으로 당신은 당신이 게임에서 가지고있는 플레이어의 수 (그 숫자의 제곱이 아니라)로 보내는 메시지의 수를 스쿼시합니다. 그러나 그 문제는 모든 사람이 게임에서 가장 느린 연결을 가진 플레이어만큼 많은 지체를 경험할 것이라는 점입니다.

이를 수행하는 또 다른 방법은 지금까지 플레이어로부터 받은 메시지 수에 관계없이 서버가 일정한 속도로 업데이트를 보내도록하는 것입니다. 초당 30 회 정도 서버를 업데이트하는 것이 일반적인 표준처럼 보입니다.

그러나 서버를 구조화하기로 결정한 후에는 게임을 개발할 때마다 모든 프레임을 몇 개의 메시지를 보내는 지 조심하십시오.

4. 총알 동기화

마지막 큰 조각은 총알을 네트워크를 통해 동기화하는 것입니다. 우리는 플레이어를 동기화하는 것과 같은 방식으로 할 수 있습니다.

- 각 클라이언트는 매 프레임마다 모든 글 머리 기호의 위치를 보냅니다.
- 서버는 그것을 모든 플레이어에게 전달합니다.
그러나 문제가 있습니다.

속임수에 대한 보안
클라이언트가 당신을 총알의 진정한 위치로 보내는 경우, 플레이어는 다른 선박이 있는 곳으로 텔레포트하는 총알 같은 가짜 데이터를 보내도록 클라이언트를 수정하여 속일 수 있습니다. 웹 페이지를 다운로드하고 JavaScript를 수정 한 다음 다시 실행하면 쉽게 이 문제를 해결할 수 있습니다. 브라우저 용 게임의 경우 문제가 아닙니다. 일반적으로 클라이언트에서 오는 데이터는 절대 신뢰할 수 없습니다.

이를 막기 위해 다른 방법을 시도합니다.

- 클라이언트는 위치와 방향으로 총알을 발사 할 때마다 내 보낸다.
- 서버는 총알의 움직임을 시뮬레이션합니다.
- 서버는 모든 총알의 위치에 각 클라이언트를 업데이트합니다.
- 클라이언트는 서버가 수신 한 위치에서 총알을 렌더링합니다.

이 방법은 클라이언트가 총알이 어디에서 발생했는지는 담당하지만, 이동 속도 또는 이동 지점이 아닙니다. 클라이언트는 자신의 보기에서 총알의 위치를 변경할 수 있지만 다른 클라이언트가 보는 것을 변경할 수는 없습니다.

이제 이것을 구현하기 위해  방출을 추가하겠습니다. 더 이상 실제 스프라이트를 만들지 않을 것입니다. 왜냐하면 그 존재와 위치가 이제 서버에 의해 완전히 결정되기 때문입니다. index.html의 새로운 총알 코드는 다음과 같습니다.
// Shoot bullet
if(game.input.activePointer.leftButton.isDown && !this.shot){
    var speed_x = Math.cos(this.sprite.rotation + Math.PI/2) * 20;
    var speed_y = Math.sin(this.sprite.rotation + Math.PI/2) * 20;
    /* The server is now simulating the bullets, clients are just rendering bullet locations, so no need to do this anymore
    var bullet = {};
    bullet.speed_x = speed_x;
    bullet.speed_y = speed_y;
    bullet.sprite = game.add.sprite(this.sprite.x + bullet.speed_x,this.sprite.y + bullet.speed_y,'bullet');
    bullet_array.push(bullet);
    */
    this.shot = true;
    // Tell the server we shot a bullet
    socket.emit('shoot-bullet',{x:this.sprite.x,y:this.sprite.y,angle:this.sprite.rotation,speed_x:speed_x,speed_y:speed_y})
}

또한 이제 클라이언트에서 총알을 갱신이 모든 부분을 주석 처리 할 수 있습니다 :
/* We're updating the bullets on the server, so we don't need to do this on the client anymore
// Update bullets
for(var i=0;i<bullet_array.length;i++){
    var bullet = bullet_array[i];
    bullet.sprite.x += bullet.speed_x;
    bullet.sprite.y += bullet.speed_y;
    // Remove if it goes too far off screen
    if(bullet.sprite.x < -10 || bullet.sprite.x > WORLD_SIZE.w || bullet.sprite.y < -10 || bullet.sprite.y > WORLD_SIZE.h){
        bullet.sprite.destroy();
        bullet_array.splice(i,1);
        i--;
    }
}
*/

마지막으로, 클라이언트에게 총알 업데이트를 수신하도록 요청해야합니다. 나는 서버가 단지 총알 업데이트라는 이벤트의 모든 총알 위치의 배열을 보내는 플레이어와 함께 할이 같은 방식으로 처리하기로 선택했습니다, 그리고 클라이언트가 만들거나 동기화해야 할 총알을 파괴 할 것이다. 다음은 그 모습입니다.
// Listen for bullet update events
socket.on('bullets-update',function(server_bullet_array){
  // If there's not enough bullets on the client, create them
 for(var i=0;i<server_bullet_array.length;i++){
      if(bullet_array[i] == undefined){
          bullet_array[i] = game.add.sprite(server_bullet_array[i].x,server_bullet_array[i].y,'bullet');
      } else {
          //Otherwise, just update it!
          bullet_array[i].x = server_bullet_array[i].x;
          bullet_array[i].y = server_bullet_array[i].y;
      }
  }
  // Otherwise if there's too many, delete the extra
  for(var i=server_bullet_array.length;i<bullet_array.length;i++){
       bullet_array[i].destroy();
       bullet_array.splice(i,1);
       i--;
   }
                   
                })

그것은 클라이언트의 모든 것입니다. 나는 이 부분을 어디에 넣을 지, 그리고 이 시점에서 모든 것을 함께 집어 넣는 방법을 알고 있다고 가정하고 있지만, 어떤 문제에 부딪히는 경우 참조를 위해 항상 최종 결과를 살펴볼 수 있다는 것을 기억하십시오.

이제 server.js에서 총알을 추적하고 시뮬레이션해야 합니다. 먼저 우리가 플레이어와 동일한 방법으로 총알을 추적 할 수있는 배열을 만듭니다.
var bullet_array = []; // Keeps track of all the bullets to update them on the server

다음으로 발사된 총알 이벤트를 듣습니다.
// Listen for shoot-bullet events and add it to our bullet array
  socket.on('shoot-bullet',function(data){
    if(players[socket.id] == undefined) return;
    var new_bullet = data;
    data.owner_id = socket.id; // Attach id of the player to the bullet
    bullet_array.push(new_bullet);
  });

이제 우리는 초당 60 번 총알을 시뮬레이션합니다.
// Update the bullets 60 times per frame and send updates
function ServerGameLoop(){
  for(var i=0;i<bullet_array.length;i++){
    var bullet = bullet_array[i];
    bullet.x += bullet.speed_x;
    bullet.y += bullet.speed_y;
     
    // Remove if it goes too far off screen
    if(bullet.x < -10 || bullet.x > 1000 || bullet.y < -10 || bullet.y > 1000){
        bullet_array.splice(i,1);
        i--;
    }
         
  }
   
}
 
setInterval(ServerGameLoop, 16);

그리고 마지막 단계는 그 함수의 어딘가에 업데이트 이벤트를 보내는 것입니다 (그러나 확실히 for 루프 바깥 쪽).
// Tell everyone where all the bullets are by sending the whole array
  io.emit("bullets-update",bullet_array);

5. 총알 충돌

이것이 우리가 구현할 마지막 핵심 기술입니다. 이제까지 구현을 계획하는 절차에 익숙해 져서 클라이언트 구현을 완전히 끝내기 전에 먼저 서버로 이동하는 것이 좋을 것입니다. 이것은 구현할 때 앞뒤로 전환하는 것보다 오류 발생이 적습니다.

충돌을 확인하는 것은 중요한 게임 플레이 메커닉이므로 치트 프루프가 되고 싶습니다. 우리는 총알에 대해서도 서버에서 구현할 것입니다. 우리가 해야 할 일은 :

- 총알이 서버의 모든 플레이어에게 충분히 근접한 지 확인하십시오.
- 특정 플레이어가 공격을 받을 때마다 모든 클라이언트에게 이벤트를 내 보냅니다.
- 클라이언트가 히트 이벤트를 듣고 우주선이 맞았을 때 우주선이 깜박이도록하십시오.

이 작업을 직접 수행 할 수 있습니다. 명중 할 때 플레이어가 깜박이도록 하려면 알파 값을 0으로 설정하면됩니다.
player.sprite.alpha = 0;

그리고 다시 전체 알파로 돌아갈 것입니다 (이것은 플레이어 업데이트에서 수행됩니다). 다른 플레이어의 경우에도 비슷한 일을 할 수 있지만 업데이트 기능에서 다음과 같이 알파를 다시 가져와야 합니다.
for(var id in other_players){
 if(other_players[id].alpha < 1){
        other_players[id].alpha += (1 - other_players[id].alpha) * 0.16;
    } else {
        other_players[id].alpha = 1;
    }
}

당신이 처리해야 할 까다로운 부분은 플레이어 자신의 총알이 그들을 맞출 수 없도록 만드는 것입니다 (그렇지 않으면 당신은 항상 당신이 발사 할 때마다 자신의 총알로 명중 할 수 있습니다).

이 구성표에서 클라이언트가 속임수를 쓰려고 시도하고 서버가 전송 한 적중 메시지를 확인하지 않으면 서버는 자신의 화면에서 볼 수있는 내용 만 변경합니다. 다른 모든 플레이어는 여전히 그 플레이어가 맞았다는 것을 볼 수 있습니다.

6. 부드러운 이동

이 단계까지 모든 단계를 수행했다면, 축하드립니다. 방금 멀티 플레이어 게임을 만들었습니다! 계속해서 친구에게 보내고 온라인 멀티 플레이어 연합 플레이어의 마술을 지켜보십시오!

게임은 완벽하게 작동하지만 우리의 작업이 멈추지 않습니다. 우리가 해결해야 할 플레이어의 경험에 영향을 미칠 수있는 몇 가지 문제가 있습니다.

- 모든 플레이어가 빠르게 연결되지 않으면 다른 플레이어의 움직임이 고르지 않게 보일 것입니다.
- 총알이 즉시 발사되지 않기 때문에 총알이 반응이 느껴질 수 있습니다. 클라이언트의 화면에 나타나기 전에 서버에서 메시지를 기다립니다.

우리는 클라이언트에서 이동에 대한 위치 데이터를 보간하여 첫 번째 문제를 해결할 수 있습니다. 그래서 우리가 충분히 빠른 업데이트를받지 못한다고 해도 우주선을 순간 이동하는 것과 반대되는 방향으로 우주선을 부드럽게 움직일 수 있습니다.

총알은 좀 더 정교해질 것입니다. 우리는 서버가 총알을 관리하기를 원합니다. 그 방법은 속임수가 아니기 때문입니다. 그러나 우리는 총알을 발사하고 촬영하는 것을 즉각적인 피드백으로 원합니다. 가장 좋은 방법은 하이브리드 방식입니다. 서버와 클라이언트 모두 총알 위치를 업데이트하면서 서버가 총알을 시뮬레이트 할 수 있습니다. 동기화가 안되면 서버가 맞다고 클라이언트의 총알 위치를 무시하십시오.

위에서 설명한 필렛 시스템을 구현하는 것은이 자습서의 범위를 벗어나지 만이 방법이 존재한다는 것을 알고있는 것이 좋습니다.

배의 위치에 대한 간단한 보간법은 매우 쉽습니다. 새 위치 데이터를 처음 수신하는 업데이트 이벤트에서 직접 위치를 설정하는 대신 목표 위치를 저장하기 만 하면됩니다.
// Update positions of other players
if(id != socket.id){
  other_players[id].target_x  = players_data[id].x; // Update target, not actual position, so we can interpolate
  other_players[id].target_y  = players_data[id].y;
  other_players[id].target_rotation  = players_data[id].angle;
}

그런 다음 업데이트 기능 (여전히 클라이언트에 있음) 내에서 다른 모든 플레이어를 반복하여 이 대상을 향해 푸시합니다.
// Interpolate all players to where they should be
for(var id in other_players){
    var p = other_players[id];
    if(p.target_x != undefined){
        p.x += (p.target_x - p.x) * 0.16;
        p.y += (p.target_y - p.y) * 0.16;
        // Interpolate angle while avoiding the positive/negative issue
        var angle = p.target_rotation;
        var dir = (angle - p.rotation) / (Math.PI * 2);
        dir -= Math.round(dir);
        dir = dir * Math.PI * 2;
        p.rotation += dir * 0.16;
    }
}

이 방법으로 서버가 초당 30 번 업데이트를 보내도록 할 수 있지만 여전히 60fps로 게임을 실행하면 매끄럽게 보입니다!

결론

휴! 우리는 방금 많은 것을 다루었습니다. 간단히 요약하면 클라이언트와 서버간에 메시지를 보내는 방법과 서버가 모든 플레이어에게 메시지를 전달하도록 하여 게임의 상태를 동기화하는 방법을 살펴 보았습니다. 이것은 온라인 멀티 플레이 경험을 만드는 가장 간단한 방법입니다.

또한 서버에서 중요한 부분을 시뮬레이션하고 클라이언트에게 결과를 알리는 방법으로 치트에 대해 게임을 보안 할 수있는 방법을 알아 냈습니다. 당신이 당신의 고객을 신뢰할수록, 게임은 더 안전해질 것입니다.

마지막으로, 우리는 클라이언트에서 보간함으로써 래그를 극복하는 방법을 보았습니다. 지연 보상은 광범위한 주제이며 매우 중요합니다 (일부 게임은 충분히 지연없이 충분히 재생할 수 없게됩니다). 서버에서 다음 업데이트를 기다리는 동안 보완하는 것은이를 완화하는 한 가지 방법 일뿐입니다. 또 다른 방법은 다음 몇 개의 프레임을 미리 예측하고 서버에서 실제 데이터를 받으면 수정하는 것입니다. 물론 이것은 매우 까다로운 작업 일 수 있습니다.

지연의 영향을 완화하는 완전히 다른 방법은 그 주위를 설계하는 것입니다. 배가 천천히 움직이게 하는 이점은 독특한 이동 메커니즘이자 갑작스런 움직임 변화를 방지하는 방법입니다. 따라서 느린 연결을 하더라도 여전히 경험을 망치지는 않습니다. 이와 같이 게임의 핵심 요소를 설계하는 동안 지연에 대한 설명은 큰 차이를 만들 수 있습니다. 때로는 최상의 솔루션은 전혀 기술적이지 않습니다.

유용 할 수도있는 Glitch의 마지막 기능 중 하나는 왼쪽 상단의 고급 설정으로 이동하여 프로젝트를 다운로드하거나 내보낼 수 있다는 것입니다.
The advanced options menu allows you to import export or download your project
멋진 정보를 얻으려면 아래의 의견에 기재하십시오! 또는 질문이나 명확한 설명이 있으면 언제든지 도와 드리겠습니다.