この記事はUnreal Engine 4 (UE4) Advent Calendar 2018の20日目の記事です。
前日の記事は0xUMAさんの UE4 で自動テストを試してみる。 でした。
概要
UE4からWindows向けのパッケージを作成したあとで動作パターンを切り替えたい時があり、はじめの頃は外部テキストファイル(xmlやjsonなど)でパラメーターを変更できるようにして対応していました。
ですが、そのうちロジック自体も変更したいという要望がでてきました。
そこでUnreal.jsを使ってJavaScriptでロジックの外出しを実施した事を書きます。
この記事で作成したサンプルプロジェクトは以下に置いてあります。
対応方法選定
UE4ではEditorからはC++コードを変更してもホットリロードできるので、直ぐに(ちょっとは待たされる)動作確認できますがパッケージ化した後には使えません。
そこでUE4のRuntimeで動作可能なスクリプト機能のプラグインがないか調べたところ、以下の3つを見つけました。
- Unreal.js
- UnrealEnginePython
- MonoUE
[MonoUE](https://mono-ue.github.io/)
MonoUEは個人的に好きなC#なので興味はありましたが、スクリプトとしてお手軽に使えなさそうだったので候補から外しました。
あと残った2つを考えましたが、個人的な好みでJavaScriptの方が良いかなと思いUnreal.jsを採用しました。
Unreal.jsとは
Unreal.jsについての基本的な情報入手はこちらが参考になりました。 2年ほど前の記事ですが現状の4.21の環境でもほぼそのまま利用できとても助かりました。
Unreal.js 入門
Unreal.js 入門 - Qiita
導入
マーケットプレイスよりUnreal.jsを追加します。
その後UE4のプロジェクトをEditorから開き、Edit->PluginsからUnreal.jsを有効にします。 その後Editorを再起動すると使用可能になります。
試しにActorをレベルに配置してJavaScript Componentを追加し、Script Source FileにSample.jsと入力します。 Content/ScriptsにSample.jsを作り以下の内容を記述します。
console.log("test")
この状態でPlayして、Output Logに"test"と表示されていたら正常に動作しています。

パッケージ後で確認
File -> Package Project -> Windows -> Windows(64-bit)でWindows用のパッケージを作成します。
パッケージ作成後、以下のディレクトリを作成したパッケージのContentにコピーします。
[Engine Install Path]\Engine\Plugins\Marketplace\UnrealJS\Content\Scripts
起動後すぐにアプリケーションを閉じて、Saved/Logs/***.log内に”test”が出力されていればOKです。
動的ロードの仕組みづくり
Unreal.jsのJavaScript Componentを使用するとjsファイルを実行することができましたが、これには以下の制限があります。
- 1回実行すると途中でJavaScriptを変更できない
- Content/Scripts以下に必ずjsファイルを置く必要がある
そこで上記制限をなくし、自由な場所にjsファイルを配置して実行中でもスクリプトを更新できる仕組みを作ってみます。 具体的に以下の方針で作成してみます。
- jsファイルには外出しロジックを記載したクラス定義のみを記述する
- jsファイルに記述するクラスの基底クラスはC++で定義しておく
- jsファイルはアプリケーション起動中でも再読み込み可能とする
- ファイルアクセス回数は最小限にする
- jsファイルはContent/Scripts以外でも置けるようにする
C++クラス実装(JSObject)
jsファイルに定義するクラスの基底クラスを定義します。
FString NotifyTrigger();
はアプリケーション側からJavaScript側にアクセスするインターフェイスです。
今回はFStringを返すメソッドにしていますが、これは状況に応じて変更します。
メソッドは複数用意しても良いです。
メソッドは必ずUFUNCTION(BlueprintImplementableEvent)にしておいて、オーバーライド専用メソッドにしておきます。そのためcpp側に実装は書きません。
UCLASS(BlueprintType)
class UNREALJSSAMPLE_API UJSObject : public UObject
{
GENERATED_BODY()
public:
UJSObject();
UFUNCTION(BlueprintImplementableEvent)
FString NotifyTrigger();
};
C++クラス実装(JSComponent)
Unreal.jsのJavaScript Componentの実装を参考に実装します。
OnRegister
Unreal.jsのJavaScript Componentほぼそのままです。 JavaScriptを使うための準備とC++側とJavaScript側の連携登録もこの時点でおこなっています。
この中でもContext->Expose(“Root”, this);
の1行を記述することで、このComponentクラスの参照がJavaScript内でRoot
として使用できるようになります。
void UJSComponent::OnRegister()
{
auto ContextOwner = GetOuter();
if (ContextOwner && !HasAnyFlags(RF_ClassDefaultObject) && !ContextOwner->HasAnyFlags(RF_ClassDefaultObject))
{
if (GetWorld() && ((GetWorld()->IsGameWorld() && !GetWorld()->IsPreviewWorld())))
{
UJavascriptIsolate* Isolate = nullptr;
UJavascriptStaticCache* StaticGameData = Cast<UJavascriptStaticCache>(GEngine->GameSingleton);
if (StaticGameData)
{
if (StaticGameData->Isolates.Num() > 0)
Isolate = StaticGameData->Isolates.Pop();
}
if (!Isolate)
{
Isolate = NewObject<UJavascriptIsolate>();
Isolate->Init(false);
Isolate->AddToRoot();
}
auto* Context = Isolate->CreateContext();
JavascriptContext = Context;
JavascriptIsolate = Isolate;
Context->Expose("Root", this);
Context->Expose("GWorld", GetWorld());
Context->Expose("GEngine", GEngine);
}
}
Super::OnRegister();
}
LoadJSFile
jsファイルを読み込んで、その中に記述されたクラスをインスタンス化し参照を保持します。
void UJSComponent::LoadJSFile()
{
if (JavascriptContext == nullptr) return;
// ScriptSourceFileにはContentからの相対パスが入っているので絶対パスに直す
auto scriptSourceFilePath = FPaths::Combine(FPaths::ProjectContentDir(), ScriptSourceFile);
scriptSourceFilePath = FPaths::ConvertRelativePathToFull(scriptSourceFilePath);
// jsファイルの中身を読み込む(まだ実行はしない)
FString script;
FFileHelper::LoadFileToString(script, *scriptSourceFilePath);
// jsに書いたクラス名を抜き出す
const FRegexPattern pattern = FRegexPattern(FString(TEXT("class\\s+(.+)\\s+extends\\s+JSObject")));
FRegexMatcher matcher(pattern, script);
if (matcher.FindNext())
{
auto className = matcher.GetCaptureGroup(1);
// スクリプトにクラスのインスンタンス化と参照保持のコードを足す
script = TEXT("(function (global) {\r\n") + script;
script += TEXT("let MyUObject_C = require('uclass')()(global,") + className + TEXT(")\r\n");
script += TEXT("let instance = new MyUObject_C()\r\n");
script += TEXT("Root.SetJsObject(instance)\r\n");
script += TEXT("})(this)");
// スクリプト実行
JavascriptContext->RunScript(script);
}
}
ポイントはjsファイルをそのままスクリプトとして実行するのではなく、jsの中身の上下にスクリプトを足してクラスのインスタンス化と参照保持まで行ってます。
script += TEXT(“Root.SetJsObject(instance)\r\n”);
の部分はOnRegister()
時に関連付けておいた仕組みを利用してスクリプト内からComponentの関数をコールしています。
これによりJavaScriptのクラスのインスタンスがC++で実装されたComponentのプロパティとして保持されます。
NotifyTrigger
ブループリントからComponent経由でJavaScriptに実装したクラスの関数をコールするための関数です。
JsObject
にjsクラスの参照が保持されているため、そこ経由でコールします。
FString UJSComponent::NotifyTrigger()
{
if (JsObject != nullptr)
{
return JsObject->NotifyTrigger();
}
return FString();
}
ブループリント実装(SampleActor)
空のActorに先ほど作成したJSComponentとTextRenderを追加します。

またjsに実装するメソッドNotifyTrigger
を呼び出す関数を作成します。
NotifyTrigger
の結果でTextRenderの文字列を書き換えるようにします。

次にjsファイルの再ロードを行う関数を作成します。
ブループリント実装 (SampleUMG)
Widgetでボタンを2つ作り、1つはSampleActorのNotifyEventメソッドを、もう1つはUpdateJSメソッドを呼び出すようにします。
jsファイル作成
スクリプトとして使用するjsファイルを作成します。
作成したファイルは任意の場所におけますが、今回はContentフォルダーと同じ階層に保存しておきます。
ここでは先にC++で実装したUJSObjectクラスを継承したクラスを定義します。
アプリケーションからはUJSObjectクラスのNotifyTrigger
が呼ばれますので、その部分の実装をスクリプト内でオーバーライドします。
class MyUObject extends JSObject {
NotifyTrigger(){
let prop1 = 1;
let prop2 = 2;
return "ABC" + prop1 + prop2;
}
}
JSComponentへjsファイルのパスを登録
レベルにSampleActorを置き、SampleActorに追加済みのJSComponentのScriptSourceFile
にjsファイルのパスをContentからの相対パスで入力します。
Contentより上の階層でも大丈夫です。
実行
EditorよりWin64のパッケージを作成します。
以下のディレクトリを作成したパッケージのContentにコピーします。
[Engine Install Path]\Engine\Plugins\Marketplace\UnrealJS\Content\Scripts
また、パッケージのContentフォルダーと同じ階層に作成したjsファイルもコピーします。
上記準備ができたらアプリケーションを実行します。
TextRenderは初期値が表示されています。
class MyUObject extends JSObject {
NotifyTrigger(){
let prop1 = 3;
let prop2 = 4;
return "ZZZ" + prop1 + prop2;
}
}
アプリケーションのUpdateJSボタンを押してから、Triggerを押すとTextRenderの表示が書き換えたスクリプトの結果に変わっています。

まとめ
Unreal.jsを使用してパッケージ作成後の状態でも動的にスクリプト機能を使用することができました。
この実装の良さは必要最低限のみJavaScriptに任せることで、ある程度速度的にも有利なのとロジックが固まったらC++のクラスに差し替える事でさらにパフォーマンスを上げられる事だとおもいます。
よろしければお試しください。
明日はfukusuke8gouさんのUMG関連のお話です。
参考
- Unreal.jsのWiki
https://github.com/ncsoft/Unreal.js/wiki - Unreal.js 入門
https://qiita.com/ConquestArrow/items/bb81ad5e63cf6bc4e549