電通総研 テックブログ

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

UE5上のオブジェクト情報をChatGPTに問い合わせる方法

こんにちは、金融ソリューション事業部の岡崎、若本です。

3DCG空間では、オブジェクトに関する情報を過度に表示し過ぎることはユーザー体験を阻害する要因になります。その一方で、あまりに情報が少ないと世界観の欠如に繋がります。そのため、質問応答などのユーザーが求める情報を必要なだけ表示する仕組みが求められることがあります。

そこで今回は、Unreal Engine(UE)上のオブジェクトの情報についてChatGPTに問い合わせる実装を行ってみたいと思います。
下図のように、あらかじめ格納した関連文献を基に、UE上でObjectに対する質問をChatGPTに答えさせることがゴールです。

準備するもの

  • Unreal Engine 5.2.0
  • OpenAI API
    • GPT-3.5-turbo(0301)を使用します。
    • 事前にユーザー登録と、APIキーの発行を実施しておきます。
  • FastAPI
    • APIとして、下記の機能を作成します。
      • オブジェクトに関連したドキュメントを登録する
      • オブジェクト名と質問を与え、答えを返す
  • Docker
    • APIの環境構築に使用します。

処理概要

下記の処理を実装します。
それぞれの機能について、()内の担当者が作業を行いました。

  • コリジョン判定(岡崎)
    • チャット可能な範囲に入ったとき、アイコンを表示します。
  • チャットUIの表示(岡崎、若本)
    • コリジョン状態でオブジェクト(Actor)をクリックし、チャット画面を表示します。
  • GPTへのリクエスト(若本)
    • UE上の情報を基に、GPTにリクエストを送ります。
  • 結果の表示(若本)
    • APIから返ってきたレスポンスをUIに反映します。

以降では、各機能の概要や実装について説明します。

手順①. GPTを用いたAPIを作成

本手順は若本が説明します。

GPTはUE内のオブジェクトについて情報を持たないため、そのままChatGPTに問い合わせても的確に質問に答えることができません。
そこで、以下の2つの機能を持つAPIを作成します。今回はUnreal Engineを実行するPC上で構築することとしました。

  • オブジェクトに関連したドキュメントを登録する(インデックス登録)
  • オブジェクト名と質問を与え、答えを返す(問い合わせ)

それぞれの機能についてエンドポイントを用意し、HTTPリクエストで実行できるようにします。
最終的には下記のようなファイル構成となります。

1.1 APIの環境構築


まずはDockerでAPIのベースとなる環境を作成します。
以下は最終的に使用したDockerの設定ファイルです。
フォルダ上でdocker containerをupするとアプリケーションが起動します(http://localhost:8000/docs/ からアクセスすることができます)。

docker-compose.yml(クリックで表示)

version: '3'

services:  
backend:  
container_name: gpt-api  
build:  
context: .  
dockerfile: ./Dockerfile  
volumes:  
\- type: bind  
source: './api'  
target: '/src'  
ports:  
\- 8000:8000  
stdin_open: true

Dockerfile(クリックで表示)

FROM python:3.8-slim-buster

RUN pip install --upgrade pip  
RUN pip install --upgrade setuptools

COPY requirements.txt /  
RUN pip install -r requirements.txt

WORKDIR /src

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--reload"]

requirements.txt(クリックで表示)

numpy  
openai  
transformers  
llama_index  
langchain  
fastapi  
uvicorn[standard]  
pandas

1.2 エンドポイントの作成


次に、APIが持つ2つの機能についてエンドポイントを作成していきます。 routers/index.pyにインデックス登録のエンドポイントを、routers/search.pyに問い合わせ用のエンドポイントを記述しています。

main.py(クリックで表示)

from fastapi import FastAPI
from routers import search, index
import os

os.environ['OPENAI_API_KEY'] = 'YOUR_OPENAI_API_TOKEN'
app = FastAPI()
app.include_router(search.router)
app.include_router(index.router)

routers/index.py(クリックで表示)

from fastapi import APIRouter
from functions.index_operation import create_index

router = APIRouter()

@router.post("/index")  
def service_create_index(dir_name:str):  
_ = create_index(dir_name)  
return 200

routers/search.py(クリックで表示)

from fastapi import APIRouter
from pydantic import BaseModel
from functions.search_operation import search

router = APIRouter()

class SearchRequestItem(BaseModel):
    dir_name: str = None
    query: str = None

class SearchResponseItem(BaseModel):
    text: str = None

@router.post("/search")
def service_search(item: SearchRequestItem) -> SearchResponseItem:
    response_text = search(item.dir_name, item.query)
    response = SearchResponseItem(text=response_text)
    return response

1.3 機能の作成


最後に、それぞれのエンドポイントで呼び出される処理を記述します。
functions/index_operation.pyにインデックス登録の処理を、functions/search_operation.pyに問い合わせの処理を実装しました。functions/api_handling.pyでは、使用するmodel(gpt-3.5-turbo)の設定を行っています。

ここで、インデックス登録はドキュメントをあらかじめベクトルにしておき、問い合わせが来た際に検索するために行います。
今回の実装では、フォルダ内のCSV/PDF/TXTファイルについて読み込みベクトル化できるようにしました。
※データの大きさや権限によってエラーが発生する可能性はありますが、今回そのエラーハンドリングまでは行っていません。

functions/api_handling.py(クリックで表示)

from llama_index import LLMPredictor, ServiceContext, PromptHelper
from langchain.chat_models import ChatOpenAI

def get_service_context():  
llm_predictor = LLMPredictor(  
llm=ChatOpenAI(  
temperature=0,  
model_name="gpt-3.5-turbo",  
max_tokens=512  
)  
)

prompt_helper = PromptHelper(
    max_input_size=4096,
    num_output=256,
    max_chunk_overlap=20
)

service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor, prompt_helper=prompt_helper)
return service_context

functions/index_operation.py(クリックで表示)

from llama_index import GPTVectorStoreIndex, SimpleDirectoryReader, download_loader
from functions.api_handling import get_service_context
import glob

def create_index(dir_name: str):
    input_dir = f'data/{dir_name}'

    service_context = get_service_context()
    
    index = None
    all_files = glob.glob(f'{input_dir}/*')
    for file in all_files:
        if file.endswith('.csv'):
            Reader = download_loader("SimpleCSVReader")
            loader = Reader()
            docs = loader.load_data(file=file)
        if file.endswith('.pdf'):
            Reader = download_loader("CJKPDFReader")
            loader = Reader()
            docs = loader.load_data(file=file)
        if file.endswith('.txt'):
            loader = SimpleDirectoryReader(input_files=[file], required_exts=['txt'])
            docs = loader.load_data()
        if index is None:
            index = GPTVectorStoreIndex.from_documents(
                        docs,
                        service_context=service_context
                    )
        else:
            for doc in docs:
                index.insert(doc)
    # indexを保存する
    if index:
        index.storage_context.persist(f"index/{dir_name}")
    return index

functions/search_operation.py(クリックで表示)

from llama_index import StorageContext, load_index_from_storage
from functions.api_handling import get_service_context

def load_index(dir_name: str):  
\# インデックスの読み込み  
storage_context = StorageContext.from_defaults(persist_dir=f"index/{dir_name}")  
index = load_index_from_storage(storage_context, service_context=get_service_context())  
return index

def search(dir_name: str, query: str):  
index = load_index(dir_name)  
\# クエリエンジンの作成  
query_engine = index.as_query_engine()  
text_query = f'下記の質問に対して、敬語で簡潔に答えてください。\n{query}'  
response = query_engine.query(text_query)  
return response.response

1.4 動作確認(データ登録)


上記のコードを記述後、動作確認を実施します。 アプリを立ち上げ後、以下のURLにアクセスすることでSwagger UI(下図)が表示され、直接動作確認をすることができます。 http://localhost:8000/docs/

まずはデータを登録してみましょう。 dataフォルダ下に新しいフォルダを作成します。ここでは、「BookInfo」というフォルダを作成しました。 なお、検索時にもこのフォルダ名を使用します。 ※ フォルダ名をもとにした管理やリクエストは少し危険ですが、今回は簡単な検証のため実装を簡略化しています。

フォルダ作成後、「BookInfo」フォルダ内にCSV/PDF/TXTファイルを格納します。 今回は自作のsample.txtを格納しました。ファイルの中身は下記のようになっています。

格納後、http://localhost:8000/docs/ にアクセスし、/indexから下記のようにインデックス登録を実行します。

indexフォルダ下に同名のフォルダが作成されます。 下記の3種類のjsonファイルが入っていれば正常にインデックスが登録できています。

1.5 動作確認(検索)


検索についても動作確認してみます。 http://localhost:8000/docs/ にアクセスし、/searchを下記のように実行してみます。

少しするとResponseが返ってきます。登録した文章の情報を基に答えられており、動作が確認できました。

複数インデックスを登録した場合はdir_nameのパラメータを変えることで、検索対象を変えて回答させることができます。

手順②. オブジェクトの準備

次に、UE上で使用するオブジェクトの準備を行っていきます。本手順は岡崎が説明します。

ここでは以下の機能や画面を作成します。

  • チャット可能な範囲に入ったとき、アイコンを表示する機能
  • オブジェクト(Actor)をクリックしてチャット画面を表示する機能

それでは各機能について説明していきます。

2.1 チャット可能な範囲に入ったとき、アイコンを表示する機能


今回はゲームでよくあるような、プレイヤーがオブジェクトに一定の距離近づくと、「ここにアイテムがあります」という意味のアイコンが表示される処理の作成。 また、さらに近づくと「アイテムにクリックできます」という意味のアイコンに変更し、アイテムがクリックできるようになる処理を作っていきます。

まずはプレイヤーがオブジェクトに近づいたことを判定する処理を作成します。

以前こちらの記事(UE5でコリジョン(衝突)判定機能を使って色々な機能を作成してみた)でも紹介しましたが、コリジョン機能を使っていきます。

まずはアクターブループリントを作成し「BP_P_Item」と命名します。 コンポーネント追加から、「Sphere Collision」を2つ追加し、それぞれ「BigSphere」と「SmallSphere」とします。

この2つのコリジョンは、「BigSphere」にプレイヤーが入った時、アイテムの上にアイコンを表示し、「SmallSphere」に入った時、アイコンを変更してアイテムをクリックできる状態に変更するために使用します。 上記を再現するため、「BigSphere」を大きく作成し、その中に「SmallSphere」を配置しています。

次にアイテムの上に表示するアイコンを作成します。 ユーザーインターフェースからウィジェットブループリントを作成し、「WBP_ItemIcon」と命名します。 作成後、下記画像のようにアイコンのウィジェットを作成し、「Is Variable」にチェックを入れておきます。

次にコリジョン用のブループリント(BP_P_Item)に戻り、「SmallSphere」の下に「Static Mesh」と「Widget」のコンポーネントを追加します。

画像ではWidgetの名前をWidgetItemIconに変更しています。

Widget」のコンポーネントの詳細タブより、「Widget Class」を先ほど作成したウィジェットブループリントの名前(WBP_ItemIcon)に変更します。

また、同じ詳細タブより「レンダリング」内の「Visible」のチェックを外しておきます。

次にイベントグラフに移動し、「On Component Begin Overlap(BigSphere)」と「On Component End Overlap(BigSphere)」のノードを作成します。 このノードは、左側のコンポーネント一覧から「BigSphere」を右クリックし、イベント追加から選ぶことができます。

下記画像のように、プレイヤーが「BigSphere」内に入った際に、デフォルトで見えない設定にしておいたアイコンウィジェットの「Visibility」の値を変更し、画面上にレンダリングさせる処理を作成します。

同様にプレイヤーが「BigSphere」からプレイヤーが出た際にアイコンを見えなくする処理も追加します。

ここまでで、下記のようにプレイヤーの位置によってアイコンが表示されたり、消したりする処理ができました。

続いてさらに近づいた際(SmallSphereに入った際)にアイコンの表示を変更する処理を作成します。

アイコン用のブループリント(WBP_ItemIcon)に戻り、アイコンを変更するための関数を「AreaEvent」という名前で作成します。 「AreaEvent」には引数として「IsInside」というBoolean値を設定しておきます。

下記画像のように「AreaEvent」から「IsInside」の値によってイメージ画像を変更するために「Set Brush from Texture」ノードを繋ぎます。

「選択する」ノードに、Falseの場合にはSmallSphereに入っていない時用の画像を設定し、Trueの場合はSmallSphereに入っている時用の画像を設定します。 (本プロジェクトの場合は下記画像の「arrow」と「circle」を使用)

「Set Brush from Texture」のターゲットにはウィジェットブループリントでimageを作成した際に命名した名前の変数が左側のタブに追加されているのでそれを使用します。

次にコリジョン用のブループリント(BP_P_Item)に戻り、「SmallSphere」に入った際にアイコンウィジェットの画像を変えるために「AreaEvent」の関数を使用する処理を作成します。 「SmallSphere」から「On Component Begin Overlap(SmallSphere)」と「On Component End Overlap(SmallSphere)」のノードを作成します。

下記画像のように「On Component Begin Overlap(SmallSphere)」と「On Component End Overlap(SmallSphere)」を「Area Event」繋げます。

「Area Event」の引数としてプレイヤーが「SmallSphere」の内部にいるかどうかの値を変数にして「In Small Collision Area」とします。 (後述の処理で使用するため変数にしておきます。)

ここまでの処理で「BigSphere」の内部にいる時に矢印のアイコンを出現させ、「SmallSphere」の内部にいる時に二重丸のアイコンに変更させる処理ができました。

2.2 オブジェクト(Actor)をクリックしてチャット画面を表示する機能


続いて実際にオブジェクトを配置し、「SmallSphere」の内部にプレイヤーがいる時のみオブジェクトにクリックができる機能と、クリックした後にチャット画面を表示する機能を作成します。

まず初めに、プレイ画面上にマウスカーソルを出現させ、クリックを有効にするために、レベルブループリントを開きます。下の画像のように「イベントBeginPlay」に「Show Mouse Cursor」と「Enable Click Events」の設定をどちらもオンに変更します。

これでプレイ画面上にマウスカーソルを出現させ、クリックができるように設定されました。

次に、先ほど作成したコリジョン用のブループリント(BP_P_Item)から子ブループリントを作成します。 親のブループリント上で右クリックを行い、「子ブループリントクラスを作成します」ボタンを押し「BP_C_Item_Book」と命名します。

子ブループリントを開き、親ブループリント作成時は特に編集しなかった「Static Mesh」に任意のメッシュ素材を選びます。

今回は「Quixel Bridge」内のメッシュを使用しました。 こちらの記事(Unreal Engine 5 を使ってワールドの地形を作成してみました)で、「Quixel Bridge」の使用方法を紹介しています。

イベントグラフを開き、「イベント ActorOnClicked」のノードを追加します。

「In Small Collision Area」の値からIF文を作成し、プレイヤーが「Small Collision」の中にいる場合のみチャット画面用のウィジェットを作成するために「ウィジェットを作成」ノードを繋げ、「Add Viewport」を付けます。

ここでチャット画面用のウィジェットブループリントを作成します。 (チャット機能の詳細は後述するので、ここでは大枠だけ作成します。) クリックしたいオブジェクト用(今回は本のオブジェクト)のウィジェットブループリントを作成し、「WBP_BookInfo」と命名します。 「Canvas Panel」内に、本のタイトルや、サムネイル用画像を置き、チャット用のスペースも確保します。 こちらの記事(UE5でコリジョン(衝突)判定機能を使って色々な機能を作成してみた)で、ウィジェットの作成方法について触れているので参考にしてください。

今回は閉じるボタン(×マーク)を押すことでウィジェットを閉じる挙動にしたいので、「Canvas Panel」内に、ボタンも追加します。 パレット追加欄に「Button」と検索し、ボタン内部に「Text」で「×」を記述し閉じるボタンを作成しました。 (ボタンの名前を「CloseButton」に変更してあります。)

階層タブより「CloseButton」を選択中に、詳細タブ内のイベント>On Click の追加ボタンを押し、イベントグラフに「On Click(CloseButton)」ノードを追加します。

「On Click(CloseButton)」ノードに「Remove from Parent」を繋ぎ、閉じるボタンが押された時チャット画面用のウィジェットが閉じるようにします。

「BP_C_Item_Book」を再び開き、「ウィジェットを作成」ノードの「Class」を今作成した「WBP_BookInfo」に変更します。

これで画面上の本のオブジェクトをクリックした際に、画面上にチャット画面のウィジェットが現れ、閉じるボタンで閉じる処理ができました。

ただこのままだと、本のオブジェクトをクリックした分だけウィジェットが開いてしまうので、現在ウィジェットが開かれている状態かどうかを判別するために「IsOpenDetailWidget」という変数を作成し、下記画像のようにフラグとして使用します。

「WBP_BookInfo」でチャット画面を閉じた後で「IsOpenDetailWidget」の変数をFalseに戻しておきます。

ここまでで、オブジェクト(Actor)をクリックしてチャット画面を表示する機能が完成しました。

手順③. チャットUIの実装

ここまでで、ベースとなるUEのオブジェクトやUI、外部APIの作成が完了しました。 以降の手順は若本が説明します。 本手順では、GPTのリクエストとのやり取りに必要になる追加のUIを用意していきます。

3.1 チャットのページUIを実装


次に、チャットのやり取りを行うためのUIを作成します。 チャットの吹き出しは動的に追加されていくため、ここでは必要なコンポーネントのみ用意します。

まずはチャット画面のベースとして、手順②で作成した「WBP_BookInfo」にText_box、button、scroll boxを追加します。

Text_boxとscroll boxをわかりやすい変数名に変更しておきます。 また、呼び出すために「Is Variable」にチェックを入れておきます。

次に、buttonにOnClickイベントを追加します。 詳細画面のOnClickを押すことで、OnClickイベントが有効になります。こちらを押しておきましょう。 詳細な処理は後の手順で作成します。

また、ここでItemの属性値を設定しておきます。 属性値を用いて後段でGPTに問い合わせる際の検索対象を変更するため、その下準備となります。 まずはWidgetの「WBP_BookInfo」に変数「WidgetAttributes」を作成します。

次に、「BP_C_ItemBook」クラスに属性値を設定しておきます。ここでは変数名を「ItemAttributes」、デフォルト値を「BookInfo」としました。

最後に、「BP_C_ItemBook」のブループリント上で、ウィジェットの作成時に自身の持つ「ItemAttributes」を「WidgetAttributes」にセットする処理を追加します。これで完成です。

3.2 チャットの吹き出しUIを実装


3.1でやりとりを行う画面は作成しましたが、メッセージを入れるための吹き出しがありません。 そこで、別途チャットの吹き出し用UI(自分用/GPT用)を作成します。

それぞれユーザーインターフェースからウィジェットブループリントを作成しましょう。

作成後、Canvas PanelとText Box(Multi-Line)を配置します。 こちらもText Boxはわかりやすい変数名に変更し、「Is Variable」にチェックを入れておきます。

3.2.1 自分のチャットを表示する

自分用の吹き出しは下記のように設定しました。背景色とFontの色/サイズを調整しています。

3.2.2 GPTの返信を表示する

GPT用の吹き出しも同様に作成します。

手順④. HTTPリクエスト用のC++クラス作成

4.1 C++クラスを作成


Unreal Engineでは、デフォルトでHTTPへのリクエストを送信するクラスは用意されていないため、APIにリクエストを送り、レスポンスを受け取るクラスを新規作成する必要があります。 ブループリントでは実現できないため、今回はC++で実装することとしました。

まずは、ツールから新規のC++クラスを作成します。

ここで、親クラスは「Actor」、名前は「HTTPActor」としました。

クラスを作成すると、エディタに飛ばされます。エディタ上でクラスを編集していきます。

4.2 C++クラスを編集


APIにリクエストを送り、レスポンスを受け取るための処理を追加していきます。

まずは、Build.csのDependencyModuleに"HTTP", "Json", "JsonUtilities"の3つを追加します。

Build.cs(クリックで表示)

// Fill out your copyright notice in the Description page of Project Settings.

using UnrealBuildTool;

public class TechBlog9 : ModuleRules  
{  
public TechBlog9(ReadOnlyTargetRules Target) : base(Target)  
{  
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HTTP", "Json", "JsonUtilities" });

PrivateDependencyModuleNames.AddRange(new string[] {  });

// Uncomment if you are using Slate UI
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });

// Uncomment if you are using online features
// PrivateDependencyModuleNames.Add("OnlineSubsystem");

// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true

}

}

また、Engine.iniの末尾にHTTPリクエストの設定を記載しておきます。

Engine.ini(クリックで表示)

[HTTP]  
HttpTimeout=60  
HttpConnectionTimeout=5  
HttpReceiveTimeout=-1  
HttpSendTimeout=-1  
HttpMaxConnectionsPerServer=32  
bEnableHttp=true  
bUseNullHttp=false  
HttpDelayTime=0.1

上記が終わったら、HTTPActor.hクラスを変更します。 筆者はEpicのcommunityを参考に#include "CoreMinimal.h"をコメントアウトしました。

後ほどブループリント上で呼び出すため、HTTPリクエストを実行するHttpMethodはUFUNCTIONを、結果を格納する変数である outputStringにはUPROPERTYを設定しておきます。

HTTPActor.h(クリックで表示)

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

// #include "CoreMinimal.h"  
#include "GameFramework/Actor.h"  
#include "Http.h"  
#include "HTTPActor.generated.h"

UCLASS()  
class TECHBLOG9_API AHTTPActor : public AActor  
{  
GENERATED_BODY()

public:  
// Sets default values for this actor's properties  
AHTTPActor();

protected:  
// Called when the game starts or when spawned  
virtual void BeginPlay() override;

public:  
// Called every frame  
virtual void Tick(float DeltaTime) override;

FHttpModule* Http;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Default) FString outputString;

// HTTP通信 UFUNCTION(BlueprintCallable, Category = "Http") void HttpMethod(FString query, FString dir_name);

// レスポンス後のイベント処理 void OnResponseReceived(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful);

};

最後に、HTTPActor.cppに具体的な処理内容を記述します。 手順①で実装したAPIの仕様に合わせ、dir_name(インデックスのフォルダ名)とquery(質問)の2つを引数としてHTTPリクエストを送り、返ってきたJsonの「text」をoutputStringに格納する処理を追加しました。

HTTPActor.cpp(クリックで表示)

// Fill out your copyright notice in the Description page of Project Settings.

#include "HTTPActor.h"

// Sets default values  
AHTTPActor::AHTTPActor()  
{  
// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.  
PrimaryActorTick.bCanEverTick = true;  
Http = &FHttpModule::Get();

}

// Called when the game starts or when spawned  
void AHTTPActor::BeginPlay()  
{  
Super::BeginPlay();

}

// Called every frame  
void AHTTPActor::Tick(float DeltaTime)  
{  
Super::Tick(DeltaTime);

}

void AHTTPActor::HttpMethod(FString query, FString dir_name)  
{  
// Jsonデータ作成  
TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject);  
JsonObject->SetStringField("dir_name", dir_name);  
JsonObject->SetStringField("query", query);

// OutputStringにJson格納 FString OutputString; TSharedRef<TJsonWriter> JsonWriter = TJsonWriterFactory<>::Create(&OutputString); FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter);

// HTTPリクエスト TSharedRef Request = Http->CreateRequest(); Request->OnProcessRequestComplete().BindUObject(this, &AHTTPActor::OnResponseReceived); Request->SetURL("http://localhost:8000/search"); Request->SetVerb("POST"); Request->SetHeader(TEXT("User-Agent"), "X-UnrealEngine-Agent"); Request->SetHeader("Content-Type", TEXT("application/json")); Request->SetContentAsString(OutputString); Request->ProcessRequest();

}

void AHTTPActor::OnResponseReceived(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)  
{  
TSharedPtr<FJsonObject> JsonObject;  
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString());

if (FJsonSerializer::Deserialize(Reader, JsonObject)) { // Jsonからtextを抽出 outputString = JsonObject->GetStringField("text"); }

}

上記の変更が終わったらbuildします。 筆者はVS Codeで編集していたため、Launch<project_name>Editor(development)でbuildしました。 Unreal Engineが起動され、projectが表示されればC++クラスの作成は完了です。

4.3 作成したクラスをブループリント化


最後に、作成したC++クラスをブループリントから呼び出すため、Blueprintクラスを作成します。 名前は「BP_HTTPActor」としました。

作成した「BP_HTTPActor」をワールドに配置すれば完了です。

手順⑤. 質問送信時の処理を実装

ここまででオブジェクトの準備、GPTの準備、UIの準備が全て終わりました。 最後に、Brueprint上ですべてを繋ぎこむ処理を「BP_C_Item_Book」のブループリント上で実装します。

以下が本手順で作成したブループリントの全体像です。 長くて見づらいため、それぞれ分けて解説します。(見やすさのため、拡大時にレイアウトを調整しています)

3.1で作成したOnclickイベント(チャットの送信ボタン押下)を起点としてブループリントを作成しました。

5.1 「送信」ボタンが押されたとき、送信した内容を表示する


送信ボタンが押下されると、TextBoxの値を取得して新しい吹き出しとして表示します。 ここでは、3.2.1で作成した自分用の吹き出しを作成し、TextBoxの値を入れてScroll Boxの中に入れています。

5.2 HTTPリクエストを送信する


4.3で配置した「BP_HTTPActor」クラスを呼び出し、HTTPMethodを実行します。ユーザーの入力とウィジェットの持つ変数「WidgetAttributes」の値をGPTのAPI側へリクエストとして送信しています。 このとき、outputStringの変数にGPTのレスポンスが格納されるまでは、Delayとloopを使用してwaitします。 また、HTTPリクエストを送った後、自身が入力したテキストをクリアする処理を記述しました。

5.3 GPTの返信内容を表示する


5.1と同様、今度はGPT用の吹き出しを作成し、outputStringの値を入れてScroll Boxの中に格納します。 最後に、outputStringの変数をクリアすれば処理は完了です。

デモ動画

本のオブジェクトに近づくことでタッチが可能になり、タッチすることでUIが表示されます。 さらにUI上で問い合わせると、オブジェクトの関連文章を基に回答できていることを確認できます。 これまでの実装により、ストレスの少ない問い合わせをUE上で実現することができました。

おわりに

今回はチャットUIを用いた、オブジェクトに対する問い合わせを実装しました。 以下、実装を担当した岡崎と若本のそれぞれの所感となります。 岡崎:今回の実装で、ブループリントでのチェンジイベント(オンクリックなど)や、コリジョンイベントに関する使用方法の基礎的な学習を行えました。今後プロダクトを作成する際、プレイヤー起因で任意の情報を出したり、プレイヤー情報を変更させるといった機能を作成する際に、数多く使うことになる機能を作成することができたため、より応用的な使い方など引き続き調査したいと思います。 若本:UE初学者であり、今回広くPythonの実装/ブループリントの実装/C++の実装を行いましたが、慣れて以降はストレスレスに開発を行うことができました。今回の実装を発展させ、非同期処理の実装や会話履歴の入れ込み、UIのリッチ化など、さらに作りこむことによってよりよいユーザー体験が得られることが期待できると感じました。

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

私たちは共に働いていただける仲間を募集しています!

みなさまのご応募、お待ちしています!

最新テクノロジー事業企画・推進担当 ◎Web3/メタバース/AI AIソリューション開発エンジニア ◎AIビジネス創出・推進に関われる   株式会社電通総研 新卒採用

執筆:@wakamoto.ryosuke、レビュー:@yamada.yShodoで執筆されました