[Unity]WebGLのプラグインをモダンに書く方法

## はじめに

UnityでWebGLビルドを行うときにブラウザと連携したくなることがあります。
公式には以下のページでやり方が解説されています。

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

`Plugins` フォルダを作り `.jslib` 拡張子のファイルを配置して以下のように書きます。

```js:hello.jslib
mergeInto(LibraryManager.library, {
    Hello: function() {
        alert("Hello, world")
    }
})
```

そしてC#のスクリプトを以下のように書きます。

```cs:Hello.cs
public class Hello : MonoBehaviour
{
    // jslibのmergeIntoに渡したオブジェクトのキーと同じ名前のメソッドを作る
    [DllImport("__Internal")]
    private static extern void Hello();
    
    void Start()
    {
        // alert("Hello, world")が実行される
        Hello();
    }
}
```

しかしこの書き方には問題点があります。
それは**ビルド時にアセットとして含めないといけない**ということです。
JavaScriptのコードを少し変えただけでUnityのビルドを行っていては時間がかかてしまいます。
今回はこの問題を解消してモダンにプラグインを書く方法を紹介します。

## jslibにはJavaScriptのコードを書ける

`.jslib` のファイルはJavaScriptのコードを書くことができます。
当たり前のように聞こえますがこれにより `mergeInto` に渡すオブジェクトをJavaScriptのコードで作成することが可能です。
以下のように書くことで `Hoge`、`Fuga`、`Piyo`のメソッドを定義することができ、実行すると`Hello`を表示するメソッドになります。

```js:hello.jslib
const plugin = {}

const methodNames = [
    'Hoge',
    'Fuga',
    'Piyo',
]

for (let i = 0; i < methodNames.length; i++) {
    const methodName = methodNames[i]
    plugin[methodName] = function () { alert('Hello'); }
}

mergeInto(LibraryManager.library, plugin)
```

## `__postset` を使用する

UnityのWebGLビルドは[emscrpiten](https://emscripten.org/index.html)を使用しています。
Unity側のドキュメントには記載されていないことでもemscripteのドキュメントを読むことでわかることがあります。
emscriptenには`__postset`という機能があります。
`__postset`は**コンパイル後のJavaScriptのソースにそのまま書き出される**という機能です。

[Interacting with code &mdash; Emscripten JavaScript limits in library files](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#javascript-limits-in-library-files)

具体的な使い方は以下の通りです。

```js:hello.jslib
mergeInto(LibraryManager.library, {
    Hello: function() {
        alert("Hello, world")
    },
    Hello__postset: 'console.log(42)'
})
```

この状態でビルドすると`console.log(42)`が書き出されます。
そのためWebGLビルドを起動するとコンソールに42が表示されます。
これだけだとあまり意味がないように見えます。
この機能はメソッドの上書きと合わせることで真価を発揮します。
例えば上記の例では`Hello`という名前のメソッドを定義していますが、`__postset`内では`_Hello`という名前でアクセスできます。
さらに`_Hello`というメソッドを上書きすることもできます。

```js:hello.jslib
mergeInto(LibraryManager.library, {
    Hello: function() {
        alert("Hello, world")
    },
    Hello__postset: '_Hello = function(){ alert("Hello__postset") }'
})
```

こうすることで`Hello`を実行したときに`Hello, world`ではなく`Hello__postset`を表示することができます。
ここで重要なのは`__postset`の内容は**そのままソースに書き出され、ブラウザ上で実行される**ということです。
つまり、**グローバルに定義されたメソッドにアクセスすることができます**。

よって以下のようにすることでUnityでWebGLビルドすることなくWebGLのプラグインを書くことができます。

```js:hello.jslib
mergeInto(LibraryManager.library, {
    Hello: function() {
    },
    Hello__postset: '_Hello = Hello'
})
```

```js:main.js
// ブラウザ側のコード
// Unityのインスタンス作成より前に定義しておく
function Hello() {
    alert('Hello from js!')
}
```

## 汎用的にする

前述のとおり`.jslib`ではJavaScriptを実行できます。
メソッド名を元に`__postset`を作ると何度も同じことを書かなくて済みます。


```js:hello.jslib
const plugin = {}

const methodNames = [
    'Hoge',
    "Fuga",
    "Piyo",
]

for (let i = 0; i < methodNames.length; i++) {
    const methodName = methodNames[i]
    plugin[methodName] = function () { }
    plugin[methodName+'__postset'] = `_${methodName} = ${methodName}`
}

mergeInto(LibraryManager.library, plugin)
```

```js:main.js
function Hoge() {
    alert('Hoge')
}
function Fuga() {
    alert('Fuga')
}
function Piyo() {
    alert('Piyo')
}
```

これである程度便利になりましたが、グローバルに色々定義したくないのでブラウザ側で以下のようなメソッドを導入します。

```js:lib.js
const functionMap = new Map()
// jslibに書くメソッド
function __unity_getBinding(methodName) {
    return functionMap.get(methodName)
}

// ブラウザ側のコードに書くメソッド
function bindFunction(methodName, func) {
    functionMap.set(methodName, func)
}
```

これを使うようにコードを修正します。

```js:hello.jslib
const plugin = {}

const methodNames = [
    'Hoge',
    "Fuga",
    "Piyo",
]

for (let i = 0; i < methodNames.length; i++) {
    const methodName = methodNames[i]
    plugin[methodName] = function () { }
    // __unity_getBindingで実際に実行するメソッドを取得する
    plugin[methodName+'__postset'] = `_${methodName} = __unity_getBinding('${methodName}')`
}

mergeInto(LibraryManager.library, plugin)
```

```js:main.js
bindFunction('Hoge', () => alert('Hoge'))
bindFunction('Fuga', () => alert('Fuga'))
bindFunction('Piyo', () => alert('Piyo'))
```

## C#とJavaScriptのデータ受け渡し

基本的に[Unityの公式ページ](https://docs.unity3d.com/ja/2020.3/Manual/webgl-interactingwithbrowserscripting.html)に書いてある通りですが文字列の渡し方は少し注意が必要です。
C#とJavaScriptで文字列を受け渡しする場合、`lengthBytesUTF8`、`stringToUTF8`、`_malloc`、`Pointer_stringify`などのメソッドを使用する必要があります。

```js:hello.jslib
mergeInto(LibraryManager.library, {
    // C#側からもらった文字列に_helloを付けて返す
    Hello: function(strPointer) {
        // C#側から渡される文字列はポインタになってるのでPointer_stringifyで文字列にする
        const str = Pointer_stringify(strPointer)
        // 渡された文字列にhelloをつける
        const result = str + '_hello';
        // そのまま文字列を返せないのでヒープを確保してそれを返す
        const bufferSize = lengthBytesUTF8(str) + 1
        const buffer = Module._malloc(bufferSize)
        // 確保したヒープに書き込み
        stringToUTF8(result, buffer, bufferSize)
        
        // C#に返したヒープは自動でfreeされる
        return buffer
    }
})
```

```cs:Hello.cs
public class Hello : MonoBehaviour
{
    [DllImport("__Internal")]
    private static extern void Hello(string str);
    
    void Start()
    {
        Debug.Log(Hello("a")); // => a_hello
        Debug.Log(Hello("b")); // => b_hello
        Debug.Log(Hello("c")); // => c_hello
    }
}
```

`Pointer_stringify`などのメソッドは`__postset`などの中でしかアクセスできません。

```js:main.js
bindFunction('Hello', strPointer => {
    // Uncaught ReferenceError: Pointer_stringify is not defined
    const str = Pointer_stringify(strPointer)
})
```

これを回避するシンプルな方法は引数として渡してしまうことです。


```js:hello.jslib
// ...省略

// __unity_getBindingの引数にPointer_stringifyを渡す
plugin[methodName+'__postset'] = `_${methodName} = __unity_getBinding(Pointer_stringify, '${methodName}')`
```

```js:lib.js
let Pointer_stringify = null
function __unity_getBinding(pointer_stringify, methodName) {
    Pointer_stringify = pointer_stringify;
    return functionMap.get(methodName)
}
```

```js:hello.js
bindFunction('Hello', strPointer => {
    // OK
    const str = Pointer_stringify(strPointer)
})
```

必要な引数は色々あるのでそれもまとめて渡せるようにします。

```js:hello.jslib
// 必要なメソッド
const helperFunctionNames = [
    'lengthBytesUTF8',
    'stringToUTF8',
    'Pointer_stringify',
]

// { lengthBytesUTF8: lengthBytesUTF8, ... }のようなオブジェクトの文字列を作成する
const helperFunctions = '{' + helperFunctionNames.map(x => `${x}:${x}`).join(',') + '}'

for (let i = 0; i < methodNames.length; i++) {
    const methodName = methodNames[i]
    plugin[methodName] = function () { }
    plugin[methodName+'__postset'] = `_${methodName} = __unity_getBinding(Module, ${helperFunctions}, '${methodName}')`
}
```

```js:lib.js
let Module = null
let helperFunctions = null
function __unity_getBinding(module, _helperFunctions, methodName) {
    Module = module
    helperFunctions = _helperFunctions
    return functionMap.get(methodName)
}
```

```js:main.js
bindFunction('Hello', strPointer => {
    // OK
    const str = helperFunctions.Pointer_stringify(strPointer)
})
```

## C#のメソッドをJavaScriptから呼び出す

公式推奨の方法は`unityInstance.SendMessage`を使用する方法ですがC#側からActionを渡してそれをJavaScriptから実行することもできます。
C#側からActionを渡したい場合、staticで定義して`MonoPInvokeCallback`属性を付けます

```cs:Hello.cs
public class Hello : MonoBehaviour
{
    [DllImport("__Internal")]
    private static extern void RegisterCallback(Action<string> callback);
    
    [MonoPInvokeCallback(typeof(Action<string>))]
    private static void Callback(string str)
    {
        Debug.Log(str);
    }
    
    void Start()
    {
        RegisterCallback(Callback);
    }
}
```

渡されたActionはJavaScript側ではポインタになっています。
このポインタを実行するためには`Module.dynCall_xxx`を使います。

```main.js
bindFunction('RegisterCallback', callbackPtr => {
    const callback = str => {
         // ヒープを確保してそれを渡す
        const bufferSize = helperFunctions.lengthBytesUTF8(str) + 1
        const buffer = Module._malloc(bufferSize)
        helperFunctions.stringToUTF8(str, buffer, bufferSize)
        
        // メソッドを実行する
        // viの部分はメソッドの引数や戻り値に応じて変更する
        Module.dynCall_vi(callbackPtr, buffer)
        
        // ヒープを解放する
        Module._free(buffer)
    }

    // 一秒毎ににコールバックを実行する
    let i = 0
    setInterval(() => {
        callback(`Hello from js: ${i++}`)
    }, 1000)
})
```

## まとめ

これらを活用することで`.jslib`にコードを書くことなくブラウザ側のコードだけでWebGLのプラグインを書くことができます。
ブラウザ側のコードということはモダンな開発環境を整えるのも難しくありませんね。

## おまけ - サンプルプロジェクト

inputフィールドがブラウザ側に配置されてあり、入力内容がUnity側に反映されます。

![webglsample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42193/092470ee-a611-559d-e5ba-9cacdd9a1eb7.gif)

デモ

https://yaegaki.github.io/UnityWebGLSample/

ソース

https://github.com/yaegaki/UnityWebGLSample

## おまけ - TypeScriptで書く

TypeScriptで書く場合のサンプルを用意しました。

https://github.com/yaegaki/unity-webgl-binding

使い方

```ts:main.ts
import {
    bindFunction,
    Pointer,
    buildFunctionVoid_Int, buildFunctionVoid_IntString,
} from 'unity-webgl-binding'

bindFunction('Init', () => {
    alert('Init!')
})

bindFunction('RegisterCallback', (x: Pointer, y: Pointer) => {
    const func1 = buildFunctionVoid_Void(x)
    const func2 = buildFunctionVoid_IntString(y)
    
    func1()
    func2(1, 'hello')
})
```