電通総研 テックブログ

電通総研が運営する技術ブログ

GameLift活用でUnrealEngineゲームのマッチング基盤を構築する【Part3】

こんにちは!金融ソリューション事業部の孫です。
Part1の記事Part2の記事では、マッチングに関するAWS側のリソースを全部構築しました。
Part3である今回は、構築したバックエンドAPIをUEクライアントに組み込んでマッチング検証を行います!

Part1、Part2が未見の方は、ぜひ内容をご確認いただきたいです!
Part1の記事はこちらです!
Part2の記事はこちらです!

UEクライアントへのAPI組み込み

前回の記事で実装したクライアントをベースに、上記に作成したAPIの呼び出し機能を実装します。
最後に、組込み完了後のクライアントを使ってマッチング処理をテストします。

  • CPPファイルの作成
    • プロジェクトフォルダに移動しUE5 Editorファイル(Gamelift_UE5.uproject)を開きます

  • GameMode CPPを作成します
    • C++ Classes 配下に、右クリックして「New C++ class」を選択します
    • 「Game Mode Base」を選択して「OfflineGameMode」名前の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ファイルを編集します
    • プレイヤーがログインボタンを押したら、マッチング処理が開始されます。
    • 処理の流れとしては、「ログインリクエスト(LoginRequest) ⇒ マッチング開始リクエスト(StartMatchMakingRequest) ⇒ マッチング結果取得リクエスト(PollMatchMakingRequest)」順番でAPIを呼び出し結果を取得します
    • マッチング結果取得にあたり、「GetWorld()->GetTimerManager().SetTimer」タイマーを設定することによって1秒のポーリング間隔で結果を取得しています

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) {
    TSharedPtr JsonObject = 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をダブルクリックし、以下のようにユーザー名とパスワード入力の画面をデザインします

  • Loginボタンの「OnClicked Events」を追加します
    • イベント処理では、プレイヤーが入力したユーザー名とパスワードを取得し、ログインAPIをリクエストします


ここまでは、クライアント側の組込みが完了しました。

マッチング機能の検証

今回、ゲームセッションは3つ作成しました。
クライアントを6つ立ち上げて、ログイン後にそれぞれのゲームセッションにランダムにマッチングで振り分けられることを確認します。

  • ログイン前の状態
    • クライアント確認 (入力待ちの状態)

  • GameLift側の確認
    • ゲームセッションが「0」であること

  • ログイン後の状態
    • クライアント確認
      • 2人ずつマッチングされて各クライアント画面でプレイヤー2人が確認されます

  • GameLift側の確認
    • ゲームセッションが「3」であること

  • 各ゲームセッション配下にプレイヤーセッションが「2」であること

終わりに

今回はFlexMatch機能を活用して、マッチング基盤を構築してみました。
この基盤は、単なるゲームで使われるものだけではなく、メタバース上でも活用可能だと考えられます。例えばイベント開催時にユーザー間のコミュニケーションを増やすことを目的として、同じ地域や性格のユーザーを同じルームに配置する、などのユースケースが挙げられます。
ゲームで用いられる上記の技術は、将来メタバース上でますます活用されていくと思いますので、引き続きゲーム領域のサーバーサイド技術を学習していきたいです。

現在ISIDはweb3領域のグループ横断組織を立ち上げ、Web3およびメタバース領域のR&Dを行っております(カテゴリー「3DCG」の記事はこちら)。
もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ぜひお気軽にご連絡ください!
私たちと同じチームで働いてくれる仲間を、是非お待ちしております!
ISID採用ページ(Web3/メタバース/AI)

参考

執筆:@chen.sun、レビュー:@wakamoto.ryosuke
Shodoで執筆されました