ADX2 for UnityのAtom Browserを拡張する

[2021/03/01更新]
ADX2 LEのアップデートに伴いAtom Windowの解説をAtom Browserに移行

[2020/06/28更新]
インポートパスの変更について、CreateGameObjectの修正方法を追記

# Atom Browserの機能追加

サウンドミドルウェアADX2のUnityプラグインには、「CRI Atom Browser」というエディタ拡張が同梱されています。
ADX2のツールから出力したパックデータをUnityプロジェクトに読み込み、内容を確認したり、サウンドを鳴らすためのゲームオブジェクトを配置できる機能です。


（以前のバージョンでは「Atom Window」という名前で、UIも少し違っていました）
![AtomWindow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19010/c36282b0-f127-3efd-c490-3a1603cda0dc.png)

ADX2は、サウンドの鳴り方を同梱ツールである「Cri Atom Craft」で行い、Unity側ではゲーム実行中の再生リクエストやパラメータ渡しを作っていくワークフローを提供します。
そのためUnity Editor上でADX2のデータそのものを加工する機能はありません。Atom Browserの主な機能は「データのインポート」と「データ内容の確認」です。

本投稿では、それらの機能を拡張する方法を紹介します。
今回は「キューのカテゴリと再生上限数（キューリミット）の追加表示」「ADX2データのインポート先を指定可能にする」の２つを紹介します。拡張後のCRI Atom Browserはこんな感じになります。

![newatomwindow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19010/b1d629db-acda-f7ef-9548-bd4f04193355.png)

### アップデート時の注意
Atom Browserのスクリプトをいじると、当然正規プラグインとの互換性は壊れます。
SDKバージョンアップデートの際にはご注意ください。

### 無償版「ADX2 LE」と製品版「ADX2」の違い
~~本投稿はADX2 LEをベースとした拡張方法を紹介していますが、製品版ADX2でも同様の拡張が可能です。製品版ADX2ではAtom Windowの代わりに「Atom Browser」というエディタ拡張が同梱されています。各項目で製品版ADX2の場合についても補足します。~~
アップデートにより差異が無くなりました。

## データのインポート先パスを指定可能にする
Atom BrowserにはAtom Craftがビルド・出力したADX2のデータをUnityのプロジェクト内にコピーする機能があります。しかし、コピー先は指定することができず、すべて「Streaming Assets」直下に配置されます。

AssetBundle対応などでStreaming Assetsに様々なファイルを使用しており、ADX2データを特定のフォルダ内に配置している場合や、ビルド時にアプリに含むADX2データを選択したい場合などは、毎回手動で移動するのは面倒です。エディタ拡張に手を入れて、コピー先パスを指定可能にしましょう。

### Atom Browserの設定ファイルに「インポート先フォルダ」のフィールドを足す
Atom Browserは設定ファイルを「CriAtomWindowPrefs」ScriptableObjcetとして保存しています。
コピー元のパスを保持しているフィールドがありますので、その下に「importFolderPath」としてstringのフィールドを追加しましょう。

```CriAtomWindowPrefs.cs

public class CriAtomWindowPrefs : ScriptableObject
{
	public string outputAssetsRoot = String.Empty;
	public string importFolderPath = String.Empty;//このフィールドを追加

	//（略）
}

```

ここに保存先のパスを記録します。

### Atom Browserに保存先パスを指定するInputFieldを足す

次に、Atom Browser上で保存先パスの指定を行えるようにInputFieldを足します。
CriAtomWindow.csのGUIImportAssetsFromAtomCraftメソッド内、outputAssetsRootにデータを渡す処理の下に次のスクリプトを足してください。

```CriAtomWindow.cs

	private void GUIImportAssetsFromAtomCraft()
	{

		//（略）
			GUILayout.EndHorizontal();

			if (criAtomWindowPrefs != null) {
				criAtomWindowPrefs.outputAssetsRoot = GUILayout.TextArea(criAtomWindowPrefs.outputAssetsRoot);
			}
			//GUILayout.Label(Application.dataPath);

		//（略）
		//以降を新規追加
			GUILayout.BeginHorizontal();
			GUILayout.Label("Import Folder Path:");

			if (GUILayout.Button("Select Folder Path")) {
				string tmpStr = String.Empty;
				tmpStr = EditorUtility.OpenFolderPanel("Select import folder", tmpStr, criAtomWindowPrefs.outputAssetsRoot);
				if (tmpStr != String.Empty) 
				{
					criAtomWindowPrefs.importFolderPath = tmpStr;
					criAtomWindowPrefs.Save();
				}
			}
			GUILayout.EndHorizontal();
			
			if (criAtomWindowPrefs != null) {
				criAtomWindowPrefs.importFolderPath = GUILayout.TextArea(criAtomWindowPrefs.importFolderPath);
			}

		//（略）
		//以上を新規追加

	}

```

旧Atom Windowの場合は次の通りです。

```CriAtomWindow.cs
	private void GUIImportAssetsFromAtomCraft()
	{
		//（略）
			GUILayout.BeginHorizontal();
			{
				if (criAtomWindowPrefs != null) {
					criAtomWindowPrefs.importFolderPath = EditorGUILayout.TextField("Import To:", criAtomWindowPrefs.importFolderPath);
				}

#if !OPENFOLDERPANEL_IS_BROKEN
				if (GUILayout.Button("...", EditorStyles.miniButton, GUILayout.Width(50))) {
					string tmpPath = "";
					string errorMsg;
					tmpPath = EditorUtility.OpenFolderPanel("Select Import Folder", criAtomWindowPrefs.importFolderPath, "");
					criAtomWindowPrefs.importFolderPath = tmpPath;
					criAtomWindowPrefs.Save();
				}
#endif
			}
			GUILayout.EndHorizontal();

		//（略）
	}

```


これで保存先のパスを指定できるようになりました。例ではStreamingAssetsの下に「adx2data」フォルダを用意して、そこを指定しています。

![AtomWindow2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19010/c73114c1-57e0-8804-18d1-b6579b44d5b2.png)

なお、この２行を足したことによってAtom Browserの縦の長さが変わってしまい、スクロールバーが出てしまいます。
次の項目を更新して、スクロールバーをなくします。

```CriAtomWindow.cs
	private void GUICueList()
	{
	//（略）
		var acbInfoList = acfInfoData.GetAcbInfoList(false, searchPath);
		if (acbInfoList.Length > this.selectedCueSheetId) {
			var acbInfo = acbInfoList[this.selectedCueSheetId];

			if (acbInfo.cueInfoList.Count > 0) {

				//ウィンドウの高さ設定を変更（元の数値：- 354.0f）
				float height = this.position.height - 390.0f;

	//（略）
```

### データコピー処理に指定したフォルダパスを使用する

最後にcriAtomWindowPrefs.importFolderPathに保存したパスへデータコピーする処理を追加します。
GUIImportAssetsFromAtomCraftメソッドの最後の方、CopyDirectoryメソッドの引数を差し替えます。

```CriAtomWindow.cs
	private void GUIImportAssetsFromAtomCraft()
	{
		//（略）
		//CopyDirectory(criAtomWindowPrefs.outputAssetsRoot, Application.dataPath);//これを消して
		CopyDirectory(criAtomWindowPrefs.outputAssetsRoot + "/StreamingAssets/", criAtomWindowPrefs.importFolderPath); //こうする
		//（略）
	}

```
outputAssetsRootで指定されるパスはデータ直上ではなく、ディレクトリStreamingAssetsが挟まりますので、それを勘案しています。
「Update Asset of "CRI Atom Craft"」をクリックすれば、指定フォルダにファイルがコピーされます。
また、コピー先のパスはScriptableObjectとして保存されるのでデータを落としても大丈夫です。

### CreateGameObjectの修正

Atom Browserには、キューシートやキュー情報が入ったAtom Sourceを生成できる「CreateGameObject」ボタンがあります。上記のインポート先パスの修正をこのボタンの挙動にも加えます

```CriAtomWindow.cs

	private void CreateAtomSourceGameObject(bool createGameObjectFlag)
	{
		//ここから追記
		if (criAtomWindowPrefs == null) {
			criAtomWindowPrefs = CriAtomWindowPrefs.Load();
			return;
		}

		var cueSheetsFolderName = criAtomWindowPrefs.importFolderPath.Replace(Application.dataPath+"/StreamingAssets/", "");
		//ここまで追記

		//中略
		//CriAtomインスタンスに設定するacfのパスに取得したフォルダ名を加える//
			CriAtom atom = GameObject.FindObjectOfType(typeof(CriAtom)) as CriAtom;
			if (atom == null) {
				atom = CriWare.managerObject.AddComponent<CriAtom>();
				atom.acfFile = cueSheetsFolderName+"/"+ acfInfoData.acfPath;
			}

		//中略

		//AddCueSheetInternalに渡すacb, awbのパスに取得したフォルダ名を加える//
			CriAtomCueSheet cueSheet = atom.GetCueSheetInternal(acbInfo.name);
			if (cueSheet == null)
			{
				var awbPath = string.IsNullOrEmpty(acbInfo.awbPath) ? String.Empty : cueSheetsFolderName + "/" + acbInfo.awbPath;
				cueSheet = atom.AddCueSheetInternal(null, cueSheetsFolderName+"/"+acbInfo.acbPath, awbPath, null);
			}

```

## Atom Browserのキューリストにカテゴリや再生数上限などを表示

デフォルトのAtom Browserでは、キューの情報として「キュー名」「キューID」「User Data」が表示されます。プロジェクトや制作体制によっては、もう少しキューの情報を確認したい場合があります。

そこで、ほかのパラメータもUnity Editor上で確認できるようにAtom Browserを拡張します。
この例では、「カテゴリ」と「キューリミット」の情報を取得・表示します。

処理の流れは、インポート時にデータ取り出し→CriAtomWindowPrefsでパースしてSerialisedフィールドに保存→Atom Browerで表示処理となっています。

#### キュー情報を保存するクラスにフィールドを追加する

まずはキューのインポート時に必要なデータを取り出す処理を書きます。
CriAtomProjinfo.csのパーシャルクラスCriAtomAcfInfo内でCueInfo保存のSerializableクラスが定義されています。これに表示したいキューの情報用フィールドを足します。

```CriAtomWindowPrefs.cs
	public class CueInfo : InfoBase {
		public bool isPublic;
		public short numLimits; //追加
		public List<string> categoryNames; //追加
		public CueInfo(string name, int id, string comment, bool isPublic, short numLimits, List<string> categoryNames) {
			this.name = name;
			this.id = id;
			this.comment = comment;
			this.isPublic = isPublic;
			this.numLimits = numLimits; //追加
			this.categoryNames = categoryNames; //追加
		}
	} /* end of class */
```

#### キュー情報の読み込み

キューが保持するデータは「カテゴリ名」ではなく「カテゴリのインデックス」になります。カテゴリ名を表示するには、CriAtomExAcfDebug.GetCategoryInfoByIndexメソッドからカテゴリ名をインデックスから取得してリストアップします。

```CriAtomWindowPrefs.cs
		List<string> categoryNames = new List<string>();
						
		foreach (var category in cueInfo.categories)
		{
			CriAtomExAcf.CategoryInfo categoryInfo = new CriAtomExAcf.CategoryInfo();
			CriAtomExAcf.GetCategoryInfoByIndex(category, out categoryInfo);
			categoryNames.Add(categoryInfo.name);
		}

		acbInfo.cueInfoList.Add(new CueInfo(cueInfo.name, cueInfo.id, cueInfo.userData, Convert.ToBoolean(cueInfo.headerVisibility),cueInfo.numLimits, categoryNames));

```

#### キュー情報の表示

キューが持つ情報を一覧で表示します。
GUICueListメソッド内の以下のBeginHorizontalエリアではCue NameやCue IDのラベル部分を表示しています。ここに新しく表示する情報を足します。

```CriAtomWindow.cs
	private void GUICueList()
	{
			//（略）
			using (var cueListTitleScope = new EditorGUILayout.HorizontalScope()) {
				if (GUILayout.Button("Cue Name", toolBarButtonStyle)) {
					if (isCueSheetAvailable) {
						acbInfoList[selectedCueSheetId].SortCueInfo(CriAtomWindowInfo.CueSortType.Name);
						this.selectedCueInfoIndex = 0;
					}
				}

			//以下を追加
				if (GUILayout.Button("Category", toolBarButtonStyle, GUILayout.Width(200))) {
					if (isCueSheetAvailable) {
						acbInfoList[selectedCueSheetId].SortCueInfo(CriAtomWindowInfo.CueSortType.Id);
						this.selectedCueInfoIndex = 0;
					}
				}
				if (GUILayout.Button("Cue Limit", toolBarButtonStyle, GUILayout.Width(70))) {
					if (isCueSheetAvailable) {
						acbInfoList[selectedCueSheetId].SortCueInfo(CriAtomWindowInfo.CueSortType.Id);
						this.selectedCueInfoIndex = 0;
					}
				}
			//以上を追加


				if (GUILayout.Button("Cue ID", toolBarButtonStyle, GUILayout.Width(70))) {
					if (isCueSheetAvailable) {
						acbInfoList[selectedCueSheetId].SortCueInfo(CriAtomWindowInfo.CueSortType.Id);
						this.selectedCueInfoIndex = 0;
					}
				}
			}
			//（略）
				if (GUILayout.Button(acbInfo.cueInfoList[i].name, EditorStyles.label)) {
					if (selectedCueInfoIndex != i) {
						StopPreview();
					}
					this.selectedCueInfoIndex = i;
				}

		//以下を追加	
				GUILayout.Label(string.Join(", " , acbInfo.cueInfoList[i].categoryNames), GUILayout.Width(200));
				GUILayout.Label(cueLimtStr, GUILayout.Width(70));
		//以上を追加	

				GUILayout.Label(acbInfo.cueInfoList[i].id.ToString(), GUILayout.Width(40));
		//（略）

```
その他のキュー情報も表示可能です。プロジェクトに応じて拡張しましょう。
表示が長くなる場合は、下部のSelectedCueエリアに表示させる方法もありです。

## 旧Atom Windowでの方法

### キュー情報を保存するクラスにフィールドを追加する(旧Atom Window)

```CriAtomProjinfo.cs

	#region CueInfo
	[Serializable]
	public class CueInfo : InfoBase
	{
		public short numLimits; //追加
		public List<string> categoryNames; //追加
		public CueInfo(string n, int inId, string com, short numLimits, List<string> categoryNames)
		{
			this.name = n;
			this.id = inId;
			this.comment = com;
			this.numLimits = numLimits; //追加
			this.categoryNames = categoryNames; //追加
		}
	} /* end of class */
	#endregion
```

### キュー情報の読み込み(旧Atom Window)

```CriAtomProjinfo.cs
private void GetAcbInfoListCore(string searchPath, ref int acbIndex)
{

		/* キュー名リストの作成 */
		CriAtomEx.CueInfo[] cueInfoList = acb.GetCueInfoList();
		foreach(CriAtomEx.CueInfo cueInfo in cueInfoList){
		    //CueInfo tmpCueInfo = new CueInfo(cueInfo.name, cueInfo.id, cueInfo.userData); //これを削除

		    //以降を追加
		    List<string> categoryNames = new List<string>();
		    CriAtomExAcfDebug.CategoryInfo categoryInfo = new CriAtomExAcfDebug.CategoryInfo();

		    foreach (var category in cueInfo.categories)
		    {

				CriAtomExAcfDebug.GetCategoryInfoByIndex(category, out categoryInfo);
				categoryNames.Add(categoryInfo.name);
		    }
							
		    CueInfo tmpCueInfo = new CueInfo(cueInfo.name, cueInfo.id, cueInfo.userData, cueInfo.numLimits, categoryNames);

		//（略）
```

### キュー情報の表示(旧Atom Window)

```CriAtomWindow.cs

	//（略）
	private void GUICueList()
	{
		GUILayout.BeginHorizontal();
		{
			GUIStyle style = new GUIStyle(EditorStyles.miniButtonMid);
			style.alignment = TextAnchor.LowerLeft;
			if (GUILayout.Button("Cue Name", style)) {
				this.SortCueList(1);
			}
			//以下を追加
			if (GUILayout.Button("Category", style, GUILayout.Width(190))) {
				this.SortCueList(0);
			}
			if (GUILayout.Button("Cue Limit", style, GUILayout.Width(70))) {
				this.SortCueList(0);
			}
			//以上を追加
			if (GUILayout.Button("Cue ID", style, GUILayout.Width(70))) {
				this.SortCueList(0);
			}
		//（略）
```

```CriAtomWindow.cs

	//（略）
	private void GUICueList()
	{
	//（略）
		GUILayout.Label(string.Join(", " , acbInfo.cueInfoList[i].categoryNames), GUILayout.Width(220));
		//以下を追加				
		GUILayout.Label(acbInfo.cueInfoList[i].numLimits.ToString(), GUILayout.Width(60));
		GUILayout.Label(acbInfo.cueInfoList[i].id.ToString(), GUILayout.Width(40));
		//以上を追加
		EditorGUILayout.EndHorizontal();
	//（略）
```

これでAtom Windowにカテゴリとキューリミットが表示できるようになりました。

![Atom3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19010/3aedd85a-6f42-f373-4a65-309443d41305.png)


## さらに拡張できること
今回は２カ所だけ拡張しましたが、Unity Editor上で確認できたら便利なことは他にもありそうです。たとえば、AISACやセレクタ等、スクリプトから設定を与える音についてAtom Browserで変更できるようになると良さそうです。