1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【UE】Unreal Engine から Google Cloud の API を叩くときの基本的な流れ

Last updated at Posted at 2024-12-23

はじめに

この記事では Unreal Engine からサービスアカウント経由で Google Cloud の API を叩くときの基本的な流れを解説します。
この記事でのテスト環境は以下の通りです。

  • Unreal Engine 5.3.2
  • Visual Studio 2022 17.11.5

手順

サービスアカウントの作成

早速、手順を解説します。
まだサービスアカウントを作成していない場合はサービスアカウントを作成します。

手順1

Google Cloud 自体の初期設定が完了していない場合は Google アカウントの作成や無料トライアルへの登録、プロジェクトの作成などの Googe Cloud の初期設定を行います。
以下の記事が参考になるかと思います。

手順2

Google Cloud にアクセスして「IAM と管理 > サービスアカウント」に行って「サービスアカウントを作成」を押します。

TSUBASAMUSU.png

手順3

「サービスアカウント名」と「サービスアカウント ID」を入力して「作成して続行」を押します。

TSUBASAMUSU.png

手順4

必要に応じて「ロールを選択」を押してロールを選択し、「続行」を押します。
例えば Cloud Storage のオブジェクトを読み取る権限をこのサービスアカウントに付与したい場合は「Cloud Storage > Storage オブジェクト閲覧者」を選択します。

TSUBASAMUSU.png

手順5

必要に応じて「サービスアカウントユーザーロール」と「サービスアカウント管理者ロール」を設定して「完了」を押します。

TSUBASAMUSU.png

秘密鍵のダウンロード

手順6

作成したサービスアカウントを選択して「キー > 鍵を追加 > 新しい鍵を作成」を押します。

TSUBASAMUSU.png

手順7

キーのタイプで「JSON」を選択して右下の「作成」を押し、秘密鍵の JSON ファイルをダウンロードします。
この JSON ファイルは大切に保管しておきましょう。

TSUBASAMUSU.png

API の使用(Cloud Translation API の場合)

ここからは実際にコードを書いて Google Cloud の API を使用します。
この記事では文章の翻訳に使える Cloud Translation API を叩きます。

手順8

Google Cloud に戻って、使いたい API の画面を開いて「有効にする」を押します。

TSUBASAMUSU.png

手順9

ここからは Unreal Engine での作業になります。
まずは Google Cloud の API を叩くときに必要なアクセストークンを取得する Latent ノードを作成します。
Latent ノード(ノードの右上に時計マークが付いている、処理を非同期で実行できるやつ)の作り方はこちらの記事が参考になるかと思います。

TSUBASAMUSU.png

コードが長く、そこそこ複雑になってしまっていますが、個人的な趣味で開発している「TsubasamusuUnrealLibrary」というブループリント用の関数ライブラリの v1.10.0 に上の画像のようなノードのコードが書かれているので添付しておきます。

ここでは UTsubasamusuStringConvertLibrary と UAsyncActionGetGoogleAccessToken という2つのクラスを定義しています。
UAsyncActionGetGoogleAccessToken が UTsubasamusuStringConvertLibrary の ConvertToBase64()ConvertToUtf8()ConvertToCharArray() を参照しています。

このプラグインのソースコードを参考にしながら build.cs に適切なモジュールを追加して以下のファイルをコピペすればこの Latent ノードを作れるはずです。

UTsubasamusuStringConvertLibrary
TsubasamusuStringConvertLibrary.h
#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "TsubasamusuStringConvertLibrary.generated.h"

UCLASS()
class TSUBASAMUSUUNREALLIBRARY_API UTsubasamusuStringConvertLibrary : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()

public:
	UFUNCTION(BlueprintPure, Category = "TSUBASAMUSU|Convert|String")
	static FString ConvertToBase64(const FString& String);

	UFUNCTION(BlueprintCallable, Category = "TSUBASAMUSU|Convert|String")
	static TArray<uint8> ConvertToUtf8(const FString& String);
	
	static TArray<char> ConvertToCharArray(const FString& String);
};
TsubasamusuStringConvertLibrary.cpp
#include "Convert/TsubasamusuStringConvertLibrary.h"

FString UTsubasamusuStringConvertLibrary::ConvertToBase64(const FString& String)
{
    FTCHARToUTF8 Utf8String(*String);

    return FBase64::Encode((const uint8*)Utf8String.Get(), Utf8String.Length());
}

TArray<uint8> UTsubasamusuStringConvertLibrary::ConvertToUtf8(const FString& String)
{
    FTCHARToUTF8 Utf8(*String);

    TArray<uint8> StringData;

    StringData.Append((uint8*)Utf8.Get(), Utf8.Length());

    return StringData;
}

TArray<char> UTsubasamusuStringConvertLibrary::ConvertToCharArray(const FString& String)
{
    FTCHARToUTF8 UTF8String(*String);

    TArray<char> CharArray;

    CharArray.Reserve(UTF8String.Length());

    CharArray.Append(UTF8String.Get(), UTF8String.Length());

    return CharArray;
}
UAsyncActionGetGoogleAccessToken
AsyncActionGetGoogleAccessToken.h
#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "AsyncActionGetGoogleAccessToken.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnSucceededToGetGoogleAccessToken, const FString&, AccessToken, int64, ExpirationUnixTime);

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnFailedToGetGoogleAccessToken);

UCLASS()
class TSUBASAMUSUUNREALLIBRARY_API UAsyncActionGetGoogleAccessToken : public UBlueprintAsyncActionBase
{
	GENERATED_BODY()

public:
	UPROPERTY(BlueprintAssignable)
	FOnSucceededToGetGoogleAccessToken Succeeded;

	UPROPERTY(BlueprintAssignable)
	FOnFailedToGetGoogleAccessToken Failed;

	UFUNCTION(BlueprintCallable, Category = "TSUBASAMUSU|Google", meta = (BlueprintInternalUseOnly = "true", WorldContext = "WorldContextObject", AutoCreateRefTerm = "Scopes"))
	static UAsyncActionGetGoogleAccessToken* AsyncGetGoogleAccessToken(UObject* WorldContextObject, const FString& PrivateKey, const FString& ServiceAccountMailAddress, const TArray<FString>& Scopes);

	void Activate() override;

private:
	FString PrivateKey;

	FString ServiceAccountMailAddress;

	TArray<FString> Scopes;

	int64 ExpirationUnixTime;

	FString GetGoogleAccessTokenHeader();

	FString GetGoogleAccessTokenPayload();

	FString GetGoogleCloudJwt();

	FString GetGoogleCloudJsonContent();

	void OnSucceeded(const FString& AccessToken);

	void OnFailed(const FString& TriedThing);
};
AsyncActionGetGoogleAccessToken.cpp
#include "Google/AsyncActionGetGoogleAccessToken.h"
#include "Interfaces/IHttpRequest.h"
#include "HttpModule.h"
#include "Interfaces/IHttpResponse.h"

#define UI UI_ST
THIRD_PARTY_INCLUDES_START
#include <openssl/sha.h>
#include <openssl/bio.h>
#include <openssl/rsa.h>
#include <openssl/pem.h>
THIRD_PARTY_INCLUDES_END
#undef UI

UAsyncActionGetGoogleAccessToken* UAsyncActionGetGoogleAccessToken::AsyncGetGoogleAccessToken(UObject* WorldContextObject, const FString& PrivateKey, const FString& ServiceAccountMailAddress, const TArray<FString>& Scopes)
{
	UAsyncActionGetGoogleAccessToken* Action = NewObject<UAsyncActionGetGoogleAccessToken>();

	Action->PrivateKey = PrivateKey;
	Action->ServiceAccountMailAddress = ServiceAccountMailAddress;
	Action->Scopes = Scopes;

	Action->RegisterWithGameInstance(WorldContextObject);

	return Action;
}

void UAsyncActionGetGoogleAccessToken::Activate()
{
	TSharedRef<IHttpRequest, ESPMode::ThreadSafe> HttpRequest = FHttpModule::Get().CreateRequest();

	FString Url = TEXT("https://oauth2.googleapis.com/token");

	HttpRequest->SetURL(Url);
	HttpRequest->SetVerb(TEXT("POST"));
	HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
	HttpRequest->SetContentAsString(GetGoogleCloudJsonContent());

	HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr HttpRequestPtr, FHttpResponsePtr HttpResponsePtr, bool bSucceeded)
		{
			if (!bSucceeded)
			{
				OnFailed(TEXT("send a HTTP request"));

				return;
			}

			if (!HttpResponsePtr.IsValid())
			{
				OnFailed(TEXT("get a FHttpResponsePtr"));

				return;
			}

			FString JsonResponse = HttpResponsePtr->GetContentAsString();

			TSharedRef<TJsonReader<>> JsonReader = TJsonReaderFactory<>::Create(JsonResponse);

			TSharedPtr<FJsonObject> JsonObject;

			if (!FJsonSerializer::Deserialize(JsonReader, JsonObject) || !JsonObject.IsValid())
			{
				UTsubasamusuLogLibrary::LogError(TEXT("The JSON response is \"") + JsonResponse + TEXT("\"."));

				OnFailed(TEXT("deserialize the JSON response"));

				return;
			}

			FString AccessToken;

			if (JsonObject->TryGetStringField(TEXT("access_token"), AccessToken) || JsonObject->TryGetStringField(TEXT("id_token"), AccessToken))
			{
				OnSucceeded(AccessToken);

				return;
			}

			FString ErrorMessage, ErrorDescription;

			if (JsonObject->TryGetStringField(TEXT("error"), ErrorMessage) && JsonObject->TryGetStringField(TEXT("error_description"), ErrorDescription))
			{
				UTsubasamusuLogLibrary::LogError(TEXT("The error is \"") + ErrorMessage + TEXT("\"."));
				UTsubasamusuLogLibrary::LogError(TEXT("The error description is \"") + ErrorDescription + TEXT("\"."));

				OnFailed(TEXT("get an access token field from the JSON response"));

				return;
			}

			UTsubasamusuLogLibrary::LogError(TEXT("The JSON response is \"") + JsonResponse + TEXT("\"."));

			OnFailed(TEXT("get any field from the JSON response"));
		});

	if (!HttpRequest->ProcessRequest()) OnFailed(TEXT("process a HTTP request"));
}

FString UAsyncActionGetGoogleAccessToken::GetGoogleAccessTokenHeader()
{
	TSharedPtr<FJsonObject> JsonObject = MakeShared<FJsonObject>();

	JsonObject->SetStringField(TEXT("alg"), TEXT("RS256"));
	JsonObject->SetStringField(TEXT("typ"), TEXT("JWT"));

	FString JsonString;

	TSharedRef<TJsonWriter<>> JsonWriter = TJsonWriterFactory<>::Create(&JsonString);

	if (FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter)) return JsonString;

	OnFailed(TEXT("create a JSON header"));

	return TEXT("");
}

FString UAsyncActionGetGoogleAccessToken::GetGoogleAccessTokenPayload()
{
	int64 IssuedUnixTime = FDateTime::UtcNow().ToUnixTimestamp();
	ExpirationUnixTime = IssuedUnixTime + 3600;

	TSharedPtr<FJsonObject> JsonObject = MakeShared<FJsonObject>();

	JsonObject->SetStringField(TEXT("iss"), ServiceAccountMailAddress);
	JsonObject->SetStringField(TEXT("scope"), FString::Join(Scopes, TEXT(" ")));
	JsonObject->SetStringField(TEXT("aud"), TEXT("https://oauth2.googleapis.com/token"));
	JsonObject->SetNumberField(TEXT("exp"), ExpirationUnixTime);
	JsonObject->SetNumberField(TEXT("iat"), IssuedUnixTime);

	FString JsonString;

	TSharedRef<TJsonWriter<>> JsonWriter = TJsonWriterFactory<>::Create(&JsonString);

	if (FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter)) return JsonString;

	OnFailed(TEXT("create a JSON payload"));

	return TEXT("");
}

FString UAsyncActionGetGoogleAccessToken::GetGoogleCloudJwt()
{
	TArray<char> PrivateKeyCharArray = UTsubasamusuStringConvertLibrary::ConvertToCharArray(PrivateKey.Replace(TEXT("\\n"), TEXT("\n")));

	BIO* Bio = BIO_new_mem_buf((void*)PrivateKeyCharArray.GetData(), -1);

	if (!Bio)
	{
		OnFailed(TEXT("create a BIO"));

		return TEXT("");
	}

	RSA* Rsa = PEM_read_bio_RSAPrivateKey(Bio, nullptr, nullptr, nullptr);

	BIO_free(Bio);

	if (!Rsa)
	{
		OnFailed(TEXT("create a RSA"));

		return TEXT("");
	}

	FString HeaderBase64 = UTsubasamusuStringConvertLibrary::ConvertToBase64(GetGoogleAccessTokenHeader());
	FString PayloadBase64 = UTsubasamusuStringConvertLibrary::ConvertToBase64(GetGoogleAccessTokenPayload());

	FString Message = HeaderBase64 + TEXT(".") + PayloadBase64;

	TArray<uint8> MessageDigest = UTsubasamusuStringConvertLibrary::ConvertToUtf8(Message);

	uint8 Digest[SHA256_DIGEST_LENGTH];

	SHA256(MessageDigest.GetData(), MessageDigest.Num(), Digest);

	uint8 Signature[256];

	unsigned int SignatureLen;

	if (RSA_sign(NID_sha256, Digest, SHA256_DIGEST_LENGTH, Signature, &SignatureLen, Rsa) != 1)
	{
		RSA_free(Rsa);

		OnFailed(TEXT("sign the RSA"));

		return TEXT("");
	}

	RSA_free(Rsa);

	return Message + TEXT(".") + FBase64::Encode(Signature, SignatureLen);
}

FString UAsyncActionGetGoogleAccessToken::GetGoogleCloudJsonContent()
{
	TSharedPtr<FJsonObject> JsonObject = MakeShared<FJsonObject>();

	JsonObject->SetStringField(TEXT("grant_type"), TEXT("urn:ietf:params:oauth:grant-type:jwt-bearer"));
	JsonObject->SetStringField(TEXT("assertion"), GetGoogleCloudJwt());

	FString JsonString;

	TSharedRef<TJsonWriter<>> JsonWriter = TJsonWriterFactory<>::Create(&JsonString);

	if (FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter)) return JsonString;

	OnFailed(TEXT("create a JSON content"));

	return TEXT("");
}

void UAsyncActionGetGoogleAccessToken::OnSucceeded(const FString& AccessToken)
{
	Succeeded.Broadcast(AccessToken, ExpirationUnixTime);

	SetReadyToDestroy();
}

void UAsyncActionGetGoogleAccessToken::OnFailed(const FString& TriedThing)
{
	UTsubasamusuLogLibrary::LogError(TEXT("Failed to ") + TriedThing + TEXT(" to get an access token for Google Cloud."));

	Failed.Broadcast();

	SetReadyToDestroy();
}

手順10

叩きたい API のドキュメントを参考にしながら、その API を叩く関数を作成します。
ヘッダーの Authorization に Google Cloud 用のアクセストークンを渡してあげるのがポイントです。
以下は Cloud Translation API を叩く例です。

SampleBlueprintFunctionLibrary.cpp
#include "SampleBlueprintFunctionLibrary.h"
#include "Interfaces/IHttpRequest.h"
#include "HttpModule.h"
#include "Kismet/KismetSystemLibrary.h"
#include "Interfaces/IHttpResponse.h"

void USampleBlueprintFunctionLibrary::Hoge(const FString& GoogleCloudAccessToken)
{
	TArray<TSharedPtr<FJsonValue>> JsonValues;

	JsonValues.Add(MakeShared<FJsonValueString>(TEXT("Hello world")));
	JsonValues.Add(MakeShared<FJsonValueString>(TEXT("My name is Tsubasamusu.")));

	TSharedPtr<FJsonObject> JsonObject = MakeShared<FJsonObject>();

	JsonObject->SetArrayField(TEXT("q"), JsonValues);
	JsonObject->SetStringField(TEXT("target"), TEXT("de"));

	FString JsonString;

	TSharedRef<TJsonWriter<>> JsonWriter = TJsonWriterFactory<>::Create(&JsonString);

	TSharedRef<IHttpRequest, ESPMode::ThreadSafe> HttpRequest = FHttpModule::Get().CreateRequest();

	FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter);

	HttpRequest->SetURL(TEXT("https://translation.googleapis.com/language/translate/v2"));
	HttpRequest->SetVerb(TEXT("POST"));
	HttpRequest->SetHeader(TEXT("Content-type"), TEXT("application/json"));
	HttpRequest->SetHeader(TEXT("Authorization"), TEXT("Bearer ") + GoogleCloudAccessToken);//アクセストークンを渡す
	HttpRequest->SetContentAsString(JsonString);

	HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequestPtr, FHttpResponsePtr HttpResponsePtr, bool bSucceeded)
		{
			UKismetSystemLibrary::PrintString(nullptr, HttpResponsePtr->GetContentAsString());
		});

	HttpRequest->ProcessRequest();
}

手順11

実際に API を叩いて動作を確認します。
アクセストークンを発行する際、API ドキュメントに書かれている必要なスコープを渡してあげるのがポイントです。

TSUBASAMUSU.png

最後に

参考記事

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?