25. 콘텐츠 지원 Part 2: 매치메이킹

유저간 Match 상대를 자동으로 선택하는 기능입니다.

매치메이킹은 MatchmakingClientMatchmakingServer 라는 두 부분으로 구분됩니다.

  • MatchmakingClient 는 플레이어를 Match 에 참여시키거나 취소시킬 때 사용합니다. Client 라는 이름이 붙긴 하지만, 서버용 클라이언트이며, 게임 서버 임장에서는 매치를 요청하는데 쓰인다고 해서 Client 라는 이름이 붙습니다.
  • MatchmakingServer 는 매치메이킹을 처리하는 서버에서 사용하며, 플레이어가 Match 참여를 요청했을 때 참여 가능한 조건인지 등을 판단하여 Match 에 참여시키거나 불가시킬 수 있습니다.

Tip

MatchmakingServer 을 특정 flavor 만 동작시키게 하는 형태로 매치메이킹만 전담하는 서버를 만들 수 있습니다.

25.1. Matchmaking Client

25.1.1. 인터페이스

MatchmakingClient 클래스의 StartMatchmaking()CancelMatchmaking() 를 이용해 매치를 시작하고 중단할 수 있습니다. (C# 의 경우 Start()Cancel()) 또한 GetMatchmakingServerInfo() 를 이용해 MatchmakingServer 의 상태를 알애날 수 있습니다.

class MatchmakingClient {
 public:
  typedef Uuid MatchId;
  typedef int64_t Type;

  // Match 에 참여한 플레이어의 정보입니다.
  struct Player {
    // 플레이어 식별자
    string id;

    // 플레이어 정보가 담긴 context 입니다.
    // Match 참여를 요청할 때 입력한 context 를
    // StartMatchmaking() 함수를 호출할 때 전달합니다.
    Json context;

    // 플레이어가 접속한 서버의 위치입니다.
    Rpc::PeerId location;
  };

  // Match 에 대한 정보입니다.
  struct Match {
    explicit Match(Type _type);
    explicit Match(const MatchId &_match_id, Type _type);

    // Match 식별자
    const MatchId match_id;

    // 매치 타입입니다. Integer(enum) 값으로 개발사에서 임의로 지정하여
    // 매치 타입을 구분지을 수 있습니다.
    const Type type;

    // 매치 타입에 매칭된 플레이어 list 입니다.
    std::vector<Player> players;

    // 해당 Match 만의 정보가 담긴 개별 context 입니다.
    // 동일한 매치타입끼리 공유되진 않습니다.
    // MatchmakingServer 의 join, leave callback 이 호출될 때
    // 해당 callback 에서 팀을 편성하는 등의 정보를 자유롭게 입력할 수 있습니다.
    Json context;
  };

  // MatchCallback 에 전달되는 매칭 요청 처리 결과입니다.
  enum MatchResult {
    kMRSuccess = 0,
    kMRAlreadyRequested,
    kMRTimeout,
    kMRError = 1000
  };

  // CancelCallback 에 전달되는 매칭 요청 취소 처리 결과입니다.
  enum CancelResult {
    kCRSuccess = 0,
    kCRNoRequest,
    kCRError = 1000
  };

  // 매칭이 완료되면 호출될 함수입니다.
  typedef function<void(const string & /*player_id*/,
                        const Match & /*match*/,
                        MatchResult /*result*/)> MatchCallback;

  // 매칭의 진행상황이 변경되면 호출될 함수입니다.
  // MatchmakingServer 의 `enable_match_progress_callback` 이 true 일 때만
  // 유효합니다.
  typedef function<void(const string & /*player_id*/,
                        const MatchId & /*match_id*/,
                        const string & /*player_id_joined*/,
                        const string & /*player_id_left*/,
                        const Json & /*match_context*/)> ProgressCallback;

  // 매칭 요청을 취소한 후 호출될 함수입니다.
  typedef function<void(const string & /*player_id*/,
                        CancelResult /*result*/)> CancelCallback;

  // 매치메이킹 서버를 선택하는 기준입니다.
  enum TargetServerSelection {
    kRandom = 0,           // 무작위로 선택합니다.
    kMostNumberOfPlayers,  // 매치메이킹 요청이 많은 서버를 선택합니다.
                           // MatchmakingServer MANIFEST 의
                           // concurrent_number_of_players_threshold 를
                           // 초과한 서버는 최대한 배제합니다.
    kLeastNumberOfPlayers  // 매치메이킹 요청이 적은 서버를 선택합니다.
  };

  // 매치메이킹 서버의 정보입니다.
  struct MatchmakingServerInfo {
    Rpc::PeerId peer_id;     // Peer ID 입니다.
    size_t player_count;     // 현재 매치메이킹 중인 플레이어 수입니다.
    bool want_more_players;  // player_count 값이 MatchmakingServer MANIFEST 의
                             // concurrent_number_of_players_threshold 를
                             // 초과하면 false 입니다.
  };

  typedef std::vector<MatchmakingServerInfo> MatchmakingServerInfoVector;

  static const WallClock::Duration kNullTimeout;
  static const ProgressCallback kNullProgressCallback;

  // 매치메이킹 서버의 정보를 가져옵니다.
  static MatchmakingServerInfoVector GetMatchmakingServerInfo();

  // Match 상대를 찾는 요청을 보냅니다.
  // target_server 인자로 전달된 기준으로 매치메이킹 서버를 선택합니다.
  static void StartMatchmaking(const Type &type, const string &player_id,
      const Json &player_context, const MatchCallback &match_callback,
      const TargetServerSelection &target_server = kRandom,
      const ProgressCallback &progress_callback = kNullProgressCallback,
      const WallClock::Duration &timeout = kNullTimeout);

  // Match 상대를 찾는 요청을 보냅니다.
  // target_server 인자로 전달된 매치메이킹 서버로 요청합니다.
  static void StartMatchmaking(const Type &type, const string &player_id,
      const Json &player_context, const MatchCallback &match_callback,
      const Rpc::PeerId &target_server,
      const ProgressCallback &progress_callback = kNullProgressCallback,
      const WallClock::Duration &timeout = kNullTimeout);

  // StartMatchmaking() 요청을 취소합니다.
  static void CancelMatchmaking(const Type &type, const string &player_id,
                                const CancelCallback &cancel_callback);
};
using funapi;


namespace MatchMaking
{

  // Match 에 참여한 플레이어의 정보입니다.
  public struct Player
  {
    // 플레이어 식별자
    public string Id;

    // 플레이어 정보가 담긴 context 입니다.
    // Match 참여를 요청할 때 입력한 context 를
    // StartMatchmaking() 함수를 호출할 때 전달합니다.
    public JObject Context;

    // 플레이어가 접속한 서버의 위치(Rpc의 PeerId)입니다.
    public System.Guid Location;
  }

  // Match 에 대한 정보입니다.
  public struct Match
  {
    // Match 식별자
    public System.Guid MatchId;

    // 매치 타입입니다. Integer 값으로 개발사에서 임의로 지정하여
    // 매치 타입을 구분지을 수 있습니다.
    public Int64 MatchType;

    // 매치 타입에 매칭된 플레이어 list 입니다.
    public ReadOnlyCollection<Player> Players;

    // 해당 Match 만의 정보가 담긴 개별 context 입니다.
    // 동일한 매치타입끼리 공유되진 않습니다.
    // MatchmakingServer 의 join, leave callback 이 호출될 때
    // 해당 callback 에서 팀을 편성하는 등의 정보를 자유롭게 입력할 수 있습니다.
    public JObject Context;
  }

  // MatchCallback 에 전달되는 매칭 요청 처리 결과입니다.
  public enum MatchResult {
    kSuccess = 0,
    kAlreadyRequested,
    kTimeout,
    kError = 1000
  };

  // CancelCallback 에 전달되는 매칭 요청 취소 처리 결과입니다.
  public enum CancelResult {
    kSuccess = 0,
    kNoRequest,
    kError = 1000
  };


  public static class Client
  {
    // 매칭이 완료되면 호출될 함수입니다.
    public delegate void MatchCallback(string player_id,
                                       Match match,
                                       MatchResult result);

    // 매칭의 진행상황이 변경되면 호출될 함수입니다.
    // MatchmakingServer 의 `enable_match_progress_callback` 이 true 일 때만
    // 유효합니다.
    public delegate void ProgressCallback(string player_id,
                                          System.Guid match_id,
                                          string player_id_joined,
                                          string player_id_left,
                                          JObject match_context);

    // 매칭 요청을 취소한 후 호출될 함수입니다.
    public delegate void CancelCallback(string player_id,
                                        CancelResult result);

    // 매치메이킹 서버를 선택하는 기준입니다.
    public enum TargetServerSelection {
      kRandom = 0,           // 무작위로 선택합니다.
      kMostNumberOfPlayers,  // 매치메이킹 요청이 많은 서버를 선택합니다.
                             // MatchmakingServer MANIFEST 의
                             // concurrent_number_of_players_threshold 를
                             // 초과한 서버는 최대한 배제합니다.
      kLeastNumberOfPlayers  // 매치메이킹 요청이 적은 서버를 선택합니다.
    };

    public struct MatchmakingServerInfo {
      public System.Guid peer_id;     // Peer ID 입니다.
      public ulong player_count;      // 현재 매치메이킹 중인 플레이어 수입니다.
      public bool want_more_players;  // player_count 값이 MatchmakingServer MANIFEST 의
                                      // concurrent_number_of_players_threshold 를
                                      // 초과하면 false 입니다.
    };

    // 매치메이킹 서버의 정보를 가져옵니다.
    public static ReadOnlyCollection<MatchmakingServerInfo> GetMatchmakingServerInfo ();

    // Match 상대를 찾는 요청을 보냅니다.
    // target_server 인자로 전달된 기준으로 매치메이킹 서버를 선택합니다.
    public static void Start (Int64 type,
                              string player_id,
                              JObject player_context,
                              MatchCallback match_callback,
                              TargetServerSelection target_server = TargetServerSelection.kRandom,
                              ProgressCallback progress_callback = null,
                              TimeSpan timeout = default(TimeSpan))

    // Match 상대를 찾는 요청을 보냅니다.
    // target_server 인자로 전달된 매치메이킹 서버로 요청합니다.
    public static void Start (Int64 type,
                              string player_id,
                              JObject player_context,
                              MatchCallback match_callback,
                              System.Guid target_server,
                              ProgressCallback progress_callback = null,
                              TimeSpan timeout = default(TimeSpan))

    // StartMatchmaking() 요청을 취소합니다.
    public static void Cancel (Int64 type,
                               string player_id,
                               CancelCallback cancel_callback);
  }
}

25.1.1.1. 매칭 시작

Match 상대를 찾는 요청을 보냅니다.

class MatchmakingClient {
 public:

  static void StartMatchmaking(const Type &type, const string &player_id,
      const Json &player_context, const MatchCallback &match_callback,
      const TargetServerSelection &target_server = kRandom,
      const ProgressCallback &progress_callback = kNullProgressCallback,
      const WallClock::Duration &timeout = kNullTimeout);

  static void StartMatchmaking(const Type &type, const string &player_id,
      const Json &player_context, const MatchCallback &match_callback,
      const Rpc::PeerId &target_server,
      const ProgressCallback &progress_callback = kNullProgressCallback,
      const WallClock::Duration &timeout = kNullTimeout);
}
namespace matchmaking
{
  public static class Client
  {
    public static void Start (Int64 type,
                              string player_id,
                              JObject player_context,
                              MatchCallback match_callback,
                              TargetServerSelection target_server = TargetServerSelection.kRandom,
                              ProgressCallback progress_callback = null,
                              TimeSpan timeout = default(TimeSpan))

    public static void Start (Int64 type,
                              string player_id,
                              JObject player_context,
                              MatchCallback match_callback,
                              System.Guid target_server,
                              ProgressCallback progress_callback = null,
                              TimeSpan timeout = default(TimeSpan))
  }
}
  • type : 매치 종류를 의미하는 integer 값으로 임의로 정의할 수 있으며, 이 값은 MatchmakingServer 에서 같은 매치 종류를 구분하는데 사용됩니다.

  • player_id : Match 참여자를 식별하는 식별자입니다.

  • player_context : MatchmakingServer 에서 참여자의 실력을 판단할 때 사용될 값으로, 매치 기준에 필요한 것들을 자유롭게 JSON 형식으로 구성하시면 됩니다.

    Important

    단, top level 의 request_time, elapsed_time attribute 는 사용할 수 없습니다.

  • match_callback : 매칭이 완료되면 호출될 함수입니다.

  • target_server : Matchmaking Server 를 선택합니다.

Note

enum TargetServerSelection 으로 자동으로 선택하거나, 직접 PeerId 를 전달할 수 있습니다.

  • progress_callback : 매칭 진행상황이 변경될 때 호출될 함수입니다. 기본 값인 kNullProgrssCallback 을 전달하면 무시됩니다.

Note

MatchmakingServerenable_match_progress_callback 이 true 일 때만 유효합니다.

  • timeout : 지정된 시간이내에 Match 가 성사되지 않으면 자동으로 취소되며, kMRTimeout 값을 result 로 match_callback 이 불립니다. 기본 값인 kNullTimeout 을 전달하면 무시됩니다.

25.1.1.2. 매칭 취소

매칭 요청을 취소합니다.

class MatchmakingClient {
 public:
  static void CancelMatchmaking(const Type &type, const string &player_id,
                                const CancelCallback &cancel_callback);
}
namespace matchmaking
{
  public static class Client
  {
    public static void Cancel (Int64 type,
                               string player_id,
                               CancelCallback cancel_callback);
  }
}
  • type : StartMatchmaking() (C# 은 Client.Start()) 함수에서 입력한 type 과 동일합니다.

  • player_id : StartMatchmaking() 함수에서 입력한 player_id 와 동일합니다.

  • callback : 매칭 요청을 취소한 후 호출될 함수입니다.

    Note

    이미 매칭이 완료되어 MatchCallback 이 호출된 플레이어에 대해 취소를 호출하면 kCRNoRequest result 가 callback 에 전달됩니다.

25.1.2. MANIFEST.json 설정

MANIFEST.json 에 다음과 같이 MatchmakingClient 컴포넌트가 존재하기만 하면 됩니다.

1
2
3
4
...
"MatchmakingClient": {
},
...

25.2. Matchmaking Server

25.2.1. 인터페이스

MatchmakingServer 클래스의 Start() 를 이용해 매치 메이킹 서버를 시작할 수 있습니다. 서버의 Install() 함수에서 이를 호출해줍니다.

class MatchmakingServer {
public:
 typedef MatchmakingClient::MatchId MatchId;
 typedef MatchmakingClient::Type Type;
 typedef MatchmakingClient::Player Player;
 typedef MatchmakingClient::Match Match;
 typedef MatchmakingClient::MatchResult MatchResult;
 typedef MatchmakingClient::CancelResult CancelResult;

 // Match 상태를 나타내며, JoinCallback 에서 매칭 완료인지 아닌지를
 // 결정할 때 사용합니다.
 enum MatchState {
   // 해당 Match 에서 요구하는 플레이어가 모이지 않았을 때 사용합니다.
   kMatchNeedMorePlayer = 0,

   // 해당 Match 에서 요구하는 플레이어가 모두 모였을 때 사용합니다.
   kMatchComplete
 };

 // 플레이어가 매치 요청을 보내면 호출됩니다.
 // 이 함수가 호출되면 매치 요청한 플레이어가 참여 조건 만족하는지 확인합니다.
 // 매치 요청을 한 플레이어가 2명 이상일 경우에만 호출됩니다.
 typedef function<bool(const Player & /*player*/,
                       const Match & /*match*/)> MatchChecker;

 // 매치가 완료되었는지 확인할 때 호출됩니다.
 typedef function<MatchState(const Match & /*match*/)> CompletionChecker;

 // 매치에 유저가 참여했을 때(MatchChecker callback 에서 참여 조건에 만족하여
 // true 가 리턴되면) 호출됩니다. Json 변수인 match.context 에 팀 편성등의
 // 정보를 저장할 때 이용합니다.
 typedef function<void(const Player & /*player*/,
                       Match * /*match*/)> JoinCallback;

 // 매치에서 유저가 나갔을 때 호출됩니다. CancelMatchmaking() 함수를
 // 호출하거나 MatchmakingServer 의 enable_dynamic_match 가 true 일 때 임의로
 // 호출됩니다.
 typedef function<void(const Player & /*player*/,
                       Match * /*match*/)> LeaveCallback;

 // 매치메이킹 처리를 위한 함수입니다.
 static void Start(const MatchChecker &match_checker,
                   const CompletionChecker &completion_checker,
                   const JoinCallback &join_cb, const LeaveCallback &leave_cb);
};
namespace matchmaking
{

  public static class Server
  {
    // Match 상태를 나타내며, JoinCallback 에서 매칭 완료인지 아닌지를
    // 결정할 때 사용합니다.
    public enum MatchState
    {
      // 해당 Match 에서 요구하는 플레이어가 모이지 않았을 때 사용합니다.
      kMatchNeedMorePlayer = 0,
      // 해당 Match 에서 요구하는 플레이어가 모두 모였을 때 사용합니다.
      kMatchComplete
    };

    // 플레이어가 매치 요청을 보내면 호출됩니다.
    // 이 함수가 호출되면 매치 요청한 플레이어가 참여 조건 만족하는지 확인합니다.
    // 매치 요청을 한 플레이어가 2명 이상일 경우에만 호출됩니다.
    public delegate bool MatchChecker (Player player, Match match);

    // 매치가 완료되었는지 확인할 때 호출됩니다.
    public delegate MatchState CompletionChecker (Match match);

    // 매치에 유저가 참여했을 때(MatchChecker callback 에서 참여 조건에 만족하여
    // true 가 리턴되면) 호출됩니다. Json 변수인 match.Context 에 팀 편성등의
    // 정보를 저장할 때 이용합니다.
    public delegate void JoinCallback (Player player, Match match);

    // 매치에서 유저가 나갔을 때 호출됩니다. CancelMatchmaking() 함수를
    // 호출하거나 MatchmakingServer 의 enable_dynamic_match 가 true 일 때 임의로
    // 호출됩니다.
    public delegate void LeaveCallback (Player player, Match match);


    // 매치메이킹 처리를 위한 함수입니다.
    public static void Start (MatchChecker match_checker,
                              CompletionChecker completion_checker,
                              JoinCallback join_callback,
                              LeaveCallback leave_callback);
  }
}

25.2.1.1. 서버 시작

Start() 를 게임 서버의 Install() 에서 호출합니다.

class MatchmakingServer {
public:
 static void Start(const MatchChecker &match_checker,
                   const CompletionChecker &completion_checker,
                   const JoinCallback &join_cb, const LeaveCallback &leave_cb);
}
namespace matchmaking
{
  public static class Server
  {
    public static void Start (MatchChecker match_checker,
                              CompletionChecker completion_checker,
                              JoinCallback join_callback,
                              LeaveCallback leave_callback);
  }
}
  • match_checker

    Match 조건을 확인해야될 때 호출됩니다.

    참여 조건(레벨, 골드 소지량 등)을 판단해서 Match 가능 여부에 따라 true 또는 false 를 반환하면 됩니다.

    • Return: 해당 유저가 매치에 참여할 수 있으면 true.
    • Argument: player : match 에 합류하려는 플레이어
    • Argument: match : 참여가 가능한지 확인할 match instance

    Note

    playermatch.playerscontext 변수에는 추가적으로 elapsed_time 이라는 attribute 이 포함되고, 해당 유저가 얼마나 오랫동안 매칭이 안되고 있는지를 저장합니다. Matchmaking 예제 를 참고하시기 바랍니다.

  • completion_checker

    match_checker 에 의하여 player 가 match 에 합류한 후 호출됩니다.

    match 가 완성되었는지 판단하는 함수입니다.

    • Return: match 가 완성되지 않았다면 kMatchNeedMorePlayer, 완성되었다면 kMatchComplete.
    • Argument: match 완성되었는지 확인할 match instance
  • join_callback

    match_checker 에 의하여 player 가 match 에 합류한 후 호출됩니다.

    • Argument: player : match 에 합류한 플레이어
    • Argument: match : match instance

    Note

    playermatch.playerscontext 변수에는 추가적으로 elapsed_time 이라는 attribute 이 포함되고, 해당 유저가 얼마나 오랫동안 매칭이 안되고 있는지를 저장합니다. 예제 를 참고하시기 바랍니다.

  • leave_callback

    match 에 합류되어 있던 player 가 match 에서 나갈 때 호출됩니다. 또한, Matchmaking client 측에서 CancelMatchmaking() 함수를 호출했을 때도 호출되며, Matchmaking server 측 MANIFEST.json 의 enable_dynamic_match 가 true 여서 서로 다른 두 match 를 조합할 수 있을 때도 호출됩니다.

    join_callback 에서 했던 팀 편성 작업을 취소하는 처리를 합니다.

    • Argument: player : 매칭을 취소한 플레이어
    • Argument: match : 플레이러를 놓친 match 인스턴스에 대한 포인터

Important

매치메이킹 서버에 등록된 콜백으로 넘겨진 argument 는 아이펀 엔진에 의해 관리되는 값이기 때문에 수정하면 안됩니다.

Match 인자의 경우, join_callbackleave_callback 에서만 수정할 수 있습니다.

Note

MANIFEST.json 상에서 MatchmakingServerenable_dynamic_match 가 true 이면 위 함수들이 자주 호출될 수 있습니다. 보다 빠른 매치 성사를 위한 iFun Engine 이 서로 다른 매치를 합치는 과정을 거치기 때문입니다.

25.2.2. MANIFEST.json 설정

MANIFEST 에 다음과 같이 MatchmakingServer 컴포넌트를 추가합니다.

...
"MatchmakingServer": {
  "enable_dynamic_match": true,
  "enable_match_progress_callback": false,
  "concurrent_number_of_players_threshold": 3000
},
...
  • enable_dynamic_match: 만일 true 이면 MatchmakingServer 는 완료되지 않은 매치들을 대상으로 매치들을 합치는 방식으로 매치 대상을 다시 찾습니다.
  • enable_match_progress_callback: 만일 true 이면 Matchmaking client 가 StartMatchmaking() 의 인자로 넘긴 progress_cb 가 주기적으로 호출되고, 이를 통해 매치메이킹의 진행 상황을 볼 수 있습니다.
  • concurrent_number_of_players_threshold: 한번에 처리할 수 있는 매치메이킹 요청 수를 지정합니다. 만일 서버가 이 숫자까지 매치 메이킹을 처리하고 있으면, Mathmaking client 에서 StartMatchmaking() 의 target_server 인자로 kMostNumberOfPlayers 를 지정해도 해당 서버는 선택되지 않습니다. (단, 모든 서버가 이 값을 초과한 경우는 불가피하게 선택될 수 있습니다.)

Important

enable_dynamic_matchenable_match_progress_callback 은 동시에 사용할 수 없습니다.

25.3. Matchmaking 예제

25.3.1. 예제 1: 레벨 차이를 고려한 2:2 매칭

다음은 플레이어의 레벨 차이가 5 이내일 경우 2:2 매치를 하는 예제입니다. 7 초 이상 경과하면 레벨 차이를 무시하고 매치합니다.

25.3.1.1. Matchmaking client

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
enum MatchType {
  kMatch1Vs1 = 0,
  kMatch2Vs2
};


// 2:2 매치를 요청하는 클라이언트 패킷 핸들러입니다.
void OnMatchmakingRequested(const Ptr<Session> &session, const Json &message) {
  string player_id = ...;

  Json context;
  context["LEVEL"] = ...;

  WallClock::Duration timeout = MatchmakingClient::kNullTimeout;
  if (enable_match_timeout) {
    timeout = WallClock::FromMsec(10 * 1000);
  }

  // 매치의 진행상황을 받기 위해 아래처럼 콜백을 등록합니다.
  // 콜백은 MatchmakingServer 의 enable_match_progress_callback 이
  // true 일 때만 호출됩니다.
  MatchmakingClient::ProgressCallback prog_cb =
      MatchmakingClient::kNullProgressCallback;
  if (enable_match_progress) {
    prog_cb = [](const string &player_id,
                 MatchmakingClient::MatchId &match_id,
                 const string &player_id_joined,
                 const string &player_id_left,
                 const Json &match_context) {
      // 1. player_id_joined.empty() 가 아니라면 player_id 에 해당하는 유저가
      //    player_id_joined 에 해당하는 유저와 예비 매칭 되었을 때 불립니다.
      // 2. player_id_left.empty() 가 아니라면 player_id 에 해당하는 유저와
      //    player_id_left 에 해당하는 유저의 예비 매칭이 취소 되었을 때
      //    불립니다.
    };
  }

  // matchmaking 을 요청합니다.
  // 매치가 성사되면 OnMatched 함수가 호출됩니다.
  MatchmakingClient::StartMatchmaking(kMatch2Vs2, player_id, context,
                                      OnMatched,
                                      MatchmakingClient::kMostNumberOfPlayers,
                                      prog_cb, timeout);

  // 클라이언트에 응답을 보내는 작업 등의 후속처리를 합니다.
}


// 매치 취소를 요청하는 클라이언트 패킷 핸들러입니다.
void OnCancelRequested(const Ptr<Session> &session, const Json &message) {
  string player_id = ...;

  // matchmaking 취소를 요청합니다.
  // matchmaking 취소 처리가 완료되면 OnCancelled 함수가 호출됩니다.
  MatchmakingClient::CancelMatchmaking(kMatch2Vs2, player_id, OnCancelled);
}


// 매치가 성사되면 호출됩니다.
void OnMatched(const string &player_id, const MatchmakingClient::Match &match,
               MatchmakingClient::MatchResult result) {
  if (result == MatchmakingClient::kMRAlreadyRequested) {
    return;
  } else if (result == MatchmakingClient::kMRTimeout) {
    return;
  } else if (result == MatchmakingClient::kMRError) {
    return;
  }

  BOOST_ASSERT(result == MatchmakingClient::kMRSuccess);

  // 매치가 성사 되었습니다.
  // match.players 에는 match 에 참여한 player 들이 포함되어 있으나,
  // matchmaking server 에서 match.context 에 아래와 같이 플레이어를
  // 팀으로 구분해 두었습니다.
  MatchmakingClient::MatchId match_id = match.match_id;

  string team_a_player1 = match.context["TEAM_A"][0].GetString();
  string team_a_player2 = match.context["TEAM_A"][1].GetString();

  string team_b_player1 = match.context["TEAM_B"][0].GetString();
  string team_b_player2 = match.context["TEAM_B"][1].GetString();

  // match id 또는 A, B 팀의 각 player id 를 이용하여 대전을 시작하도록 합니다.
  // 클라이언트에 응답을 보내는 작업 등의 후속처리를 합니다.
  ...
}


// 매치가 취소되면 호출됩니다.
void OnCancelled(const string &player_id, MatchmakingClient::CancelResult result) {
  if (result == MatchmakingClient::kCRNoRequest) {
    return;
  } else if (result == MatchmakingClient::kCRError) {
    return;
  }

  BOOST_ASSERT(result == MatchmakingClient::kCRSuccess);

  // 클라이언트에 응답을 보내는 작업 등의 후속처리를 합니다.

  ...
}
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
using funapi;
using funapi.Matchmaking;

class MatchExample
{
  enum MatchType
  {
    kMatch1Vs1 = 0,
    kMatch2Vs2
  };

  static bool enable_match_timeout = true;
  static bool enable_match_progress = true;

  // 2:2 매치를 요청하는 클라이언트 패킷 핸들러입니다.
  static void OnMatchMakingRequested(Session session, JObject message)
  {
    string player_id = ...;

    JObject context = new JObject();
    context["LEVEL"] = ...;

    TimeSpan timeout = TimeSpan.Zero;
    if (enable_match_timeout) {
      timeout = WallClock.FromMsec (10 * 1000);
    }

    // 매치의 진행상황을 받기 위해 아래처럼 콜백을 등록합니다.
    // 콜백은 MatchmakingServer 의 enable_match_progress_callback 이
    // true 일 때만 호출됩니다.
    Client.ProgressCallback progress_cb = null;
    if (enable_match_progress) {
      progress_cb = (string player_id2,
                     Guid match_id,
                     string player_id_joined,
                     string player_id_left,
                     JObject match_context) => {
        // 1. player_id_joined.empty() 가 아니라면 player_id 에 해당하는 유저가
        //    player_id_joined 에 해당하는 유저와 예비 매칭 되었을 때 불립니다.
        // 2. player_id_left.empty() 가 아니라면 player_id 에 해당하는 유저와
        //    player_id_left 에 해당하는 유저의 예비 매칭이 취소 되었을 때
        //    불립니다.
      });
    }

    // matchmaking 을 요청합니다.
    // 매치가 성사되면 OnMatched 함수가 호출됩니다.
    Client.Start ((long) MatchType.kMatch2Vs2,
                  player_id,
                  context,
                  OnMatched,
                  Client.TargetServerSelection.kMostNumberOfPlayers,
                  progress_cb,
                  timeout);

    // 클라이언트에 응답을 보내는 작업 등의 후속처리를 합니다.
  }


  // 매치 취소를 요청하는 클라이언트 패킷 핸들러입니다.
  static void OnCancelRequested(Session session, JObject message)
  {
    string player_id = "player_id";

    // matchmaking 취소를 요청합니다.
    // matchmaking 취소 처리가 완료되면 OnCancelled 함수가 호출됩니다.
    Client.Cancel((long) MatchType.kMatch2Vs2, player_id, OnCancelled);
  }


  static void OnMatched(string player_id, Match match, MatchResult result)
  {
    if (result == MatchResult.kAlreadyRequested)
    {
      return;
    } else if (result == MatchResult.kTimeout) {
      return;
    } else if (result == MatchResult.kError) {
      return;
    }

    Log.Assert (result == MatchResult.kSuccess);

    // 매치가 성사 되었습니다.
    // match.players 에는 match 에 참여한 player 들이 포함되어 있으나,
    // matchmaking server 에서 match.context 에 아래와 같이 플레이어를
    // 팀으로 구분해 두었습니다.
    Guid match_id = match.MatchId;

    string team_a_player1 = (string) match.Context ["TEAM_A"] [0];
    string team_a_player2 = (string) match.Context ["TEAM_A"] [1];
    string team_b_player1 = (string) match.Context ["TEAM_B"] [0];
    string team_b_player2 = (string) match.Context ["TEAM_B"] [1];

    // match id 또는 A, B 팀의 각 player id 를 이용하여 대전을 시작하도록 합니다.
    // 클라이언트에 응답을 보내는 작업 등의 후속처리를 합니다.
    // ...
  }

  static void OnCancelled(string player_id, CancelResult result)
  {
    if (result == CancelResult.kNoRequest) {
      return;
    } else if (result == CancelResult.kError) {
      return;
    }

    Log.Assert (result == CancelResult.kSuccess);

    // 클라이언트에 응답을 보내는 작업 등의 후속처리를 합니다.
    ...
  }
};

25.3.1.2. 매치메이킹 서버

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
// 매치메이킹 클라이언트 예제에서 살펴본 MatchType 과 동일합니다.
enum MatchType {
  kMatch1Vs1 = 0,
  kMatch2Vs2
};


static bool MyServer::Install(const ArgumentMap &arguments) {
  ...

  // MatchmakingServer 역할을 하는 서버에서 Start 함수를 호출하여
  // MatchmakingServer 를 시작합니다. 다음 4 개의 함수를 인자로 전달합니다.
  MatchmakingServer::Start(CheckMatch, CheckCompletion, OnJoined, OnLeft);

  ...
}


// player 가 match 에 참여해도 되는지 검사합니다.
bool CheckMatch(const MatchmakingServer::Player &player,
                const MatchmakingServer::Match &match) {
  if (match.type == kMatch1Vs1) {
    ...
  } else if (match.type == kMatch2Vs2) {
    // 이 match 에 참여한 player 들 중에 match 를 찾기 시작한지
    // 가장 오래된 유저의 경과 시간이 7 초가 넘었으면
    // 레벨 차이를 무시하고 match 에 합류한다고 가정하겠습니다.
    // context 의 elapsed_time 값으로 경과시간을 알 수 있습니다.
    int64_t max_elapsed_time_in_sec =
        player.context["elapsed_time"].GetInteger();
    for (size_t i = 0; i < match.players.size(); ++i) {
      max_elapsed_time_in_sec =
          std::max(elapsed_time_in_sec,
                   match.players[i].context["elapsed_time"].GetInteger());
    }
    if (max_elapsed_time_in_sec > 7) {
      // 7 초가 넘었다면 레벨 차이를 무시하고 match 에 합류시킵니다.
      return true;
    }

    // MatchmakingClient::StartMatchmaking() 함수를 호출할 때 전달했던 context 로부터
    // match 에 참여하려는 플레이어의 레벨을 가져옵니다.
    int64_t player_level = player.context["LEVEL"].GetInteger();

    // match 에 참여한 플레이어들의 레벨을 가져옵니다.
    for (size_t i = 0; i < match.players.size(); ++i) {
      int64_t member_level = match.players[i].context["LEVEL"].GetInteger();

      // match 에 참여하려는 플레이어의 레벨이
      // match 에 참여한 플레이어들과의 레벨 차이가 5보다 크다면
      // match 에 합류시키지 않습니다.
      if (abs(player_level - member_level) > 5) {
        return false;
      }
    }

    // 모든 조건에 만족하여 플레이어를 match 에 합류시킵니다.
    return true;
  }
}


// JoinMatch 함수가 불린 후 호출됩니다. 해당 매치가 완료되었는지 판단합니다.
MatchmakingServer::MatchState CheckCompletion(
    const MatchmakingServer::Match &match) {

  if (match.type == kMatch1Vs1) {
    ...
  } else if (match.type == kMatch2Vs2) {
    if (match.players.size() == 4) {
      return MatchmakingServer::kMatchComplete;
    }

    // 위의 CheckMatch 함수와 마찬가지로 match.players[i].context 의
    // elapsed_time 을 읽어 일정 시간이 지났다면 두 명만 있더라도 AI 를
    // 포함하여 대전을 진행시키는 등의 처리를 할 수 있습니다.
  }

  return MatchmakingServer::kMatchNeedMorePlayer;
}


// 조건을 만족하여 CheckMatch 함수가 true 를 반환하면 이 함수가 호출됩니다.
// 이제 플레이어는 match 에 참여하게 되었습니다.
// 여기서 팀을 구분해 줍니다. (Json 타입인 match->context 에 자유롭게
// match 의 context 를 저장할 수 있습니다.)
void OnJoined(const MatchmakingServer::Player &player,
              MatchmakingServer::Match *match) {

  if (match->type == kMatch1Vs1) {
    ...
  } else if (match->type == kMatch2Vs2) {
    // 만약 match 에 대한 context 가 만들어지지 않았다면
    // 초기 설정을 진행합니다.
    if (match->context.IsNull()) {
      match->context.SetObject();
      match->context["TEAM_A"].SetArray();
      match->context["TEAM_B"].SetArray();
    }

    // match->context 에 구성되어 지는 팀 정보를 토대로
    // 인원수가 적은 팀으로 참가시킵니다.
    if (match->context["TEAM_A"].Size() < match->context["TEAM_B"].Size()) {
      match->context["TEAM_A"].PushBack(player.id);
    } else {
      match->context["TEAM_B"].PushBack(player.id);
    }
  }
}


// Match 에 참여해 있던 player 가 MatchmakingClient::CancelMatchmaking() 을
// 요청했습니다.
// match->context 의 팀 정보에서 해당 player 를 삭제합니다.
void OnLeft(const MatchmakingServer::Player &player,
            MatchmakingServer::Match *match) {

  if (match->type == kMatch1Vs1) {
    ...
  } else if (match->type == kMatch2Vs2) {
    // OnJoined() 에서 match->context 에 입력했던 팀 정보에서 삭제합니다.

    std::vector<Json *> teams;
    teams.push_back(&(match->context["TEAM_A"]));
    teams.push_back(&(match->context["TEAM_B"]));

    // 매치 취소를 요청한 플레이어 id 와 동일하면
    // member list 에서 제거하여 팀 정보에서 플레이어를 삭제합니다.
    BOOST_FOREACH(Json *member_list, teams) {
      Json::ValueIterator itr = member_list->Begin();
      while (itr != member_list->End()) {
        if (player.id == itr->GetString()) {
          member_list->RemoveElement(itr);
          return;
        }
        ++itr;
      }
    }
  }

  // 이 함수 호출이 완료되면 MatchmakingClient::CancelMatchmaking() 함수에
  // 전달한 CancelCallback 이 호출됩니다.
}
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
//////////////////////////////////////////////////////////////////////////////
// game server, matchmaking server 공통
//////////////////////////////////////////////////////////////////////////////

// 매치메이킹 클라이언트 예제에서 살펴본 MatchType 과 동일합니다.
enum MatchType {
  kMatch1Vs1 = 0,
  kMatch2Vs2
};

//////////////////////////////////////////////////////////////////////////////
// matchmaking server
//////////////////////////////////////////////////////////////////////////////

public static void Install(ArgumentMap arguments)
{
  ...
  // MatchmakingServer 역할을 하는 서버에서 Start 함수를 호출하여
  // MatchmakingServer 를 시작합니다. 다음 4 개의 함수를 인자로 전달합니다.
  funapi.Matchmaking.Server.Start (CheckMatch, CheckCompletion,
                                   OnJoined, OnLeft);
  ...
}

public static bool CheckMatch(Player player, Match match)
{
  if (match.MatchType.Equals(MatchType.kMatch1Vs1)) {
    // 별도 설명을 생략합니다. kMatch2Vs2 를 참고하시기 바랍니다.
     ...
  } else if (match.MatchType.Equals(MatchType.kMatch2Vs2)) {
    // 이 match 에 참여한 player 들 중에 match 를 찾기 시작한지
    // 가장 오래된 유저의 경과 시간이 7 초가 넘었으면
    // 레벨 차이를 무시하고 match 에 합류한다고 가정하겠습니다.
    // context 의 elapsed_time 값으로 경과시간을 알 수 있습니다.
    long max_elapsed_time_in_sec =
    (long)player.Context ["elapsed_time"];

    foreach (Player other in match.Players) {
      max_elapsed_time_in_sec = System.Math.Max (
      max_elapsed_time_in_sec, (long)other.Context ["elapsed_time"]);
    }

    if (max_elapsed_time_in_sec > 7) {
      // 7 초가 넘었다면 레벨 차이를 무시하고 match 에 합류시킵니다.
      return true;
    }

    // match 의 다른 player 들과 5 레벨 이상 차이나면
    // match 에 참여하지 않는다고 가정하겠습니다.

    // match 에 참여하려는 플레이어의 레벨을 가져옵니다.
    // 레벨은 MatchmakingClient.Start() 함수를 호출할 때
    // 전달했던 context 에 있습니다.
    long player_level = (long)player.Context ["LEVEL"];

    // match 에 참여한 플레이어들의 레벨을 가져옵니다.
    foreach (Player other in match.Players) {
      long member_level = (long)other.Context ["LEVEL"];
      // match 에 참여하려는 플레이어의 레벨이
      // match 에 참여한 플레이어들과의 레벨 차이가 5보다 크다면
      // match 에 합류시키지 않습니다.
      if (System.Math.Abs (player_level - member_level) > 5) {
        return false;
      }
    }
    // 필요하다면 다른 조건도 검사합니다.
    // 모든 조건에 만족하여 플레이어를 match 에 합류시킵니다.
    return true;
  }
  return false;
}

public static funapi.Matchmaking.Server.MatchState CheckCompletion(
    Match match)
{
  if (match.MatchType.Equals(MatchType.kMatch1Vs1)) {
    // 별도 설명을 생략합니다. kMatch2Vs2 를 참고하시기 바랍니다.
    ...
  }  else if (match.MatchType.Equals(MatchType.kMatch2Vs2)) {
    if (match.Players.Count == 4) {
      // 4 명이 모두 모였습니다.
      return funapi.Matchmaking.Server.MatchState.kMatchComplete;
    }
    // 위의 CheckMatch 함수와 마찬가지로 match.players[i].context 의
    // elapsed_time 을 읽어 일정 시간이 지났다면 두 명만 있더라도 AI 를
    // 포함하여 대전을 진행시키는 등의 처리를 할 수 있습니다.
  }
  return funapi.Matchmaking.Server.MatchState.kMatchNeedMorePlayer;
}

public static void OnJoined(Player player, Match match)
{
  // CheckMatch 함수에서와 마찬가지로 여기서도 kMatch2Vs2 만 처리하겠습니다.
  if (match.MatchType.Equals(MatchType.kMatch1Vs1)) {
    // 별도 설명을 생략합니다. kMatch2Vs2 를 참고하시기 바랍니다.
    ...
  } else if (match.MatchType.Equals(MatchType.kMatch2Vs2)) {
    if (match.Context["TEAM_A"] == null &&
        match.Context["TEAM_B"] == null) {
      match.Context["TEAM_A"] = new JArray();
      match.Context["TEAM_B"] = new JArray();
    }

    // match.Context 에 구성되어 지는 팀 정보를 토대로
    // 인원수가 적은 팀으로 참가시킵니다.
    JArray team_a_arr = (JArray) match.Context ["TEAM_A"];
    JArray team_b_arr = (JArray) match.Context ["TEAM_B"];

    if (team_a_arr.Count < team_b_arr.Count) {
      team_a_arr.Add (player.Id);
    } else {
      team_b_arr.Add (player.Id);
    }
  }
}

public static void OnLeft(Player player, Match match)
{
  // OnJoined 함수에서와 마찬가지로 여기서도 kMatch2Vs2 만 처리하겠습니다.

  if (match.MatchType.Equals(MatchType.kMatch1Vs1)) {
    // 별도 설명을 생략합니다. kMatch2Vs2 를 참고하시기 바랍니다.
    ...
  } else if (match.MatchType.Equals(MatchType.kMatch2Vs2)) {
    // OnJoined() 에서 match.Context 에 입력했던 팀 정보에서 삭제합니다.

    // 편의를 위해 teams 에 팀 정보를 입력합니다.
    List<JArray> teams = new List<JArray> ();
    teams.Add ((JArray) match.Context ["TEAM_A"]);
    teams.Add ((JArray) match.Context ["TEAM_B"]);

    // 매치 취소를 요청한 플레이어 id 와 동일하면
    // member list 에서 제거하여 팀 정보에서 플레이어를 삭제합니다.
    foreach (var team in teams) {
      int index = 0;
      foreach (string id in team) {
        if (id == player.Id) {
          team.RemoveAt (index);
          return;
        }
        index++;
      }
    }
  }
}

25.3.2. 예제 2: 매칭이 안되면 조건을 바꿔서 다시 매칭하기

다음은 Stage 를 나누어 Matchmaking 을 하는 예제입니다. 각 Stage 별로 매치 조건이 다르며, 일정 시간 동안 Matchamking 이 안되면 다음 Stage 에서 시도합니다.

25.3.2.1. 메치메이킹 클라이언트

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
enum Stage {
  kStage1 = 1,
  kStage2,
  kStage3,
  kStageEnd
};


// Matchmaking 을 시작하기 위한 클라이언트 패킷 핸들러입니다.
// Stage 1 로 matchmaking 을 시작합니다.
void OnMatchmakingRequested(const Ptr<Session> &session, const Json &message) {
  StartMatchmaking(session, kStage1);
}


void StartMatchmaking(const Ptr<Session> &session, int64_t stage) {
  string player_id = ...

  Json player_context;
  player_context["player_id"] = player_id;
  player_context["player_level"] = 10;
  player_context["character_level"] = 60;
  player_context["play_count"] = 100;

  LOG(INFO) << "start matchmaking: id=" << player_id << ", stage=" << stage;

  WallClock::Value timeout = WallClock::FromSec(10);
  MatchmakingClient::StartMatchmaking(stage, player_id, player_context,
                                      bind(&OnMatched,
                                           session, stage, _1, _2, _3),
                                      MatchmakingClient::kMostNumberOfPlayers,
                                      MatchmakingClient::kNullProgressCallback,
                                      timeout);
}


// Matchmaking 결과를 처리합니다.
void OnMatched(const Ptr<Session> &session, const int64_t &stage,
               const string &player_id,
               const MatchmakingClient::Match &match,
               MatchmakingClient::MatchResult result) {
  if (result == MatchmakingClient::kMRError) {
    LOG(ERROR) << "matchmaking error.";
    return;
  } else if (result == MatchmakingClient::kMRAlreadyRequested) {
    LOG(WARNING) << "matchmaking already requested.";
    return;
  } else if (result == MatchmakingClient::kMRTimeout) {
    // 타임아웃 되었으면, 다음 stage 로 다시 시도합니다.
    int64_t next_stage = stage + 1;
    if (next_stage == kStageEnd) {
      LOG(WARNING) << "no stage to try.";
      return;
    }
    StartMatchmaking(session, next_stage);
    return;
  }

  LOG(INFO) << "matchmaking completed!";

  // Match 가 성사되었습니다. 게임을 시작하도록 합니다.
  // player_id 에 대한 콜백이며, match.players 에 다른 플레이어들의
  // 정보가 담겨 있습니다.
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
enum Stage {
  kStage1 = 1,
  kStage2,
  kStage3,
  kStageEnd
};


// Matchmaking 을 시작하기 위한 클라이언트 패킷 핸들러입니다.
// Stage 1 로 matchmaking 을 시작합니다.
public static void OnMatchmakingRequested(Session session, JObject message)
{
  StartMatchmaking(session, kStage1);
}


public static void StartMatchmaking(Session session, long stage)
{
  string player_id = ...

  JObject player_context = new JObject ();
  player_context ["player_id"] = player_id;
  player_context ["player_level"] = 10;
  player_context ["character_level"] = 60;
  player_context ["play_count"] = 100;

  Log.Info("start matchmaking: id= {0}, stage= {1}", player_id, stage);

  Client.MatchCallback match_cb = new Client.MatchCallback((
      string match_player_id, Match match, MatchResult result) => {
    OnMatched(session, stage, match_player_id, match, result);
  });

  TimeSpan timeout = WallClock.FromSec(10);
  funapi.Matchmaking.Client.Start(stage,
                                  player_id,
                                  player_context,
                                  match_cb,
                                  Client.TargetServerSelection.kMostNumberOfPlayers,
                                  null,
                                  timeout);
}


// Matchmaking 결과를 처리합니다.
public static void OnMatched(Session session,
                             long stage,
                             string player_id,
                             Match match,
                             MatchResult result) {
  if (result == MatchResult.kError) {
    Log.Error ("matchmaking error.");
    return;
  } else if (result == MatchResult.kAlreadyRequested) {
    Log.Warning ("matchmaking already requested.");
    return;
  } else if (result == MatchResult.kTimeout) {
    // 타임아웃 되었으면, 다음 stage 로 다시 시도합니다.
    long next_stage = stage + 1;
    if (next_stage == (long) Stage.kStageEnd) {
      Log.Warning ("no stage to try again.");
      return;
    }
  StartMatchmaking (session, next_stage);
  return;
  }

  Log.Info ("matchmaking completed!");

  // Match 가 성사되었습니다. 게임을 시작하도록 합니다.
  // player_id 에 대한 콜백이며, match.players 에 다른 플레이어들의
  // 정보가 담겨 있습니다.
}

25.3.2.2. 메치메이킹 서버

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
enum Stage {
  kStage1 = 1,
  kStage2,
  kStage3,
  kStageEnd
};


static bool MyServer::Install(const ArgumentMap &arguments) {
  MatchmakingServer::Start(MatchChecker, CompletionChecker, JoinCb, LeaveCb);
}


// 주어진 player 가 match 에 합류할지 판단합니다.
bool MatchChecker(const MatchmakingServer::Player &player1,
                  const MatchmakingServer::Match &match) {
  BOOST_ASSERT(match.players.size() > 0);

  const MatchmakingServer::Player &player2 = match.players.front();

  if (match.type == kStage1) {
    // player level 로 판단합니다.
    int64_t player_level1 = player1.context["player_level"].GetInteger();
    int64_t player_level2 = player2.context["player_level"].GetInteger();
    if (abs(player_level1 - player_level2) <= 3) {
      return true;
    }
  } else if (match.type == kStage2) {
    // character level 로 판단합니다.
    int64_t char_level1 = player1.context["character_level"].GetInteger();
    int64_t char_level2 = player2.context["character_level"].GetInteger();
    if (abs(char_level1 - char_level2) <= 10) {
      return true;
    }
  } else if (match.type == kStage3) {
    // play count 로 판단합니다.
    int64_t play_count1 = player1.context["character_level"].GetInteger();
    int64_t play_count2 = player2.context["character_level"].GetInteger();
    if (abs(play_count1 - play_count2) <= 30) {
      return true;
    }
  } else {
    BOOST_ASSERT(false);
  }

  return false;
}


// Match 가 완료 되었는지 판단합니다.
MatchmakingServer::MatchState CompletionChecker(const MatchmakingServer::Match &match) {
  if (match.players.size() == 2) {
    return MatchmakingServer::kMatchComplete;
  }
  return MatchmakingServer::kMatchNeedMorePlayer;
}


// Match 에 player 가 참여할 때 불립니다. 필요시 match->context 에 JSON 형태로
// 추가적인 데이터를 저장할 수 있습니다.
void JoinCb(const MatchmakingServer::Player &player,
            MatchmakingServer::Match *match) {
  // do nothing.
}


// Match 에서 player 가 나갈 때 불립니다. player 가 Cancel() 을 호출한 경우입니다.
// JoinCb() 에서 match->context 에 저장한 것을 삭제하는 등의 처리를 합니다.
void LeaveCb(const MatchmakingServer::Player&player,
             MatchmakingServer::Match *match) {
  // do nothing.
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
enum Stage {
  kStage1 = 1,
  kStage2,
  kStage3,
  kStageEnd
};


public static void Install (ArgumentMap arguments)
{
  ...
  funapi.Matchmaking.Server.Start (MatchChecker, CompletionChecker,
                                   JoinCb, LeaveCb);
  ...
}


// 주어진 player 가 match 에 합류할지 판단합니다.
bool MatchChecker(Player player1, Match match) {
  Log.Assert (match.Players.Count > 0);

  Player player2 = match.Players[0];

  if (match.MatchType.Equals(Stage.kStage1))
  {
    // player level 로 판단합니다.
    long player_level1 = (long) player1.Context["player_level"];
    long player_level2 = (long) player2.Context["player_level"];
    if (System.Math.Abs(player_level1 - player_level2) <= 3) {
      return true;
    }
  } else if (match.MatchType.Equals(Stage.kStage2)) {
    // character level 로 판단합니다.
    long char_level1 = (long) player1.Context["character_level"];
    long char_level2 = (long) player2.Context["character_level"];
    if (System.Math.Abs(char_level1 - char_level2) <= 10) {
      return true;
    }
  } else if(match.MatchType.Equals(Stage.kStage3)) {
    // play count 로 판단합니다.
    long play_count1 = (long) player1.Context["character_level"];
    long play_count2 = (long) player2.Context["character_level"];
    if (System.Math.Abs(play_count1 - play_count2) <= 30) {
      return true;
    }
  } else {
    Log.Assert (false);
  }
  return false;
}


// Match 가 완료 되었는지 판단합니다.
funapi.Matchmaking.Server.MatchState CompletionChecker(Match match) {
  if (match.Players.Count == 2) {
    return funapi.Matchmaking.Server.MatchState.kMatchComplete;
  }
  return funapi.Matchmaking.Server.MatchState.kMatchNeedMorePlayer;
}


// Match 에 player 가 참여할 때 불립니다. 필요시 match.Context 에 JSON 형태로
// 추가적인 데이터를 저장할 수 있습니다.
void JoinCb(Player player, Match match) {
  // do nothing.
}


// Match 에서 player 가 나갈 때 불립니다. player 가 Cancel() 을 호출한 경우입니다.
// JoinCb() 에서 match.Context 에 저장한 것을 삭제하는 등의 처리를 합니다.
void LeaveCb(Player player, Match match) {
  // do nothing.
}

25.3.3. 예제 3: Group vs. Group

Matchmaking 은 플레이어 단위가 아니라 그룹 단위로도 가능합니다. 다음은 2 인으로 구성된 그룹에서 평균 레벨이 10 이상 차이 나지 않는 다른 그룹과 matchmaking 하는 예제입니다. 친구끼리 파티를 맺고 다른 파티와 경쟁을 하는 시나리오에 해당됩니다.

25.3.3.1. 메치메이킹 클라이언트

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
enum MatchType {
  kMatch1Vs1 = 0,
  kMatch2Vs2,
  kGroupMatch2Vs2
};


// 클라이언트로부터 Group matchmaking 요청을 수신하는 패킷 핸들러입니다.
// 어떻게 그룹핑 되는지는 패킷 안에 실려 온다고 가정합니다.
void OnMatchmakingRequested(const Ptr<Session> &session, const Json &message) {
  string group_member1_id = ...;
  int64_t group_member1_level = ...;
  string group_member2_id = ...;
  int64_t group_member2_level = ...;

  // group 의 첫 번째 player 가 group 을 대표하도록 합니다.
  string group_id = group_member1_id;

  Json group_ctxt;

  Json group_member1;
  group_member1["id"] = group_member1_id;
  group_member1["level"] = group_member1_level;
  group_ctxt["members"].PushBack(group_member1);

  Json group_member2;
  group_member2["id"] = group_member2_id;
  group_member2["level"] = group_member2_level;
  group_ctxt["members"].PushBack(group_member2);

  MatchmakingClient::StartMatchmaking(kGroupMatch2Vs2, group_id, group_ctxt,
                                      OnMatched,
                                      MatchmakingClient::kMostNumberOfPlayers,
                                      MatchmakingClient::kNullProgressCallback,
                                      WallClock::FromSec(60));
}


void OnMatched(const string &group_id,
               const MatchmakingClient::Match &match,
               MatchmakingClient::MatchResult result) {
  if (result == MatchmakingClient::kMRAlreadyRequested) {
    ...
    return;
  } else if (result == MatchmakingClient::kMRTimeout) {
    ...
    return;
  } else if (result == MatchmakingClient::kMRError) {
    ...
    return;
  }

  BOOST_ASSERT(result == MatchmakingClient::kMRSuccess);

  // matchmaking server 에서 match.context 에 아래와 같이 플레이어의
  // 정보를 입력해 두었습니다.

  MatchmakingClient::MatchId match_id = match.match_id;

  string group_a_player1 = match.context["GROUP_A"][0].GetString();
  string group_a_player2 = match.context["GROUP_A"][1].GetString();

  string group_b_player1 = match.context["GROUP_B"][0].GetString();
  string group_b_player2 = match.context["GROUP_B"][1].GetString();

  // group a 와 group b 의 대전을 시작하는 처리를 합니다.
  // (클라이언트에 응답을 보내는 작업 등)
  ...
}
추후 업데이트 됩니다.

25.3.3.2. 메치메이킹 서버

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
enum MatchType {
  kMatch1Vs1 = 0,
  kMatch2Vs2,
  kGroupMatch2Vs2
};


static bool MyServer::Install(const ArgumentMap &arguments) {
  ...
  MatchmakingServer::Start(CheckMatch, CheckCompletion, OnJoined, OnLeft);
  ...
}


// Group 이 match 에 참여해도 되는지 검사합니다.
bool CheckMatch(const MatchmakingServer::Player &group,
                const MatchmakingServer::Match &match) {

  if (match.type == kMatch1Vs1) {
    ...
  } else if (match.type == kMatch2Vs2) {
    ...
  } else if (match.type == kGroupMatch2Vs2) {

    // 레벨은 MatchmakingClient::StartMatchmaking() 함수를 호출할 때
    // 전달했던 context 에 있습니다.

    int64_t group_a_avg_level = 0;
    {
      const Json &ctxt = group.context;
      int64_t member_1 = ctxt["members"][0]["level"].GetInteger();
      int64_t member_2 = ctxt["members"][1]["level"].GetInteger();
      group_a_avg_level = (member_1 + member_2) / 2;
    }

    int64_t group_b_avg_level = 0;
    {
      const Json &ctxt = match.players.front().context;
      int64_t member_1 = ctxt["members"][0]["level"].GetInteger();
      int64_t member_2 = ctxt["members"][1]["level"].GetInteger();
      group_b_avg_level = (member_1 + member_2) / 2;
    }

    if (abs(group_a_avg_level - group_b_avg_level) > 10) {
      return false;
    }

    // 필요하다면 다른 조건도 검사합니다.

    // 모든 조건에 만족하여 플레이어를 match 에 합류시킵니다.
    return true;
  }
}


// JoinMatch 함수가 불린 후 호출됩니다. 해당 매치가 성사 되었는지 판단합니다.
MatchmakingServer::MatchState CheckCompletion(
    const MatchmakingServer::Match &match) {

  if (match.type == kMatch1Vs1) {
    ...
  } else if (match.type == kMatch2Vs2) {
    ...
  } else if (match.type == kGroupMatch2Vs2) {
    if (match.players.size() == 2) {
      // 2 그룹이 모두 모였습니다. (총 4 명)
      return MatchmakingServer::kMatchComplete;
    }

    // 각 그룹이 고정된 인원수가 아니면(여기서는 2 명) 아래처럼
    // 처리할 수 있습니다.
    // int64_t members = 0;
    // for (size_t i = 0; i < match.players.size(); ++i) {
    //   const MatchmakingServer::Player &group = match.players[i];
    //   members += group.context["members"].Size();
    // }
    // if (members >= 4) {
    //   return MatchmakingServer::kMatchComplete;
    // }

  }

  return MatchmakingServer::kMatchNeedMorePlayer;
}


// 조건을 만족하여 CheckMatch 함수가 true 를 반환하면 이 함수가 호출됩니다.
// 이제 group 은 match 에 참여하게 되었습니다. 여기서 match 의 context 를
// 업데이트 합니다. 이 context 는 OnMatched 에서 쓰게 됩니다.
void OnJoined(const MatchmakingServer::Player &group,
              MatchmakingServer::Match *match) {

  if (match->type == kMatch1Vs1) {
    ...
  } else if (match->type == kMatch2Vs2) {
    ...
  } else if (match->type == kGroupMatch2Vs2) {
    // join 한 group 을 group A 또는 group B 로 지정합니다.

    if (not match->context.HasAttribute("GROUP_A")) {
      match->context["GROUP_A"].SetArray();
      match->context["GROUP_A"].PushBack(group.context["members"][0]["id"]);
      match->context["GROUP_A"].PushBack(group.context["members"][1]["id"]);
      return;
    }

    if (not match->context.HasAttribute("GROUP_B")) {
      match->context["GROUP_B"].SetArray();
      match->context["GROUP_B"].PushBack(group.context["members"][0]["id"]);
      match->context["GROUP_B"].PushBack(group.context["members"][1]["id"]);
      return;
    }

    BOOST_ASSERT(false);
  }
}


// match 에 참여해 있던 group 이 MatchmakingClient::CancelMatchmaking() 을
// 요청했습니다. 위 OnJoined()  함수에서 업데이트한 정보를 삭제합니다.
void OnLeft(const MatchmakingServer::Player &group,
            MatchmakingServer::Match *match) {

  if (match->type == kMatch1Vs1) {
    ...
  } else if (match->type == kMatch2Vs2) {
    ...
  } else if (match->type == kGroupMatch2Vs2) {

    string id = group.context["members"][0]["id"].GetString();

    if (match->context.HasAttribute("GROUP_A")) {
      if (match->context["GROUP_A"][0].GetString() == id) {
        match->context.RemoveAttribute("GROUP_A");
        return;
      }
    }

    if (match->context.HasAttribute("GROUP_B")) {
      if (match->context["GROUP_B"][0].GetString() == id) {
        match->context.RemoveAttribute("GROUP_B");
        return;
      }
    }

    BOOST_ASSERT(false);
  }

  // 이 함수 호출이 완료되면 MatchmakingClient::CancelMatchmaking() 함수에
  // 전달한 CancelCallback 이 호출됩니다.
}
추후 업데이트 됩니다.