UnityのHierarchyとInspectorを実機で使えるようにした話

この記事は [KLab Advent Calendar 2017](https://qiita.com/advent-calendar/2017/klab) 24日目の記事です。

# はじめに
UnityEditorはゲームを動作させながら、HierarchyやInspectorを使ってゲームの内容を変更できるので、すごく開発が捗りますよね。

筆者自身、長い間Cocos2d-xのプロジェクトに関わっていたため、その辺りは特に感じているところです。

ただUnityEditorは便利なツールではあるのですが、実機開発になった途端に多くの機能が使えなくなってしまいます。
もちろん殆どの作業がUnityEditorだけで完結するため問題になる事はあまりありませんが、やはり実機確認が必要なシーンはどうしても発生しますよね。

例えば以下のようなもの。

- UI/UXの調整
- 機種依存問題（シェーダー等）
- グラフィックの品質確認
- プラットフォーム独自機能の実装
- パフォーマンスチューニング

今回この問題を解決した話と、そこから派生したツールの紹介をしたいと思います。

# 課題と解決方法
まずHierarchy・Inspectorを実機でも使えるようにすれば、以下の作業が改善されると考えました。

- UI/UXの調整
- 機種依存問題（シェーダー等）


# ベースとなる仕組みの実装
実機でHierarchy・Inspectorを使えるようにするためには以下の仕組みがあれば実現できそうです。
早速実装していきましょう。

- UnityEditorと実機間で常時接続
- ホットリロード


## UnityEditorと実機間で常時接続
常時接続はUnityEditorと実機をTCP接続して実現します。
実機をTCPサーバー、UnityEditorをTCPクライアントとしてC#の[System.Net.Sockets.TcpClient](https://msdn.microsoft.com/ja-jp/library/system.net.sockets.tcpclient)クラスを使って実装しました。

さらにadb、iproxyコマンドを使って接続ポートをUSBトンネリングする事でUSB経由の接続が可能となります。

```shell
# ios
iproxy [local port] [server port] [ios UDID]
```
```shell
# android
adb -s [Android serial_number] forward tcp:[local port] tcp:[server port]
```

**これでUnityEditorと実機間で高速かつオフラインでの双方向通信が可能となりました。**


## ホットリロード
ホットリロードについては、インタプリタ型言語のLuaを使って実現する事にします。

選定理由はUnityに対応しているライブラリがいくつかあったのと、社内的にユーザーが多いからです。
ただ、いくつかのライブラリを検証しましたが、Unityのバージョン[^1]のせいなのかまともに動作しないものや、IL2CPPに対応していないものがあったりして、結果的にsluaというライブラリを採用しました。

ちなみに検証段階では存在しなかったので試していないですが、最近tencentから公開された[xLua](https://github.com/Tencent/xLua)も良さそうです。
*（※今回のライブラリの選定ですが、あくまでツール利用なのでパフォーマンス、安定性等は考慮に入れていません。）*

[^1]: Unityのバージョンは5.6を使用しています。

### sluaからUnityAPIの実行例
以下のサンプルコードのようにUnityAPIは同名になっているのでLuaの基本構文さえ覚えれば、すんなり実装できると思います。

*Luaのサンプルコード*

```lua
import ‘UnityEngine’
function main()
    local obj = GameObject.Find(“Player”)
    obj:SetActive(false)
end
```

### Luaを利用したUnityEditorと実機間のデータの送受信
Luaスクリプトを使ったUnityEditorと実機のやり取りが簡単にできる仕組みを作っていきます。

以下がUnityEditor側から実行するC#のコードになります。

```c#
// Luaスクリプト
var luaScript = @"
import ‘UnityEngine’
local json = require 'json'
function main()
  local player = {}
  player.Name = 'Taro'
  return json.decode(player)
end
";

// UnityEditor(C#)からLuaファイルを送信
Client.SendLuaScript(
  luaScript,
  json =>
  {
    var player = JsonUtility.FromJson<Player>(json);
  }
);
```

*Client.SendLuaScript関数の処理の流れ*

1. UnityEditorからClient.SendLuaScript関数でLuaスクリプトを実機に送信。
2. 実機でLuaスクリプトのmain関数を実行し、main関数の戻り値をUnityEditorに送信。
3. Client.SendLuaScriptの第二引数のコールバック関数が呼ばれます。引数の変数*json*はmain関数の戻り値です。

# Hierarchy、Inspectorの実装
ベース機能ができたので、HierarchyとInspectorを実装していきます。 

## Hierarchy
まずはGameObjectの階層構造をJson化するLuaスクリプトを作成します。

```lua
import ‘UnityEngine’
local json = require 'json'

local index = 1
function main()
  local hierarchy = {}
  for i=0, UnityEngine.SceneManagement.SceneManager.sceneCount-1 do
    local sceneObj = UnityEngine.SceneManagement.SceneManager.GetSceneAt(i)
    local scene = {}
    scene.Name = sceneObj.name
    hierarchy[index] = scene
    -- Scene内のGameObjectを取得
    setGameObjects(scene, hierarchy)
  end
  return json.decode(hierarchy)
end
```

このLuaスクリプトを実機に送信して、受け取ったJsonを使ってUnityEditor拡張で作成したオリジナルのHierarchyウィンドウを表示すると完成です。

<img width="305" alt="hierarchy.png" src="https://qiita-image-store.s3.amazonaws.com/0/216057/7dc3ff7f-ef8a-7066-5e0e-37f351e59788.png">

## Inspector
InspectorもHierarchyと同様にGameObjectの情報（コンポーネント等）をJson化するLuaスクリプトを使ってInspectorウィンドウを作成します。

<img width="263" alt="inspector_recttransform.png" src="https://qiita-image-store.s3.amazonaws.com/0/216057/73d43a30-6eae-f479-7f7d-9b0ca000a8d1.png">

InspectorからTransformを変更したいので、Transformを変更するLuaスクリプトを実装します。

```c#
var luaScript = @"
import 'UnityEngine'
function main(obj)
  obj.transform.position = Vector3(@@position.x@@, @@position.y@@, @@position.z@@)
end
";

// Luaスクリプトの一部をInspectorに入力した値に置換
luaScript = luaScript
  .Replace("@@position.x@@", inspector.RectTransform.Position.x)
  .Replace("@@position.y@@", inspector.RectTransform.Position.y)
  .Replace("@@position.z@@", inspector.RectTransform.Position.z);

// UnityEditor(C#)からLuaファイルを送信
Client.SendLuaScript(
  luaScript,
  json => {}
);
```

このようにInspectorで入力した値をLuaスクリプトに反映させてから実機に送信する事で、InspectorからのTransformの変更が可能となります。

## Inspecterからシェーダーを置き換える
次にInspectorからのシェーダーの置き換えに対応させていきます。
Transformの実装と異なる点はシェーダーファイルを実機に転送する必要があるところです。


*処理手順*

1. Inspectorで選択したシェーダーをアセットバンドル化。
2. アセットバンドルファイルを実機に転送。
3. シェーダーを置き換えるLuaスクリプトを送信。  


*シェーダーを置き換えるLuaスクリプト*

```lua
-- 引数のobjはInspectorに表示されているGameObjectのインスタンスです。
function main(obj)
  local ab = AssetBundle.LoadFromFile("/path/to/assetbuneles/sample.shader.assetbundle")
  local assets = ab:LoadAllAssets()
  local shader = assets[1]
  ab:Unload(false)
  local component = obj:GetComponent('Renderer')
  component.materials[1].shader = shader
end
```

# 課題の解決
- UI/UXの調整
- 機種依存問題（シェーダー等）

これで上記の課題を改善する事ができました。
UI/UXについてはプランナーなどの非技術だけで調整ができるようになったところも良かったです。
時間のかかるシェーダーの実機開発も改善されました。

# その他のツールの紹介
今回実装した仕組を使って作成したツールが他にもあるので、いくつか紹介したいと思います。

## ツール紹介1: DeviceExplorer
実機のディレクトリの内容を確認できるEditor拡張です。
UnityEditorのプロジェクトウィンドウのように実機のディレクトリがUnityEditor内にツリー状で表示されます。

<img width="460" alt="explorer.png" src="https://qiita-image-store.s3.amazonaws.com/0/216057/ba0a7c6c-8e82-8fce-b30f-f60528325099.png">

ファイルを選択するとファイルのフォーマットに合わせた内容が先ほど作成したInspectorに表示されるようになっています。
例えばアセットバンドルファイルの場合は内包されているアセットの内容を表示したり、
テクスチャの場合はInspector上でプレビュー表示をする事もできます。

<img width="283" alt="inspector_preview.png" src="https://qiita-image-store.s3.amazonaws.com/0/216057/66a65e02-4cdb-154b-bfdb-7b01975a4620.png">

ちなみにLuaからC#の標準クラスを呼び出せるラッパークラスを作成して使いました。
例えばLuaはIO処理が弱いのでC#のSystem.IOをLuaから呼び出せるようにして使ったりしています。

*sluaを使うと簡単にC#のラッパークラスが作成できますが、今回はsluaの使い方については省略させて頂きます。*

*Luaサンプルコード*

```lua
import 'UnityEngine'
import 'CSharpWrapper.IO'

function main()
  parse_directory("/data/data/sample.org/files/assets")
end

function parse_directory(dir)
  local files = Directory.GetFiles(dir)
  for i = 1, #files do
    Debug.Log(files[i])
  end
  local dirs = Directory.GetDirectories(dir)
  for i = 1, #dirs do
    Debug.Log(dirs[i])
    parse_directory(dirs[i])
  end
end
```
こうなるとC#のコードを書いているのと殆ど変わらなくなってきますよね。


## ツール紹介2: AssetTransfer 
実機にUSB経由で高速にアセットを転送できるツールです。
デバッガーなどのUnityEditorを所有していないかたでも使用できるようにスタンドアローンアプリとして提供しています。 
PCに接続している複数の端末に同時に転送する事もできるようにしています。
デバッガーが毎日多くの端末を準備しているのですが、その時間の短縮にも役立っています。
さらにUSBを使うのでWifi回線の渋滞も改善されます。

## ツール紹介3: ApplicationMonitor
クライアントアプリの情報をリアルタイムに監視する事ができるUnityEditor拡張です。
情報とは、パフォーマンス情報やアプリ画面に表示されていないような内部データ等になります。
このようなデバッグ情報は今まではアプリ画面に出していましたが、UnityEditor上に表示するようになったので、画面サイズの制限がなくなったのと描画負荷もかからなくなりました。


# 補足
この仕組みですがUnityEditor側はWindows・Macどちらでも動作し、
実機側もiOS・AndroidのOSのバージョンに制限なく動作します。

実機だけではなくUnityEditorで動作しているアプリでも使用可能です。


# おわりに
いくつかツールを紹介しましたが、
常時接続とLuaを使えば実機開発の多くの事が改善するのでオススメです。

ちなみにUnity2017からは以下の2つのクラスが追加されていたので、常時接続環境はこちらで代替できそう気がします。
（使った事はないので使えなかったらごめんなさい。）

- [UnityEngine.Networking.PlayerConnection.PlayerConnection](https://docs.unity3d.com/ScriptReference/Networking.PlayerConnection.PlayerConnection.html)
- [UnityEditor.Networking.PlayerConnection.EditorConnection](https://docs.unity3d.com/ja/current/ScriptReference/Networking.PlayerConnection.EditorConnection.html)



とうとう最終日となりました。
明日、25日目は[takahashi-yoさん](https://qiita.com/takahashi-yo)です。よろしくお願いします。