UE5 BlueprintでJavaScriptを動かす

2.2K Views

March 14, 25

スライド概要

2025/3/14に開催されたUE Tokyoにて、Unreal Engine 5でJavaScript(TypeScript)を動かしLuaのようなノリでロジックを記述したりフローを記述したりすることを目指した取り組みについて発表しました。

profile-image

エンジニア。Rikkaという小規模なWebサービス運営会社兼ゲームスタジオの代表をしています。

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

ダウンロード

関連スライド

各ページのテキスト
1.

UE5 Blueprintから JavaScriptを動かす 乾 夏衣 合同会社六花 @_kaiinui

2.

UEでもJavaScriptが使いたい 黒魔術? いいえ、違います。 Luaを動かしていいんなら、JSでもいいよね?

3.

モチベーション 1. 簡単なロジックをTypeScriptで書きたい a. ロジックを単体テストしたい 2. 処理自体をTypeScriptで記述したい a. 例: シナリオのスクリプトなど

4.

しくみ QuickJSを使います。 QuickJSは、最近登場したJSでモバイルネイティブアプリを書けるフレームワーク「Lynx」のバックエンドにも使われている、組み込み向けJSランタイムです。 組み込み向けなのでメモリフットプリントが少なく、数MB程度

5.
[beta]
デモ
std::string execute(const std::string& module_source, const std::string& arg) {
qjs::Runtime rt;
qjs::Context ctx(rt);
try {
qjs::Value module_val = ctx.eval(module_source);
qjs::Value default_export = ctx.global()["main"];
if (!JS_IsFunction(ctx.ctx, default_export)) {
throw std::runtime_error("main() is not a function");
}
qjs::Value result = ctx.eval("main(`" + arg + "`)");
std::string value = result.as<std::string>();
return value;
} catch (const qjs::exception& e) {
qjs::Value exception_val = e.get();
std::string exception_str = exception_val.as<std::string>();
throw std::runtime_error("JavaScript exception: " + exception_str);
} catch (const std::exception& e) {
throw std::runtime_error("Error: " + std::string(e.what()));
}
}
※簡易的にmainと宣言された関数を実行していますが、export defaultした関数を実行するようにしたほうが色々良いです。
6.

Blueprintから呼べるようにする C++でUFUNCTION(BluePrintCallable)を宣言することで、BPからC++の関数が呼び出せるようになります。 public: UFUNCTION(BluePrintCallable, Category = "UJSEvalBlueprintFunctionLibrary") static FString Eval(FString src, FString arg); ※ソースコードを渡して、返り値をFStringで受け取るようにしています。実用上は、パスを指定しバンドルしたファイルから読み出すようにします。

7.

利用シーン Blueprintは情報を集め、Evalに渡します。 Evalでは、ユニットテスト済みのJSロジックを使い判定を行います。 結果をみて、Blueprint側で分岐制御を行います。 複雑な判定 Eval Switch on String

8.
[beta]
使い道1: ロジック
APIレスポンスから、キャラクターの表示状態を判定するロジック。
とてもシンプルだが、それでも分岐は多く、BPで記述するのは大変
export default function checkState(character) {
const json = JSON.parse(character);
const { hp, maxHp, status } = json;
const percent = hp / maxHp;
if (status === "poisoned" && hp !== 0) {
return "poisoned"; // HPに関係なく、毒エフェクトを出す。ただし、hp=0は死んでるのでdeadにした
}
if (percent > 0.9) {
return "healthy"; // 元気な状態で描画
} else if (percent > 0.3) {
return "moderate"; // 普通な状態で描画
} else if (hp > 0) {
return "low"; // 危険状態で描画
}
return "dead"; // 死亡状態で描画
}
9.
[beta]
開発体験: TypeScriptで書く
JavaScriptのままだと、型がなく開発がつらいです。
JS界のビルドシステムViteを噛ませ、ビルドした結果をUnreal Engineのプロジェクトフォルダに出力して利用するようにします。
export default function checkState(character) {
const json = JSON.parse(character);
const { hp, maxHp, status } = json;
const percent = hp / maxHp;
if (status === "poisoned" && hp !== 0) {
return "poisoned"; // HPに関係なく、毒エフェクトを出す。ただし、hp=0は死んでるのでdeadにした
}
if (percent > 0.9) {
return "healthy"; // 元気な状態で描画
} else if (percent > 0.3) {
return "moderate"; // 普通な状態で描画
} else if (hp > 0) {
return "low"; // 危険状態で描画
}
return "dead"; // 死亡状態で描画
}
10.
[beta]
開発体験: ユニットテストを書く
vitest等でユニットテストを書けます。
C++などでロジックを記述するのに比べ、ビルドタイムがない分高速にテストのイテレーションを回すことができます。
今回はシンプルなロジックですが、すべて網羅すると10以上のケースになります。
export default function checkState(character) {
const json = JSON.parse(character);
const { hp, maxHp, status } = json;
const percent = hp / maxHp;
if (status === "poisoned" && hp !== 0) {
return "poisoned"; // HPに関係なく、毒エフェクトを出す。ただし、hp=0は死んでるのでdeadにした
}
if (percent > 0.9) {
return "healthy"; // 元気な状態で描画
} else if (percent > 0.3) {
return "moderate"; // 普通な状態で描画
} else if (hp > 0) {
return "low"; // 危険状態で描画
}
return "dead"; // 死亡状態で描画
}
11.

使い道2: 処理フローを制御する たとえば、セリフやキャラクターの表示を制御するような、シナリオスクリプトの記述をしたい。 次のような仕組みで実現できそう。

12.

使い道2: 処理フローを制御する 1. QuickJSのcontextを永続化する a. こうすることで、Evalしたあとの状態(global変数など)を保持することができます。 2. QuickJSにC++の関数をバインドし、CustomEventをディスパッチできるようにする a. たとえば、`ShowScript`、`ShowCharaImage` のようなCustomEventを定義して、それぞれBlueprintで表示処理を実装 3. Blueprint等でInputEventなどをJSに入力する仕組みを作る。 a. InputEventが来たことをEvalしてJS contextに知らせます。 b. JS側は、現在の状態とInputEventをもとに、必要であればEventをディスパッチします

13.

misc: JSって重くないの? QuickJSは埋め込み向けに作られているので、とても軽量です。 メモリフットプリントは数MBから数十MB程度です。 実用的にも、最近ByteDanceがリリースしたネイティブアプリをJavaScriptで記述できる「Lynx」のJSエンジンになっており、同社のアプリにも既に埋め込まれているようです。