27. Contents support part 2: Matchmaking

This feature automatically matches users.

Matchmaking is divided into MatchmakingClient and MatchmakingServer.

  • MatchmakingClient is used when having players join or cancel joining a match. Though the name Client is added, the client is for server use to request matches from the game server; thus the name Client is added.

  • MatchmakingServer is used for the server that handles matchmaking, and when a player asks to join a match, it judges whether that player can join to allow or deny participation.

Tip

You can make a server that only handles matchmaking and run it in the MatchmakingServer flavor.

27.1. Matchmaking Client

27.1.1. Interface

You can start and stop matching using the StartMatchmaking() and CancelMatchmaking() MatchmakingClient classes. (Start() and Cancel() for C#) You can also use GetMatchmakingServerInfo() to check the MatchmakingServer status.

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

  // Structure about player who joined the match
  struct Player {
    // identifier for player
    string id;

    // Custom json data
    // You can specify it when you call StartMatchmaking() to join the match.
    Json context;

    // the location of the server that player has joined.
    Rpc::PeerId location;
  };

  // Structure about match
  struct Match {
    explicit Match(Type _type);
    explicit Match(const MatchId &_match_id, Type _type);

    // Match identifier
    const MatchId match_id;

    // match type
    // You can specify Integer(enum) value as unique match type to identify match.
    const Type type;

    // player list in the match
    std::vector<Player> players;

    // custom context in the match
    // It is shared through same match type
    // and only accessible on specific callback(join, leave) in MatchmakingServer.
    // for example, you may use the context to setup the team.
    Json context;
  };

  // Match result in MatchCallback
  enum MatchResult {
    kMRSuccess = 0,
    kMRAlreadyRequested,
    kMRTimeout,
    kMRError = 1000
  };

  // Cancel result in CancelCallback
  enum CancelResult {
    kCRSuccess = 0,
    kCRNoRequest,
    kCRError = 1000
  };

  // Callback function that will be invoked after match completed
  typedef function<void(const string & /*player_id*/,
                        const Match & /*match*/,
                        MatchResult /*result*/)> MatchCallback;

  // Callback function that will be invoked after match updated
  // only available when 'enable_match_progress_callback' is true
  typedef function<void(const string & /*player_id*/,
                        const MatchId & /*match_id*/,
                        const string & /*player_id_joined*/,
                        const string & /*player_id_left*/)> ProgressCallback;

  typedef function<void(const string & /*player_id*/,
                        const Match & /*match*/,
                        const string & /*player_id_joined_or_updated*/,
                        const string & /*player_id_left*/)> ProgressCallback2;

  // Callback function that will be invoked after match cancelled
  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);

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

  // Match 상대를 찾는 요청을 보냅니다.
  // target_server 인자로 전달된 매치메이킹 서버로 요청합니다.
  static void StartMatchmaking2(const Type &type, const string &player_id,
      const Json &player_context, const MatchCallback &match_callback,
      const Rpc::PeerId &target_server,
      const ProgressCallback2 &progress_callback = kNullProgressCallback2,
      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);

    public delegate void ProgressCallback2(string player_id,
                                           Match match,
                                           string player_id_joined_or_updated,
                                           string player_id_left);


    // 매칭 요청을 취소한 후 호출될 함수입니다.
    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))

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

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

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

Note

The ProgressCallback 2 callback is available in version 1.0.0-2768 Experimental and higher.

27.1.1.1. Starting matching

Sends a request to find match partners.

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);

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

  static void StartMatchmaking2(const Type &type, const string &player_id,
      const Json &player_context, const MatchCallback &match_callback,
      const Rpc::PeerId &target_server,
      const ProgressCallback2 &progress_callback = kNullProgressCallback2,
      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))

    public static void Start2 (Int64 type,
                               string player_id,
                               JObject player_context,
                               MatchCallback match_callback,
                               TargetServerSelection target_server = TargetServerSelection.kRandom,
                               ProgressCallback2 progress_callback = null,
                               TimeSpan timeout = default(TimeSpan))

    public static void Start2 (Int64 type,
                               string player_id,
                               JObject player_context,
                               MatchCallback match_callback,
                               System.Guid target_server,
                               ProgressCallback2 progress_callback = null,
                               TimeSpan timeout = default(TimeSpan))
  }
}
  • type: The integer value meaning the match type. You can define this yourself, and differentiates matches of the same type on the MatchmakingServer.

  • player_id: Identifier for match participants.

  • player_context: The value used to judge participant ability level on the MatchmakingServer. You can freely specify match requirements in JSON format.

    Important

    You cannot use top-level request_time or elapsed_time attributes.

  • match_callback: Function invoked when matchmaking is complete.

  • target_server: Selects the matchmaking server.

Note

This can be automatically selected with enum TargetServerSelection or by sending PeerId directly.

  • progress_callback: Function invoked when matching progress changes. Ignored if the default value kNullProgrssCallback is sent.

Note

Only valid when the MatchmakingServer’s enable_match_progress_callback is true.

  • timeout: The match is automatically canceled if not achieved within a set time, and match_callback is called with the kMRTimeout value as the result. Ignored if the default value kNullTimeout is sent.

27.1.1.2. Canceling matching

The matching request is canceled.

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: Same as the type entered in StartMatchmaking() (Client.Start() in C#).

  • player_id: Same as the player_id entered in StartMatchmaking().

  • callback: Function invoked after a matching request is canceled.

    Note

    If cancel is invoked for players for whom MatchCallback was invoked when matching was completed, kCRNoRequest is passed to the callback.

27.1.1.3. 매치 진행 상태 확인

진행 상태 콜백을 통해 매치에 참가하거나 매치를 떠난 플레이어를 확인할 수 있습니다.

// 매칭의 진행상황이 변경되면 호출될 함수입니다.
// 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*/)> ProgressCallback;

typedef function<void(const string & /*player_id*/,
                      const Match & /*match*/,
                      const string & /*player_id_joined_or_updated*/,
                      const string & /*player_id_left*/)> ProgressCallback2;
// 매칭의 진행상황이 변경되면 호출될 함수입니다.
// 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);

public delegate void ProgressCallback2(string player_id,
                                        Match match,
                                        string player_id_joined_or_updated,
                                        string player_id_left);

27.1.1.4. 플레이어 컨텍스트 확인

매치 플레이어 컨텍스트 정보는 매치가 완료되거나 아래에서 설명할 상태 변경 콜백 호출 시 Match 정보를 통해 받을 수 있습니다. 각 플레이어의 context는 Match 안에 포함된 players 목록을 통해 볼 수 있습니다.

// Match 에 참여한 플레이어의 정보입니다.
struct Player {
  ...

  // 플레이어 정보가 담긴 context로 Match 참여를 위한 StartMatchmaking() 함수를 호출할 때 전달합니다.
  // 이 정보는 매치 완료 또는 ProgressCallback2를 통해 받은 Match의
  // players 목록 안에서 확인할 수 있습니다.
  Json context;

  ...
}

...

// Match 에 대한 정보입니다.
struct Match {
  ...

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

  ...
}
// Match 에 참여한 플레이어의 정보입니다.
public struct Player
{
  ...

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

  ...
}

// Match 에 대한 정보입니다.
public struct Match
{
  ...

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

  ...
}

context 형태는 다음과 같이 정의됩니다.

ex) StartMatchmaking 시 {"level":22} 를 context에 넣었을 경우

{
  "request_time":28445593765, // 매치 요청 시간(단위: 타임스탬프)
  "elapsed_time":0, // 매치 요청 후 지난 시간(단위: 초)
  "user_data":
  {
    "level":22
  }
}
ex) StartMatchmaking 시 {"level":22} 를 context에 넣었을 경우

{
  "request_time":28445593765, // 매치 요청 시간(단위: 타임스탬프)
  "elapsed_time":0, // 매치 요청 후 지난 시간(단위: 초)
  "user_data":
  {
    "level":22
  }
}

27.1.1.5. 매치 진행 상태 변경

매치메이킹 클라이언트에서는 매치 대기 중인 플레이어의 정보(context)를 변경할 수 있습니다. 이 때 변경된 플레이어의 정보는 ProgressCallback2의 인자(player_id_joined_or_updated)와 Match 안에 포함된 players 목록을 통해 볼 수 있으며 최종적으로는 매치 완료 시점에서 Match 안에 포함된 players를 확인할 수 있습니다.

static void UpdateMatchPlayerContext(
  const Type &type, const string &player_id, const Json &new_context);
namespace matchmaking
{
  public static class Client
  {
    public static void UpdateMatchPlayerContext (Int64 type,
                                                 string player_id,
                                                 JObject player_context);
  }
}

Note

c++ 은 1.0.0-2768 Experimental 버전 이상에서, c# 은 1.0.0-xxxx Experimental 버전 이상에서만 사용 가능합니다.

27.1.2. MANIFEST.json settings

Include a MatchmakingClient component in MANIFEST.json as follows.

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

27.2. Matchmaking Server

27.2.1. Interface

You can use the matchmaking server class Start() to start the matchmaking server. This is invoked by the server’s Install() function.

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);
  }
}

27.2.1.1. Starting the server

Start() is invoked by the game server’s 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

    Invoked when match requirements must be checked.

    Requirements for participation (level, gold owned, etc.) are judged and this is returned as true or false depending on whether a match is available.

    • Return: True if this user can join the match.

    • Argument: player: Player to join the match

    • Argument: match: Match instance to evaluate

    Note

    An attribute called elapsed_time is additionally included in player and match.players context variables and saves the time this user has remained unmatched. Please refer to Matchmaking examples.

  • completion_checker

    Invoked by match_checker after a player joins a match.

    This function checks whether a match has been made.

    • Return: If there was no match, then kMatchNeedMorePlayer; if there was a match, then kMatchComplete.

    • Argument: Match instance to evaluate for whether a match was made

  • join_callback

    Invoked by match_checker after a player joins a match.

    • Argument: player: Player who joined the match

    • Argument: match : match instance

    Note

    An attribute called elapsed_time is additionally included in player and match.players context variables and saves the time this user has remained unmatched. Here are some examples.

  • leave_callback

    Invoked when a player who had joined a match leaves it. Also invoked when the CancelMatchmaking() function is invoked by the matchmaking client, and two different matches can be combined when the matchmaking server’s MANIFEST.json enable_dynamic_match is true.

    Handles cancellation of team formation from join_callback.

    • Argument: player: Player for whom matching was canceled

    • Argument: match: Pointer for match instances missing players

Important

This cannot be modified because arguments passed as callbacks registered on the matchmaking server are managed by iFun Engine.

Only join_callback and leave_callback match parameters can be modified.

Note

If the MatchmakingServer’s enable_dynamic_match is true in MANIFEST.json, the above functions may be invoked often. This is because iFun Engine combines different matches to achieve faster matches.

27.2.2. MANIFEST.json settings

Add the MatchmakingServer component to MANIFEST as follows.

...
"MatchmakingServer": {
  "enable_dynamic_match": true,
  "enable_match_progress_callback": false,
  "concurrent_number_of_players_threshold": 3000
},
...
  • enable_dynamic_match: If true, the MatchmakingServer searches again for matches by combining incomplete matches.

  • enable_match_progress_callback: If true, progress_cb, passed as a StartMatchmaking() parameter by the matchmaking client, is periodically invoked, and matchmaking progress can be checked through this.

  • concurrent_number_of_players_threshold: Sets the number of matchmaking requests to handle concurrently. If a server’s number of matchmaking requests reaches this number, that server is not matched even if kMostNumberOfPlayers is set in the matchmaking client on StartMatchmaking()’s target_server. (Note that if all servers exceed this value, they may be unavoidably selected.)

Important

You cannot use enable_dynamic_match and enable_match_progress_callback simultaneously.

27.3. Matchmaking examples

27.3.1. Example 1: 2:2 matching with level differences

The following is an example of a 2:2 match when the level difference between players is less than 5. If 7 seconds are exceeded, a match is made regardless of level differences.

27.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
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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
enum MatchType {
  kMatch1Vs1 = 0,
  kMatch2Vs2
};

const bool enable_match_timeout = true;
const bool enable_match_progress = true;

// 2:2 매치 요청을 처리하는 클라이언트 메시지 핸들러입니다.
void OnMatchmakingRequested(const Ptr<Session> &session,
                                const Json &message) {
  // 클라이언트는 다음 메시지 형태로 매칭을 요청한다고 가정합니다.
  // {
  //   "account_id": "id",
  //   "user_data": {
  //      "level": 70,
  //      "ranking_score": 1500,
  //      ...
  //   }
  // }

  // 1. 계정 정보, AccountManager 로 로그인한 계정 ID 를 입력합니다.
  const string &account_id = message["account_id"].GetString();

  // 2. user_data, 매치메이킹 시 사용할 유저 데이터 입니다.
  //   "user_data": {
  //      "level": 70,
  //      "ranking_score": 1500,
  //      ...
  //   }
  const Json &user_data = message["user_data"];

  // 3. 서버 선택 방법
  //    - kRandom: 서버를 랜덤하게 선택합니다.
  //    - kMostNumberOfPlayers; 사람이 가장 많은 서버에서 매치를 수행합니다.
  //    - kLeastNumberOfPlayers; 사람이 가장 적은 서버에서 매치를 수행합니다.
  const MatchmakingClient::TargetServerSelection target_selection =
      MatchmakingClient::kRandom;

  // 4. 매치 타임아웃 설정
  //    - 타임아웃이 필요하지 않은 경우 kNullTimeout 을 설정합니다.
  const WallClock::Duration timeout = MatchmakingClient::kNullTimeout;
  if (enable_match_timeout) {
    // 60초 타임아웃
    timeout = WallClock::FromSec(60);
  }

  // 5. 매칭 상황을 받기 위해 아래처럼 콜백을 등록합니다.
  // MANIFEST.json 파일의 MatchmakingServer 항목에
  // `enable_match_progress_callback` 값을 true 로 설정해야 콜백을 호출합니다.
  // 두 유저 ID(참가/나간) 값 모두 빈 콜백은 호출하지 않습니다.
  MatchmakingClient::ProgressCallback2 progress_cb =
      MatchmakingClient::kNullProgressCallback;
  if (enable_match_progress) {
    progress_cb = [](const string &player_id,
                     const MatchmakingClient::Match &match,
                     const string &player_id_joined_or_updated,
                     const string &player_id_left,
                     const Json &match_context) {
      // 1. player_id_joined.empty() 가 아니라면 유저(player_id)가 새로운
      //    유저(player_id_joined_or_updated)와 매칭 대기열에 참여한 경우입니다.
      // 2. player_id_left.empty() 가 아니라면 유저(player_id)가 속한
      //    매칭 대기열에서 기존 유저(player_id_left)가 나강 경우를 뜻합니다.
    };
  }

  // 매칭을 요청합니다. OnMatchCompleted 함수에서 결과를 확인할 수 있습니다.
  MatchmakingClient::StartMatchmaking2(
      kMatch2Vs2, account_id, user_data,
      OnMatchCompleted, target_selection,
      progress_cb, timeout);

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

// 매치가 성사되면 호출됩니다.
void OnMatched(const string &player_id,
                 const MatchmakingClient::Match &match,
                 MatchmakingClient::MatchResult result) {
  //
  // 매치 결과를 받는 콜백 핸들러입니다. 매치를 요청한 각 플레이어를 대상으로
  // 핸들러를 호출합니다.
  // 매치를 정상적으로 성사한 경우: match_id 로 직렬화 한 이벤트에서 실행합니다.
  // 매치 성사에 실패한 경우: 직렬화하지 않은 이벤트에서 실행합니다.
  //
  // 매치메이킹에 참여하는 사람이 많지 않거나 matchmaking_server_wrapper.cc 에서
  // 정의한 매치 조건이 까다로운 경우, 클라이언트가 매치메이킹을 요청하는 시점과
  // 매치가 성사되는 이 시점의 차가 커질 수 있습니다.
  //
  // 따라서 클라이언트는 매치메이킹 요청을 보낸 후 이 핸들러에서 메시지를 보내기
  // 전까지 다른 행동을 할 수 있도록 만들어야 합니다.
  //

  if (result != MatchmakingClient::MatchResult::kMRSuccess) {
    // MatchmakingClient::MatchResult 결과에 따라 어떻게 처리할 지 결정합니다.
    if (result == MatchmakingClient::MatchResult::kMRError) {
      // 엔진 내부 에러입니다.
      // 일반적으로 RPC 서비스를 사용할 수 없을 때 발생합니다.
    } else if (result == MatchmakingClient::MatchResult::kMRAlreadyRequested) {
      // 이미 이 account_id 로 매치메이킹 요청을 했습니다. 매치 타입이 달라도
      // ID 가 같으면 이미 매칭을 요청한 것으로 간주합니다.
    } else if (result == MatchmakingClient::MatchResult::kMRTimeout) {
      // 매치메이킹 요청을 지정한 시간 안에 수행하지 못했습니다.
      // 더 넓은 범위의 매치 재시도, 단순 재시도를 할 수 있습니다.
    }

    return;
  }

  LOG_ASSERT(result == MatchmakingClient::kMRSuccess);

  // 매칭을 성공적으로 완료했습니다.

  // 매치 결과 ID 입니다.
  const MatchmakingClient::MatchId match_id = match.match_id;
  // 매치메이킹 서버에서 정의한 JSON 컨텍스트 입니다.
  // 매치메이킹 요청(StartMatchmaking2) 시 인자로 넣는 context 와 다른
  // context 입니다.
  const Json &match_context = match.context;
  // 매치메이킹 요청 시 지정한 매치 타입입니다.
  const int64_t match_type = match.type;
  // 이 매칭에 참여한 유저 목록입니다.
  const std::vector<MatchmakingClient::Player> &players = match.players;

  // join_callback, leave_callback 에서 정의한 팀 정보를 가져옵니다.
  // JSON 스키마 정보는 MatchmakingServer 예제를 참고해주세요.
  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();

  // 매칭 ID, A, B 팀의 각 유저 ID 를 이용하여 대전을 시작합니다.
  // 클라이언트에 응답을 보내는 작업 등의 후속처리를 합니다.
  // ... 생략 ...
}


// 2:2 매칭 취소를 요청하는 클라이언트 메시지 핸들러입니다.
void OnMatchCancelRequested(const Ptr<Session> &session,
                                const Json &message) {
  // 클라이언트는 다음 메시지 형태로 매칭을 취소합니다.
  // {
  //   "account_id": "id",
  // }

  const string &account_id = message["account_id"].GetString();

  // 매칭을 취소합니다.
  // 매칭 취소 결과는 OnCancelled 함수에서 확인할 수 있습니다.
  MatchmakingClient::CancelMatchmaking(
      kMatch2Vs2, account_id, OnMatchCancelled);
}


// 매칭 취소를 처리한 후 호출합니다.
void OnMatchCancelled(const string &account_id,
                        MatchmakingClient::CancelResult result) {
  if (result == MatchmakingClient::kCRNoRequest) {
    // 매칭 요청이 없는 유저 ID 로 취소를 요청한 경우. 간발의 차로 매칭 후
    // 취소 요청을 처리할 경우 발생할 수 있습니다.
    return;
  } else if (result == MatchmakingClient::kCRError) {
    // 엔진 내부 에러입니다. RPC 서비스 사용이 불가능한 경우 발생할 수 있습니다.
    return;
  }

  LOG_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
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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
using funapi;
using funapi.Matchmaking;

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

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

    // 2:2 매치 요청을 처리하는 클라이언트 메시지 핸들러입니다.
    public static void OnMatchMakingRequested (Session session, JObject message)
    {
        // 클라이언트는 다음 메시지 형태로 매칭을 요청한다고 가정합니다.
        // {
        //   "account_id": "id",
        //   "user_data": {
        //      "level": 70,
        //      "ranking_score": 1500,
        //      ...
        //   }
        // }

        // 1. 계정 정보, AccountManager 로 로그인한 계정 ID 를 입력합니다.
        String account_id = message["account_id"].ToString();

        // 2. user_data, 매치메이킹 시 사용할 유저 데이터 입니다.
        //   "user_data": {
        //      "level": 70,
        //      "ranking_score": 1500,
        //      ...
        //   }
        JObject user_data = message["user_data"].ToJObject();

        // 3. 서버 선택 방법
        //    - kRandom: 서버를 랜덤하게 선택합니다.
        //    - kMostNumberOfPlayers; 사람이 가장 많은 서버에서 매치를 수행합니다.
        //    - kLeastNumberOfPlayers; 사람이 가장 적은 서버에서 매치를 수행합니다.
        Client.TargetServerSelection target_server = Client.TargetServerSelection.kRandom;

        // 4. 매치 타임아웃 설정
        //    - 타임아웃이 필요하지 않은 경우 kNullTimeout 을 설정합니다.
        TimeSpan timeout = Client.kInfinite;
        if (enable_match_timeout)
        {
            // 60초 타임아웃
            timeout = WallClock.FromSec(60);
        }

        // 5. 매칭 상황을 받기 위해 아래처럼 콜백을 등록합니다.
        // MANIFEST.json 파일의 MatchmakingServer 항목에
        // `enable_match_progress_callback` 값을 true 로 설정해야 콜백을 호출합니다.
        // 두 유저 ID(참가/나간) 값 모두 빈 콜백은 호출하지 않습니다.
        Client.ProgressCallback2 progress_cb = null;
        if (enable_match_progress)
        {
            progress_cb = (string player_id,
                           Match match,
                           string player_id_joined_or_updated,
                           string player_id_left) =>
            {
                // 1. player_id_joined.empty() 가 아니라면 유저(player_id)가 새로운
                //    유저(player_id_joined_or_updated)와 매칭 대기열에 참여한 경우입니다.
                // 2. player_id_left.empty() 가 아니라면 유저(player_id)가 속한
                //    매칭 대기열에서 기존 유저(player_id_left)가 나강 경우를 뜻합니다.
            };
        }

        // 매칭을 요청합니다. OnMatchCompleted 함수에서 결과를 확인할 수 있습니다.
        Client.Start2((long)MatchType.kMatch2Vs2, account_id, user_data,
            OnMatchCompleted, target_server, progress_cb, timeout);
    }

    static void OnMatchCompleted (string account_id, Match match, MatchResult result)
    {
        //
        // 매치 결과를 받는 콜백 핸들러입니다. 매치를 요청한 각 플레이어를 대상으로
        // 핸들러를 호출합니다.
        // 매치를 정상적으로 성사한 경우: match_id 로 직렬화 한 이벤트에서 실행합니다.
        // 매치 성사에 실패한 경우: 직렬화하지 않은 이벤트에서 실행합니다.
        //
        // 매치메이킹에 참여하는 사람이 많지 않거나 matchmaking_server_wrapper.cc 에서
        // 정의한 매치 조건이 까다로운 경우, 클라이언트가 매치메이킹을 요청하는 시점과
        // 매치가 성사되는 이 시점의 차가 커질 수 있습니다.
        //
        // 따라서 클라이언트는 매치메이킹 요청을 보낸 후 이 핸들러에서 메시지를 보내기
        // 전까지 다른 행동을 할 수 있도록 만들어야 합니다.
        //
        if (result != MatchResult.kSuccess)
        {
            // MatchmakingClient::MatchResult 결과에 따라 어떻게 처리할 지 결정합니다.
            if (result == MatchResult.kError)
            {
                // 엔진 내부 에러입니다.
                // 일반적으로 RPC 서비스를 사용할 수 없을 때 발생합니다.
            }
            else if (result == MatchResult.kAlreadyRequested)
            {
                // 이미 이 account_id 로 매치메이킹 요청을 했습니다. 매치 타입이 달라도
                // ID 가 같으면 이미 매칭을 요청한 것으로 간주합니다.
            }
            else if (result == MatchResult.kTimeout)
            {
                // 매치메이킹 요청을 지정한 시간 안에 수행하지 못했습니다.
                // 더 넓은 범위의 매치 재시도, 단순 재시도를 할 수 있습니다.
            }
            return;
        }

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

        // 매칭을 성공적으로 완료했습니다.

        // 매치 결과 ID 입니다.
        System.Guid match_id = match.MatchId;
        // 매치메이킹 서버에서 정의한 JSON 컨텍스트 입니다.
        // 매치메이킹 요청(StartMatchmaking2) 시 인자로 넣는 context 와 다른
        // context 입니다.
        JObject match_context = match.Context;
        // 매치메이킹 요청 시 지정한 매치 타입입니다.
        long match_type = match.MatchType;
        // 이 매칭에 참여한 유저 목록입니다.
        IReadOnlyCollection<Player> players = match.Players;

        // join_callback, leave_callback 에서 정의한 팀 정보를 가져옵니다.
        // JSON 스키마 정보는 MatchmakingServer 예제를 참고해주세요.
        String team_a_player1 = match.Context["TEAM_A"][0].ToString();
        String team_a_player2 = match.Context["TEAM_A"][1].ToString();

        String team_b_player1 = match.Context["TEAM_B"][0].ToString();
        String team_b_player2 = match.Context["TEAM_B"][1].ToString();

        // 매칭 ID, A, B 팀의 각 유저 ID 를 이용하여 대전을 시작합니다.
        // 클라이언트에 응답을 보내는 작업 등의 후속처리를 합니다.
        // ... 생략 ...
    }

    // 2:2 매치 요청을 처리하는 클라이언트 메시지 핸들러입니다.
    public static void OnMatchCancelRequested (Session session, JObject message)
    {
        // 클라이언트는 다음 메시지 형태로 매칭을 취소합니다.
        // {
        //   "account_id": "id",
        // }

        String account_id = message["account_id"].ToString();

        // 매칭을 취소합니다.
        // 매칭 취소 결과는 OnCancelled 함수에서 확인할 수 있습니다.
        Client.Cancel((long)MatchType.kMatch2Vs2, account_id, OnMatchCancelled);
    }

    static void OnMatchCancelled (String account_id, CancelResult result)
    {
        if (result == CancelResult.kNoRequest)
        {
            // 매칭 요청이 없는 유저 ID 로 취소를 요청한 경우. 간발의 차로 매칭 후
            // 취소 요청을 처리할 경우 발생할 수 있습니다.
            return;
        }
        else if (result == CancelResult.kError)
        {
            // 엔진 내부 에러입니다. RPC 서비스 사용이 불가능한 경우 발생할 수 있습니다.
            return;
        }

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

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

27.3.1.2. Matchmaking server

  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
152
153
154
155
156
157
158
159
160
// 매치메이킹 클라이언트 예제에서 살펴본 MatchType 과 동일합니다.
enum MatchType {
  kMatch1Vs1 = 0,
  kMatch2Vs2
};


static bool MyServer::Install(const ArgumentMap &arguments) {
  // 매치메이킹 서버 콤포넌트를 실행할 게임 서버에서 Start 함수를 호출하여
  // 핸들러를 등록합니다. 4 개의 콜백 함수를 인자로 전달합니다.
  // 빈 핸들러는 허용하지 않습니다.
  MatchmakingServer::Start(
      CheckPlayerRequirements, CheckMatchRequirements, OnJoined, OnLeft);
}


// player 가 match 에 참여해도 되는지 검사합니다.
bool CheckPlayerRequirements(const MatchmakingServer::Player &player,
                             const MatchmakingServer::Match &match) {
  //
  // 매치 조건 검사 핸들러 함수
  //

  const string &account_id = player.id;
  const Json &user_context = player.context;
  // 매칭 요청 시 user_data 로 지정한 값입니다.
  const Json &user_data = player.context["user_data"];

  // 플레이어가 이 매치에 참여해도 괜찮은 지 검사합니다.
  if (match.type == kMatch1Vs1) {
    // 이 예제에서는 1:1 조건 예제를 생략합니다.
  } else if (match.type == kMatch2Vs2) {

    // 매칭에 참여한 유저 중 한 사람이라도 요청한 지 7 초가 넘었다면
    // 매칭에 참여하는 규칙입니다.
    // context 의 elapsed_time 값으로 경과시간을 알 수 있습니다.
    int64_t max_elapsed_time_in_sec =
        user_context["elapsed_time"].GetInteger();
    for (size_t i = 0; i < match.players.size(); ++i) {
      int64_t elapsed_time_in_sec =
          match.players[i].user_context["elapsed_time"].GetInteger();
      max_elapsed_time_in_sec =
          std::max(max_elapsed_time_in_sec, elapsed_time_in_sec);
    }

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

    // MatchmakingClient::StartMatchmaking() 함수를 호출할 때 전달했던
    // context 에서 매칭메이킹을 요청한 사용자의 유저 레벨을 가져옵니다.
    int64_t user_level = user_data["level"].GetInteger();

    // 이 매칭에 참여 중인 다른 유저들의 레벨을 가져와 비교합니다.
    for (size_t i = 0; i < match.players.size(); ++i) {
      if (match.players[i].id == account_id) {
        // match.players 객체에는 요청자 자신도 포함되어 있기 때문에
        // 나 자신은 제외합니다.
        continue;
      }

      const Json &member_data = match.players[i].context["user_data"];
      int64_t member_level = member_data["level"].GetInteger();

      // 매칭에 참여 중인 유저들의 레벨과 매칭에 참여하는 유저의 레벨을 비교합니다.
      // 한 사람이라도 레벨 차이가 5를 초과하면 매칭에 참여하지 않습니다.
      if (abs(user_level - member_level) > 5) {
        return false;
      }
    }

    // 이 유저는 모든 조건을 만족합니다. 매칭에 참여합니다.
    return true;
  }
}


MatchmakingServer::MatchState CheckMatchRequirements(
    const MatchmakingServer::Match &match) {

  // CheckPlayerRequirements 함수에서 true 를 반환하면 호출합니다.
  // 매칭을 완료하는 데 더 많은 사람이 필요한 지 검사합니다.
  if (match.type == kMatch1Vs1) {

    // 이 예제에서는 1:1 조건 예제를 생략합니다.

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

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

    // CheckPlayerRequirements 함수와 마찬가지로 match.players[i].context
    // 객체의 elapsed_time 을 읽어 일정 시간이 지났다면, AI 를 추가하여 대전을
    // 진행하는 로직을 이 곳에서 처리할 수 있습니다.
    // ... 생략 ...
  }

  return MatchmakingServer::kMatchNeedMorePlayer;
}


void OnJoined(const MatchmakingServer::Player &player,
              MatchmakingServer::Match *match) {
  // CheckPlayerRequirements 함수에서 true 를 반환하면 호출합니다.
  // 이제 플레이어는 이 매칭에 참여합니다.
  // 여기서 팀을 구분해 줍니다. (Json 타입인 match->context 에 자유롭게
  // match 의 context 를 저장할 수 있습니다.)
  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);
    }
  }
}


void OnLeft(const MatchmakingServer::Player &player,
            MatchmakingServer::Match *match) {
  // 매칭에 참여 중인 유저가 MatchmakingClient::CancelMatchmaking 함수를 호출하면
  // 이 핸들러를 호출합니다.
  // 이 함수 호출이 끝나면 MatchmakingClient::CancelMatchmaking() 함수에
  // 전달한 CancelCallback 을 호출합니다.
  if (match->type == kMatch1Vs1) {
    // 이 예제에서는 1:1 조건 예제를 생략합니다.
  } else if (match->type == kMatch2Vs2) {
    // OnJoined() 에서 추가했던 팀 정보에서 해당 유저를 삭제합니다.

    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;
      }
    }
  }
}
  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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
public class Server
{
    public static bool Install(ArgumentMap arguments)
    {
        ...
        funapi.Matchmaking.Server.Start(
            MatchExample.CheckPlayerRequirements,
            MatchExample.CheckMatchRequirements,
            MatchExample.OnJoined, MatchExample.OnLeft);
        ...
    }
}

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

    public static bool CheckPlayerRequirements (Player player, Match match)
    {
        //
        // 매치 조건 검사 핸들러 함수
        //


        // 플레이어가 이 매치에 참여해도 괜찮은 지 검사합니다.
        string account_id = player.Id;

        if (match.MatchType == (long)MatchType.kMatch1Vs1)
        {
            // 이 예제에서는 1:1 조건 예제를 생략합니다.
        }
        else if (match.MatchType == (long)MatchType.kMatch2Vs2)
        {
            JObject user_context = player.Context;

            // 엔진 내부에서 추가하는 값입니다.
            Log.Assert(user_context["request_time"] != null);
            Log.Assert(user_context["elapsed_time"] != null);
            Log.Assert(user_context["user_data"] != null);

            // 매칭 요청 시 user_data 로 지정한 값입니다.
            JObject user_data = user_context["user_data"].ToJObject();


            // 매칭에 참여한 유저 중 한 사람이라도 요청한 지 7 초가 넘었다면
            // 매칭에 참여하는 규칙입니다.
            // context 의 elapsed_time 값으로 경과시간을 알 수 있습니다.
            long max_elapsed_time_in_sec = user_context["elapsed_time"].Value<long>();
            foreach (Player p in match.Players)
            {
                long elapsed_time_in_sec = p.Context["elapsed_time"].Value<long>();
                max_elapsed_time_in_sec =
                    Math.Max(max_elapsed_time_in_sec, elapsed_time_in_sec);
            }

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

            // MatchmakingClient::StartMatchmaking() 함수를 호출할 때 전달했던 context 로부터
            // match 에 참여하려는 플레이어의 레벨을 가져옵니다.
            long user_level = user_data["level"].Value<long>();
            foreach (Player p in match.Players)
            {
                if (p.Id == account_id)
                {
                    continue;
                }

                JObject member_data = p.Context["user_data"].ToJObject();
                long member_level = member_data["level"].Value<long>();

                if (Math.Abs(user_level - member_level) > 5)
                {
                    return false;
                }

            }
        }

        // 이 유저는 모든 조건을 만족합니다. 매칭에 참여합니다.
        return true;
    }

    // CheckPlayerRequirements 함수에서 true 를 반환하면 호출합니다.
    // 매칭을 완료하는 데 더 많은 사람이 필요한 지 검사합니다.
    public static funapi.Matchmaking.Server.MatchState CheckMatchRequirements(Match match)
    {
        // CheckPlayerRequirements 함수에서 true 를 반환하면 호출합니다.
        // 매칭을 완료하는 데 더 많은 사람이 필요한 지 검사합니다.
        if (match.MatchType == (long)MatchType.kMatch1Vs1)
        {
            // 이 예제에서는 1:1 조건 예제를 생략합니다.
        }
        else if (match.MatchType == (long)MatchType.kMatch2Vs2)
        {
            if (match.Players.Count == 4)
            {
                return funapi.Matchmaking.Server.MatchState.kMatchComplete;
            }

            // CheckPlayerRequirements 함수와 마찬가지로 match.players[i].context
            // 객체의 elapsed_time 을 읽어 일정 시간이 지났다면, AI 를 추가하여 대전을
            // 진행하는 로직을 이 곳에서 처리할 수 있습니다.
        }

        return funapi.Matchmaking.Server.MatchState.kMatchNeedMorePlayer;
    }

    public static void OnJoined(Player player, Match match)
    {
        // CheckPlayerRequirements 함수에서 true 를 반환하면 호출합니다.
        // 이제 플레이어는 이 매칭에 참여합니다.
        // 여기서 팀을 구분해 줍니다. (Json 타입인 match->context 에 자유롭게
        // match 의 context 를 저장할 수 있습니다.)
        if (match.MatchType == (long)MatchType.kMatch1Vs1)
        {
            // 이 예제에서는 1:1 조건 예제를 생략합니다.
        }
        else if (match.MatchType == (long)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 = (JArray)match.Context["TEAM_A"];
            JArray team_b = (JArray)match.Context["TEAM_B"];
            if (team_a.Count < team_b.Count)
            {
                team_a.Add(player.Id);
            }
            else
            {
                team_b.Add(player.Id);
            }
        }
    }

    public static void OnLeft(Player player, Match match)
    {
        // 매칭에 참여 중인 유저가 MatchmakingClient::CancelMatchmaking 함수를 호출하면
        // 이 핸들러를 호출합니다.
        // 이 함수 호출이 끝나면 MatchmakingClient::CancelMatchmaking() 함수에
        // 전달한 CancelCallback 을 호출합니다.
        if (match.MatchType == (long)MatchType.kMatch1Vs1)
        {
            // 이 예제에서는 1:1 조건 예제를 생략합니다.
        }
        else if (match.MatchType == (long)MatchType.kMatch2Vs2)
        {
            // OnJoined() 에서 추가했던 팀 정보에서 해당 유저를 삭제합니다.
            List<JArray> teams = new List<JArray>
            {
                (JArray)match.Context["TEAM_A"],
                (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++;
                }
            }
        }
    }
}

27.3.2. Example 2: Changing requirements and rematching when matching fails

The following example shows matchmaking divided into stages. Match requirements differ at each stage, and if matchmaking is unsuccessful within a certain time, the next stage is attempted.

27.3.2.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
enum Stage {
  kStage1 = 1,
  kStage2,
  kStage3,
  kStageEnd
};


// 매칭 요청을 처리할 클라이언트 메시지 핸들러입니다.
// 1 단계 조건부터 시작합니다.
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);
}


// 매칭 결과를 처리합니다.
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) {
    // 타임아웃 발생. 다음 단계에 해당하는 조건으로 다시 시도합니다.
    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!";

  // 매칭을 완료했습니다. 이제 게임을 시작할 수 있습니다.
}
 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
enum Stage {
  kStage1 = 1,
  kStage2,
  kStage3,
  kStageEnd
};


// 매칭 요청을 처리할 클라이언트 메시지 핸들러입니다.
// 1 단계 조건부터 시작합니다.
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);
}


// 매칭 결과를 처리합니다.
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!");

  // 매칭을 완료했습니다. 이제 게임을 시작할 수 있습니다.
}

27.3.2.2. Matchmaking server

 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
enum Stage {
  kStage1 = 1,
  kStage2,
  kStage3,
  kStageEnd
};


static bool MyServer::Install(const ArgumentMap &arguments) {
  MatchmakingServer::Start(
      CheckPlayerRequirements, CheckMatchRequirements, OnJoined, OnLeft);
}


// 이 유저 그룹이 매칭에 참여할 수 있는지 검사합니다.
bool CheckPlayerRequirements(const MatchmakingServer::Player &player1,
                             const MatchmakingServer::Match &match) {
  BOOST_ASSERT(match.players.size() > 0);

  // 매칭 요청 시 user_data 로 지정한 값입니다.
  const Json &user_data1 = player1.context["user_data"];

  // 경쟁자 데이터
  const MatchmakingServer::Player &player2 = match.players.front();
  const Json &player2_context = player2.context;
  const Json &user_data2 = player2_context["user_data"];

  if (match.type == kStage1) {
    // 1단계 조건입니다. 유저 레벨을 기준으로 매칭 참여를 판단합니다.
    int64_t player_level1 = user_data1["player_level"].GetInteger();
    int64_t player_level2 = user_data2["player_level"].GetInteger();
    if (abs(player_level1 - player_level2) <= 3) {
      return true;
    }
  } else if (match.type == kStage2) {
    // 2단계 조건입니다. 케릭터 레벨을 기준으로 매칭 참여를 판단합니다.
    int64_t char_level1 = user_data1["character_level"].GetInteger();
    int64_t char_level2 = user_data2["character_level"].GetInteger();
    if (abs(char_level1 - char_level2) <= 10) {
      return true;
    }
  } else if (match.type == kStage3) {
    // 3단계 조건입니다. 게임 플레이 횟수로 매칭 참여를 판단합니다.
    int64_t play_count1 = user_data1["play_count"].GetInteger();
    int64_t play_count2 = user_data2["play_count"].GetInteger();
    if (abs(play_count1 - play_count2) <= 30) {
      return true;
    }
  } else {
    BOOST_ASSERT(false);
  }

  return false;
}

// 매칭을 완료할 수 있는지 검사합니다.
MatchmakingServer::MatchState CheckMatchRequirements(
    const MatchmakingServer::Match &match) {
  if (match.players.size() == 2) {
    return MatchmakingServer::kMatchComplete;
  }
  return MatchmakingServer::kMatchNeedMorePlayer;
}


// 유저가 이 매칭에 참여할 때 호출합니다. 필요시 match->context 에 JSON 형태로
// 추가 데이터를 저장할 수 있습니다.
void OnJoined(const MatchmakingServer::Player &player,
              MatchmakingServer::Match *match) {
  // do nothing.
}

// 매칭에 참여 중인 유저가 MatchmakingClient::CancelMatchmaking 함수를 호출하면
// 이 핸들러를 호출합니다.
// OnJoined 함수에서 match->context 에 저장한 데이터가 있을 경우, 이 데이터를
// 삭제하는 과정이 필요할 수 있습니다.
void OnLeft(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
74
75
76
77
78
79
enum Stage {
  kStage1 = 1,
  kStage2,
  kStage3,
  kStageEnd
};

public class Server
{
    public static bool Install(ArgumentMap arguments)
    {
        ...
        funapi.Matchmaking.Server.Start(
            MatchExample.CheckPlayerRequirements,
            MatchExample.CheckMatchRequirements,
            MatchExample.OnJoined, MatchExample.OnLeft);
        ...
    }
}

// 유저가 매칭에 참여할 수 있는지 판단합니다.
bool CheckPlayerRequirements (Player player1, Match match) {
  Log.Assert (match.Players.Count > 0);

  // 매치 메이킹을 요청한 유저의 데이터
  JObject user_data1 = player1.Context["user_data"];

  Player player2 = match.Players[0];
  JObject user_data2 = player2.Context["user_data"];

  if (match.MatchType.Equals(Stage.kStage1))
  {
    // 1단계 조건입니다. 유저 레벨을 기준으로 매칭 참여를 판단합니다.
    long player_level1 = (long) user_data1["player_level"];
    long player_level2 = (long) user_data2["player_level"];
    if (System.Math.Abs(player_level1 - player_level2) <= 3) {
      return true;
    }
  } else if (match.MatchType.Equals(Stage.kStage2)) {
    // 2단계 조건입니다. 케릭터 레벨을 기준으로 매칭 참여를 판단합니다.
    long char_level1 = (long) user_data1["character_level"];
    long char_level2 = (long) user_data2["character_level"];
    if (System.Math.Abs(char_level1 - char_level2) <= 10) {
      return true;
    }
  } else if(match.MatchType.Equals(Stage.kStage3)) {
    // 3단계 조건입니다. 게임 플레이 횟수로 매칭 참여를 판단합니다.
    long play_count1 = (long) user_data1["play_count"];
    long play_count2 = (long) user_data2["play_count"];
    if (System.Math.Abs(play_count1 - play_count2) <= 30) {
      return true;
    }
  } else {
    Log.Assert (false);
  }
  return false;
}

// 매칭을 완료할 수 있는지 검사합니다.
funapi.Matchmaking.Server.MatchState CheckMatchRequirements (Match match) {
  if (match.Players.Count == 2) {
    return funapi.Matchmaking.Server.MatchState.kMatchComplete;
  }
  return funapi.Matchmaking.Server.MatchState.kMatchNeedMorePlayer;
}

// 유저가 이 매칭에 참여할 때 호출합니다. 필요시 match->context 에 JSON 형태로
// 추가 데이터를 저장할 수 있습니다.
void OnJoined (Player player, Match match) {
  // do nothing.
}

// 매칭에 참여 중인 유저가 MatchmakingClient::CancelMatchmaking 함수를 호출하면
// 이 핸들러를 호출합니다.
// OnJoined 함수에서 match.Context 에 저장한 데이터가 있을 경우, 이 데이터를
// 삭제하는 과정이 필요할 수 있습니다.
void OnLeft (Player player, Match match) {
  // do nothing.
}

27.3.3. Example 3: Group vs. group

Matchmaking can also match groups rather than players. The following is an example of matchmaking with different 2-person groups with average level differences of 10 or more. The scenario assumes competition between parties of friends.

27.3.3.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
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 의 대전을 시작하는 처리를 합니다.
  // (클라이언트에 응답을 보내는 작업 등)
  ...
}
 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
enum MatchType {
  kMatch1Vs1 = 0,
  kMatch2Vs2,
  kGroupMatch2Vs2
};

// 클라이언트로부터 받은 그룹 매칭 요청을 처리하는 메시지 핸들러입니다.
public static void OnMatchmakingRequested(Session session, JObject message) {
  string group_member1_id = ...;
  long group_member1_level = ...;
  string group_member2_id = ...;
  long group_member2_level = ...;

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

  JObject group_ctxt = new JObject();
  group_ctxt["members"] = new JArray();
  JArray members = (JArray)group_ctxt["members"];

  JObject group_member1 = new JObject();
  group_member1["id"] = group_member1_id;
  group_member1["level"] = group_member1_level;
  members.Add(group_member1);

  JObject group_member2 = new JObject();
  group_member2["id"] = group_member2_id;
  group_member2["level"] = group_member2_level;
  members.Add(group_member2);

  funapi.Matchmaking.Client.Start(
      (long)MatchType.kGroupMatch2Vs2, group_id, group_ctxt,
      OnMatched,
      funapi.Matchmaking.Client.TargetServerSelection.kMostNumberOfPlayers,
      null,
      WallClock.FromSec(60));
}


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

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

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

  System.Guid match_id = match.MatchId;

  string group_a_player1 = match.Context["GROUP_A"][0].ToString();
  string group_a_player2 = match.Context["GROUP_A"][1].ToString();

  string group_b_player1 = match.Context["GROUP_B"][0].ToString();
  string group_b_player2 = match.Context["GROUP_B"][1].ToString();


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

27.3.3.2. Matchmaking server

  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
152
153
154
enum MatchType {
  kMatch1Vs1 = 0,
  kMatch2Vs2,
  kGroupMatch2Vs2
};


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


// 이 유저 그룹이 매칭에 참여할 수 있는지 검사합니다.
bool CheckPlayerRequirements(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["user_data"];
      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["user_data"];
      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;
    }

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

    // 모든 조건에 만족합니다. 이 유저를 매칭에 참여시킵니다.
    return true;
  }
}


// JoinMatch 함수가 불린 후 호출됩니다. 해당 매치가 성사 되었는지 판단합니다.
MatchmakingServer::MatchState CheckMatchRequirements(
    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];
    //   const Json &ctxt = group.context["user_data"];
    //   members += ctxt["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 로 지정합니다.
    const Json &ctxt = group.context["user_data"];
    if (not match->context.HasAttribute("GROUP_A")) {
      match->context["GROUP_A"].SetArray();
      match->context["GROUP_A"].PushBack(ctxt["members"][0]["id"]);
      match->context["GROUP_A"].PushBack(ctxt["members"][1]["id"]);
      return;
    }

    if (not match->context.HasAttribute("GROUP_B")) {
      match->context["GROUP_B"].SetArray();
      match->context["GROUP_B"].PushBack(ctxt["members"][0]["id"]);
      match->context["GROUP_B"].PushBack(ctxt["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) {

    const Json &ctxt = group.context["user_data"];
    string id = ctxt["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 이 호출됩니다.
}
  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
152
153
154
155
156
enum MatchType {
  kMatch1Vs1 = 0,
  kMatch2Vs2,
  kGroupMatch2Vs2
};

public static bool Install(ArgumentMap arguments)
{
    ...
    funapi.Matchmaking.Server.Start(
        CheckPlayerRequirements,
        CheckMatchRequirements,
        OnJoined, OnLeft);
    ...
}

public static bool CheckPlayerRequirements(
    funapi.Matchmaking.Player group, funapi.Matchmaking.Match match) {

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

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

    long group_a_avg_level = 0;
    {
      JObject ctxt = group.Context["user_data"].ToJObject();
      long member_1 = ctxt["members"][0]["level"].Value<long>();
      long member_2 = ctxt["members"][1]["level"].Value<long>();
      group_a_avg_level = (member_1 + member_2) / 2;
    }

    long group_b_avg_level = 0;
    {
      JObject ctxt = match.Players[0].Context["user_data"].ToJObject();
      long member_1 = ctxt["members"][0]["level"].Value<long>();
      long member_2 = ctxt["members"][1]["level"].Value<long>();
      group_b_avg_level = (member_1 + member_2) / 2;
    }

    if (Math.Abs(group_a_avg_level - group_b_avg_level) > 10) {
      // 조건에 맞지 않습니다.
      return false;
    }

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

    // 모든 조건에 만족합니다. 이 유저를 매칭에 참여시킵니다.
    return true;
  }
  return false;
}


// JoinMatch 함수가 불린 후 호출됩니다. 해당 매치가 성사 되었는지 판단합니다.
public static funapi.Matchmaking.Server.MatchState CheckMatchRequirements(
    funapi.Matchmaking.Match match) {

  if (match.MatchType  == (long)MatchType.kMatch1Vs1) {
    ...
  } else if (match.MatchType  == (long)MatchType.kMatch2Vs2) {
    ...
  } else if (match.MatchType  == (long)MatchType.kGroupMatch2Vs2) {
    if (match.Players.Count == 2) {
      // 2 그룹이 모두 모였습니다. (총 4 명)
      return funapi.Matchmaking.Server.MatchState.kMatchComplete;
    }

    // 각 그룹이 고정된 인원수가 아니면(여기서는 2 명) 아래처럼
    // 처리할 수 있습니다.
    // long members = 0;
    // for (int i = 0; i < match.Players.Count; ++i) {
    //   funapi.Matchmaking.Player group = match.Players[i];
    //   JObject user_data = group.Context["user_data"].ToJObject();
    //   JArray group_members = (JArray)user_data["members"];
    //   members += group_members.Count;
    // }
    // if (members >= 4) {
    //   return funapi.Matchmaking.Server.MatchState.kMatchComplete;
    // }
  }

  return funapi.Matchmaking.Server.MatchState.kMatchNeedMorePlayer;
}


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

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

    JObject user_data = group.Context["user_data"].ToJObject();

    if (match.Context["GROUP_A"] == null) {
      match.Context["GROUP_A"] = new JArray();
      JArray group_a = (JArray)match.Context["GROUP_A"];
      group_a.Add(user_data["members"][0]["id"]);
      group_a.Add(user_data["members"][1]["id"]);
      return;
    }

    if (match.Context["GROUP_B"] == null) {
      match.Context["GROUP_B"]= new JArray();
      JArray group_b = (JArray)match.Context["GROUP_B"];
      group_b.Add(user_data["members"][0]["id"]);
      group_b.Add(user_data["members"][1]["id"]);
      return;
    }
  }
}


// match 에 참여해 있던 group 이 Matchmaking.Client.CancelMatchmaking() 을
// 요청했습니다. 위 OnJoined()  함수에서 업데이트한 정보를 삭제합니다.
public static void OnLeft(
    funapi.Matchmaking.Player group, funapi.Matchmaking.Match match) {

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

    JObject user_data = group.Context["user_data"].ToJObject();
    string id = user_data["members"][0]["id"].ToString();

  if (match.Context["GROUP_A"] != null) {
      if (match.Context["GROUP_A"][0].ToString() == id) {
        match.Context.Remove("GROUP_A");
        return;
      }
    }

    if (match.Context["GROUP_B"] != null) {
      if (match.Context["GROUP_B"][0].ToString() == id) {
        match.Context.Remove("GROUP_B");
        return;
      }
    }
  }

  // 이 함수 호출이 완료되면 Matchmaking.Client.CancelMatchmaking() 함수에
  // 전달한 CancelCallback 이 호출됩니다.
}