UnityのDLC(==AssetBudle)のダウンロード方法一案

いくつかやり方があるとは思いますが一つ以下の方法を考えました。

#図
まず初回の場合
![dlc_download1.png](https://qiita-image-store.s3.amazonaws.com/0/17192/a52a26c4-0509-113c-4429-98ae7595b415.png)

更新があった場合
![dlc_donwload2.png](https://qiita-image-store.s3.amazonaws.com/0/17192/d6930e49-d022-fbba-41e6-1fdeb1478114.png)

更新がない場合
![dlc_download3.png](https://qiita-image-store.s3.amazonaws.com/0/17192/cb33644b-c0fa-23bc-976e-35322cd0586a.png)

#クライアント
以下、ソースコード。ちょっと長いです。
「using LitJson」はLitJson
「using Csv」はLumenWorks.Framework.IO.CSV.CachedCsvReader
他にUniWebのアセットを使っているのでPro版じゃないと多分動きません。

```csharp:DlcCheckDownLoad.cs
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.IO;

using LitJson;

//DLCのダウンロードが必要かサーバに確認するクラス
//必要があればダウンロードする
public class DlcCheckDownLoad : MonoBehaviour {
	//ダウンロードが必要なものList
	private List<DlcCsvRow> needDownLoadList = new List<DlcCsvRow>();
	private int needDownLoadListCount;

	void Start () {
		string url = "/dlc/check"; //DLCのダウンロードが必要か確認するURL
		string postJsonStr = "{}";

		StartCoroutine (
			HttpAPIs.Post(url, postJsonStr, new HttpAPIs.HttpResponseHandler(DlcCheckResponseCallBack))
		);
	}

	private void DlcCheckResponseCallBack (BaseResponse baseResponse) {
		//クライアントが送信したDLCファイルのハッシュ値が
		//サーバで持っているDLCファイルのハッシュ値と違うため
		//サーバがクライアントにDLCダウンロードを指示
		if (baseResponse.gameCode == BaseResponse.requestDlcGameCode) {
			DownLoad dl = gameObject.AddComponent<DownLoad> ();
			dl.downLoadCompleteByteHandler = DownLoadDlcFileCallBack;
			dl.url = "/static/DLC.csv"; //サーバ上のDLCファイルの場所
			dl.isAssetBundle = false;
		}
	}

	//DLCファイルをダウンロード成功したあと呼ばれる関数
	private void DownLoadDlcFileCallBack (byte[] bytes) {
		Debug.Log ("DLCファイルに差分があるのでダウロードしました");

		//クライアントが持っている古いDLCファイルをHashTableに変換する
		//初回アクセスで所持していない場合は空のHashTable
		DlcCsv oldDlc = new DlcCsv();
		Hashtable oldHb = oldDlc.GetHashTableFromCsv (Application.dataPath + "/DLC.csv");

		//サーバから落とした最新のDLCファイルを一時的に保存
		File.WriteAllBytes(Application.dataPath + "/TempDLC.csv" , bytes);
		//新たにダウンロードした新しいDLCファイルをHashTabelに変換する
		DlcCsv newDlc = new DlcCsv();
		Hashtable newHb = newDlc.GetHashTableFromCsv (Application.dataPath + "/TempDLC.csv");

		//新たにダウンロードしたDLCファイルを一行ずつループ
		foreach (DictionaryEntry kv in newHb) {
			DlcCsvRow newRow = (DlcCsvRow)kv.Value;
			DlcCsvRow oldRow = (DlcCsvRow)oldHb[kv.Key];

			//新規にダウンロードするもの又は、既にダウンロードしているもので更新された場合
			if (null == oldRow || oldRow.version < newRow.version) {
				needDownLoadList.Add(newRow);
			}
		}

		needDownLoadListCount = needDownLoadList.Count;
		Debug.Log ("AssetBundleのダウンロード状況: " + needDownLoadListCount.ToString() + "/" + needDownLoadList.Count.ToString());
		for(int i=0; i<=needDownLoadList.Count-1; i++) {
			DlcCsvRow dlcRow = needDownLoadList[i];
			DownLoad dl = gameObject.AddComponent<DownLoad>();
			dl.downLoadCompleteAssetBundleHandler = DownLoadAssetBundleCallBack;
			//サーバ上のAssetBundleのパス
			dl.url = "/static/assetBundle/" + dlcRow.name + ".assetbundle";
			dl.isAssetBundle = true;
			dl.version = dlcRow.version;
		}
	}

	//AssetBundleのダウンロード後が成功したとき呼ばれる関数
	private void DownLoadAssetBundleCallBack (AssetBundle assetBundle) {
		needDownLoadListCount -= 1;

		Debug.Log ("AssetBundleのダウンロード状況: " + needDownLoadListCount.ToString() + "/" + needDownLoadList.Count.ToString());
        
		if (needDownLoadListCount <= 0) {
			//クライアントが持っているDLCファイルを最新にする
			using (FileStream fs = new FileStream(Application.dataPath + "/TempDLC.csv", FileMode.Open, FileAccess.Read)) {
				byte[] bs = new byte[fs.Length];
				fs.Read(bs, 0, bs.Length);
				File.WriteAllBytes(Application.dataPath + "/DLC.csv" , bs);
            }

			Debug.Log("アセットバンドルのダウンロードが全て完了しました");
        }
	}
	
}
```

```csharp:HttpAPIs.cs
using System.Collections;
using UnityEngine;
using LitJson;

//レスポンスを受け取るクラス
public class BaseResponse {
	//サーバとクライアントで通信の共通の認識コードを定義
	public const int requestDlcGameCode = 1; // DLCのダウンロード必要

	public int gameCode;
}

//HTTP通信を行うクラス
public class HttpAPIs {
	//通信が成功したときCallされる関数
	public delegate void HttpResponseHandler (BaseResponse baseResponse); 

	public static IEnumerator Post (string url, string postJsonStr, HttpResponseHandler httpResponseHandler) {
		url = Constants.SERVER_DOMAIN + url;

		HTTP.Request r = new HTTP.Request ("POST", url);
		
		// Headerを作成
		r.headers.Add ("Content-Type", "application/json; charset=UTF-8");
		//サーバでHeaderのあるクライアントのDLCファイルのハッシュ値を受信しチェックする
		r.headers.Add("DLC_HASH", Common.GetHashCode(Application.dataPath + "/DLC.csv"));
		
		// タイムアウト秒数を設定
		r.timeout = 3;
		
		yield return r.Send();

		// なにかエラー発生
		if (r.exception != null) {
			Debug.Log ("post request error: " + r.exception.ToString ());

			// タイムアウト発生
			if (r.exception is System.TimeoutException) {
				Debug.Log ("Request timed out.");
				// それ以外のエラー
			} else {
				Debug.Log ("Exception occured in request.");
			}
		} else if (r.response.status != 200) { 
			Debug.Log ("post request code:" + r.response.status);
		// 成功
		} else {
			BaseResponse baseResponse = JsonMapper.ToObject<BaseResponse> (r.response.Text);
			httpResponseHandler (baseResponse);

			Debug.Log ("WWW Success. " + url);
		}
		
	}
}
```

```csharp:Common.cs
using System;
using System.IO;
using UnityEngine;

//共通処理クラス
public class Common {
    
    //与えられたファイルのハッシュ値を取得
	//ファイルが存在しないは空ハッシュ値を戻す
	public static string GetHashCode (string path) {
		string hash_code = "";

		if (File.Exists (path)) {
			using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read)) {
				byte[] bytes = System.Security.Cryptography.SHA1.Create().ComputeHash(fs);
				hash_code = BitConverter.ToString(bytes).Replace("-", "").ToLower();
            }
		}

		Debug.Log ("GetHashCode: " + hash_code);
		
		return hash_code;	
	}
	
}
```

```csharp:DownLoad.cs
using UnityEngine;
using System;
using System.Collections;

/* 使用方法
 * 1, AddComponentする
 * DownLoad dl = gameObject.AddComponent<DownLoad> ();
 * 
 * 2, ダウンロードが成功したとき呼ばれる関数をいれる
 * AssetBundleなどはdownLoadCompleteAssetBundleHandlerを使う
 * dl.downLoadCompleteByteHandler = <ダウンロードが成功したとき呼ばれる関数>
 * 
 * 3, ダウンロードするファイルなどのサーバROOTからのURLをいれる
 * dl.url = "/<ダウンロードするファイルなど>";
 * 
 * 4, AssetBundleなどでキャッシュを使うか
 * dl.isAssetBundle = <true or false>
 * 
 * 5, AssetBundleなどの場合はそのバージョン番号
 * dl.version = <バージョン番号>
 * 
 * urlは必須
 * 
 * Ex)
 * DownLoad dl = gameObject.AddComponent<DownLoad> ();
 * dl.downLoadCompleteByteHandler = DownLoadDlcFileCallBack;
 * dl.url = "/DLC.csv";
 * dl.isAssetBundle = false;
 * 
*/

//ファイル1つをダウンロードするためクラス
public class DownLoad : MonoBehaviour {
	//ダウンロードするファイルなどのサーバROOTからのURL
    private string _url;
	public string url {
		get {return _url;}
		set {_url = Constants.SERVER_DOMAIN + value;}
	}

	//ダウンロードが成功したときCallされる関数
	public delegate void DownLoadCompleteByteHandler (byte[] bytes);
	public DownLoadCompleteByteHandler downLoadCompleteByteHandler;
	public delegate void DownLoadCompleteAssetBundleHandler (AssetBundle assetBundle);
	public DownLoadCompleteAssetBundleHandler downLoadCompleteAssetBundleHandler;

	//AssetBundleなどでキャッシュを使うか
	public bool isAssetBundle;
	//AssetBundleなどの場合はそのバージョン番号
	public int version;
	
	void Start () {
		StartCoroutine (DoDownLoad());
	}
		
	private IEnumerator DoDownLoad () {
		if (isAssetBundle) {
			using (WWW www = WWW.LoadFromCacheOrDownload (url, version)) {
				while (!www.isDone) {
					//<総ダウンロードbyte> = (<この一ファイルの総ダウンロードbyte> * (int)(www.progress * 100.0f)) / 100;
					yield return null;
				}

				yield return www;

				if (www.error == null) {
					if (downLoadCompleteAssetBundleHandler != null)
						downLoadCompleteAssetBundleHandler (www.assetBundle);

					www.assetBundle.Unload(true);

					Debug.Log("DownLoad Success. " + url);
				} else {
					Debug.LogError("DownLoad Failure. " + url + "\n" + www.error);
                }
			}
		} else {
			using (WWW www = new WWW(url)) {
				yield return www;
                
				if (www.error == null) {
                    if (downLoadCompleteByteHandler != null)
						downLoadCompleteByteHandler (www.bytes);

					Debug.Log("DownLoad Success. " + url);
				} else {
					Debug.LogError("DownLoad Failure. " + url + "\n" + www.error);
                }
			}
		}

		Destroy(this);
	}

}
```

```csharp:BaseCsv.cs
using System.IO;
using System.Collections;

using Csv;

//Csvのマスターデータを管理する親クラス
abstract public class BaseCsv {

	//CsvファイルをHashTableに格納
	//ない場合は空のHashTableを戻す
	public Hashtable GetHashTableFromCsv (string path) {
		Hashtable hb = new Hashtable();
		
		if (File.Exists (path)) {
			StreamReader reader = new StreamReader(path);
			string strCsv = reader.ReadToEnd();
			reader.Close();
			reader.Dispose();
			
			CachedCsvReader csvReader = new CachedCsvReader (new StringReader (strCsv), true);

			hb = GetHashTableFromCsvReader(csvReader);

			csvReader.Dispose ();
		}
		
		return hb;
	}

	//各CSVファイルを各々のインスタンスを生成し、それをHashTabelに格納する関数
	abstract protected Hashtable GetHashTableFromCsvReader (CachedCsvReader csvReader);
}
```

```csharp:DlcCsv.cs
using UnityEngine;
using System.Collections;

using Csv;

//DLCファイルのレコード
public class DlcCsvRow {
	public string name;
	public int version;
}

//DLCファイル
public class DlcCsv: BaseCsv {
	
	protected override Hashtable GetHashTableFromCsvReader (CachedCsvReader csvReader) {
		Hashtable hb = new Hashtable();

		while (csvReader.ReadNextRecord()){
			DlcCsvRow o = new DlcCsvRow();

			o.name = csvReader[0];
			o.version = int.Parse(csvReader[1]);

			hb.Add(o.name, o);
		}

		return hb;
	}
	
}
```

```csharp:Constants.cs
public class Constants {
	public const string SERVER_DOMAIN = "http://localhost:8000";
}
```

#クライアントフロー
おおまかに処理の流れをいうと
DlcCheckDownLoad.csのStart
↓
HttpAPIs.csのPost
サーバとPost通信を行う
レスポンスがくるのを待つ
↓
DlcCheckDownLoad.csのDlcCheckResponseCallBack
ここでDLCの更新が必要かクライアントがわかる
それで必要だったらダウンロードを行う
↓
DownLoad.csのDoDownLoad
最新のDLCファイルをダウンロード
↓
DlcCheckDownLoad.csのDownLoadDlcFileCallBack
更新があるAssetBundleを判定
↓
DownLoad.csのDoDownLoad
更新があるAssetBundleをダウンロード
↓
DlcCheckDownLoad.csのDownLoadAssetBundleCallBack
異常なく全部ダウンロードが終わったら、クライアントとサーバのDLCファイルの同期

#サーバ
Webサーバをローカルに立てて検証しました。
クライアントからHeaderで送られてくるハッシュ値をみて
{"gameCode": 1}もしくは{}などjson形式で戻すようにしてみてください。
受付つけるURLは"http://localhost:8000/dlc/check"
あと、DLC.csvやAssetBundleも配置しておいてください。