はじめに
unreal://asset/open?path=%2fgame%2fthirdperson%2fmaps%2fthirdpersonmap
上記のようなリンクをクリックすることで、エディタ上で指定したアセットを開く
マップを開き、指定座標にカメラを移動する
といった動作が実現できるようにします。利用者はリンクを共有するだけで、複雑な操作を手軽に共有することが可能となります。
よく目にするリンクといえば http://
ですが、今回使うのは unreal://
です。実はこの先頭部分は自由に決めれるようになっていて、PC 上で対応するアプリケーションがあれば引数付きで起動することができる、という仕組みです。これらは カスタム URL スキーム
と呼ばれます。
サンプルを公開します
- UnrealProtocol.uplugin(UE5 用のプラグイン)
- リンクを中継するアプリケーション
exe
及び、インストーラmsi
のプロジェクト
Github に一式あげてます。詳しくはリンク先の README をお読みください。
大まかな仕組みの流れ
- UE5 エディタ内でリンクを生成、テキストをコピペする
- リンクをクリックする
- レジストリから中継アプリを実行、起動中の UE5 エディタに HTTP 経由でリクエストする
- UnrealProtocol プラグインが通信を受け取り、処理を起動する
カスタム URL スキームについて
実行リンクを動作させるためには、ローカル PC の レジストリ
を修正する必要があります。とはいえレジストリエディタなどを使うのは面倒なので、今回はインストーラからレジストリを登録するようにします。
インストーラについては、上記を参考に作成しました。これで UnrealProtocolHandler.exe
を起動し、実行リンクの引数を受け取る準備ができました。
プロセス間の通信に RemoteControl を使う
UE5 ではプロセス間の通信を行うために RemoteControl
というプラグインが提供されています。REST 形式の API を用いて BP と同様の公開範囲にアクセスすることができ、エディタ外部から処理を実行したり情報を取得することができます。
UE5 ドキュメントではいくつか情報が欠けていますが、内容はほぼ変わらないため UE4 ドキュメント も参照するとよいでしょう。
呼び出し先の関数を定義する
PUT remote/object/call
でやり取りします。これで全ての UObject
を継承したオブジェクトに対して、BP と同じように操作することができるようになります。
まずは通信の受け取り側、エディタ側の実装について見ていきましょう。
UFUNCTION(BlueprintCallable)
static bool Hoge(int32 Fuga, FString& OutString);
UFUNCTION(BlueprintCallable)
を指定して外部に公開します。
{
"ObjectPath" : "/Script/[ModuleName].Default__MyFunctionLibrary",
"FunctionName" :"Hoge",
"Parameters" : {
"Fuga" : 100
}
}
静的関数への呼び出しは ClassDefaultObject
を指定します。
{
"OutString": "HogeHoge",
"ReturnValue":true
}
参照でのパラメータ定義は出力値となります。この辺のルールは BP と変わりません。
外部クライアントから RemoteControl にリクエストする
REST API をリクエストする処理を作成します。クライアントは作りやすい言語で作るのがよいと思いますが、今回は C++ で作成しました。しかし調べてすぐ出てくる方法には少し課題が・・・。
特定の Content-Type を受け付けてくれない問題
例えば Microsoft が公開している cpprestsdk ですが、以下のように処理を実装します。
http_client cli(L"http://localhost");
cli.request(methods::PUT, L"/remote/object/call", body, L"application/json");
これでリクエストを飛ばすと、エディタからは以下のようなエラーが返ってきます。
Request content type must be application/json
application/json
を指定してるのになぜ?これは実際にはライブラリ内部の実装で Charset
の情報が付与され、application/json; charset=utf-8
となって送信されているためです。
json の仕様としては UTF-8 として扱われるため、わざわざ Charset を指定するのも冗長になるからつけなくてもいいよね、ということらしいです。しかし Charset を除外する方法がライブラリにない!
WinRT での実装についても同様に Charset 付与が行われます。
今回は上記のライブラリを使うことにしました。これで自由に Content-Type
を指定できるようになりました。
エンジン改造する?
bool WebRemoteControlInternalUtils::IsRequestContentType(const FHttpServerRequest& InRequest, const FString& InContentType, FString* OutErrorText)
{
if (const TArray<FString>* ContentTypeHeaders = InRequest.Headers.Find(TEXT("Content-Type")))
{
#if 1 // "application/json"が含まれていればjsonリクエストとみなす
if (ContentTypeHeaders->Num() > 0 && (*ContentTypeHeaders)[0].Contains(InContentType))
#else
if (ContentTypeHeaders->Num() > 0 && (*ContentTypeHeaders)[0] == InContentType)
#endif
{
return true;
}
}
(公開する以上はできるだけ改造を含めず納めたかったので)今回は見送りましたが、とはいえ別の目的として RemoteControl
を使うときは cpprestsdk
などの利用も検討することがあるかもしれません。その場合はこの辺の処理を修正することになるかと思います。
UE5 プラグイン側の実装
エディタ側で通信を受け取ったあとの処理の話とか。
リンクを生成する
AssetActionUtility
ActorActionUtility
を用いて、BP 処理を簡単に実行させることができます。UnrealProtocol.uplugin
ではコンテンツディレクトリにエディタユーティリティがそれぞれ含まれており、コンテキストメニューに項目が追加されます。
リンクの文字列は URL エンコードする
FString FGenericPlatformHttp::UrlEncode(const FStringView UnencodedString)
FString FGenericPlatformHttp::UrlDecode(const FStringView EncodedString)
文字列の中に /
:
"
などの記号が含まれると json 構文解析を阻害してしまうので、エンコードしてから処理に渡します。
文字列をクリップボードにコピーする
FPlatformApplicationMisc::ClipboardCopy(const TCHAR* Str)
上記関数を呼ぶことでクリップボードにコピーされます。
処理を受け取ったときに画面をアクティブ化する
if ( const TSharedPtr<SWindow> Window = FGlobalTabmanager::Get()->GetRootWindow() )
{
if ( Window->IsWindowMinimized() )
{
// Windowの最小化を解除する
Window->Restore();
}
// エディタを強制的にアクティブ化
Window->HACK_ForceToFront();
// 注意を引くためにWindow上部を点滅させる
// Window->FlashWindow();
}
リンクの実行は外部アプリで行い、その際 UE エディタは非アクティブであることが多いかと思います。通常の WebURL と同じように、操作を強制的にエディタへ移行します。