【Unity】AssetBundleの互換性を壊さずにasmdefを適用するには？

## はじめに

Unityでユーザーがアップロードした3Dコンテンツ等を扱う、UGCタイプのサービスを作る場合に、選択肢は大きく２つあるといえます
- `UnityのAssetBundleを使う方法`
	- UGCコンテンツの製作者は、Unity上で作ったprefabを、サービスのSDKを利用して、AssetBundleに変換してクラウドストレージ等にアップロードします。
	- 再生側のユーザーは、上記のAssetBundleをクラウドストレージ等からダウンロードしてきて、これをUnityで作ったアプリでインスタンス化してprefabに戻して再生します。
	- 事例: VRChat, STYLY
- `UnityのAssetBundleを使わない方法`
	- 標準規格のモデルデータや、独自規格のデータの組み合わせでコンテンツを表現します。
    - 事例: Roblox

これは重要な選択肢で、メリットやデメリットがありますが、ここでは `AssetBundleを使う`  選択をしたケースについて扱います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71421/732b5431-bdcd-8c6c-d7ed-f427ba0fa51e.png)


## 問題点: クラスをasmdef化した場合に、従来のAssetBundleを復元できなくなる

Assembly Definition File (asmdef)の定義をせずに、MonoBehaviourを継承したクラスA（コンポーネントA）からAssetBundleを作成したとします。当然ながら、この状態のままであれば、このAssetBundleから正常にコンポーネントAを復元することができます。

プロジェクトが進むにつれ、Assembly Definition File (asmdef)を利用したいという要求が高まります。asmdefを利用すると、TestRunnerに対応したテストコードを書くための条件を満たすことができますし、コードの依存関係を整理することもできます。

そんなこんなで、AssetBundleにしたコンポーネントを含むライブラリをasmdef化したくなったとしましょう。このとき、以下のような問題があります。

`残念ながら、クラスAに対してAssembly Definition Fileを設定すると、従来の構成で作成したAssetBundleを復元することはできません。`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71421/2ecd974a-b438-df3c-ebba-eefa732a2ba2.png)


復元に失敗する理由ですが、AssetBundleを正常に復元するためには、以下の３つの情報が整合している必要があるためです。
- クラス名
- namespace名
- アセンブリ名

参考:
https://forum.unity.com/threads/asset-bundle-cannot-find-scripts-from-assembly-definition-files.531804/#post-3521699


Assembly Definition File を定義すると、スクリプトの所属するアセンブリ名（≒DLL名称)が変化してしまいます。このため、AssetBundleから復元することができなくなってしまいます。

さて、困りました。`過去のAssetBundleの互換性を考慮すると、クラスAはasmdef化できない` ということがわかりました。

さて、ここから解決策を考えていきますが、やりたい内容によって解決策がかわってきます。

## 何がやれれば良いか？

やりたいことは、例えば以下のようなことと思います。

- 課題1: AssetBundleに入れたコンポーネントのロジックをTestRunnerでテスト可能にしたい（のでasmdef化したい）
- 課題2: asmdef化されたコード内から、AssetBundleに入れたクラスと同じコンポーネントをヒエラルキー上で探したり、メソッドを実行したりしたい
- 課題3: asmdef化されたコード内から、AssetBundleに入れたクラスと同じコンポーネントを追加したり、メソッドを実行したりしたい

それぞれに対して対策手法を提示します。


## 課題1: AssetBundleに入れたコンポーネントのロジックをTestRunnerでテスト可能にしたい（のでasmdef化したい）

対策手法: コンポーネントの中身だけを別クラス化し、asmdef配下に入れる

クラスAそのものをasmdef化するのは諦めましょう。しかし実質的にそれを実現する方法はあります。クラスAの中身だけを別クラスに抜き出してしまいます。

asmdef外からasmdef内を参照することはできますから、コンパイルの問題はありません。

このとき、クラスAのシリアライズされるメンバ変数の形を維持するのが重要です。ここが変わってしまうと復元に失敗してしまいます。（シリアライズされるメンバの追加は可能ですが慎重に・・・）

### 従来コード

```csharp
public class A : MonoBehaviour {
    [SerializeField]
    private string title;

    public void Start() {
        Debug.Log("Hello! " + title);
    }
}
```


### 新コード

asmdef配下
```csharp
public class InternalA {
    public void Start(string title) {
        Debug.Log("Hello! " + title);
    }
}
```

asmdef外
```csharp
public class A : MonoBehaviour {
    [SerializeField]
    private string title;

    private InternalA internal;
    public void Start() {
      internal.Start(title);
    }
}
```

InternalA はasmdef配下のため、TestRunnderでテストが可能になります。


## 課題2: asmdef化されたコード内から、AssetBundleに入れたクラスと同じコンポーネントをヒエラルキー上で探したい、メソッドを実行したい

対策手法: asmdef内に定義した`MonoBehaviour派生クラス`を継承するようにする。

AssetBundleに追加したコンポーネントに、格納時に存在していなかったクラスを継承させても問題はありません。
これを利用して、ヒエラルキー上から探せるようにします。
また、asmdef配下のほうのMonoBehaviour派生クラスにabstractなメソッドを定義して、それをasmdef外のクラスでoverrideするようにすれば、メソッドを呼ぶこともできます。

### 従来コード

```csharp
public class A : MonoBehaviour {
    [SerializeField]
    private string title;
    public void Hello() { ... }
}
```


### 新コード

asmdef配下
```csharp
public abstract class HogeBehaviour : MonoBehaviour
{
    public abstract void Hello();
}
```

asmdef外
```csharp
public class A : HogeBehaviour {
    [SerializeField]
    private string title;
    public override void Hello() { ... }
}
```

asmdef内から該当コンポーネントを探し、Hello() を呼ぶコード

```csharp
var obj = FindObjectOfType<HogeBehaviour>();
obj.Hello();
```

## 課題3: asmdef化されたコード内から、AssetBundleに入れたクラスと同じコンポーネントを追加したり、メソッドを実行したりしたい

対策手法: asmdef内にコンポーネントを追加するメソッドを備えたインタフェースを定義し、このインタフェースを実装し、コンポーネントを追加するクラスをasmdef外に実装する。

### 従来コード

```csharp
public class A : MonoBehaviour {
    [SerializeField]
    private string title;
    public void Hello() { ... }
}
```

従来のコンポーネント追加、メソッド呼び出しコード
```csharp
var a = gameObject.AddComponent<A>();
a.Hello();
```

### 新コード

asmdef配下
```csharp
public interface IHoge {
    void Hello();
}

public interface IHogeAdder
{
    IHoge AddHogeComponent(GameObject go);
}

// 話をシンプルにするため、staticプロパティを利用した構成にします
public class Setting {
    public static IHogeAdder HogeAdder { set; get; }
}
```

asmdef外
```csharp
public class AComponentAdder : IHogeAdder
{
    public IHoge AddHogeComponent(GameObject go) {
        return go.AddComponent<A>();
    }
}

public class A : MonoBehaviour, IHoge {
  [SerializeField]
  private string title;
  public void Hello() { ... }
}
```

asmdef外で行う初期化コード
```
Setting.HogeAdder = new AComponentAdder();
```

新しいコンポーネント追加、メソッド呼び出しコード (asmdef内で実行）
```csharp
IHoge componentA = Setting.HogeAdder.AddHogeComponent(gameObject);
componentA.Hello();
```

## おわりに

コードは完全な形ではないので、わからないところがあればコメントを頂ければと思います。

ここまで見ておわかりかと思いますが、AssetBundleをそのままにして互換性を維持したままコードを変えよう、というのはなかなかにややこしい話です。今回は触れなかった話もあります。互換性を維持せず、コードが変わったらAssetBundleは捨てて新規にアップする、という世界のほうが開発者にとってはローコストで嬉しいといえるでしょう。それでも互換性を維持したい場合は工夫が必要になってきます。

## 宣伝

Psychic VR Lab ではUnityの複雑性と戦う開発者を募集しております！