こんにちは!金融ソリューション事業部の孫です。
Part1の記事とPart2の記事では、マッチングに関するAWS側のリソースを全部構築しました。
Part3である今回は、構築したバックエンドAPIをUEクライアントに組み込んでマッチング検証を行います!
Part1、Part2が未見の方は、ぜひ内容をご確認いただきたいです!
Part1の記事はこちらです!
Part2の記事はこちらです!
UEクライアントへのAPI組み込み
前回の記事で実装したクライアントをベースに、上記に作成したAPIの呼び出し機能を実装します。
最後に、組込み完了後のクライアントを使ってマッチング処理をテストします。
- CPPファイルの作成
- プロジェクトフォルダに移動しUE5 Editorファイル(Gamelift_UE5.uproject)を開きます
- GameMode CPPを作成します
- 下記図のように「world Setting」パネルを開いて、その中の「GameMode Override」項目内容は上記作った「OfflineGameMode」に設定します
- ログイン画面をコントロールするCPPを作成します
- 上記と同様な手順で、「New C++ Class」を選択します
- 「UserWidget」を選択して「OfflineMainMenuWidget」名前のCPPを作成します
- VS2019でOfflineGameMode CPPファイルを編集します
- 「Gamelift_UE5.Build.cs」に「"Http", "Json", "JsonUtilities"」モジュールを追加します
- 「OfflineGameMode.cpp」と「OfflineGameMode.h」を以下のように編集します
### <OfflineGameMode.cpp> #include "OfflineGameMode.h" #include "Gamelift_UE5Character.h" #include "UObject/ConstructorHelpers.h" AOfflineGameMode::AOfflineGameMode() { // set default pawn class to our Blueprinted character static ConstructorHelpers::FClassFinder<APawn> PlayerPawnBPClass(TEXT("/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter")); if (PlayerPawnBPClass.Class != NULL) { DefaultPawnClass = PlayerPawnBPClass.Class; } }
### <OfflineGameMode.h> #pragma once #include "CoreMinimal.h" #include "GameFramework/GameModeBase.h" #include "OfflineGameMode.generated.h" UCLASS() class GAMELIFT_UE5_API AOfflineGameMode : public AGameModeBase { GENERATED_BODY() public: AOfflineGameMode(); };
- VS2019でOfflineMainMenuWidget CPPファイルを編集します
OfflineMainMenuWidget.hコード
#pragma once #include "Http.h" #include "CoreMinimal.h" #include "Blueprint/UserWidget.h" #include "OfflineMainMenuWidget.generated.h" UCLASS() class GAMELIFT_UE5_API UOfflineMainMenuWidget : public UUserWidget { GENERATED_BODY() public: UOfflineMainMenuWidget(const FObjectInitializer& ObjectInitializer); UFUNCTION(BlueprintCallable) void OnLoginClicked(); UPROPERTY(EditAnywhere) FString ApiGatewayEndpoint; UPROPERTY(EditAnywhere) FString LoginURI; UPROPERTY(EditAnywhere) FString StartMatchMakingURI; UPROPERTY(EditAnywhere) FString PollMatchMakingURI; UPROPERTY(BluePrintReadWrite) FString user; UPROPERTY(BluePrintReadWrite) FString pass; UPROPERTY() FTimerHandle PollMatchmakingHandle; private: FHttpModule* Http; FString IdToken; FString MatchmakingTicketId; void LoginRequest(FString usr, FString pwd); void OnLoginResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful); void StartMatchMakingRequest(FString idt); void StartMatchMakingResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful); void PollMatchMakingRequest(); void PollMatchMakingResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful); };
OfflineMainMenuWidget.cppコード
#include "OfflineMainMenuWidget.h" #include "Json.h" #include "JsonUtilities.h" #include "Kismet/GameplayStatics.h" UOfflineMainMenuWidget::UOfflineMainMenuWidget(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { Http = &FHttpModule::Get(); ApiGatewayEndpoint = FString::Printf(TEXT("「API URLで書き換え」")); LoginURI = FString::Printf(TEXT("/login")); StartMatchMakingURI = FString::Printf(TEXT("/startmatchmaking")); PollMatchMakingURI = FString::Printf(TEXT("/pollmatchmaking")); IdToken = ""; MatchmakingTicketId = ""; } void UOfflineMainMenuWidget::OnLoginClicked() { LoginRequest(user, pass); } void UOfflineMainMenuWidget::LoginRequest(FString usr, FString pwd) { TSharedPtrJsonObject = MakeShareable(new FJsonObject()); JsonObject->SetStringField(TEXT("username"), *FString::Printf(TEXT("%s"), *usr)); JsonObject->SetStringField(TEXT("password"), *FString::Printf(TEXT("%s"), *pwd)); FString JsonBody; TSharedRef > JsonWriter = TJsonWriterFactory<>::Create(&JsonBody); FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter); TSharedRef LoginHttpRequest = Http->CreateRequest(); LoginHttpRequest->SetVerb("POST"); LoginHttpRequest->SetURL(ApiGatewayEndpoint + LoginURI); LoginHttpRequest->SetHeader("Content-Type", "application/json"); LoginHttpRequest->SetContentAsString(JsonBody); LoginHttpRequest->OnProcessRequestComplete().BindUObject(this, &UOfflineMainMenuWidget::OnLoginResponse); LoginHttpRequest->ProcessRequest(); } void UOfflineMainMenuWidget::OnLoginResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bwasSuccessful) { if (bwasSuccessful) { TSharedPtr JsonObject; TSharedRef > Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString()); if (FJsonSerializer::Deserialize(Reader, JsonObject)) { IdToken = JsonObject->GetObjectField("tokens")->GetStringField("IdToken"); StartMatchMakingRequest(IdToken); } } } void UOfflineMainMenuWidget::StartMatchMakingRequest(FString idt) { TSharedRef StartMatchMakingHttpRequest = Http->CreateRequest(); StartMatchMakingHttpRequest->SetVerb("GET"); StartMatchMakingHttpRequest->SetURL(ApiGatewayEndpoint + StartMatchMakingURI); StartMatchMakingHttpRequest->SetHeader("Content-type", "application/json"); StartMatchMakingHttpRequest->SetHeader("Authorization", idt); StartMatchMakingHttpRequest->OnProcessRequestComplete().BindUObject(this, &UOfflineMainMenuWidget::StartMatchMakingResponse); StartMatchMakingHttpRequest->ProcessRequest(); } void UOfflineMainMenuWidget::StartMatchMakingResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bwasSuccessful) { if (bwasSuccessful) { TSharedPtr JsonObject; TSharedRef > Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString()); if (FJsonSerializer::Deserialize(Reader, JsonObject)) { if (JsonObject->HasField("ticketId")) { MatchmakingTicketId = JsonObject->GetStringField("ticketId"); GetWorld()->GetTimerManager().SetTimer(PollMatchmakingHandle, this, &UOfflineMainMenuWidget::PollMatchMakingRequest, 1.0f, true, 1.0f); } } } } void UOfflineMainMenuWidget::PollMatchMakingRequest() { TSharedPtr RequestObj = MakeShareable(new FJsonObject); RequestObj->SetStringField("ticketId", MatchmakingTicketId); FString RequestBody; TSharedRef > Writer = TJsonWriterFactory<>::Create(&RequestBody); if (FJsonSerializer::Serialize(RequestObj.ToSharedRef(), Writer)) { TSharedRef PollMatchMakingHttpRequest = Http->CreateRequest(); PollMatchMakingHttpRequest->SetVerb("POST"); PollMatchMakingHttpRequest->SetURL(ApiGatewayEndpoint + PollMatchMakingURI); PollMatchMakingHttpRequest->SetHeader("Content-type", "application/json"); PollMatchMakingHttpRequest->SetHeader("Authorization", IdToken); PollMatchMakingHttpRequest->OnProcessRequestComplete().BindUObject(this, &UOfflineMainMenuWidget::PollMatchMakingResponse); PollMatchMakingHttpRequest->SetContentAsString(RequestBody); PollMatchMakingHttpRequest->ProcessRequest(); } } void UOfflineMainMenuWidget::PollMatchMakingResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bwasSuccessful) { if (bwasSuccessful) { TSharedPtr JsonObject; TSharedRef > Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString()); if (FJsonSerializer::Deserialize(Reader, JsonObject)) { FString IpAddress = JsonObject->GetObjectField("PlayerSession")->GetStringField("IpAddress"); FString Port = JsonObject->GetObjectField("PlayerSession")->GetStringField("Port"); TArray > Players = JsonObject->GetObjectField("Players")->GetArrayField("L"); TSharedPtr Player = Players[0]->AsObject()->GetObjectField("M"); FString PlayerSessionId = Player->GetObjectField("PlayerSessionId")->GetStringField("S"); FString PlayerId = Player->GetObjectField("PlayerId")->GetStringField("S"); FString LevelName = IpAddress + ":" + Port; const FString& Options = "?PlayerSessionId=" + PlayerSessionId + "?PlayerId=" + PlayerId; UGameplayStatics::OpenLevel(GetWorld(), FName(*LevelName), false, Options); } } }
- ユーザーログイン用BluePrintの作成
- Blueprintsフォルダに移動し、右クリックして「User Interface」⇒「Widget Blueprint」で「WBP_OfflineMainMenu」BluePrintを作成します
- 「WBP_OfflineMainMenu」BluePrintを選択し、「Open Level Blueprint」をクリックしてBlueprintエディターを開きます
- 以下の図のようにBluePrintを編集します
- ゲーム開始をトリガーとして、ユーザー名とパスワード入力のログイン画面を表示します
- 「WBP_OfflineMainMenu」BluePrintをダブルクリックし、以下のようにユーザー名とパスワード入力の画面をデザインします
ここまでは、クライアント側の組込みが完了しました。
マッチング機能の検証
今回、ゲームセッションは3つ作成しました。
クライアントを6つ立ち上げて、ログイン後にそれぞれのゲームセッションにランダムにマッチングで振り分けられることを確認します。
- ログイン前の状態
- クライアント確認 (入力待ちの状態)
- GameLift側の確認
- ゲームセッションが「0」であること
- ログイン後の状態
- クライアント確認
- 2人ずつマッチングされて各クライアント画面でプレイヤー2人が確認されます
- クライアント確認
- GameLift側の確認
- ゲームセッションが「3」であること
- 各ゲームセッション配下にプレイヤーセッションが「2」であること
終わりに
今回はFlexMatch機能を活用して、マッチング基盤を構築してみました。
この基盤は、単なるゲームで使われるものだけではなく、メタバース上でも活用可能だと考えられます。例えばイベント開催時にユーザー間のコミュニケーションを増やすことを目的として、同じ地域や性格のユーザーを同じルームに配置する、などのユースケースが挙げられます。
ゲームで用いられる上記の技術は、将来メタバース上でますます活用されていくと思いますので、引き続きゲーム領域のサーバーサイド技術を学習していきたいです。
現在ISIDはweb3領域のグループ横断組織を立ち上げ、Web3およびメタバース領域のR&Dを行っております(カテゴリー「3DCG」の記事はこちら)。
もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ぜひお気軽にご連絡ください!
私たちと同じチームで働いてくれる仲間を、是非お待ちしております!
ISID採用ページ(Web3/メタバース/AI)
参考
- https://docs.aws.amazon.com/ja_jp/gamelift/latest/flexmatchguide/match-events.html
- https://docs.unrealengine.com/5.0/en-US/setting-up-a-game-mode-in-unreal-engine/
- https://docs.unrealengine.com/5.0/en-US/blueprints-visual-scripting-in-unreal-engine/
- https://www.youtube.com/watch?v=lhABExDSpHE&list=PLuGWzrvNze7LEn4db8h3Jl325-asqqgP2&index=7
執筆:@chen.sun、レビュー:@wakamoto.ryosuke
(Shodoで執筆されました)