1. はじめに
前回はマッチメイクのAPIを叩く準備までできたので、今度は実際にマッチマイクの実装を追加していきます。
APIのドキュメントはこちら。
APIは以下の種類があるようです。
Tickets
- Create a matchmaking ticket
- マッチングのチケット作成
- Delete a matchmaking ticket
- マッチングのチケット削除
- Gets the status of a ticket match assignment in the matchmaker
- マッチメイクの状態を確認
- Create a matchmaking ticket
Backfill
- Approve a backfill ticket
- バックフィルチケットの承認
- Create a backfill ticket
- バックフィルチケットを作成
- Delete a backfill ticket
- バックフィルチケットを削除
- Update a backfill ticket
- バックフィルチケットの更新
- Approve a backfill ticket
上記を使用していきます。マッチングを最短で検証するため、必要最低限のAPIだけ追加していきます。 (エラーのハンドリングなどは一旦なし)
2. マッチングチケット作成
Create a matchmaking ticket
こちらのAPIを使って、マッチングチケットを発行します。
今回はマッチングのトリガーとして、BPをワールド上に配置して、BPのOverlapでイベントを設定します。
void ACPP_MatchUI::NotifyActorBeginOverlap(AActor* OtherActor) { Super::NotifyActorBeginOverlap(OtherActor); UE_LOG(LogTemp, Log, TEXT("ACPP_MatchUI::NotifyActorBeginOverlap %s"), *GetNameSafe(OtherActor)); StartMatching(); }
以下のような関数を追加します。
AuthToken
に関しては、前回取得できるよになった accessToken
を設定してください。
void ACPP_MatchUI::StartMatching() { if (bIsMatching) { return; } bIsMatching = true; UE_LOG(LogTemp, Log, TEXT("ACPP_MatchUI::StartMatching")); if (MatchAuth->GetAuthToken().IsEmpty()) { UE_LOG(LogTemp, Warning, TEXT("ACPP_MatchUI::NotifyActorBeginOverlap MatchAuth->GetAuthToken() is Empy")); return; } // Matching Request if (MatchMakeRequest) { MatchMakeRequest->CreateMatchMakeTicket(MatchAuth->GetAuthToken()); GetWorldTimerManager().SetTimer(MatchingStatusHandle, this, &ACPP_MatchUI::TryMatchMakeStatus, 3.f, true); } }
リクエスト用のクラスを追加して、以下のように追加します。
リクエストのURLもConfig.csに追加します。
static const FString MATCHMAKE_TICKETS_URL = TEXT("https://matchmaker.services.api.unity.com/v2/tickets");
void UCPP_MatchMakeRequest::Init() { Http = NewObject(); } void UCPP_MatchMakeRequest::CreateMatchMakeTicket(const FString& AuthToken) { // リクエストボディの作成 TSharedPtr JsonObject = MakeShareable(new FJsonObject); // キュー名設定 JsonObject->SetStringField("queueName", TEXT("MultiPlay")); // Attributes設定 TSharedPtr Attributes = MakeShareable(new FJsonObject); Attributes->SetStringField("region", TEXT("Asia")); // region JsonObject->SetObjectField("attributes", Attributes.ToSharedRef()); // player 設定 TSharedPtr Player = MakeShareable(new FJsonObject); Player->SetStringField("id", FGuid().NewGuid().ToString()); // playerId // 必要あればオプション追加 // 配列に追加 TArray > PlayerObjects; PlayerObjects.Add(MakeShareable(new FJsonValueObject(Player.ToSharedRef()))); JsonObject->SetArrayField("players", PlayerObjects); // OutputStringにJson書き出し FString OutputString; TSharedRef > JsonWriter = TJsonWriterFactory<>::Create(&OutputString); FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter); // リクエスト Http->Request( *MATCHMAKE_TICKETS_URL, EHttpVerbs::POST, *GetHeader(AuthToken), OutputString, [=](const FString& Response) { // レスポンスをパース TSharedPtr ResponseObject; TSharedRef > Reader = TJsonReaderFactory<>::Create(Response); if (FJsonSerializer::Deserialize(Reader, ResponseObject)) { // チケットIDを取得 TicketId = ResponseObject->GetStringField(TEXT("id")); } }); }
すると、以下のようにチケットが発行されるかと思います。
取得したチケットIDをもとにStatusを1秒に1回程度(今回は3秒に1回にしてます。)確認します。
追加したコードは以下。
static const FString MATCHMAKE_TICKET_STATUS_URL = TEXT("https://matchmaker.services.api.unity.com/v2/tickets/status");
void ACPP_MatchUI::TryMatchMakeStatus() { if (MatchMakeRequest->GetTicketId().IsEmpty()) { return; } if (MatchMakeRequest->GetMatchingStatus().IsFound()) { // タイマークリア GetWorldTimerManager().ClearTimer(MatchingStatusHandle); // マップ遷移 ToConnect(); return; } MatchMakeRequest->GetMatchMakeTicketStatus(MatchAuth->GetAuthToken()); } void UCPP_MatchMakeRequest::GetMatchMakeTicketStatus(const FString& AuthToken) { const FString& URL = MATCHMAKE_TICKET_STATUS_URL + TEXT("?id=") + TicketId; // リクエスト Http->Request( URL, EHttpVerbs::GET, *GetHeader(AuthToken), TEXT(""), [=](const FString& Response) { // レスポンスをパース TSharedPtrResponseObject; TSharedRef > Reader = TJsonReaderFactory<>::Create(Response); if (FJsonSerializer::Deserialize(Reader, ResponseObject)) { MatchingStatus.IP = ResponseObject->GetStringField(TEXT("ip")); MatchingStatus.Port = ResponseObject->GetStringField(TEXT("port")); MatchingStatus.Status = ResponseObject->GetStringField(TEXT("status")); MatchingStatus.AssignmentType = ResponseObject->GetStringField(TEXT("assignmentType")); MatchingStatus.MatchId = ResponseObject->GetStringField(TEXT("matchId")); } }); }
上記のように追加して、プレイすると以下のようにレスポンスが返ってくるかと思います。
レスポンスに含まれる。IP
と Port
をもとにマップ遷移します。
void ACPP_MatchUI::ToConnect() const { // マップ遷移 const FString& URL = MatchMakeRequest->GetMatchingStatus().IP + TEXT(":") + MatchMakeRequest->GetMatchingStatus().Port; UGameplayStatics::OpenLevel(this, *URL, true, TEXT("")); }
すると、以前に作成したUnity Game ServicesのサーバーにJoinすることができます。
サービスアカウントのダッシュボードのServersを確認すると、 Allocated
となっているのも確認できると思います。
これで基本的なチケット発行から、サーバーへの遷移はできるようになりました。
ここから、複数人のプレイヤーをマッチングさせます。
3. バックフィルチケットを使ってマッチング
該当のサーバーのMaxPlayerが1以上になっていれば、まだ複数人が該当のサーバーにジョインできる状態になります。
ただ、普通にもう一つクライアントを立ち上げてチケットを発行しても、別のサーバーに入ってしまうだけになります。
そこでサーバー側でバックフィルを登録する処理を追加します。
以前追加した ACPP_GameSession
のクラスに以下を追加します。
void ACPP_GameSession::InitMultiplayServerSystem() { if (ServerQueryHandlerSubsystem) { return; } UGameInstance* GameInstance = GetWorld()->GetGameInstance(); GameServerSubsystem = GameInstance->GetSubsystem(); GameServerSubsystem->SubscribeToServerEvents(); GameServerSubsystem->OnAllocate.AddDynamic(this, &ACPP_GameSession::OnAllocate); ServerQueryHandlerSubsystem = GameInstance->GetSubsystem (); // Auth設定 MatchAuth = NewObject (); // Matching Request MatchMakeRequest = NewObject (); if (MatchMakeRequest) { MatchMakeRequest->Init(); } }
また、サーバー側でAPIを叩くのにも認証が必要となります。
認証のクラスに以下の処理を追加します。
void UCPP_MatchAuth::ServerAuthRequest() { // URLを作成 const FString URL = TEXT("http://localhost:8086/token"); UHTTP* Request = NewObject(); Request->Request(*URL, EHttpVerbs::GET, TEXT(""), TEXT(""), [=](const FString& Response) { // レスポンスをパース TSharedPtr JsonObject; TSharedRef > Reader = TJsonReaderFactory<>::Create(Response); if (FJsonSerializer::Deserialize(Reader, JsonObject)) { // AuthTokenを取得 AuthToken = JsonObject->GetStringField(TEXT("token")); } }); }
void ACPP_GameSession::OnReadyServerSuccess() { UE_LOG(LogTemp, Log, TEXT("OnReadyServerSuccess")); // Auth認証 if (MatchAuth) { MatchAuth->ServerAuthRequest(); } // Palyload AllocationのDelegateセット OnPlayloadSuccess.BindDynamic(this, &ACPP_GameSession::OnPayloadAllocationSuccess); OnPlayloadFailure.BindDynamic(this, &ACPP_GameSession::OnPayloadAllocationFailed); GameServerSubsystem->GetPayloadAllocation(OnPlayloadSuccess, OnPlayloadFailure); }
なぜ http://localhost:8086/token
なのかは謎ですが、、ひとまずこの処理を追加して、サーバービルドして、ファイルをアップロードしてのちに、接続させて、サーバーのログを確認します。
また、一緒に GetPayloadAllocation
にてマッチング情報も取得しておきます。
void ACPP_GameSession::OnPayloadAllocationSuccess(FString Payload) { UE_LOG(LogTemp, Log, TEXT("OnPayloadAllocationSuccess Payload(%s)"), *Payload); // Modelでパース ResultModel.ParseFromJson(Payload); if (!ResultModel.BackfillTicketId.IsEmpty()) { // 1秒に一回リクエスト GetWorldTimerManager().SetTimer(BackfillApproveHandle, this, &ACPP_GameSession::TryApproveBackfill, 1.f, true); } } void ACPP_GameSession::OnPayloadAllocationFailed(FMultiplayPayloadAllocationErrorResponse ErrorResponse) { UE_LOG(LogTemp, Log, TEXT("OnPayloadAllocationFailed : %s"), *ErrorResponse.ErrorMessage); }
レスポンスのModelに関しては以下を定義
struct FMatchmakingResultsModel { struct FTeamInfo { FString TeamName; FString TeamId; TArrayPlayerIDs; void Parse(const FString& JsonString) { TSharedPtr ResponseObject; TSharedRef > Reader = TJsonReaderFactory<>::Create(JsonString); if (FJsonSerializer::Deserialize(Reader, ResponseObject)) { TeamName = ResponseObject->GetStringField(TEXT("TeamName")); TeamId = ResponseObject->GetStringField(TEXT("TeamId")); TArray > PlayerIDsArray = ResponseObject->GetArrayField(TEXT("PlayerIds")); for (TSharedPtr PlayerID : PlayerIDsArray) { PlayerIDs.Add(PlayerID->AsString()); } } } }; struct FPlayerInfo { FString Id; FString CustomData; FString QosResults; void Parse(const FString& JsonString) { TSharedPtr ResponseObject; TSharedRef > Reader = TJsonReaderFactory<>::Create(JsonString); if (FJsonSerializer::Deserialize(Reader, ResponseObject)) { Id = ResponseObject->GetStringField(TEXT("Id")); CustomData = ResponseObject->GetStringField(TEXT("CustomData")); QosResults = ResponseObject->GetStringField(TEXT("QosResults")); } } }; struct FMatchProperties { FString Region; FString BackfillTicketId; TArray Teams = TArray (); TArray Players = TArray (); void Parse(TSharedPtr & JsonObject) { Region = JsonObject->GetStringField(TEXT("Region")); BackfillTicketId = JsonObject->GetStringField(TEXT("BackfillTicketId")); TArray > TeamInfoArray = JsonObject->GetArrayField(TEXT("Teams")); for (TSharedPtr TeamJson : TeamInfoArray) { FTeamInfo Team; Team.Parse(TeamJson->AsString()); Teams.Add(Team); } TArray > PlayerArray = JsonObject->GetArrayField(TEXT("Players")); for (TSharedPtr PlayerJson : PlayerArray) { FPlayerInfo Player; Player.Parse(PlayerJson->AsString()); Players.Add(Player); } } }; FMatchProperties MatchProperties; FString QueueName; FString PoolName; FString EnvironmentId; FString BackfillTicketId; FString MatchId; FString PoolId; void ParseFromJson(const FString& JsonString) { TSharedPtr ResponseObject; TSharedRef > Reader = TJsonReaderFactory<>::Create(JsonString); if (FJsonSerializer::Deserialize(Reader, ResponseObject)) { TSharedPtr MatchPropertiesJsonObj = ResponseObject->GetObjectField(TEXT("MatchProperties")); MatchProperties.Parse(MatchPropertiesJsonObj); QueueName = ResponseObject->GetStringField(TEXT("QueueName")); PoolName = ResponseObject->GetStringField(TEXT("PoolName")); EnvironmentId = ResponseObject->GetStringField(TEXT("EnvironmentId")); BackfillTicketId = ResponseObject->GetStringField(TEXT("BackfillTicketId")); MatchId = ResponseObject->GetStringField(TEXT("MatchId")); PoolId = ResponseObject->GetStringField(TEXT("PoolId")); } } };
上記で、バックフィルチケットとサーバー用のTokenが取得できたので、こちらをもとに バックフィルの承認をします。
void ACPP_GameSession::TryApproveBackfill() { if (ServerQueryHandlerSubsystem->GetCurrentPlayers() >= ServerQueryHandlerSubsystem->GetMaxPlayers()) { // 人数が入ってきたら終了 GetWorldTimerManager().ClearTimer(BackfillApproveHandle); // バックフィル削除 MatchMakeRequest->DeleteBackfillTicket(MatchAuth->GetAuthToken(), ResultModel.BackfillTicketId); return; } if (!MatchAuth->GetAuthToken().IsEmpty()) { MatchMakeRequest->ApproveBackfill(MatchAuth->GetAuthToken(), ResultModel.BackfillTicketId); } }
※MaxPlayerの最大値を動的にマッチメイクから取得できるかと思っていたら、そのような機能はなさそうなので、別途ベタに追加してます。。
// BackFill API static const FString MATCHMAKE_BACKFILL_URL = TEXT("https://matchmaker.services.api.unity.com/v2/backfill");
void UCPP_MatchMakeRequest::ApproveBackfill(const FString& AuthToken, const FString& BackfillId) { const FString& URL = MATCHMAKE_BACKFILL_URL + TEXT("/") + BackfillId + TEXT("/approvals"); // リクエスト Http->Request( URL, EHttpVerbs::POST, *GetHeader(AuthToken), TEXT(""), [=](const FString& Response) { // レスポンスをパース TSharedPtrResponseObject; TSharedRef > Reader = TJsonReaderFactory<>::Create(Response); if (FJsonSerializer::Deserialize(Reader, ResponseObject)) { BackfillApprovealResponse.Id = ResponseObject->GetStringField(TEXT("id")); BackfillApprovealResponse.Created = ResponseObject->GetIntegerField(TEXT("created")); const TSharedPtr PropertyObj = ResponseObject->GetObjectField(TEXT("properties")); BackfillApprovealResponse.Properties.Parse(PropertyObj); BackfillApprovealResponse.Attributes = ResponseObject->GetStringField(TEXT("attributes")); BackfillApprovealResponse.Connection = ResponseObject->GetStringField(TEXT("connection")); } }); }
上記を追加したら、再度サーバービルドをして、アップロードしなおし、2人でプレイしてみます。
無事に同じサーバーに入ることができました。
基本的には上記の処理でマッチメイクができるようになったかと思います。
他、細かいルール設定や、ロビーなどを使っていい感じにマッチメイクできるかと思いますが、別途検証していきたいと思います。