현재 Knight Online이라는 프로젝트를 진행하고 있고, 싱글 컨텐츠외에 멀티 플레이를 통해 보스 레이드를 하는 컨텐츠를 구현 하고있다.
그래서 이번 글에서는 멀티 플레이 컨텐츠 구현 과정 중에서도 같은 게임 룸에 입장시킬 플레이어 들을 모아서 처리하는 매칭 서버 구현 과정에 대해 정리해 놓으려고 한다.
매칭 서버?
일반적인 매칭 서버라고 하면 플레이어의 능력치나 레벨 등에 따라 조건에 맞는 플레이어들을 모아 하나의 게임룸 안에 몰아서 처리하는 방식으로 처리를 하는데, 이 과정 자체는 정해진 정답이 없었다.
게임 개발 관련 커뮤니티나 구글링을 통해 어떤 식으로 매칭과정을 구현하는 지 알아보았지만 방법이 너무 다양하기도 하고 조건에 맞는 플레이어들을 찾아서 처리하는 것 또한 기획의 영역이라고 생각이 들었다.
그래서 내 나름대로 '여태 해왔던 온라인 게임들의 매칭과정들은 어땠지?' 라는 생각을 한켠에 두고 내가 구현할 매칭서버를 설계하였다.
우선 서버는 MatchQueue라고 하는 매칭을 요청한 플레이어들을 담아놓는 컨테이너를 관리한다. 플레이어로부터 매칭 요청 패킷이 들어온 경우 플레이어 들을 이 MatchQueue라는 컨테이너에 삽입한다.
Match Queue에서 Flush 함수를 통해 게임룸에 입장시킬 플레이어들이 충분히 모였는지 확인하고, 모인 경우 그 플레이어들을 Pop한 후 게임 룸에 입장시킨다.
동시에 서버에서의 처리는 끝났으므로 플레이어 들에게 매칭 완료 패킷을 전송하여 씬 전환 및 다른 플레이어들의 정보를 불러드릴 준비를 시킨다.
기본적인 동작 과정은 위와 같다. 아무런 처리 없이 위의 과정을 구현하는 것은 어렵진 않으나 비동기 프로그래밍을 고려하면서 컴퓨터 자원을 관리하며 동작을 수행시키기 위해서는 몇 가지 처리가 필요했다.
class MatchManager
{
#region Singleton
static MatchManager _instance = new MatchManager();
public static MatchManager Instance { get { return _instance; } }
#endregion
MatchQueue _matchQueue = new MatchQueue();
MatchManager()
{
}
// 매칭 큐 요청 -> req = true 요청, req = false = 요청 취소, 취소라면 매치 큐에서 빼야함
public void MatchReq(Player player, bool req)
{
if(req)
{
_matchQueue.Add(player);
}
else
{
_matchQueue.Remove(player);
}
}
public void MatchQueueFlush()
{
// TODO : 매치 큐를 열어서 플레이어가 게임에 입장할만큼 모였는지 확인
// TEMP : 플레이어가 2명 모였다면 방 생성 후 입장
List<Player> matchedPlayers = _matchQueue.MatchedPlayers();
if(matchedPlayers != null)
{
// 방생성
// TEMP : 방의 id는 방에 들어갈 첫번째 플레이어의 session id
GameRoom room = GameLogic.Instance.Add(matchedPlayers[0].Session.SessionId);
Console.WriteLine($"{room.RoomId} 번 방을 생성");
foreach(Player player in matchedPlayers)
{
// 매칭 완료 패킷에 룸 ID를 전송하여 플레이어가 entergame 패킷을 전송해서
// 게임에 직접 접속하게끔 유도
S_RaidMatch matchOkPacket = new S_RaidMatch();
matchOkPacket.Matched = true;
matchOkPacket.RoomNum = room.RoomId;
matchOkPacket.Player = player.Info;
player.Session.Send(matchOkPacket);
}
}
}
}
매칭을 요청한 플레이어들을 관리하는 MatchQueue를 관리하는 MatchManager 클래스이다. 클래스 내의 함수 중 MatchQueueFlush라는 함수는 게임룸에 입장시키기에 충분한 플레이어가 모였는 지 검사한다.
플레이어가 충분히 모였다면 플레이어들을 서버가 직접 방으로 들여보내는것이 아니라 패킷내에 입장 해야할 RoomId를 넣어서 전송하여 플레이어가 매칭 완료 패킷을 받은 후 클라이언트에서 EnterGame 패킷을 보내 클라에서 직접 게임 룸 입장 요청을 처리하도록 하였다.
사실 방을 생성한 후 바로 서버에서 클라이언트들을 방으로 입장시키고 클라이언트에 통보하는 식으로 하여도 상관은 없지만, 서버가 방으로 들여보내는 과정 중에 클라이언트에서 매칭 취소를 할 수도 있고 인터넷이 끊길 수도 있기에 위와 같은 방식으로 처리하였다. 우선 현재 진행중인 프로젝트는 온라인 뿐만 아니라 개인이 던전(게임룸)을 생성하여 클리어하는 싱글 플레이 컨텐츠도 있다. 방을 혼자 사용하는 경우에도 서버에서 방을 만들고 그 게임룸에 플레이어가 입장하여 게임을 진행하는 방식이다.
코드의 재사용성 측면에서 봐도, 만약 지금처럼 서버에서 클라이언트를 바로 생성한 게임룸으로 들여보내고 클라에 통보하는 방식으로 구현을 하는 경우 싱글 플레이 컨텐츠에서 사용하는 게임룸 입장 프로세스를 그대로 따를 수 있다. 즉, 게임룸 입장 과정 자체를 굳이 새로 만들 필요가 없어진다.
class MatchQueue
{
#region Singleton
static MatchQueue _instance = new MatchQueue();
public static MatchQueue Instance { get { return _instance; } }
#endregion
List<Player> _matchQueue = new List<Player>();
private static int MatchPlayerNum = 2;
object _lock = new object();
public void Add(Player player)
{
lock(_lock)
{
_matchQueue.Add(player);
}
}
public void Remove(Player player)
{
lock (_lock)
{
if (_matchQueue.Contains(player))
_matchQueue.Remove(player);
}
}
public List<Player> MatchedPlayers()
{
if(_matchQueue.Count >= MatchPlayerNum) // TEMP 플레이어 2명 이상
{
List<Player> matchedPlayers = new List<Player>();
List<Player> _matchQueue_cpy = new List<Player>(_matchQueue);
foreach(Player player in _matchQueue)
{
matchedPlayers.Add(player);
_matchQueue_cpy.Remove(player);
if (matchedPlayers.Count == MatchPlayerNum)
break;
}
_matchQueue = _matchQueue_cpy;
Console.WriteLine($"대기중인 플에이어 {_matchQueue.Count} 명");
return matchedPlayers;
}
return null;
}
}
현재 매칭 서버에서 사용하는 MatchQueue 컨테이너이다. 컨테이너 이름은 MatchQueue지만 실제로는 List이다. Queue로 구현할 경우 중간에 있는 플레이어만 빼낼 수 없어서 Queue는 사용할 수 없다. 그러면 왜 MatchQueue인지 의문이 들지만 MatchList같은건 어감이 좀 이상하니 저런 식으로 네이밍 하였다.
MatchQueue는 특별할 게 없다. 다만 여러 쓰레드가 한번에 MatchQueue에 접근할 것을 생각하여 Add와 Remove에는 lock을 걸어 보호하였다.
MatchedPlayers라는 함수를 이용하여 MatchQueue에 방으로 입장가능한 플레이어가 있는 경우 그 플레이어들을 반환하도록 하였다. 매칭이 완료된 플레이어들은 MatchQueue에서 제거한다.
static void MatchTask()
{
while(true)
{
MatchManager.Instance.MatchQueueFlush();
Thread.Sleep(0);
}
}
위에서 설계한 매치 서버를 돌리는 부분이다. Thread.Sleep(0)을 선언해 둔 이유는 매칭 가능한 플레이어가 없는 경우 굳이 while문을 돌면서 컴퓨터 자원을 먹지 않고 곧바로 제어권을 다른 컴퓨터 자원이 필요한 곳으로 보내주기 위함이다. (만약 계속 MatchQueue를 검사하게 했다면 서버에서 계속 쓰레드 하나가 매칭서버만 처리하면서 놀게된다.)
결과 화면
현재 매칭 서버는 테스트를 위해 따로 플레이어를 매칭시키는 조건(능력치, 레벨 등...)을 설정하지 않고 임의의 2명의 플레이어가 모이면 방에 입장하도록 구현하였다. 위의 이미지에서는 하나의 클라이언트에서만 매칭 버튼을 눌러 매칭을 시작하였고, 서버에서도 매칭 요청을 정상적으로 받은 것을 확인하였다.
나머지 한개의 클라이언트에서도 매칭 요청을 하여 서버로 매칭 요청 패킷을 전송하였고, 2명의 플레이어가 정상적으로 게임 룸에 입장한 것을 확인하였다.
처음에 언급하였듯이 매칭 서버라는 게 정답이 없는 것 같아서 구현을 해보고 나서도 이게 맞는 방법인지는 아직 의구심이 든다. 그래도 구현이라는 게 정답을 찾는 게 아니라 최선을 찾는 것이라는 마인드로 작업을 하니 마음은 한결 가벼운 것 같다. 프로젝트 완성까지 화이팅이다.
'포트폴리오 > 서버연동 온라인 arpg - Knight Online' 카테고리의 다른 글
[포트폴리오(Knight Online)] - 룸 단위 위치 동기화 (0) | 2023.07.17 |
---|---|
[포트폴리오(Knight Online)] - 보상 시스템 구현 (0) | 2023.06.03 |
[포트폴리오(Knight Online)] - 비동기 버그 관련 수정 (0) | 2023.05.16 |
[포트폴리오(Knight Online)] - 퀘스트 시스템 구현 (0) | 2023.05.16 |