【Unity】【AssetBundle】Unity2017~Unity2018.1 のためのAssetBundle システム構築方法 (2018/09/19追記)

# はじめに
- この記事は主にUnity2017.1 ~ Unity2018.1 向けの話になります。
- 内容としては初級者〜中級者レベルで、AssetBundle の仕組みを作らなければならない人向けです。
- 他のバージョンは様々な記事があるので参考リンクに記載してあるページを見ていただければと思います。
- コードについてはダミーコードを載せているので、コピペしただけでは動かないと思います。

**2022/07/01追記**
2022年現在ではAddressables で十分運用タイトルで利用可能なため、基本的にはAddressablesを用いましょう

## 用語
| 用語     | 意味 | 
|:--------------------------:|:------------------|
| Asset(アセット)     |訳すと資産。Prefab,Animation,Texture,AudioClip,Scene などのゲームで用いるリソース全般を指す言葉|
| AssetBundle  |複数のAssetをパッキングしてまとめたもの。複数存在可能|
| AssetBundleBrowser|Unity謹製AssetBundle管理ツール。後述で説明します |
| AssetBundleManifest | AssetBundleのバージョンや名前、依存関係などをまとめたYAML( or バイナリ) ファイル。AssetBundleと一緒に自動生成される|
| ビルド | AssetBundleを作ること。APIこそ公式であるがツールはないので大抵は自作する |


# AssetBundle とは
ざっくりと説明すると「アプリ外部から素材などをダウンロード〜ロードする仕組み」+「ロードする素材群をパック（まとめる）するための仕組み」です。
## メリット
アプリ内部に一部リソースを持たなくていいため

- アプリのサイズを抑えることができる
    - モバイルアプリだと各プラットフォームで3G/4G回線でDL出来るアプリのサイズ制限がある（Apple:150MB, GooglePlay:100MB)
    - ユーザーとしてはDLしやすくなるためDL数が伸びやすい
    - 動画やボイスなど重いファイルは必要な時にDLする仕組みに出来る
- サーバーにAssetBundleを置くことができる
    - アプリ申請なしにリソースの更新が可能
        - アプリ内イベントなどのリソース追加が容易
        - 見た目修正程度であれば、AssetBundleを更新して、ユーザーに最新のものをDLしてもらえればすぐ治る
    - （アプリの作りによるが）必要な時に必要なもののみDLできる
    - リソースのバージョン管理も行える

# AssetBundle の仕組みの導入
## 楽したい方へ
- もし極力コードを書きたくないという方は既存のパッケージの利用をオススメします。
@neon-izm　さんが紹介しているコチラなどを参考にしてもらうと良いと思います。
https://qiita.com/neon-izm/items/b105130fac060ec40ad4
- ちなみにAutoyaの導入も考えましたがUnityAdsが必要だったりしたので辞めました
- Unity謹製AssetBundleManagerは5.X系以降はあまりメンテされてなかったのでオススメしません。ただ、コードを書く時の参考に利用するのはありだと思います


なんらかの事情でパッケージ導入がダメな人はここから一緒に頑張りましょう（悲しみ）

## 機能の確認
AssetBundleで最低限必要なこととしては

- AssetBundle のDownload処理
- AssetBundle のローカルキャッシュへSave/Load 処理
- AssetBundle のLoad/Unload 処理
- AssetBundle のビルド処理
- シミュレーションモードの実装

だと思います。

AssetBundle のDownload ~ Unload までの流れはCEDECの講演でとてもわかりやすく解説されているのでぜひ見て見てください。
https://creator.game.cyberagent.co.jp/?p=4791

これらが出来た上で実装したい項目としては

- AssetBundle の暗号化
- AssetBundle のバージョン管理

あたりが候補として上がってくると思います。

## ワークフロー
最低限必要な「Download」「Cache管理」「Load」を踏まえると、フローとしては以下のような感じになります。
![AssetBundleFlow (1).png](https://qiita-image-store.s3.amazonaws.com/0/65257/80ea0fe6-2221-a0fe-7e94-32eff2860d92.png)

### Download処理
Downloadといっても「ManifestのDownload」と「AssetBundleのDownload」とでは少し話（やコード）が変わってくるので順を追って説明していきます。

#### AssetBundleManifestのDownload
AssetBundleを使うためにも、その情報が記載されているManifestが無いと始まりません。
Manifestに関しては特に制約とかが無いのでUnityWebRequest.GetAssetBundle()　でとってくればOKです。

```csharp:AssetBundleDownloader.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;

const int TIME_OUT = 60; //[sec]

public delegate void OnCompleteDownloadManifest( bool _isSucceeded, AssetBundle _bundle);

public IEnumerator DoDownloadManifest( string _manifestName, OnCompleteDownloadManifest _onComplete)
{
    // Validation
    if( string.IsNullOrEmpty(_manifestName) )
    {
        _onComplete?.Invoke( false, null );
        yield break;
    }
    // uri とありますがurl と認識してもらって差し支えないです
    string uri = $"{AssetManager.I.GetServerUrl()}/{_manifestName}";

    // GetAssetBundle 関数ではcrcやhash128 を指定するものがありますが、
    // 基本的にManifestファイルはキャッシュに保存せずオンメモリで扱うことが多い
    // & サーバー側からもらったバージョン(int やhash値)を見て必要になって
　　　　　　　　//   これを叩いているはずなのであえて指定していません
    #if UNITY2018_OR_NEWER
    // なんでバージョンによってAPI名というかクラス名すら変わるかなぁ...(運用で困る)
    UnityWebRequestAssetBundle webRequest = UnityWebRequestAssetBundle.GetAssetBundle( uri:uri);
    #else
    UnityWebRequest webRequest = UnityWebRequest.GetAssetBundle( uri:uri);
    #endif

    webRequest.timeout = TIME_OUT;
    // DL 開始 & 終了待機
    #if UNITY_2017_3_OR_NEWER
    yield return webRequest.SendWebRequest();
    #else
    yield return webRequest.Send();
    #endif
    

    bool isSucceeded = ( ! webRequest.isHttpError && !webRequest.isNetworkError);
    _onComplete?.Invoke( isSucceeded, DownloadHandlerAssetBundle.GetContent(webRequest) );
}

```

一方AssetBundleに関してはこんな感じ

```csharp:AssetBundleDownloader.cs
public delegate IEnumerator OnCompleteDownloadEachBundle( string _bundleName, Hash128 _bundleHash);
public delegate void OnCompleteDownloadAllBundle( int _isSucceededCount, string[] _failedList); 
public class DownloadRequestParam
{
    public string AssetName;
    public Hash128 AssetHash;
    public OnCompleteDownloadEachBundle OnCompleteDownload;
}

public IEnumerator DoDownloadAssetBundles( DownloadRequestParam[] _reqParams, OnCompleteDownloadAllBundle _onAllComplete)
{
    // DL 対象が無い
    if( _reqParams == null || _reqParams.Length < 1)
    {
        _onAllComplete?.Invoke( 0, new string[]{} );
        yield break;
    }

    // 失敗したリストを用意
    List<string> failedList = new List<string>();

    //各アセットのDownload
    foreach( var p in _reqParams)
    {
        Debug.Assert( p != null, "ReqParam is NULL");
        string uri = $"{AssetManager.I.GetServerUrl()}/{p.AssetName}";
        #if UNITY_2018_OR_NEWER
        using( UnityWebRequest webRequest = UnityWebRequest.Get( uri ) )
        #else
        using( UnityWebRequest webRequest = UnityWebRequest.Get( uri ) )
        #endif
        {
            // 重複ダウンロード禁止
            if( IsAlreadyDownloading(uri))
            {
                continue;
            }
            // Cacheにあるならそっちのロード処理に任せればいい
            if( AssetBundleCacheController.IsCached( p.AssetName, p.AssetHash )
            {
                continue;
            }
            //古いキャッシュがあるとDownload失敗するので削除
            AssetBundleCacheController.ClearTargetOldAssetCache( p.AssetName, p.AssetHash);
            // DL 開始 & 終了待機
            Debug.Log($"[Download] {uri}");
            #if UNITY_2017_3_OR_NEWER
            yield return webRequest.SendWebRequest();
            #else
            yield return webRequest.Send();
            #endif

            bool isSucceeded = ( ! webRequest.isHttpError && !webRequest.isNetworkError);
            if( isSucceeded )
            {
                if( webRequest.downloadHandler.data != null )
                {
                    // Cache に保存
                    bool ret = AssetBundleCacheController.WriteAssetBundle( p.AssetName, p.AssetHash, webRequest.downloadHandler.data );
                    Debug.Log($"[Cache] WriteFile{p.AssetName}:{ret}" );
                }
                yield return p.OnCompleteDownload?.Invoke( p.AssetName, p.AssetHash );
            }
            else    
            {
                failedList.Add( p.AssetName);
            }
        }
    }
    // 全工程の終了を確実にするため1f 待つ
    yield return null;
    // 終了コールバック
    _onAllComplete?.Invoke( _reqParams.Length - failedList.Count, failedList.ToArray() );
}
```

### ローカルキャッシュへSave/Load
Unity に[Cachingクラス](https://docs.unity3d.com/ja/current/ScriptReference/Caching.html) というものがありますが、2015年頃まではUniteのコロプラさんセッションで話題になっていたUnityのCacheシステムは運用系のゲームではとてもじゃないですが使いづらかった状態でした。（数万AssetがあるとisReady がいつまでもReady になってくれなかったり、Cacheの削除は全削除しかできない→全Assetの再ダウンロードを強要してしまうなど）
しかし2017 からは、UnityWebRequestを使うとAssetBundleに関しては自動的にCache 管理をしてくれるという魔法の仕組みがAPIでは提供されていたり、個別削除APIが追加されたりして、[こちら](https://qiita.com/su10/items/98d7a9f89923b09a2d3c)をはじめ、様々なブログ等でUnity2017で「Caching周りが改善された」という記事をよく目にしました。
~~当時は自分も「やったぜ」と思っていましたが、検証を行なった結果、怒りで頭がおかしくなりそうでした。
そうです。**「Cachingシステムがまともに動作していない」** のです。
（ちゃんと個別削除APIとか動いてますよという方は報告いただけると嬉しいです。）~~

~~APIを叩くもののCacheファイルはいつまでたっても削除されず、削除は全削除しか出来ないので１個ファイルに更新があるたびに全AssetBundleの落とし直しを余儀なくされます。~~

~~ということで**Cachingに関しては using System.IO; して普通にファイル操作を行わないとダメ(つまり自前で実装する)**です。~~
#### **2018/09/19追記：**
@k7a さんから情報を頂き再度検証を行いました。
基本的には k7a さんの記事 [[Unity 2018.2] AssetBundleのキャッシュを完全に理解する](https://qiita.com/k7a/items/23d909ffeea3bab7dfcb) で記載の通りでした。（k7aさん、情報提供ありがとうございます）
※検証環境：Unity2018.4.8f1 

自動キャッシュ, 削除に関しては CachedAssetBundle 経由で行うこと（もしくは同等の値を直接設定）で動作の確認が取れました。

ただし、検証の結果**Caching.IsVersionCached に関しては動作に問題がありました**。
いつの間にか[公式リファレンス](https://docs.unity3d.com/2017.4/Documentation/ScriptReference/Caching.IsVersionCached.html) が**obsolete になっていた** 通りで、CachedAssetBundle.name, CachedAssetBundle.hash の組み合わせでも uri, hash128 の組み合わせそれぞれのパターンで応答を見た所、Cacheされているにも関わらずIsVersionCached が常にfalse を返していました。

対象のAssetBundleがCacheされているかどうかは現状知るすべが無いという感じですね。
（何かご存知の方は教えていただけますと助かります。）

#### 削除機能の自前実装

削除機能を実装するならこんな感じになるかと思います。

```csharp:AssetBundleCacheController.cs
/// <summary>
/// 指定したAssetBundleをキャッシュから削除
/// </summary>
/// <param name="_bundleName">AssetBundle名</param>
/// <param name="_hash">Hash 値</param>
/// <returns></returns>
public static bool ClearTargetAssetCache( string _bundleName, Hash128 _hash )
{
    if( !IsCached( _bundleName, _hash))
    {
        Debug.LogWarning($"Not Cached:{_bundleName}(Hash:{_hash.ToString()}" );
        return false;
    }

    string path = CreateAssetPath( _bundleName, _hash );

    File.Delete( path );
    return true;
}

/// <summary>
/// 指定したAssetBundleでバージョンが古いもの(=指定Hash値以外のもの)をキャッシュから削除
/// </summary>
/// <param name="_bundleName">AssetBundle名</param>
/// <returns></returns>
public static bool ClearTargetOldAssetCache( string _bundleName, Hash128 _hash )
{
    string dirName = CreateAssetDirName( _bundleName );
    if( string.IsNullOrEmpty( _bundleName))
    {
        Debug.LogWarning(" Argument Error");
        return false;
    }

    // Dirが存在しないので実質削除完了と等価
    if( !Directory.Exists( dirName))
    {
        return true;
    }

    // 指定Dirにファイルがなければ、実質削除完了と等価
    string[] filePaths = Directory.GetFiles( dirName );
    if( filePaths == null || filePaths.Length < 1)
    {
        return true;
    }
    string path = CreateAssetPath( _bundleName, _hash );
    foreach( string p in filePaths)
    {
      if( p == path)
      {
          continue;
      }
      File.Delete( p );
    }
    return true;
}

/// <summary>
/// Cache Dir 上にファイルが存在するかどうか
/// </summary>
/// <param name="_assetBundleName"></param>
/// <param name="_hash"></param>
/// <returns></returns>
public static bool IsCached( string _assetBundleName, Hash128 _hash )
{
    // 読込先
    string path = CreateAssetPath( _assetBundleName, _hash);
    //Validation
    if( string.IsNullOrEmpty( path) )
    {
        return false;
    }

    return File.Exists( path );
}

```

Saveに関してはFile.WriteAllBytes とかFileStreamを使って書き出せばOK。
LoadはAssetBundleクラスのLoadFromFileAsyncメソッドを使えばOK。

### AssetBundle のLoad/Unload 処理
#### Load

AssetBundle のロード処理は少し厄介です。
というのも**同一のAssetBundleを２回ロードしようとするとエラー**になります。
なのでロードリクエストはしっかり自前で管理しないと簡単にロードリクエストが重複してエラーになって進行不能になったり・・・

よって
- ロード済みのAssetBundleは需要がなくなるまでは保持していた方がいい
- ロードが重複しないようにリクエストをしっかり管理する

ことが大事になってきます。

```csharp:AssetBundleLoadController.cs

private IEnumerator LoadAssetBundleFromCache( string _assetBundleName, Hash128 _hash)
{
    // 既にロード済み
    if( TempLoadedDecryptAssetBundleNameList.Contains( _assetBundleName))
    {
        yield break;
    }
    //対象アセットバンドルが既にロード済み?
    if( AssetManager.I.IsLoadedBundle( _assetBundleName))
    {
        yield break;
    }

    //一時ロードするAssetBundleリストに登録
    RegisterTempAssetBundle( _assetBundleName );

    string path = AssetBundleCacheController.CreateAssetPath(_assetBundleName,_hash);
    Debug.Log("[Cache] Load:{path}");
    AssetBundleCreateRequest req = AssetBundle.LoadFromFileAsync( path );

    yield return req;

    // Load成功していたら登録
    bool isSucceeded = ( req.assetBundle != null );

    // Download 成功
    if( isSucceeded )
    {
        // Download してきたAssetBundle の登録
        AssetManager.I.RegisterAssetData(_assetBundleName, new AssetData( req.assetBundle, _hash ) );    
    }

    // リクエスト終了
    UnregisterTempAssetBundle( _assetBundleName );
}
```

#### Unload
迂闊に破棄をしてしまうと、同一フレームの後続の処理でロードしようとすると死んだりするので、
「**完全に需要がなくなったタイミングで破棄**」するのが退治になります。
なので、自前で**参照カウント**を管理して、参照カウントが0になったタイミングで破棄してあげればOK。

こんな感じでAssetBundleを管理するクラスを作って、管理

```csharp:AssetData.cs

/// <summary>
/// Load済みAssetBundle に関する情報まとめクラス
/// </summary>
public class AssetData : IDisposable
{
    //--------------------------------------------
    // メンバ変数
    //--------------------------------------------
    #region ===== MEMBER_VARIABLES =====

    // AssetBundle名
    string m_bundleName = "";
    public string BundleName{get{return m_bundleName;}}

    // ロード対象のAssetBundle
    private AssetBundle m_targetBundle = null;
    public AssetBundle TargetBundle{get{return m_targetBundle;}}

    // 参照カウンタ
    private int m_refCount = 0;
    public  int RefCount{get{return m_refCount;}}

    // このBundle のhash 値
    private Hash128 m_assetHash;
    public  Hash128 AssetHash{get{return m_assetHash;}}

    #endregion //) ===== MEMBER_VARIABLES =====



    //--------------------------------------------
    // 初期化
    //--------------------------------------------
    #region ===== INIT =====

    public AssetData()
    {
        m_bundleName = "";
        m_refCount = 0;
        m_targetBundle = null;
    }

    public AssetData( AssetBundle _loadedBundle, Hash128 _hash ) : this()
    {
        m_targetBundle = _loadedBundle;
        m_bundleName = _loadedBundle.name;
        m_assetHash = _hash;
    }

    #endregion //) ===== INIT =====

    //--------------------------------------------
    // Dispose
    //--------------------------------------------
    #region ===== DISPOSE =====

    public void Dispose( )
    {
        // 念のためチェック
        if( TargetBundle != null )
        {
            TargetBundle.Unload( false );
        }
    }

    #endregion //) ===== DISPOSE =====


    //--------------------------------------------
    // 参照カウンタ
    //--------------------------------------------
    #region ===== REFERENCE_COUNT =====

    public void SetReference( )
    {
        ++m_refCount;
    }

    public void SetRelease()
    {
        --m_refCount;
        if( RefCount < 1 )
        {
            //Unload
            TargetBundle.Unload( false );
            m_targetBundle = null;
        }
    }

    #endregion //) ===== REFERENCE_COUNT =====


    //--------------------------------------------
    // Asset
    //--------------------------------------------
    #region ===== ASSETS =====

    /// <summary>
    /// このAssetBundle に対象のAssetが存在するか
    /// </summary>
    /// <param name="_assetName"></param>
    /// <returns></returns>
    public bool IsContainsAsset( string _assetName )
    {
        if( TargetBundle == null )
        {
            return false;
        }
        string[] names = TargetBundle.GetAllAssetNames();
        foreach( string name in names )
        {
            if( name == _assetName )
            {
                return true;
            }
        }
        return false;
    }

    #endregion //) ===== ASSETS =====
}

```


### AssetBundle のビルド処理
#### ビルドの前に
Unity謹製ツールの[AssetBundleBrowser](https://github.com/Unity-Technologies/AssetBundles-Browser)の導入を強くオススメします。

リンクのGithubから落としてくるか、UnityPackageManagerからDLで導入出来ます。
ただ、導入するときはEditor以下のみで、それ以外は削除してしまった方がいいです。
特にTest以下のコードは不要なのと` using Boo.Lang.Runtime;` という見たく無いコードが紛れてビルドがこけたりするので要注意です（Unityさん、メンテお願いします）

導入メリットとしては

- 設定しているAssetBundleをGUIでリスト化して見れる
- 各AssetBundleの依存関係や参照しているリソースも見ることが可能
- リソースをD&DでAssetBundleとして設定可能
- Inspector下の小さいところで名前設定をしなくていい
- GUIなのでアーティストさんも操作可能→エンジニアのタスク軽減の効果も期待できる

など色々とメリットがあるので非常にオススメです。


#### ビルドスクリプト
基本的には[BuildPipeline.BuildAssetBundle](https://docs.unity3d.com/ja/current/ScriptReference/BuildPipeline.BuildAssetBundles.html) を叩いてあげれば一応はAssetBundleは焼けます。

また、Manifestをちゃんと読み込めれば「更新のあったアセットのみビルド」が行われるインクリメンタルビルドが行われるので、ビルド時間の短縮も自動的にやってくれます。

ここに関しては割と運用スタイルによって異なってくるので割愛します。

# Apple のDownloadサイズ表示義務規約について
@midnightSuyama さんの[Apple規約解説記事](https://qiita.com/midnightSuyama/items/d1addebbc9a620d28eaa)（いつもお世話になってます）でも紹介されている通り、追加リソースのダウンロード時にダウンロードサイズを明記する必要がでました。

>If your app needs to download additional resources, disclose the size of the download and prompt users before doing so. Existing apps must comply with this guideline in any update submitted after January 1, 2019.

WTF!!
![rage.png](https://qiita-image-store.s3.amazonaws.com/0/65257/677b6e58-a2bd-8551-2bc0-9437b8b8cdbf.png)

既存アプリは対応猶予期間がありますが、新規アプリはマスト対応です。
はい。Unityの既存の機能じゃどうしようもありません。

AssetBundleManifestには「Bundle名」「依存関係」の情報があるので、ここに「サイズ」を追加できると嬉しいですね。

方針としては
- AssetBundleManifest にサイズの項目を追加して読めるようにする
- 独自Manifestを作成して対応する

になります。

## 拡張マニフェスト対応
データを加工したAssetBundleManifestを読むのは技術的に辛かったので、独自マニフェスト実装~~に逃げました~~をすることにしました。

### 独自マニフェスト作成上の注意点

- Hash値(Hash128) はそれぞれのAssetBundleの個別マニフェストにはありますが、本体には記載されないです
- AssetBundleManifestはYAMLで書かれている
- Unity上でのパース速度でいえば JSON > YAML （[検証記事](https://qiita.com/Marimoiro/items/e998584d973baab3aac8))

### ワークフロー

1. AssetBundleビルド時に、Bundle名とHash128の値をまとめたテキストファイルを出力
2. YAMLを解析できる外部ツールで解析
3. 出力先Dirを探索してサイズを取得
4. １. のハッシュ値情報, 2.のYAML情報, 3. のサイズ情報を統合
5. 独自マニフェスト.json を出力
6. 他のAssetBundleと一緒に独自マニフェスト.json をアップ
7. 公式AssetBundleManifestの代わりに独自マニフェストをDLして利用

というワークフローになります。

### YAML の解析
個人的にはShellScriptがいいのですが、たまたま他の人から反発を受けたので他を検討したところ
[こちら](https://maku77.github.io/python/io/yaml.html) を見つけてしまったため、「あ、Pythonならクッソ簡単に行けそう」と判断してPythonで実装していくことにしました。

早速homebrew でpython3, pip, PyYAML を入れて環境を設定します。

#### 解析ツールの実装

正直python を書くのって指で数える程度しか無いですが、こんな感じで生成しました

```python:extManifestGenerator.py
#!/usr/bin/env python
# coding: UTF-8

import yaml
import json
import os
import sys

# コマンドラインから情報を取得
buildTarget=sys.argv[1]
outputFormat=sys.argv[2]
path=sys.argv[3]


# ファイルの読み込み 
with open( path+'/'+buildTarget+'.manifest', 'rt') as fp:
    text = fp.read()

# Parse YAML
data = yaml.safe_load(text)

# Init Output
output = {}
output['ManifestFileVersion']=data['ManifestFileVersion']
output['CRC']=data['CRC']
output['BundleList']=list()

# Hashリストのロード
with open(path+'/'+buildTarget+'Hash.yaml', 'rt') as hashfp:
    hashListText = hashfp.read()

# Parse YAML
hashListYaml = yaml.safe_load(hashListText)

# Manifest Info

# Create Manifest
infos = data['AssetBundleManifest']['AssetBundleInfos']
for k1, v1 in infos.items():
    bundle={}
    for k2, v2 in v1.items():
        # 名前一致で検索
        if k2 == 'Name' :
            filepath = path+'/'+v2
            size = os.path.getsize(filepath)
            # 名前を設定
            bundle[k2]=v2
            # Size設定
            bundle['Size']=size
            # Hash 値を取得
            for k3, v3 in hashListYaml['HashList'].items():
                if v3['Name'] == v2:
                    bundle['Hash']=v3['Hash']

        elif k2 == 'Dependencies':
            bundle['Dependencies']= list()
            for dependItem, dependBundleName in v2.items():
                bundle['Dependencies'].append( dependBundleName )

    # パースしたバンドル情報を追加
    output['BundleList'].append( bundle )

if outputFormat == 'YAML' : 
    with open(path+'/'+buildTarget+'.yaml', "w") as wf:
        wf.write( yaml.dump(output, default_flow_style=False))
elif outputFormat == 'JSON' :
    with open(path+'/'+buildTarget+'.json', "w") as wf:
        wf.write( json.dumps(output, sort_keys=True, indent=2))

```


### Unity側の独自マニフェスト
基本的にはAssetBundleManifestで実装しているAPIを作りつつ、サイズを返すAPIを生やしていきます。
Interface 的には以下のような感じで実装していけば問題ないはずです。

```csharp:IExtManifest.cs

public interface IExtManifest
{
    //--------------------------------------------
    // AssetBundleManifestAPI
    //--------------------------------------------
    #region ===== BUNDLE_MANIFEST_API =====

    /// <summary>
    /// 全バンドルリストを返す
    /// </summary>
    /// <returns></returns>
    public string[] GetAllAssetBundles();

    /// <summary>
    /// Variant 情報ありでバンドルリストを返す
    /// </summary>
    /// <returns></returns>
    public string[] GetAllAssetBundlesWithVariant();

    /// <summary>
    /// 指定バンドルの依存バンドルリストを返す
    /// </summary>
    /// <param name="assetBundleName"></param>
    /// <returns></returns>
    public string[] GetAllDependencies(string _assetBundleName);

    /// <summary>
    /// 指定バンドルと直接依存関係にあるバンドルリストを返す
    /// </summary>
    /// <param name="assetBundleName"></param>
    /// <returns></returns>
    public string[] GetDirectDependencies(string _assetBundleName);

    /// <summary>
    /// 指定バンドルのハッシュ値を返す
    /// </summary>
    /// <param name="assetBundleName"></param>
    /// <returns></returns>
    public Hash128 GetAssetBundleHash128(string _assetBundleName);

    /// <summary>
    /// 指定バンドルのハッシュ値を返す
    /// </summary>
    /// <param name="assetBundleName"></param>
    /// <returns></returns>
    public string GetAssetBundleHash(string _assetBundleName);


    #endregion //) ===== BUNDLE_MANIFEST_API =====

    //--------------------------------------------
    // BundleSizeAPI
    //--------------------------------------------
    #region ===== BUNDLE_SIZE_API =====

    /// <summary>
    /// 指定バンドルのサイズを取得[byte]
    /// </summary>
    /// <param name="_assetBundleName"></param>
    /// <returns></returns>
    public int GetBundleSize( string _assetBundleName);

    /// <summary>
    /// 指定バンドルサイズの合計値を返す
    /// </summary>
    /// <param name="_assetBundleNames"></param>
    /// <returns></returns>
    public int CalcTotalDownloadSize( params string[] _assetBundleNames);

    #endregion //) ===== BUNDLE_SIZE_API =====
}
```


# 実装に当たって

実際に作って遭遇した問題などを書いていきます

## APIの方針
- Facade パターンのススメ
    - Addressable AssetもFacadeパターン   
    - 窓口となるSingletonクラスのAPI以外は外部からは見えないようにする方が使うときに便利
- Download 関数は「個別ダウンロード」と「一括ダウンロード」と２つ用意しておくと良いです
    - 最近のゲームは初回に一括でダウンロードすることが多くオンデマンドでのダウンロードは珍しい
    - 初回DLで満足は厳禁!! iPhone のOSによる自動キャッシュ削除でいつのまにか消えている場合がある
    - Androidは常にユーザーの任意タイミングでキャッシュを消せるのを念頭におく
    - 個人的には初回一括DL＋オンデマンドの併用をオススメ。
    - インゲームへの遷移時間短縮や、マルチプレイ時はマッチング時間短縮のためにも、追加で必要なリソースをオンデマンドでDLできる制約を入れておいた方が無難です。（直前に多量のDLを走らせないためにも）
- SetServerUrl() を用意して、なるべくURLを埋め込まない
    - クライアントコードは基本的に全部解読される前提
    - サーバーリソースを引き抜かれないためにも自己防衛しましょう

## LoadRequest の制御
非同期でロードリクエストを出す際、コールバックが呼ばれる前に呼び出し側がDestroyされるパターンがあり、それでエラーになることが頻発したので、リクエスト発行時にハンドラを返して、呼び出し元がDestroyされるときに、ハンドラを介してコールバックを実行させないように制御しないとダメでした。

## AssetBundle の命名規則
- そもそも仕様で大文字が使えない(英数小文字のみ)
- ディレクトリ名+ファイル名をAssetBundle名にしておくと、名称被りが起きづらいし、用途を管理しやすいです

## 最適化について
- Coroutine よりも使えるならTask, UniRxやAsync/Await などをうまく使った方がいいです
- Native でやれば絶対早くなります。
    - 出来るのは大人数の開発者がいる or 超スーパーエンジニアがいる場合のみ
    - Native のコードは公開されないので個人開発や中小企業は素直にUnityAPIにあやかるべき

## 暗号化について
- [ここ](https://qiita.com/snaka/items/a4c16719888bbd23067f) のまとめがすごくよくまとまっています(すごくおすすめです)
- AssetBundle自体をAESを利用して暗号化をする場合、一旦暗号化AssetBundleをメモリに全て載せないとダメなので、Chunkロードが使えないデメリットがある
- パフォーマンスをとるか、暗号化をとるかは要検討案件です

# まとめ
Unity2018.2 〜が使えるなら頑張ってAdressable Assetsを使って見た方が今後のためにも良いと思います。
出来ることなら、もう2度と作りたくないですね（白目


# 参考

https://creator.game.cyberagent.co.jp/?p=4791

https://qiita.com/neon-izm/items/b105130fac060ec40ad4

https://qiita.com/fukaken5050/items/e69572f963c26015a716

https://qiita.com/su10/items/98d7a9f89923b09a2d3c

https://qiita.com/Marimoiro/items/e998584d973baab3aac8

https://qiita.com/snaka/items/a4c16719888bbd23067f

https://qiita.com/k7a/items/23d909ffeea3bab7dfcb