static funapi.Matchmaking.Server.MatchState CheckMatchRequirements(funapi.Matchmaking.Match match) { // // 매치 완료 조건 검사 핸들러 함수입니다. // long total_players_for_match = MatchmakingType.GetNumberOfMaxPlayers(match.MatchType); // 총 플레이어 수가 매치 완료 조건에 부합하는 지 검사합니다. if (match.Players.Count != total_players_for_match) { // 아직 더 많은 플레이어가 필요합니다. Log.Info("Waiting for more players: match_id={0}, match_type={1}, total_players_for_match={2}, current players={3}", match.MatchId, match.MatchType, total_players_for_match, match.Players.Count); return(funapi.Matchmaking.Server.MatchState.kMatchNeedMorePlayer); } Log.Info("Matchmaking is done: match_id={0}, match_type={1}, total_players_for_match={2}, current players={3}", match.MatchId, match.MatchType, total_players_for_match, match.Players.Count); // 매치메이킹이 끝났으니 이 정보를 토대로 데디케이티드 서버 생성을 요청합니다. DedicatedServerHelper.SpawnDedicatedServer(match); return(funapi.Matchmaking.Server.MatchState.kMatchComplete); }
public static void ProcessSpawnOrMatchmaking( Session session, JObject message, SessionResponse.SessionResponseHandler handler) { Log.Assert(session != null); // 메시지 핸들러 함수는 세션 ID 를 이벤트 태그로 하는 이벤트 위에서 실행합니다. // 아래는 이 함수를 메시지 핸들러 위에서 실행하게 강제하게 검사하는 식입니다. // 세션 ID 별로 이 함수를 직렬화하지 않으면 동시에 서로 다른 곳에서 // 이 세션에 접근할 수 있습니다. Log.Assert(Event.GetCurrentEventTag() == session.Id); // // 매치메이킹 + 데디케이티드 서버 스폰 요청 예제 // // 유저 2명, 4명, 6명을 대상으로 새로운 데디케이티드 서버 프로세스를 생성합니다. // // 클라이언트는 다음 메시지 형태로 매치메이킹을 요청합니다. // { // // Facebook ID 또는 구글+ ID 등 고유한 ID 를 사용해야 합니다. // "account_id": "id", // // 매치 타입, 이 파일에 있는 MatchType 정의 참조 // "match_type": 1 // "user_data": { // "level": 70, // "ranking_score": 1500, // ... // }, // } if (message[kAccountId] == null || message[kAccountId].Type != JTokenType.String || message[kMatchType] == null || message[kMatchType].Type != JTokenType.Integer || message[kUserData] == null || message[kUserData].Type != JTokenType.Object) { Log.Error("Missing required fields: '{0}' / '{1}' / '{2}': session_id={3}, message={4}", kAccountId, kMatchType, kUserData, session.Id, message.ToString()); handler(SessionResponse.ResponseResult.FAILED, new SessionResponse(session, 400, "Missing required fields.", new JObject())); return; } // // 매치메이킹 요청 인자 설정 가이드 // // 1. 매치 타입 // 매치 타입을 식별하는 용도로 사용합니다. 정해진 값이 없으므로 // 서버에서 정의한 매치 타입과 일치하는지 확인할 필요가 있습니다. long match_type = message[kMatchType].Value <long>(); if (!MatchmakingType.IsValidMatchType(match_type)) { Log.Error("Invalid match_type: session_id={0}, message={1}", session.Id, message.ToString()); handler(SessionResponse.ResponseResult.FAILED, new SessionResponse(session, 400, "Invalid arguments.", new JObject())); return; } // 2. 계정(account) ID // AccountManager 로 로그인한 계정 ID 를 입력합니다. string account_id = message[kAccountId].Value <string>(); // 요청한 계정과 로그인 중인 세션이 일치하는 지 검사합니다. // 이 검사를 통해 다른 유저 ID 로 매칭 요청하는 것을 방지할 수 있습니다. if (AccountManager.FindLocalSession(account_id).Id != session.Id) { Log.Info("ProcessMatchmaking denied: bad request: session_id={0}, message={1}", session.Id, message.ToString()); handler(SessionResponse.ResponseResult.FAILED, new SessionResponse(session, 400, "Access denied for this account.", new JObject())); return; } // 3. user_data, 매치메이킹 및 생성한 데디케이티드 서버 안에서 사용할 // 플레이어의 데이터 입니다. 서버는 클라이언트에서 보낸 user_data 를 복사한 후 // 요청 시간을 추가합니다. 따라서 매치메이킹 시 사용할 데이터는 최종적으로 // 다음과 같습니다. // "user_data": { // "level": 70, // "mmr_score": 1500, // "req_time": 1544167339, // ... // }, JObject user_data = message[kUserData].ToJObject(); if (user_data[MatchmakingType.kMatchLevel] == null || user_data[MatchmakingType.kMatchLevel].Type != JTokenType.Integer || user_data[MatchmakingType.kMMRScore] == null || user_data[MatchmakingType.kMMRScore].Type != JTokenType.Integer) { // 매치메이킹 요청에 필요한 인자가 부족합니다. Log.Error("Missing required fields: session_id={0}, message={1}", session.Id, message.ToString()); handler(SessionResponse.ResponseResult.FAILED, new SessionResponse(session, 400, "Missing required fields.", new JObject())); return; } // 매치메이킹 없이 스폰을 진행하는 타입이면, 즉시 스폰을 시작합니다. // 매치메이킹은 2명 이상인 경우에만 동작한다는 점에 주의해주세요. // 1명이 매치메이킹 큐에 들어가면 매치메이킹 콜백을 호출하지 않습니다. // 연습 게임 또는 데디케이티드 서버 내부에서 사람을 채운 후 게임을 시작할 때 // 이러한 방식을 사용할 수 있습니다. if (match_type == (long)MatchmakingType.MatchType.kNoMatching) { SpawnOrSendUser(account_id, user_data, match_type); return; } // 4. OnMatchCompleted 콜백 // 매칭을 종료했을 때 결과를 받을 콜백 함수를 지정합니다. // 매치 결과는 콜백 함수의 funapi.Matchmaking.Client.MatchResult 타입으로 // 확인할 수 있습니다. // 5. 서버 선택 방법 // - kRandom: 서버를 랜덤하게 선택합니다. // - kMostNumberOfPlayers; 사람이 가장 많은 서버에서 매치를 수행합니다. // - kLeastNumberOfPlayers; 사람이 가장 적은 서버에서 매치를 수행합니다. funapi.Matchmaking.Client.TargetServerSelection target_selection = funapi.Matchmaking.Client.TargetServerSelection.kRandom; // 6. OnMatchProgressUpdated 콜백 // 매치 상태를 업데이트 할 때마다 결과를 받을 콜백 함수를 지정합니다. // 타임 아웃 (기본 값: default(TimeSpan)) TimeSpan timeout = default(TimeSpan); // 이 세션에서 요청한 매치 타입을 기록해둡니다. LogMatchHistory(session, account_id, match_type); Log.Info("Requesting a matchmaking: session_id={0}, account_id={1}, match_type={2}, user_data={3}", session.Id, account_id, match_type, user_data.ToString()); funapi.Matchmaking.Client.MatchCallback match_cb = new funapi.Matchmaking.Client.MatchCallback( (string player_id, funapi.Matchmaking.Match match, funapi.Matchmaking.MatchResult result) => { OnMatchCompleted(player_id, match, result, session, handler); }); funapi.Matchmaking.Client.Start2( match_type, account_id, user_data, match_cb, target_selection, OnMatchProgressUpdated, timeout); }
public static void CancelMatchmaking( Session session, JObject message, SessionResponse.SessionResponseHandler handler) { // 메시지 핸들러 함수는 세션 ID 를 이벤트 태그로 하는 이벤트 위에서 실행합니다. // 아래는 이 함수를 메시지 핸들러 위에서 실행하게 강제하게 검사하는 식입니다. // 세션 ID 별로 이 함수를 직렬화하지 않으면 동시에 서로 다른 곳에서 // 이 세션에 접근할 수 있습니다. Log.Assert(Event.GetCurrentEventTag() == session.Id); // 클라이언트는 다음 메시지 형태로 매치메이킹 취소를 요청합니다. // { // "account_id": "id", // "match_type": 1 // } if (message[kAccountId] == null || message[kAccountId].Type != JTokenType.String || message[kMatchType] == null || message[kMatchType].Type != JTokenType.Integer) { Log.Error("Missing required fields: '{0}' / '{1}': session_id={2}, message={3}", kAccountId, kMatchType, session.Id, message.ToString()); handler(SessionResponse.ResponseResult.FAILED, new SessionResponse(session, 400, "Missing required fields.", new JObject())); return; } // 매치 타입 long match_type = message[kMatchType].Value <long>(); if (!MatchmakingType.IsValidMatchType(match_type)) { Log.Error("Invalid match_type: session_id={0}, message={1}", session.Id, message.ToString()); handler(SessionResponse.ResponseResult.FAILED, new SessionResponse(session, 400, "Invalid arguments.", new JObject())); return; } if (match_type == (long)MatchmakingType.MatchType.kNoMatching) { // 매치메이킹 기능을 쓰지 않으므로 취소 처리한다. handler(SessionResponse.ResponseResult.OK, new SessionResponse(session, 200, "OK.", new JObject())); } // 계정 string account_id = message[kAccountId].Value <string>(); // 요청한 계정과 로그인 중인 세션이 일치하는 지 검사합니다. // 이 검사를 통해 다른 유저 ID 로 매칭을 취소하는 행위를 방지할 수 있습니다. if (AccountManager.FindLocalSession(account_id).Id != session.Id) { Log.Info("CancelMatchmaking denied: bad request: session_id={0}, message={1}", session.Id, message.ToString()); handler(SessionResponse.ResponseResult.FAILED, new SessionResponse(session, 400, "Access denied for this account.", new JObject())); return; } funapi.Matchmaking.Client.CancelCallback cancel_callback = new funapi.Matchmaking.Client.CancelCallback( (string account_id2, funapi.Matchmaking.CancelResult result2) => { if (result2 != funapi.Matchmaking.CancelResult.kSuccess) { // kCRNoRequest (요청하지 않은 매치) 또는 // kCRError (엔진 내부 에러)가 올 수 있습니다. handler(SessionResponse.ResponseResult.FAILED, new SessionResponse(session, 500, "Internal server error.", new JObject())); } else { handler(SessionResponse.ResponseResult.OK, new SessionResponse(session, 200, "OK.", new JObject())); } }); Log.Info("Canceling matchmaking: session_id={0}, account_id={1}, match_type={2}", session.Id, account_id, match_type); funapi.Matchmaking.Client.Cancel(match_type, account_id, cancel_callback); }
public static void SendUser( long match_type, string account_id, JObject user_data, DedicatedServerHelper.SendUserCallback send_callback) { Log.Assert(MatchmakingType.IsValidMatchType(match_type)); bool found = false; Guid target_match_id; JObject target_match_data = new JObject(); // 매치 타입과 일치하는 플레이어 수를 선택합니다. long total_players_for_match = MatchmakingType.GetNumberOfMaxPlayers(match_type); do { lock (match_lock) { // 현재 활성화된 매치를 검사하여 플레이어가 부족한 서버를 찾습니다. // 순서는 UUID 를 따르므로 별다른 우선 순위가 없습니다. // 조금 더 공정한 절차가 필요하다면 MyMatchInfo 에 모든 플레이어 없이 게임을 진행한 // 시간을 기록한 후, 이를 기준으로 우선순위를 부여할 수 있습니다. // // 1. 먼저 매치 타입으로 매치 ID 맵을 가져옵니다. if (!match_type_map.ContainsKey(match_type)) { break; } // 2. ID 에 해당하는 매치 목록을 순회하면서 필요한 플레이어보다 부족한 // 서버를 찾습니다. foreach (Guid match_id in match_type_map[match_type]) { Log.Assert(match_map.ContainsKey(match_id)); MyMatchInfo info = match_map[match_id]; if (info.players.Count < total_players_for_match) { // 서버를 찾았습니다. target_match_id = info.match_id; target_match_data = info.match_data; found = true; break; } } } }while (false); if (!found) { // 모든 서버가 매치에 필요한 플레이어를 확보했거나 서버가 없습니다. // 더 이상 진행할 수 없습니다. Log.Info("There's no available server to send user"); send_callback(false); } else { // 난입을 요청합니다. List <string> account_ids = new List <string> { account_id }; List <JObject> user_data_list = new List <JObject> { user_data }; DedicatedServerManager.SendCallback send_cb = new DedicatedServerManager.SendCallback( (Guid match_id2, List <string> users2, bool success2) => { OnUserSent(match_id2, users2, success2, target_match_data, match_type, send_callback); }); DedicatedServerManager.SendUsers( target_match_id, target_match_data, account_ids, user_data_list, send_cb); } }