はじめに
私がUnityで作った VRMViewMeister は幅広い環境でVRMのアニメーション作成を楽しんでもらうため、WebGLビルドで展開している。
ネイティブな各OSのビルドより諸々制約はあるのだが、あえて?苦難の道を進んでいる。(そのせいでURP対応やWebXR対応も苦労してしまっているが)
UnityのWebGLビルドではC# - JavaScript間のデータのやり取りでは最初なかなか苦労した。それもあって、もしGodot でVRMViewMeisterを(あえて)Webエクスポート版にするなら、同じようにGodot - JavaScript間のデータのやり取りの方法を調べなくてはと思った。
今回はそれを調べて簡単ではあるが動作を確認したので、覚え書きとして記事にまとめた。
ゲームエンジン - JavaScript間のやりとり
Unityでは
公式マニュアルではこのあたりを参考にしたい。
特にキモなのは次のページだろう。
UnityからJavaScriptにアクセスする
詳しくはマニュアルを参照するとして、簡単には次の手順で準備する。
1, .jslib拡張子で目的のJavaScriptコードを用意する
var JSPluginTest = {
ReceiveStringVal : function (val) {
var str = UTF8ToString(val);
//あとでJavaScript側から取得できるようなコード
//下記は適当なサンプル
var cur = AppQueue.current();
if (cur) {
AppDB.temp.setItem(cur.key,str).then(function(value){
AppQueue.execute(cur.key,cur,value);
});
}
},
}
mergeInto(LibraryManager.library, JSPluginTest);
2, AssetフォルダのPluginsフォルダに配置する
3, 呼び出したいC#コード中で次のように定義する
public class NewBehaviourScript : MonoBehaviour {
[DllImport("__Internal")]
private static extern void ReceiveStringVal(string val);
...
}
4, 次のように呼び出す
string ret = "hogehogeほげ";
#if !UNITY_EDITOR && UNITY_WEBGL
ReceiveStringVal(ret);
#endif
一つのプロジェクトで複数のビルドをする場合はその旨プリプロセッサで明示しておく。
JavaScriptからUnityにアクセスする
1, JavaScript側で createUnityInstance
して返ってきたオブジェクトを保存しておく
var unity_instance = await createUnityInstance(canvas, ...);
2, SendMessageメソッドに呼び出したUnity側のオブジェクト名、メソッド名、パラメータを受け渡して呼び出す
unity_instance.SendMessage("VRM01","SetBlendShape",JSON.stringify({...}));
Unityでのポイント
UnityからJavaScriptを呼び出すには .jslibファイルを用意し、そこで定義した処理名をC#側にも定義するのがポイントである。
逆にJavaScriptからUnityを呼び出すには、 SendMessageの使い方さえわかればOK。
というように、比較的わかりやすい。
ただし、Unity - JavaScript間は非同期でやり取りされるため、jsからUnityを呼び出し、なにか処理した値を返す。それを連続して行う、その順番を重要視するアプリの場合、いろいろ工夫が必要だ。
自作のアプリでの工夫
※この段落は読み飛ばして頂いてOKです。
VRMViewMeisterでは簡単なキューオブジェクトを作って、それをjsでもUnity側のjslibでも呼び出して、なんとか処理の順番を確保している。
呼び出す際はjs側で次のようにして呼び出すように工夫した。
AppQueue.add(new queueData(
{target:"ManageAnimation",method:'SetupTimeline',param:{param1:1, param2:"hoge")},
"setuptimeline",QD_INOUT.returnJS,
(val) => {
console.log(val);
}
));
AppQueue.add(new queueData(
{target:"ManageAnimation",method:'StartAllTimeline',param:"start"},
"",QD_INOUT.toUNITY,
null
));
AppQueue.start();
queueDataというのがUnityに実際渡す際のパラメータ群である。
target
- オブジェクト名
method
- メソッド名
param
- パラメータ(この段階ではObjectでもOK)
"setuptimeline"
- IndexedDBのキー名。Unityがjslibの処理内でjsに返す際、IndexedDBに保存している。それをjsから取得する際のキー名をここで渡して、jsとUnityの処理の関連性を担保している。
QD_INOUT.returnJS
- jsに値を返す際に指定するenum値。
コールバック
- Unityから返ってきた値を処理する。
QD_INOUT.toUNITY
- Unityからは値を返さないと明示するenum値
AppQueue内では順番に SendMessage
に渡していき、Unity側で処理が行われてjslibの関数を呼び出す。
そのjslibの関数内では現在処理しているIndexedDBのキー名を見て、そのキーに対して値をセットする。
そしてjs側では値を受け取ったらキューを削除し、値はコールバックに返す。
長々と書いたが、ようやjs側からUnityの処理を連続で呼び出しても、戻って来る順番が担保できていなかったため、このようなオブジェクトを作って非同期をやりくりした。
ここまでするなら普通に各OS版を作ったほうがまだ健全だったかもしれない。
同じことをGodotでやらなくてはいけないのか???
Godot では
公式マニュアルでは下記にあたる。ほぼここを見れば大丈夫かもしれない。
ほとんどの準備はGodot側で行う印象だ。
Unityのjslibのように個別にファイルを用意する必要はなく、呼び出したい gdscript側で必要に応じて使えばよいだけな模様。
GodotからJavaScript
JavaScriptのデータを取得するには次のメソッドを呼び出す。
var window = JavaScriptBridge.get_interface("window")
#---独自のオブジェクトの場合
var myjs = JavaScriptBridge.get_interface("myjs")
これで、グローバルスコープなwindowオブジェクトや、任意のグローバルオブジェクトの myjsオブジェクトを参照できる。
たとえばjs側で次のように定義していたとする。
var myjs = {
hoge : 123,
baz : "",
get_hoge : () => {
return `value is ${this.hoge}`;
}
set_baz : (val) => {
this.baz = val;
}
}
すると、Godot側ではこのように使える。
@export label01: Label
var myjs = JavaScriptBridge.get_interface("myjs")
label01.text = myjs["hoge"]
print(myjs.get_hoge())
Godot側でlabel01というLabel型の変数があり、そのラベルにjs側で定義された値をセットしたいとする。
そういう場合、js側で使うかのようにmyjsオブジェクトのプロパティをそのまま使用する。
JavaScriptからGodot
jsからGodotを呼び出す場合、Godot側とjs側で次のように準備する。
#---指定の関数をjsのコールバックとして定義
var _cb_lineedit_my = JavaScriptBridge.create_callback(_my_lineedit_callback)
func _ready():
#myjsオブジェクトを取得
var myjs = JavaScriptBridge.get_interface("myjs")
#myjsにsetFromGodotというメソッドを作成して紐づける
myjs.setFromGodot = _cb_lineedit_my
func _my_lineedit_callback(args):
var console = JavaScriptBridge.get_interface("console")
var myjs = JavaScriptBridge.get_interface("myjs")
console.log("_my_lineedit_callback")
if (len(args) > 0):
#受け渡されたパラメータをラベルに表示したり・・・
label01.text = args[0]
#逆にjs側に値をセットしたり
myjs["hoge"] = OS.get_name()
return OS.get_name()
var myjs = {
hoge : 123,
}
...
//別の処理中
var logtest = myjs.setFromGodot("js call Godot!");
console.log(logtest);
この例の場合、Godot側でmyjsに setFromGodot
というプロパティに _cb_lineedit_my
というコールバック関数をセットしている。js側で setFromGodot
が実際に定義されていなくても構わない。
この _cb_lineedit_my
はGodotの _my_lineedit_callback
という関数に紐づいている。
この処理内ではjsから受け渡されるパラメータは配列で渡ってくる。なので args では受け取った順に参照することになる。
ちなみにGodot側でmyjs.hogeに OS.get_name()でビルドの環境を取得して返している。そうするとjs側に受け渡される。
コード中にもう一つ例がある。
jsのconsoleオブジェクトを参照し、文字列をログに返している。
このように、js側のオブジェクトに定義されたメソッドへと値を返すこともできる。
つまり、JavaScriptからGodotを呼び出し、その中でGodotからjsに返すという、やり取りを連続させやすくなっている。
Godotのポイント
- JavaScript側の何をGodot側に公開するのか、あらかじめ大きなオブジェクトでまとめておくとよいかも。
- Godot側ではJavaScript側の任意のグローバルスコープ内のオブジェクトを取得できる。
- JavaScriptからGodotを呼び出す場合、Godot側でjs側の任意のグローバルオブジェクトにメソッドを埋め込んでもらい、それを呼び出してGodot側の機能を実行する。
冷静に考えるとなにかセキュリティ的な問題が起きそうな気がしないでもないが、そこはJavaScript側の開発と、Godot側の開発で分担したときにうまいこと連携を取ればGodot EngineでのWebエクスポートの開発も問題なく進められるのかもしれない。
終わりに
まだ調べ始めでしかないので感触では、UnityとGodotどっちもどっちといった印象だ。
ただ自由度といざというときの改修度ではGodotのほうがやりやすいか。ただ両環境での連携が取れていないと、デバッグ等が大変になりそうだ。
しかしUnityで問題にしていた非同期で呼び出した際の処理の順番は、Godotではメソッドを呼びだせばそれだけで済むので、いかようにでも工夫のしようがありそうだ。
Webエクスポートなアプリを作る際の参考になれば幸いである。