[포트폴리오(Knight Online)] - 룸 단위 위치 동기화
종강하자 마자 조금씩 진행하던 포트폴리오를 이악물고 달려서 완성시켰다. 프로젝트를 진행하면서 크고작은 시스템들을 설계하고 구현하였고, 오늘은 그중에서도 구현할 당시에 애를 많이 먹었던 위치동기화에 대해 정리하는 글을 작성해보려한다.
동기화?
먼저 동기화란 무엇일까? 위키 백과를 인용해 보자면, '동기화(同期化, synchronization)는 시스템을 동시에 작동시키기 위해 여러 사건들을 조화시키는 것을 의미한다.' 라고 하는데, 컴퓨터 과학 분야에서의 의미는 조금 다르다.
컴퓨터 과학에서 동기화는 클라이언트와 서버 데이터베이스상의 데이터일치를 의미한다. 게임에서의 동기화도 이런 의미로 사용되는 용어로 볼 수 있다.
그럼 다시 본론으로 들어가서 위치 동기화란 무엇인가? 앞에서 언급하였듯이 동기화는 클라이언트와 서버간의 데이터 일치를 의미한다. 위치 동기화는 서버가 가지고 있는 클라이언트가 가진 플레이어의 위치 정보와 서버가 가지고 있는 플레이어의 위치정보의 일치를 의미한다.
예시를 들어 보자. Client1, 2가 3x3 형태의 tile map 던전에 들어와 있는 상태라고 가정하면, 위와 같이 표현할 수 있을 것이다. Client1은 Player1을, Client2는 Player2를 조종하는 상태이고 이 정보는 서버에서도 동일하게 들고 있어야 한다.
이제 이 상태에서 Client1이 자신의 캐릭터를 (0, 0)에서 (1, 0)으로 이동한다고 하여보자.
그렇다면 위와 같이 Player1의 위치가 이동하였다는 사실을 서버로 전송하여야 한다. 물론 서버의 설계에 따라 Player1의 이동 패킷을 서버로 전송한 후에 서버의 허락을 받고 Player1의 위치를 이동시키는 경우도 있으나 이 글에서는 선 이동, 후 보고 형태의 서버라고 가정하겠다.
그럼 이 다음의 상황은 어떻게 될까?
Client1의 위치 이동 보고를 받은 서버는 자신이 가진 유저들의 위치 정보를 갱신하고 갱신된 정보를 Player1과 같은 방에 있던 Player2의 세션 정보를 참조하여 Client2에게 위치 이동에 대한 갱신 패킷을 전송하게 된다.
결과적으로 Client1에서 위치 이동이 일어 났고, 이를 앞서 언급한 일련의 위치 동기화 과정을 거쳐 같은 방에 있던 Client2 상에서의 Player1의 위치가 갱신 (서버와 동기화) 되는 것이다.
이번 포트폴리오에서 구현한 룸 단위 위치 동기화는 위에서 언급한 내용들을 코드로써 구현한 것이다. 물론 이 과정 속에서 위치 보정도 어느정도 필요하다.
위치 보정?
사실 이 위치 보정은 이미 온라인 게임에서 많이 겪어 봤던 일들이다.
https://www.youtube.com/watch?v=r4ZaolMQOzE
이전에 NDC에서 카트라이더의 위치 동기화 관련해서 발표했던 영상이다. (지금봐도 흥미로운 주제이다.) 5분 51초까지가 이 글에서 설명한 서버의 동기화 부분이다. 물론 영상에서는 서버의 허락(Validation 체크)후에 이벤트 결과를 클라이언트들에게 뿌리는 경우를 설명하고 있다.
다시 본론으로 돌아와서, 카트라이더 같은 온라인 게임을 하다보면 다른 플레이어의 캐릭터가 순간이동을 하는 것을 자주 겪었을 것이다. 이는 서버에서의 위치와 클라이언트의 위치 정보가 맞지않기 때문에 위치 보정을 하는 과정에서 생기는 현상이라고 볼 수 있다. (물론 이 과정또한 예상 값을 계산하여 자연스럽게 만들어 줄 수도 있다.)
게임의 장르에 따라 다른 중요성
영상의 5분대 에서 이런 동기화 방식의 한계점에 대해 설명해주고 있는데, 카트라이더와 같은 레이싱 게임이나 FPS 게임의 경우 위치 동기화가 매우 중요한 요소이지만, RPG류 게임에서는 그 중요성이 어느정도 낮아진다. 오히려 플레이어의 정확한 위치 보다는 공격이나 피격, 데미지 판정 등의 이벤트 처리 속도가 좀더 중요시 되는 편이다.
이번에 제작한 게임의 장르가 RPG인만큼, 패킷의 딜레이에 의한 어느정도의 플레이어 위치의 오차는 감수하고 앞서 설명한 과정대로 위치 동기화를 진행하였다. 이제 일부 코드를 살펴 보겠다.
public void HandleMove(Player player, C_Move movePacket)
{
if (player == null)
return;
// 서버에서의 플레이어 이동 처리
ObjectInfo info = player.Info;
info.PosInfo = movePacket.PosInfo;
// 다른 클라이언트에게 같은 정보를 전달
S_Move resMovePacket = new S_Move();
resMovePacket.ObjectId = player.Info.ObjectId;
resMovePacket.PosInfo = movePacket.PosInfo;
resMovePacket.PosInfo.DirInfo = movePacket.PosInfo.DirInfo;
Broadcast(resMovePacket);
}
서버에서 클라이언트로부터 위치 이동 요청 패킷을 처리하는 코드 부분이다. 앞서 설명한 것처럼 플레이어가 이동한 위치 정보를 받아서 같은 방 내에 있는 클라이언트들에게 Broadcasting 하는 방식이다.
이제 클라이언트 측에서의 코드도 한번 보겠다.
public override PositionInfo PosInfo
{
get { return _positionInfo; }
set
{
if (_positionInfo.Equals(value))
return;
// 서버상의 위치를 통해 클라이언트에서의 위치 갱신
_positionInfo = value;
// 방향 정보도 동시에 갱신
DirInfo = value.DirInfo;
// 상태를 갱신하면서 스킬 애니메이션 재생도 자동으로 이루어진다.
STATE = (CharacterState)value.State;
}
}
클라이언트 측에서는 따로 함수를 지정하진 않았고, PlayerController 클래스의 위치정보를 수정하면 자동으로 애니메이션 재생과 새로운 위치로 이동하도록 property를 선언해 놓았다.
public void MovePosition()
{
Vector3 _moveDir = VectorPosInfo - transform.position;
if (_moveDir.magnitude < 0.001f) // 목적지 도착
{
STATE = CharacterState.Idle;
}
else
{
transform.forward = ForwardDir;
transform.position += ForwardDir * Time.deltaTime * STAT.Speed;
if(_moveDir.magnitude > 1f) // 오차 보정
{
transform.position = VectorPosInfo;
}
}
}
물론 클라이언트 상에서의 플레이어 위치와 서버로부터 받은 위치가 너무 차이나는 경우 위치보정이 필요한데, 이 프로젝트의 경우 위의 코드처럼 강제적으로 서버가 보내준 위치대로 이동 시켜주도록 하였다. 따라서 인터넷 환경의 문제로 패킷의 딜레이가 길어지면 상대 플레이어가 툭툭 끊기면서 이동하는 듯한 느낌을 받을 수는 있다.
위치 동기화 과정을 처음 배웠을 때는 정말 놀라운 기술이라고 느꼈다. 동시에 이런 멀티 플레이 게임이 이런 부분에서 쉽지 않구나 라는 것도 깨달았다. 플레이어의 단순한 이동 하나하나를 서버에서 처리한 후에 다시 모든 클라이언트에게 뿌려서 클라이언트들이 같은 화면을 보게 하는 것이 이런 과정을 거치기 때문에 가능하다는 것은 다시 봐도 놀랍다.