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

아이펀 엔진은 멀티플레이 그룹을 만들어주는 매치메이킹 기능을 제공합니다. 이 기능을 사용해서 레벨, 맵, 게임 모드 등 게임 요소를 기준으로 조건에 부합하는 유저들끼리 게임을 진행하도록 구현할 수 있습니다.

매치메이킹 기능은 아이펀 엔진 내부에서 MatchmakingServer 콤포넌트와 MatchmakingClient 콤포넌트로 동작합니다.

  • MatchmakingServer: 매치메이킹 클라이언트 콤포넌트로부터 받은 매칭 요청을 조건에 맞춰 그룹으로 묶는 역할을 담당합니다. 주로 메시지 핸들러에서 사용자가 매칭 참여를 요청했을 때, 사용자가 지정한 조건(맵, 인원수 등)에 부합하는 그룹이 있는지 검사하거나 매치메이킹의 여러 상황에서 미리 등록된 핸들러들을 실행하는 역할을 합니다.
  • MatchmakingClient: 사용자의 매칭 관련 요청들을 MatchmakingServer 로 전달하는 역할을 하며 매칭 참여 또는 취소 요청을 할 수 있습니다. 일반적으로 사용자의 메시지를 처리하는 이벤트 핸들러 내에서 사용합니다.

Tip

MatchmakingServer 콤포넌트를 특정 flavor 를 가진 게임 서버만 동작하게 하면 매치메이킹 전용 서버를 만들 수 있습니다.

26.1. MatchmakingServer 콤포넌트

26.1.1. MANIFEST 설정하기

MatchmakingServer 콤포넌트를 사용하기 위해서는 먼저 MANIFEST.json 파일에 아래와 같이 서비스들이 정의되어 있는지 확인합니다.

먼저, RpcService 가 필요합니다.

1
2
3
4
5
6
...
"RpcService": {
  "rpc_enabled": true,
  ...
},
...

그 다음으로 MatchmakingServer 콤포넌트를 정의합니다. 몇 가지 하위 파라미터들은 정의하지 않을 경우 기본값으로 동작합니다.

1
2
3
4
5
6
7
...
"MatchmakingServer": {
  "enable_dynamic_match": true,
  "enable_match_progress_callback": false,
  "concurrent_number_of_players_threshold": 3000
},
...
  • enable_dynamic_match: true 인 경우 같은 매칭 타입(Type)을 가진 매칭 대기 중인 유저들을 임의로 섞은 후 다시 합치는 형태로 매치메이킹을 진행합니다. false 인 경우 같은 매칭 타입(Type)에 한해 매칭을 요청한 순서대로 매치메이킹을 진행합니다.

Warning

MANIFEST.json 파일의 MatchmakingServer 항목 중 enable_dynamic_match 값이 true 면 엔진 내부에서 보다 빠른 매칭을 위해 여러 매칭을 조정하면서 콜백함수들을 잦은 주기로 호출할 수 있습니다.

  • enable_match_progress_callback: 이 값을 true 로 설정하면 매치메이킹 클라이언트가 MatchmakingClient::startMatchmaking() 를 호출하면서 인자로 넘긴 progress_cb 을 호출합니다. 이를 통해 매칭 진행 상황을 받아볼 수 있습니다.
  • concurrent_number_of_players_threshold: 매치메이킹 서버가 메치메이킹 풀에 유지하는 매칭 요청들의 임계값입니다. 특정 매치메이킹 서버의 매칭 요청 수가 이 값을 넘어서면 매치메이킹 서버로 선택되지 않습니다. (단, 매치메이킹 서버가 하나이거나 모든 매치메이킹 서버가 임계값을 넘어선 경우 제외)

Important

enable_dynamic_match 설정과 enable_match_progress_callback 설정은 동시에 사용할 수 없습니다.

26.1.2. 인터페이스

26.1.2.1. Start()

Start() 함수를 호출하면 매치메이킹 서버를 시작할 수 있습니다. 이 함수는 게임 서버 초기화에 해당하는 Install() 함수 안에서 호출할 것을 권장합니다.

Important

매치메이킹 서버에 등록한 콜백 함수 인자들은 엔진 내부에서 매칭을 관리할 때 사용하기 때문에 수정을 허용하지 않습니다. 단, 예외적으로 Match.context 객체는 join_callbackleave_callback 에서만 수정할 수 있습니다.

Tip

Start() 에 등록하는 콜백 핸들러 4개는 MatchID 별로 직렬화한 후 순차적으로 실행합니다. 따라서 핸들러 함수 안에서 Match.context 객체를 변경할 때는 별도 동기화가 필요하지 않습니다.

1
2
3
4
5
6
7
class MatchmakingServer {
public:
 static void Start(const MatchChecker &match_checker,
                   const CompletionChecker &completion_checker,
                   const JoinCallback &join_cb,
                   const LeaveCallback &leave_cb);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
namespace matchmaking
{
  public static class Server
  {
    public static void Start (MatchChecker match_checker,
                              CompletionChecker completion_checker,
                              JoinCallback join_callback,
                              LeaveCallback leave_callback);
  }
}

26.1.2.2. 콜백 핸들러

MatchmakingServerStart() 함수를 호출 할 때 콜백 핸들러들을 매개변수로 전달하면 매칭이 진행되는 상황에 맞는 핸들러를 실행합니다. 아래 콜백 함수들에서 인자로 사용하는 PlayerMatch 객체의 정의는 클래스 정의 를 참고하시기 바랍니다.

  • match_checker

    이 매칭에 참여한 유저가 참여 조건과 부합하는 지 확인하기 위해 호출합니다. 참여 조건(레벨, 골드 소지량 등)을 판단해서 참여(true) 또는 참여할 수 없음(false)을 반환하면 됩니다.

    • 리턴 값: 해당 유저가 매치에 참여할 수 있으면 true 를 반환하면 됩니다.
    • player 인자: 매칭에 참여하는 플레이어 정보를 가리킵니다.
    • match 인자: 매칭 정보를 가리킵니다.
typedef function<bool(
    const Player & /* 유저 정보 */,
    const Match & /* 매칭 정보 */
)> MatchChecker;
public delegate bool MatchChecker (
    Player player, /* 유저 정보 */
    Match match    /* 매칭 정보 */
);

Note

playermatch.playerscontext 객체 안에는 추가적으로 request_timeelapsed_time 속성이 붙습니다.

Note

매치메이킹 클라이언트 클래스의 StartMatchmaking2() 함수 호출 시 지정한 JSON 객체는 user_data 속성 안에 있습니다. 이에 대한 설명은 매치메이킹 예제 를 참고하시기 바랍니다.

  • completion_checker

    match_checker 에서 true 를 반환하여 유저가 매칭에 합류하면 호출합니다. 이 함수는 매칭이 완료됐는지, 더 많은 사람이 필요한 지 판단하는 데 사용합니다.

    • 리턴 값: 매칭을 완료하는 데 더 많은 사람이 필요한 경우 kMatchNeedMorePlayer, 매칭에 필요한 사람이 모두 모인 경우 kMatchComplete 를 반환하면 됩니다.
    • match 인자: 매칭 완료를 판단할 매칭 정보를 가리키는 객체입니다.
typedef function<MatchState(
    const Match & /* 매칭 정보 */
)> CompletionChecker;
public delegate MatchState CompletionChecker (
    Match match /* 매칭 정보 */
);
  • join_callback

    match_checker 에서 true 를 반환하여 유저가 매칭에 합류하면 호출합니다. Match.context 객체에 팀을 구분하기 위한 정보 등을 저장할 수 있습니다.

    • player 인자: 매칭에 참여한 유저 정보를 가리킵니다.
    • match 인자: 매칭 정보를 가리킵니다. 이 객체의 context 변수를 수정할 수 있습니다.
typedef function<void(
    const Player & /* 유저 정보 */,
    Match * /* 매칭 정보 */
)> JoinCallback;
public delegate void JoinCallback (
    Player player, /* 유저 정보 */
    Match match    /* 매칭 정보 */
);

Note

playermatch.playerscontext 변수에는 기본적으로 elapsed_time 이라는 속성이 포함되고, 해당 유저의 매칭 요청이 진행된 시간을 확인 할 수 있습니다. 이 값이 큰 사용자의 우선순위를 높이여서 매칭을 처리할 수 있습니다. 매치메이킹 예제 를 참고하시기 바랍니다.

  • leave_callback

    매칭을 진행 중인 유저가 CancelMatchmaking() 을 호출해서 매칭을 취소하면 호출합니다. 또는 MANIFEST.json 파일의 MatchmakingServer 항목 중 enable_dynamic_matchtrue 일 때 엔진 내부에서 매칭을 조정하면서 호출할 수 있습니다.

    앞서 join_callback 함수에서 Match.context 객체에 팀 편성 정보를 저장한 경우, 이 작업을 취소하는 처리가 필요합니다.

    • player 인자: 매칭을 취소한 유저 정보를 가리킵니다.
    • match 인자: 매칭 정보를 가리킵니다.
typedef function<void(
    const Player & /* 유저 정보 */,
    Match * /* 매칭 정보 */
)> LeaveCallback;
public delegate void LeaveCallback (Player player, Match match);

26.1.2.3. 클래스 정의

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

 // 매칭 상태를 정의합니다. JoinCallback 에서 매칭 여부를 결정할 때 사용합니다.
 enum MatchState {
   // 매칭을 완료하는 데 더 많은 유저가 필요할 때 사용합니다.
   kMatchNeedMorePlayer = 0,
   // 이 매치에 필요한 유저가 모두 모였을 때 사용합니다.
   kMatchComplete
 };

 // 유저가 매칭을 요청할 때 호출합니다.
 // 이 함수 안에서는 매칭을 요청한 유저가 참여 조건과 부합하는지 확인해야 합니다.
 // 매치을 요청한 유저가 2명 이상일 경우에만 호출합니다.
 typedef function<bool(const Player & /* 유저 정보 */,
                         const Match & /* 매칭 정보 */)> MatchChecker;

 // 매칭을 완료할 준비가 됐는지 확인할 때 호출됩니다.
 typedef function<MatchState(const Match & /*match*/)> CompletionChecker;

 // 유저가 매칭에 참여했을 때(MatchChecker callback 에서 참여 조건에 만족하여
 // true 를 반환하면) 호출됩니다. Match 객체 안에 있는 context(JSON)로
 // 팀 편성 정보 등을 저장하는 데 사용할 수 있습니다.
 typedef function<void(const Player & /* 유저 정보 */,
                         Match * /* 매칭 정보 */)> JoinCallback;

 // 유저가 매칭을 취소했을 때 호출합니다.
 // 유저가 CancelMatchmaking() 함수를 호출하거나 MANIFEST.json 파일의
 // MatchmakingServer 항목 중 enable_dynamic_match 가 true 일 때
 // 엔진 내부에서 매칭을 조정하면서 호출할 수 있습니다.
 typedef function<void(const Player & /* 유저 정보 */,
                         Match * /* 매칭 정보 */)> LeaveCallback;

 // 앞서 설명한 매치메이킹 처리 함수들을 등록하는 함수입니다.
 static void Start(const MatchChecker &match_checker,
                     const CompletionChecker &completion_checker,
                     const JoinCallback &join_cb,
                     const LeaveCallback &leave_cb);
};
 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
namespace matchmaking
{
  public static class Server
  {
    // 매칭 상태를 정의합니다. JoinCallback 에서 매칭 여부를 결정할 때 사용합니다.
    public enum MatchState
    {
      // 매칭을 완료하는 데 더 많은 유저가 필요할 때 사용합니다.
      kMatchNeedMorePlayer = 0,
      // 이 매치에 필요한 유저가 모두 모였을 때 사용합니다.
      kMatchComplete
    };

    // 유저가 매칭을 요청할 때 호출합니다.
    // 이 함수 안에서는 매칭을 요청한 유저가 참여 조건과 부합하는지 확인해야 합니다.
    // 매치을 요청한 유저가 2명 이상일 경우에만 호출합니다.
    public delegate bool MatchChecker (Player player, Match match);

    // 매칭을 완료할 준비가 됐는지 확인할 때 호출됩니다.
    public delegate MatchState CompletionChecker (Match match);

    // 유저가 매칭에 참여했을 때(MatchChecker callback 에서 참여 조건에 만족하여
    // true 를 반환하면) 호출됩니다. Match 객체 안에 있는 context(JSON)로
    // 팀 편성 정보 등을 저장하는 데 사용할 수 있습니다.
    // MANIFEST.json 파일의 MatchmakingServer 항목 중 enable_dynamic_match
    // 값이 true 면 엔진 내부에서 빠른 매칭을 위해 매칭을 조정하면서
    // 잦은 주기로 이 함수를 호출할 수 있습니다.
    public delegate void JoinCallback (Player player, Match match);

    // 유저가 매칭을 취소했을 때(CancelMatchmaking() 함수 호출) 호출합니다.
    // MANIFEST.json 파일의 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);
  }
}

26.2. MatchmakingClient 콤포넌트

26.2.1. MANIFEST 설정하기

MatchmakingClient 콤포넌트를 사용하기 위해 MANIFEST.json 파일을 설정하는 법에 대해서 설명하겠습니다. 먼저, MatchmakingClient 콤포넌트를 사용하기 위해서는 RPC 서비스가 필요합니다.

1
2
3
4
5
6
...
"RpcService": {
  "rpc_enabled": true,
  ...
},
...

다음으로 MatchmakingClient 콤포넌트를 추가합니다. 하위 항목은 정의하지 않아도 됩니다.

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

26.2.2. MatchmakingClient 인터페이스

MatchmakingClient 클래스를 사용해서 매칭 요청을 보내거나 진행 중인 매칭 요청을 취소할 수 있습니다. 추가로 GetMatchmakingServerInfo() 를 이용하면 MatchmakingServer 들의 정보를 확인할 수 있습니다.

26.2.2.1. 매칭 요청

매칭 요청에 사용할 수 있는 함수는 StartMatchmaking()StartMatchmaking2() 가 있습니다. 두 함수의 차이는 progress_callback 함수로 전달 받는 매개변수 타입이 다릅니다. 이 차이에 대해서는 아래 progress_callback 항목에서 자세히 설명하겠습니다.

 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
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);
}
 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
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 : 매치메이킹 방식을 의미하는 임의의 정수 값을 의미합니다. MatchmakingServer 는 같은 type 의 요청끼리 매칭을 진행합니다. 예를 들어, 빠른 대전 이나 랭킹 대전 각각을 의미하는 정수 값을 통해 매칭을 구별할 수 있습니다.
  • player_id : 매칭에 참여할 유저 ID 를 지정합니다. AccountManager 콤포넌트에서 사용하는 식별자를 사용합니다.
  • player_context : 유저 정보를 JSON 오브젝트 형식으로 정의합니다. 이 정보는 MatchmakingServer 콤포넌트에서 매칭을 진행하기 위한 기준 데이터로 사용하거나, MatchmakingClient 콤포넌트에서 유저 정보가 필요할 때 사용할 수 있습니다.
  • match_callback : 매칭 요청이 성공하거나 타임아웃으로 중단 된 경우 실행할 콜백 함수입니다. MatchResult 값에 따라서 후속 처리를 진행합니다.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
            public enum MatchResult {
                    kSuccess = 0,
                    kAlreadyRequested,
                    kTimeout,
                    kError = 1000
            };

typedef function<void(const string & /*player_id*/,
                      const Match & /*match*/,
                      MatchResult /*result*/)> MatchCallback;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
enum MatchResult {
  kMRSuccess = 0,
  kMRAlreadyRequested,
  kMRTimeout,
  kMRError = 1000
};

            public delegate void MatchCallback (string player_id, /*player_id*/
                                    Match match, /*match*/
                                    MatchResult result /*result*/);
  • target_server : 매칭을 요청할 서버를 명시적으로 지정할 수 있습니다. enum TargetServerSelection 으로 자동 선택하거나, GetMatchmakingServerInfo() 함수로 MatchmakingServer 정보를 받아서 PeerId 를 지정할 수 있습니다.
  • progress_callback : 매치에 유저가 참여하거나 이탈 한경우 또는 유저 데이터가 변경 됐을 때 호출하는 콜백입니다. 기본 값인 kNullProgrssCallback 을 지정하면 호출하지 않습니다. 자세한 내용은 콜백 함수로 매칭 상태 확인하기 을 참고 해 주세요.

Note

MANIFEST.json 파일의 MatchmakingServer 항목에 enable_match_progress_callback 값을 true 로 설정해야 콜백이 호출됩니다.

  • timeout : 지정한 시간 동안 매칭 성사가 이루어지지 않으면 자동으로 취소됩니다. 이 때 결과 값으로 kMRTimeout 를 반환하는 match_callback 을 호출합니다. 기본 값인 kNullTimeout 을 전달하면 무시합니다.

26.2.2.2. 매칭 요청 취소

진행 중인 매칭 요청에 대한 취소를 시도하고, callback 함수를 통해서 결과를 확인 할 수 있습니다.

1
2
3
4
5
6
class MatchmakingClient {
 public:
  static void CancelMatchmaking(const Type &type,
                                const string &player_id,
                                const CancelCallback &cancel_callback);
}
1
2
3
4
5
6
7
8
9
namespace matchmaking
{
  public static class Client
  {
    public static void Cancel (Int64 type,
                               string player_id,
                               CancelCallback cancel_callback);
  }
}
  • type : 매칭을 요청할 때과 같은 MatchType 값을 사용해야 합니다.

  • player_id : 매칭 요청을 취소할 유저 ID 를 입력합니다.

  • callback : 매칭 요청 취소 결과를 받을 콜백 함수를 등록합니다.

    Note

    이미 매칭이 끝난 후 MatchCallback 까지 호출을 마친 유저 ID 에 대해서 매칭 요청을 취소할 경우 kCRNoRequest 를 결과값으로 하는 콜백을 호출합니다.

1
2
3
4
5
6
7
8
enum CancelResult {
  kCRSuccess = 0,
  kCRNoRequest,
  kCRError = 1000
};

typedef function<void(const string & /*유저 ID*/,
                      CancelResult /*결과*/)> CancelCallback;
1
2
3
4
5
6
7
8
9
            public enum CancelResult {
                    kSuccess = 0,
                    kNoRequest,
                    kError = 1000
            };

            public delegate void CancelCallback (
                      string player_id, /*유저 ID*/
                      CancelResult result /*결과*/);

26.2.2.3. 유저 데이터 변경

1
2
3
static void UpdateMatchPlayerContext(const Type &type,
                                     const string &player_id,
                                     const Json &new_context);
1

26.2.2.4. 콜백 함수로 매칭 상태 확인하기

StartMatchmaking() 또는 StartMatchmaking2() 할수를 호출할 때 전달하는 progress callback 함수를 통해서 등록하면 매치에 유저가 참여 또는 이탈하거나 데이터를 업데이트하는 등의 변화를 확인할 수 있습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 매칭 상태를 변경한 후 호출하는 함수입니다.
// MANIFEST.json 파일의 MatchmakingServer 항목에
// `enable_match_progress_callback` 값을 true 로 설정해야 콜백을 호출합니다.
// 두 유저 ID(참가/나간) 값 모두 빈 콜백은 호출하지 않습니다.
typedef function<void(const string & /* 유저 ID */,
                      const MatchId & /* 매치 ID */,
                      const string & /* 참여한 유저 ID */,
                      const string & /* 나간 유저 ID */)> ProgressCallback;

typedef function<void(const string & /* 유저 ID */,
                      const Match & /* 매치 정보 */,
                      const string & /* 참여 또는 변경한 유저 ID */,
                      const string & /* 나간 유저 ID */)> ProgressCallback2;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 매칭 상태를 변경한 후 호출하는 함수입니다.
// MANIFEST.json 파일의 MatchmakingServer 항목에
// `enable_match_progress_callback` 값을 true 로 설정해야 콜백을 호출합니다.
// 두 유저 ID(참가/나간) 값 모두 빈 콜백은 호출하지 않습니다.
public delegate void ProgressCallback(string player_id /* 유저 ID */,
                                      System.Guid match_id /* 매칭 ID */,
                                      string player_id_joined /* 참여한 유저 ID */,
                                      string player_id_left /* 나간 유저 ID */);

public delegate void ProgressCallback2(string player_id /* 유저 ID */,
                                       Match match /* 매칭 ID */,
                                       string player_id_joined_or_updated /* 참여한 유저 ID */,
                                       string player_id_left /* 나간 유저 ID */);

전달받는 두번째 매개변수에 따라 ProgressCallbackProgressCallback2 의 두 종류가 있으며, 각각 StartMatchmaking(), StartMatchmaking2() 함수와 짝을 이룹니다.

참여 또는 나가거나 데이터를 업데이트 한 유저 식별자는 반드시 하나만 전달 받습니다.

26.2.2.5. 매치 데이터와 유저 데이터 활용

MatchmakingServer 콤포넌트는 매칭에 참여하는 유저들이 비슷한 실력인지, 또는 같은 조건을 가졌는지 판단할 수 있는 기준이 필요합니다. 이런 것들을 판단하는 데 필요한 정보들은 Match 객체 안에 있는 매치 컨텍스트(context)와 각 플레이어 객체 안에 있는 플레이어 컨텍스트를 사용하면 됩니다.

 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
  // 매칭을 요청한 유저 정보입니다.
  struct Player {
    ...

    // 매치메이킹 클라이언트 및 서버 콤포넌트는 StartMatchmaking(),
    // StartMatchmaking2() 함수 호출 시 지정한 user_data 를
    // 다음과 같이 가공하여 저장합니다. 'request_time', 'elapsed_time' 값은
    // 변경할 수 없습니다.
    //
    // {
    //   "request_time":6447934119, // 요청 시각
    //   "elapsed_time":0,          // 요청 이후 지난 시간(단위: 초)
    //   "user_data": {
    //     // StartMatchmaking2() 안에 넣은 user_data
    //   }
    // }
    Json context;

    ...
  }

  ...

  // 매칭 정보입니다.
  struct Match {
    ...

    // 이 매칭에 참여 중인 플레이어 전체 목록입니다.
    // 매칭을 요청한 나 자신도 포함합니다.
    std::vector<Player> players;

    // 매칭 정보를 담는 객체입니다. Player::context 와는 다른 객체이므로 사용 시
    // 주의해야 합니다. 이 정보는 매치메이킹 서버 콤포넌트 시작 시
    // 인자로 지정하는 JoinCallback, LeaveCallback 에서 수정할 수 있습니다.
    // LeaveCallback 보다 JoinCallback 이 가장 먼저 불리므로, 이 콜백 함수에서
    // context 가 널(NULL)인 경우 초기화하고, 그 이후에 사용하면 됩니다.
    Json context;

    ...
  }
 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
  // 매칭을 요청한 유저 정보입니다.
  public struct Player
  {
    ...

    // 매치메이킹 클라이언트 및 서버 콤포넌트는 StartMatchmaking(),
    // StartMatchmaking2() 함수 호출 시 지정한 user_data 를
    // 다음과 같이 가공하여 저장합니다. 'request_time', 'elapsed_time' 값은
    // 변경할 수 없습니다.
    //
    // {
    //   "request_time":6447934119, // 요청 시각
    //   "elapsed_time":0,          // 요청 이후 지난 시간(단위: 초)
    //   "user_data": {
    //     // StartMatchmaking2() 안에 넣은 user_data
    //   }
    // }
    public JObject Context;

    ...
  }

  // 매칭 정보입니다.
  public struct Match
  {
    ...

    // 이 매칭에 참여 중인 플레이어 전체 목록입니다.
    // 매칭을 요청한 나 자신도 포함합니다.
    public ReadOnlyCollection<Player> Players;

    // 매칭 정보를 담는 객체입니다. Player::context 와는 다른 객체이므로 사용 시
    // 주의해야 합니다. 이 정보는 매치메이킹 서버 콤포넌트 시작 시
    // 인자로 지정하는 JoinCallback, LeaveCallback 에서 수정할 수 있습니다.
    // LeaveCallback 보다 JoinCallback 이 가장 먼저 불리므로, 이 콜백 함수에서
    // context 가 널(NULL)인 경우 초기화하고, 그 이후에 사용하면 됩니다.
    public JObject Context;

    ...
  }

26.2.2.6. 매칭 상태 변경

MatchmakingClient 콤포넌트는 매칭 대기 중인 유저 컨텍스트(context)를 변경할 수 있습니다. 정보를 변경한 유저 정보는 ProgressCallback2의 인자(player_id_joined_or_updated)와 Match 객체 안의 players 객체에서 확인할 수 있습니다.

1
2
static void UpdateMatchPlayerContext(
  const Type &type, const string &player_id, const Json &new_context);
1
  지원 예정입니다.

Note

1.0.0-2768 Experimental 버전 이상에서만 사용 가능합니다.

26.2.2.7. 클래스 정의

  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
class MatchmakingClient {
 public:
  typedef Uuid MatchId;
  typedef int64_t Type;

  // 매칭 중인 플레이어 정보입니다. 매칭 완료 또는 ProgressCallback2 핸들러의
  // Match 객체 안에 있는 players 목록을 통해 확인할 수 있습니다.
  struct Player {
    // AccountManager 에서 사용하는 유저 ID 입니다.
    string id;

    // 매치메이킹 클라이언트 및 서버 콤포넌트는 StartMatchmaking(),
    // StartMatchmaking2() 함수 호출 시 지정한 user_data 를
    // 다음과 같이 가공하여 저장합니다. 'request_time', 'elapsed_time' 값은
    // 변경할 수 없습니다.
    //
    // {
    //   "request_time":6447934119, // 요청 시각
    //   "elapsed_time":0,          // 요청 이후 지난 시간(단위: 초)
    //   "user_data": {
    //     // StartMatchmaking2() 안에 넣은 user_data
    //   }
    // }
    Json context;

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

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

    // 매칭을 식별할 수 있는 ID 입니다. UUID 를 사용하며 매치메이킹 콤포넌트에서
    // 제공하는 모든 콜백 함수는 이 ID 단위로 직렬화하여 실행합니다.
    const MatchId match_id;

    // 매칭 타입입니다. 매칭 요청 시 매칭의 종류나 형태를 구분할 때 사용합니다.
    // 임의의 값을 자유롭게 사용하면 됩니다.
    // (Match::Type 은 정수를 재 정의한 타입입니다)
    const Type type;

    // 이 매칭에 참여 중인 플레이어 전체 목록입니다.
    // 매칭을 요청한 나 자신도 포함합니다.
    std::vector<Player> players;

    // 매칭 정보를 담는 객체입니다. Player::context 와는 다른 객체이므로 사용 시
    // 주의해야 합니다. 이 정보는 매치메이킹 서버 콤포넌트 시작 시
    // 인자로 지정하는 JoinCallback, LeaveCallback 에서 수정할 수 있습니다.
    // LeaveCallback 보다 JoinCallback 이 가장 먼저 불리므로, 이 콜백 함수에서
    // context 가 널(NULL)인 경우 초기화하고, 그 이후에 사용하면 됩니다.
    Json context;
  };

  // MatchCallback 함수 인자로 매칭 결과를 정의합니다.
  enum MatchResult {
    kMRSuccess = 0,  // 매칭 성공
    kMRAlreadyRequested,  // 이미 같은 유저 ID 로 요청한 내역이 있습니다.
    kMRTimeout,  // 요청 시간이 만료됐습니다.
    kMRError = 1000  // 내부 서버 에러
  };

  // CancelCallback 함수 인자로 매치메이킹 취소 결과를 정의합니다.
  enum CancelResult {
    kCRSuccess = 0,  // 취소 성공
    kCRNoRequest,    // 이 유저 ID 로 매칭을 요청한 내역이 없습니다.
    kCRError = 1000  // 내부 서버 에러
  };

  // 매칭이 성사되면 이 핸들러에 등록한 함수를 호출합니다.
  typedef function<void(const string & /* 유저 ID */,
                        const Match & /* 매칭 정보 */,
                        MatchResult /* 매칭 결과 */)> MatchCallback;

  // 매칭 상태를 업데이트할 때마다 호출합니다.
  // 기본 값인 `kNullProgrssCallback` 을 지정하면 무시합니다.
  // MANIFEST.json 파일의 MatchmakingServer 항목에
  // `enable_match_progress_callback` 값을 true 로 설정해야 콜백을 호출합니다.
  // 두 유저 ID(참가/나간) 값 모두 빈 콜백은 호출하지 않습니다.
  typedef function<void(const string & /* 유저 ID */,
                        const MatchId & /* 매치 ID */,
                        const string & /* 참가한 유저 ID */,
                        const string & /* 나간 유저 ID */)> ProgressCallback;

  // 매칭 ID 대신 매칭 정보를 사용합니다.
  // UpdateMatchPlayerContext() 함수를 사용하기 위해서는 반드시 이 형태의 콜백
  // 함수를 등록해야 합니다.
  typedef function<void(const string & /* 유저 ID */,
                        const Match & /* 매치 객체 */,
                        const string & /* 참가한/업데이트한 유저 ID */,
                        const string & /* 나간 유저 ID */)> ProgressCallback2;

  // 매칭 요청을 취소할 때 이 핸들러를 호출합니다.
  typedef function<void(const string & /* 유저 ID */,
                        CancelResult /* 취소 결과 */)> CancelCallback;

  // 매칭 요청 시 선호하는 서버를 선택할 떄 사용합니다.
  enum TargetServerSelection {
    kRandom = 0,           // 무작위로 선택합니다.
    kMostNumberOfPlayers,  // 매치메이킹 요청이 많은 서버를 선택합니다.
                           // 진행 중인 요청수가 MANIFEST.json 파일 내
                           // concurrent_number_of_players_threshold 를
                           // 초과한 서버는 최대한 배제합니다.
    kLeastNumberOfPlayers  // 매치메이킹 요청이 적은 서버를 선택합니다.
  };

  // 매치메이킹 서버 정보입니다.
  struct MatchmakingServerInfo {
    Rpc::PeerId peer_id;     // 매치메이킹 서버가 동작 중인 피어 ID 입니다.
    size_t player_count;     // 현재 매칭 대기 중인 플레이어 수입니다.
    bool want_more_players;  // 이 서버가 더 많은 사용자를 수용할 수 있는지를
                             // 결정하는 값입니다.
                             // player_count 값이 MANIFEST.json 파일 내
                             // concurrent_number_of_players_threshold 값을
                             // 넘으면 false, 그렇지 않으면 true 입니다.
  };

  typedef std::vector<MatchmakingServerInfo> MatchmakingServerInfoVector;

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

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

  // 매치메이킹 서버로 매칭 요청을 보냅니다.
  // 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);

  // 매치메이킹 서버로 매칭 요청을 보냅니다.
  // 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);

  // 매치메이킹 서버로 매칭을 요청합니다.
  // 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);

  // 매치메이킹 서버로 매칭을 요청합니다.
  // 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);

  // 매칭 요청을 취소합니다.
  static void CancelMatchmaking(const Type &type, const string &player_id,
                                const CancelCallback &cancel_callback);
};
  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
using funapi;


namespace MatchMaking
{
  // 매칭 중인 플레이어 정보입니다. 매칭 완료 또는 ProgressCallback2 핸들러의
  // Match 객체 안에 있는 players 목록을 통해 확인할 수 있습니다.
  public struct Player
  {
    // AccountManager 에서 사용하는 유저 ID 입니다.
    public string Id;

    // 매치메이킹 클라이언트 및 서버 콤포넌트는 StartMatchmaking(),
    // StartMatchmaking2() 함수 호출 시 지정한 user_data 를
    // 다음과 같이 가공하여 저장합니다. 'request_time', 'elapsed_time' 값은
    // 변경할 수 없습니다.
    //
    // {
    //   "request_time":6447934119, // 요청 시각
    //   "elapsed_time":0,          // 요청 이후 지난 시간(단위: 초)
    //   "user_data": {
    //     // StartMatchmaking2() 안에 넣은 user_data
    //   }
    // }
    public JObject Context;

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

  // 매칭 정보입니다.
  public struct Match
  {
    // 매칭을 식별할 수 있는 ID 입니다. UUID 를 사용하며 매치메이킹 콤포넌트에서
    // 제공하는 모든 콜백 함수는 이 ID 단위로 직렬화하여 실행합니다.
    public System.Guid MatchId;

    // 매칭 타입입니다. 매칭 시 매칭의 종류나 형태를 구분할 때 사용합니다.
    // 임의의 값을 자유롭게 사용하면 됩니다.
    public Int64 MatchType;

    // 이 매칭에 참여 중인 플레이어 전체 목록입니다.
    // 매칭을 요청한 나 자신도 포함합니다.
    public ReadOnlyCollection<Player> Players;

    // 매칭 정보를 담는 객체입니다. Player::context 와는 다른 객체이므로 사용 시
    // 주의해야 합니다. 이 정보는 매치메이킹 서버 콤포넌트 시작 시
    // 인자로 지정하는 JoinCallback, LeaveCallback 에서 수정할 수 있습니다.
    // LeaveCallback 보다 JoinCallback 이 가장 먼저 불리므로, 이 콜백 함수에서
    // context 가 널(NULL)인 경우 초기화하고, 그 이후에 사용하면 됩니다.
    public JObject Context;
  }

  // MatchCallback 함수 인자로 매칭 결과를 정의합니다.
  public enum MatchResult {
    kSuccess = 0,  // 매칭 성공
    kAlreadyRequested,  // 이미 같은 유저 ID 로 요청한 내역이 있습니다.
    kTimeout,  // 요청 시간이 만료됐습니다.
    kError = 1000  // 내부 서버 에러
  };

  // CancelCallback 함수 인자로 매치메이킹 취소 결과를 정의합니다.
  public enum CancelResult {
    kSuccess = 0,  // 취소 성공
    kNoRequest,    // 이 유저 ID 로 매칭을 요청한 내역이 없습니다.
    kError = 1000  // 내부 서버 에러
  };


  public static class Client
  {
    // 매칭이 성사되면 이 핸들러에 등록한 함수를 호출합니다.
    public delegate void MatchCallback(string player_id /* 유저 ID */,
                                       Match match /* 매칭 정보 */,
                                       MatchResult /* 매칭 결과 */ result);

    // 매칭 상태를 업데이트할 때마다 호출합니다.
    // MANIFEST.json 파일의 MatchmakingServer 항목에
    // `enable_match_progress_callback` 값을 true 로 설정해야 콜백을 호출합니다.
    // 두 유저 ID(참가/나간) 값 모두 빈 콜백은 호출하지 않습니다.
    public delegate void ProgressCallback(
        string player_id /* 유저 ID */,
        System.Guid match_id /* 매칭 ID */,
        string player_id_joined,
        string player_id_left);

    // 매칭 ID 대신 매칭 정보를 사용합니다.
    // UpdateMatchPlayerContext() 함수를 사용하기 위해서는 반드시 이 형태의 콜백
    // 함수를 등록해야 합니다.
    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,  // 매치메이킹 요청이 많은 서버를 선택합니다.
                             // 진행 중인 요청이 MANIFEST.json 의
                             // concurrent_number_of_players_threshold 를
                             // 초과한 서버는 최대한 배제합니다.
      kLeastNumberOfPlayers  // 매치메이킹 요청이 적은 서버를 선택합니다.
    };

    // 매치메이킹 서버 정보입니다.
    public struct MatchmakingServerInfo {
      // 매치메이킹 서버가 동작 중인 피어 ID 입니다.
      public System.Guid peer_id;
      // 현재 매칭 요청 중인 플레이어 수입니다.
      public ulong player_count;
      // 이 서버가 더 많은 사용자를 수용할 수 있는지를
      // 결정하는 값입니다.
      // player_count 값이 MANIFEST.json 파일 내
      // concurrent_number_of_players_threshold 값을
      // 넘으면 false, 그렇지 않으면 true 입니다.
      public bool want_more_players;
    };

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

    // 매치메이킹 서버로 매치 요청을 보냅니다.
    // 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))

    // 매치메이킹 서버로 매칭을 요청합니다.
    // 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))

    // 매치메이킹 서버로 매칭을 요청합니다.
    // 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))

    // 매치메이킹 서버로 매칭을 요청합니다.
    // 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))

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

26.3. 매치메이킹 예제

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

다음은 유저 레벨 차이가 5 미만인 경우에만 매칭을 수락하는 2:2 매치메이킹 예제입니다. 7 초 이상 경과하면 레벨 차이를 무시하고 매칭을 수락합니다.

26.3.1.1. MatchmakingClient 코드

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

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

26.3.1.2. MatchmakingServer 코드

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

26.3.2. 예제 2: 매칭이 안됐을 때 조건을 바꿔 다시 매칭하기

다음은 단계를 나눠 매치메이킹을 진행하는 예제입니다. 각 단계 별로 매칭 조건이 다르며, 일정 시간 동안 매칭이 안되면 다음 단계에 해당하는 조건으로 다시 시도합니다.

26.3.2.1. MatchmakingClient 코드

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

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

26.3.2.2. MatchmakingServer 코드

 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.
}

26.3.3. 예제 3: Group vs. Group

이 예제는 유저 단위 매칭이 아닌 그룹 단위 매칭에 대해 설명합니다. 각 그룹은 2명의 유저로 구성되어 있으며 그룹 평균 레벨이 10 이하인 다른 그룹과 매칭하는 예제입니다. 친구 유저와 함께 파티를 맺고 다른 파티와 경쟁을 할 때 이러한 방식의 매치메이킹을 사용할 수 있습니다.

26.3.3.1. MatchmakingClient 코드

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


// 클라이언트로부터 받은 그룹 매칭 요청을 처리하는 메시지 핸들러입니다.
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 의 대전을 시작하는 처리를 합니다.
  // (클라이언트에 응답을 보내는 작업 등)
  ...
}
추후 업데이트 됩니다.

26.3.3.2. MatchmakingServer 코드

  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
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];
    //   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 이 호출됩니다.
}
추후 업데이트 됩니다.