UnityのWebGLでJavaScriptを使いやすくする

KazuhiroYonekuraPosted by

はじめまして!Unityエンジニアの米倉です!

弊社では現在Unityを使ったWebGLプラットフォーム向けのゲーム開発を行っており、まだまだノウハウが少ない分野に苦戦しながら日々開発しております。

開発しているとさまざまな問題にぶつかるわけですが、UnityのWebGL周りはとりわけネット上にも情報が少なく、比較的難易度が高いように感じます。

ということで、今回はWebGL×Unityの開発がしやすくなるノウハウとして、WebGL向けUnity開発で便利なJavaScriptの動かし方について紹介したいと思います!


まずは基本的なUnity WebGL × JavaScriptの動かし方を知る

基本的な方法はUnity公式リファレンスにて案内されています。

https://docs.unity3d.com/ja/current/Manual/webgl-interactingwithbrowserscripting.html

上記のリファレンスどおりにネイティブプラグイン(.jslib)に処理を書けば、UnityのC#からJavaScriptの処理を動かせます。

ただし、この方法にはいくつか難点があります。

やってみてすぐわかる問題点…

リファレンスどおりjslibにJavaScript処理を書けば動くということで、そのままガリガリ実装したいのですが、以下の問題にすぐ直面してしまいます。

  • jslibファイルが書きづらい(.jslibという独自のファイル形式、中身はJavaScript)
  • jslibのビルド失敗時に、どこで失敗したか追いづらい
  • jslibを更新するたびにプロジェクト全体をビルドしないと動作確認できない

上記のような理由から、実装した処理を確認するのに時間がかかり、とても開発が進めづらいです。

jslibを更新するたびにプロジェクトをビルドしなおさなければならないのが特に致命的で、ちょっとしたエラーと格闘するだけで時間が溶けていきます。
そこで、今回紹介する方法を取ることで、Unityをビルドしなおさずに処理を更新することを実現しました。

jslibとJavaScript処理を分離させる

今回紹介する方法の大まかな流れは以下の図に示すとおりです。

jslibの他に新たにJavaScriptファイルを用意し、連携させる方法をとっていきます。

要点としては、js処理を従来のように直接jslibファイルに書かず、JavaScriptファイルに分離することで開発しやすくしようという話になります。

分離したJavaScriptファイルは、Webサーバーに置いたり、StreamingAssetsに含める利用法がとれます。(Assets内に含める場合、StreamingAssets以外に.jsファイルを入れるとコンパイルされてしまうので注意してください)

なお、こちらの処理が動くのは通常のjslibの使用法と同じく、ブラウザ上での実行時のみになりますのでご注意ください。


具体的な実装例

ここからは、実際にjslibとjs処理を分離するために行った実装例を紹介していきます。

実装するファイルは以下の3点です。

  1. jslibと連携するためのC#クラス
  2. jslibファイル
  3. 実行したいjs処理を記述したJavaScriptファイル

1.jslibと連携するためのC#クラスの実装

ここでは、jslibの処理を行うためのC#クラスの準備を行います。

以下、2点の関数がjslibで使えるように宣言を記述していきます。(後ほど、jslib側に実装を用意します)

  • 初めにJavaScriptをロードするためのInjectionJs関数
  • ロードされたJavaScriptにメッセージを送るためのExecuteJs関数

ここの宣言方法については公式リファレンスに記載されている方法と特に差はありませんが、以下に具体例を記載します。

1-a. scriptタグ埋め込み用のInjectionJs関数

...
#if UNITY_WEBGL && !UNITY_EDITOR
 // jslibの関数を使う場合に必須
 [DllImport("__Internal")]
 private static extern string InjectionJs(string url, string id);
#endif
...
...
 // 任意のC#クラスから使用するためのpublicな関数
 public static void Load(string url, string id)
 {
#if UNITY_WEBGL && !UNITY_EDITOR
    // 上記DLLImportのメソッド定義と一致させる
    InjectionJs(url,id);
#endif
 }
...

1-b. 埋め込んだjsの実行用のExecuteJs関数

...
#if UNITY_WEBGL && !UNITY_EDITOR
 [DllImport("__Internal")]
 private static extern void ExecuteJs(string id, string methodName,string jsonData,string callbackGameObjectName);
#endif
...
...
 // 任意のC#クラスから使用するためのpublicな関数
 public static void Execute(string id, string methodName,string parameterJson, string callbackGameObjectName)
 {
#if UNITY_WEBGL && !UNITY_EDITOR
    ExecuteJs(id, methodName, parameterJson, callbackGameObjectName);
#endif
 }
...

2. jslibファイルの実装

ここではjslibの実装部分の記述を行います。

先程準備した以下の2つの関数の、具体的な処理をjslibに記載していきます。

  • 初めにJavaScriptをロードするためのInjectionJs関数
  • ロードされたJavaScriptにメッセージを送るためのExecuteJs関数

2-a. InjectionJs

この関数では、外部のJavaScriptをsrcタグURLHTML要素として生成する処理を記載していきます。

この処理が、外部JavaScriptを実行できるようにするためのコア部分の処理となります。

...
// 指定URLのJavaScriptをWebGL実行ページ上にscriptタグで埋め込む実装
 InjectionJs:function(url,id){
     url = Pointer_stringify(url);
     id = Pointer_stringify(id);
 
    // idを渡して、同じidだった場合は要素を生成しないようにしておく
    if(!document.getElementById(id))
    {
       var s = document.createElement("script");
       s.setAttribute('src',url);
       s.setAttribute('id',id);
       document.head.appendChild(s);
    }
 },
...

※idなどで識別子をつけてあげて、重複したJavaScriptをロードしないようにしたりしましょう。

2-b. ExecuteJs

実行用であるExecuteJs関数では、上記のInjectionJs関数でロードされたJavaScriptに向けて、PostMessageを送る処理を記載します。

PostMessageで送るパラメータは任意ですが、ここでは以下の4つ

  • 実行したいJavaScriptの識別子(id)
  • 実行したいJavaScriptの関数名
  • 関数のパラメータ
  • コールバックのGameObject名

を送るように実装しています。

特に、コールバックのGameObject名を渡しておくことで、JS処理が終わったときにUnityへメッセージを返せるようになるのでおすすめです(後述)。

...
// 指定したメソッドを実行するための関数例(~.jslib内)
 ExecuteJs:function(id,methodName,jsonData,callbackGameObjectName){
     id = Pointer_stringify(id);
     methodName = Pointer_stringify(methodName);
     jsonData = Pointer_stringify(jsonData);
     callbackGameObjectName = Pointer_stringify(callbackGameObjectName);
 
     var jsonObj = JSON.parse(jsonData);
     jsonObj.Id = id;
     jsonObj.MethodName = methodName;
   // メッセージを返してもらうためのGameObject名を渡しておく
     jsonObj.CallbackGameObjectName = callbackGameObjectName;
 
     // PostMessage
     var messageString = JSON.stringify(jsonObj);
     window.postMessage(messageString, "*");
 },
...

※PostMessageはMozillaが制定している通信用メソッドで、window間でのメッセージのやりとりを行えます。

3. JavaScriptファイルの実装

ここからは分離される側のJavaScriptファイルでの処理を記載します。JavaScriptファイルは新規作成し、おまじないを書いて疎通できるようにしていきます。

2-b.のExecuteJsによりUnityからPostMessageが送信されるので、PostMessageを受け取るイベントリスナを書くことでメッセージが受け取れるようになります。

今回の実装例では、メッセージで関数名とパラメータを受け取って、evalでそのまま実行する仕組みにしています。

3-a. イベントリスナ、メッセージを解釈してevalする関数

jslibからSendMessageされたjsonをパースし、evalで関数実行する機能を準備します。

イベントリスナさえ実装できればUnityからのメッセージが受け取れるため、その後の処理はどのように実現しても構いません。

...
// Unityと連携するための関数群
hoge = function() {
    return {
        // Unityからのメッセージを受け取るハンドラ登録
        InitializationEventListener: function() {
            window.addEventListener('message', function(event) {
                hoge.ExecuteJs(event.data);
              }, false);
          },
        // 受け取ったメッセージから、evalを使って関数を呼び出す
        ExecuteJs: function(message) {
            if (typeof (message) !== "string" && !(message instanceof String) || message == "null") {
                return;
            }
            var parameterObject = JSON.parse(message);
            var methodName = parameterObject.MethodName;
            var evalString = methodName + '(parameterObject)';
            eval(evalString);
          }
      };
  }();
... 

例として、上記のようにおまじない的にひとまとめにしてイベントリスナとevalを書いておくようにすると、さまざまな機能を.jsファイル単位で追加していけるようになります。

※実際の実装ではホストのチェックなど他の処理も行っています。

3-b. ExecuteJsによってevalで実行される任意の関数例

以下は渡されたHTMLデータをiFrameで開くJS機能の例です。

...
ShowHtml: function(parameterObject) {
    // IFrame生成
    webview.method.CreateIframe(parameterObject);
    // HTML読み込み
    var iframe = document.getElementById('webViewIframe');
    iframe.contentWindow.location.href = 'about:blank';
    iframe.onload = function() {
        // IFrame内のドキュメントを取得する
        var iframeDocument = iframe.contentWindow.document;
        // ドキュメントにHTMLソースを書き込む
        iframeDocument.open();
        iframeDocument.write(parameterObject.Content);
        iframeDocument.close();
        iframe.onload = function() {};
      };
  },
...

3-c. 任意の関数内でコールバックを返す例

JavaScript側からUnity側に何か通知したい場合は、gameInstance.SendMessageでメッセージが送信できます。

予め、送信先のGameObject名を渡しておくと便利です。

... 
// UnityにSendMessageする(~.js内) 
    gameInstance.SendMessage( 
        'GameObject名', 
        'GameObjectにアタッチされたコンポーネントのPublic関数名',
        'メッセージ内容' // jsonなどパラメータ 
    );
 ... 

※ブラウザ上での実行時にはgameInstanceが公開されているので、いきなりgameInstance.~と書いても大丈夫です。
※Unityのバージョンにより少し書き方に差異がある可能性があります。


全体の構成図

分離したJavaScriptをWebサーバーに置いて使用する場合、WebGL実行時には以下のような構成になります。

この構成であれば、JavaScriptを修正してもUnityをビルドし直す必要がなく、WebGLで動くUnityのブラウザをリロードするだけで新しいJavaScriptがロードし直されるようになります。


最後に

この方法を使うと、WebGL×Unityで動くゲームにJavaScriptを組み合わせるのが少し楽になると思います。
とりわけ、比較的複雑なJavaScript機能をUnityと組み合わせたい場合、素早く実装とテストを回す必要がでてくるかと思います。その場合に従来の方式だととても時間が足りなくなってしまうため、ぜひ検討されてみてはいかがでしょうか。

また、弊社ではWebGL×Unityのゲーム開発において引き続き検証や試行錯誤を続けてまいります。今後も何か情報が得られたら、こちらのブログにて紹介していく予定です。

ここまでお読み頂きありがとうございました!