ADX2ロボット機能でScriptableObjectを作ってみる

# はじめに
ADX2の生成したデータの内容を取得するのに、実行時には関数があるのですが、
エディター時に読み取れるといろいろ便利になるということで、UnityのScriptableObjectの形で取れるものを作ってみました。

あと、ADX2の生成したACBデータでは、AtomCraftで設定した情報（コメント情報など）が無くなってしまうので、
それらも情報としてとれるようにしています。

これ
1年ほど前に紹介した
[ADX2ビルド時にPythonでScriptableObjectを作る](https://qiita.com/tatmos/items/33543bddede4fee80c6b) で紹介したものと中身はほぼ同じです。（ちょっとだけ進化しています）

これの[ADX2ロボット](https://game.criware.jp/manual/adx2_tool/latest/criatom_tools_atomcraft_api.html)対応版の紹介になります。

# スクリプト
ちょっと大きいですが、

1.キューシートを指定し、
2.キューシートを解析し一時データに記録
3.一時データから
4.ScriptableObjectを生成するC#コードを生成し、出力先へ保存する。

キューシートの指定は、毎度選択すると間違えそうなのと、選択順も影響しそうなので、初回以降はコード側で指定するような形にしています。

``` MakeAcbDataCreator.py
# --Description:[tatmos]キューシートごとに、キューシート情報（キュー情報など）BGM.assetsなどを作ります。
import sys
import cri.atomcraft.project
import cri.atomcraft.project as acproject
import cri.atomcraft.debug as acdebug

#固定または選択したキューシート
UseFixCueSheet = True

if not UseFixCueSheet:
    # 選択しているキューシートを得る
    selected_CueSheets = acproject.get_selected_objects("CueSheet")["data"]
    if not selected_CueSheets :
        acdebug.warning("生成するキューシートを選択してください.")
        sys.exit()
else:
    # パスからキューシートを得る（一度上のフラグを変更してログからコピーする）
    selected_CueSheets = list()
    selected_CueSheets.append(
cri.atomcraft.project.get_object_from_path("/WorkUnits/BGM/CueSheetFolder/BGM/BGM")["data"])
    selected_CueSheets.append(
cri.atomcraft.project.get_object_from_path("/WorkUnits/Megalith/CueSheetFolder/ALTDEUS/SE")["data"])
    selected_CueSheets.append(
cri.atomcraft.project.get_object_from_path("/WorkUnits/VOICE/CueSheetFolder/VOICE_/VOICE")["data"])

acdebug.log("生成するキューシート")

# オブジェクトパス表示
for cusheet in selected_CueSheets:
    acdebug.log("cusheet Path:\"{0}\"".format(acproject.get_object_path(cusheet)["data"] ))

#-----------

g_currentCueName = ""  #キュー名
g_currentCueSheetName = "" #キューシート名
g_currentCueFolderName = "" #キューフォルダー名
g_seqcallbackList = list() #見つけたコールバック名リスト


class CueData:
    def __init__(self):
        self.name = ""
        self.blockNames = list()
        self.blockEndPoses = list()
        self.aisacNames = list()
        self.comment = ""
        self.userData = ""
        self.omit = False	#オミットされたキューかどうか

class CueCategory:
    def __init__(self):
        self.name = ""
        self.cues = list()
        self.private = False        #プライベートキューフォルダーかどうか

class AdxAcbData:
    def __init__(self):
        self.name = ""
        self.cueCategories = dict()
#-----------

def writeHeader(outstr):
    outstr += "// このファイルはADX2ビルド時にadx2xml_to_str_cs.pyで生成しています。\n"
    outstr += "// キューシートごとに、キューシート情報（キュー情報など）BGM.assetsを作ります。\n"
    outstr += "using UnityEngine;\n"
    outstr += "using UnityEditor;\n"
    outstr += "namespace MyDearest {\n"
    outstr += "	public static class AcbDataCreator {\n"
    outstr += "		[MenuItem (\"Window/MDSound/CreateAcbData\")]\n"
    outstr += "		public static void Create () {\n"
    outstr += "\t\t\tDebug.Log (\"<b>[MyDSound]</b>[AcbDataCreator] 生成\");\n"
    
    return outstr

def writeFooter(assetoutpath,outstr):
    outstr += "\t\t}\n"
    outstr += "\t}\n"
    outstr += "}\n"

    return outstr

def writeCueCategoryHeader(outstr, cueCategoryName):
    outstr += "\t\t\t\t{\n"
    outstr += "\t\t\t\t\tADXAcbData.CueCategory cueCategory = new ADXAcbData.CueCategory ();\n"
    outstr += "\t\t\t\t\tcueCategory.Name = \"" + cueCategoryName + "\";\n"
    outstr += "\t\t\t\t\tacb.CueCategories.Add (cueCategory);\n"   
    return outstr


def writeCueCategoryFooter(outstr):
    outstr += "\t\t\t\t}\n"    
    return outstr

def writeCueSheetHeader(outstr,assetoutpath,g_currentCueSheetName):
    outstr += "\t\t\t{\n"        
    outstr += "\t\t\t\tADXAcbData acb = (ADXAcbData)AssetDatabase.LoadAssetAtPath (\"" + assetoutpath + g_currentCueSheetName + ".asset\", typeof (ADXAcbData));\n"
    outstr += "\t\t\t\tif (acb == null) {\n"
    outstr += "\t\t\t\t\tacb = ScriptableObject.CreateInstance<ADXAcbData> ();	//	無い時は作る\n"
    outstr += "\t\t\t\t\tAssetDatabase.CreateAsset (acb, \"" + assetoutpath + g_currentCueSheetName + ".asset\");\n"
    outstr += "\t\t\t\t\tacb = (ADXAcbData)AssetDatabase.LoadAssetAtPath (\"" + assetoutpath + g_currentCueSheetName + ".asset\", typeof (ADXAcbData));\n"
    outstr += "\t\t\t\t}\n"
    outstr += "\t\t\t\tacb.CueCategories.Clear ();\n"
    outstr += "\t\t\t\tacb.Name = \"" + g_currentCueSheetName + "\";\n"
    return outstr

def writeCueSheetFooter(outstr):
    outstr += "\t\t\t\tEditorUtility.SetDirty (acb);\n"
    outstr += "\t\t\t}\n"
    return outstr

def writeCueHeader(outstr,g_currentCueName):
    outstr += "\t\t\t\t\t{\n"
    outstr += "\t\t\t\t\t\tADXAcbData.CueData cueData = new ADXAcbData.CueData ();\n"
    outstr += "\t\t\t\t\t\tcueData.Name = \"" + g_currentCueName + "\";\n"
    return outstr

def writeCueFooter(outstr):
    outstr += "\t\t\t\t\t\tcueCategory.Cues.Add (cueData);\n"
    outstr += "\t\t\t\t\t}\n"
    return outstr

#-------
acbList = list()

def conv(selected_CueSheets,outpath,assetoutpath):

    #AcbListの構築
    for cusheet in selected_CueSheets:
        acb = AdxAcbData()
        acb.name = acproject.get_value(cusheet, "Name")["data"]
        acbList.append(acb)  

        #キューフォルダーを集める
        cueFolders = acproject.find_objects(cusheet, "CueFolder")["data"]  
        for cueFolder in cueFolders:   
            cueFolderName = acproject.get_value(cueFolder, "Name")["data"]
       
            if cueFolderName not in acbList[-1].cueCategories:   
                #キューフォルダーをキューカテゴリに
                cueCategory = CueCategory()
                cueCategory.name = cueFolderName
                acbList[-1].cueCategories[cueCategory.name] = cueCategory   # acbListにカテゴリー追加

                #キューを集める 
                cues = acproject.find_objects(cueFolder, "Cue")["data"]     
                for cue in cues:   
                    cueData = CueData()
                    cueData.name = acproject.get_value(cue, "Name")["data"]
                    if acproject.get_value(cue, "UserData")["succeed"]:
                        cueData.userData = acproject.get_value(cue, "UserData")["data"]
                    if acproject.get_value(cue, "Comment")["succeed"]:
                        acdebug.log("Comment {0} ".format(acproject.get_value(cue, "Comment")["data"]))
                        cueData.comment = acproject.get_value(cue, "Comment")["data"].replace('\"', '-')
                    if acproject.get_value(cue, "Omit")["succeed"]:
                        cueData.omit = acproject.get_value(cue, "Omit")["data"]
                    
                    acbList[-1].cueCategories[cueCategory.name].cues.append(cueData)              
                    #AISAC
                    aisacs = acproject.find_objects(cue, "Aisac")["data"]  
                    for aisac in aisacs:   
                        #acdebug.log("aisac {0} ".format(acproject.get_value(aisac, "Name")["data"])
                        aisacControlType = acproject.get_value(aisac, "AisacControlType")["data"]
                        if aisacControlType != "AutoModulation":
                            acdebug.log("aisacControlType {0} ".format(aisacControlType))
                            aisacControl = acproject.get_value(aisac, "AisacControl")["data"]
                            tmpAisacControlName = acproject.get_value(aisacControl, "Name")["data"]
                            acbList[-1].cueCategories[cueCategory.name].cues[-1].aisacNames.append(tmpAisacControlName)
                    #LinkAisac
                    linkAisacs = acproject.find_objects(cue, "AisacLink")["data"]  
                    for linkAisac in linkAisacs:   
                        linkAisacTgt = acproject.get_value(linkAisac, "LinkAisac")["data"]
                        aisacControlType = acproject.get_value(linkAisacTgt, "AisacControlType")["data"]
                        if aisacControlType != "AutoModulation":
                            #acdebug.log("linkAisac {0} {1}".format(acproject.get_value(linkAisac, "Name")["data"],acproject.get_object_path(linkAisacTgt)["data"]))
                            aisacControl = acproject.get_value(linkAisacTgt, "AisacControl")["data"]
                            tmpAisacControlName = acproject.get_value(aisacControl, "Name")["data"]
                            acbList[-1].cueCategories[cueCategory.name].cues[-1].aisacNames.append(tmpAisacControlName)
                   #Block
                    blocks = acproject.find_objects(cue, "Block")["data"]  
                    for block in blocks: 
                        blockName = acproject.get_value(block, "Name")["data"]
                        blockDivisionNum = acproject.get_value(block, "BlockDivisionNum")["data"]
                        blockEndPositionMs = acproject.get_value(block, "BlockEndPositionMs")["data"]

                        acbList[-1].cueCategories[cueCategory.name].cues[-1].blockNames.append(blockName)
                        acbList[-1].cueCategories[cueCategory.name].cues[-1].blockEndPoses.append(blockEndPositionMs)   




# キュー追加
    #----------

    outstr = ""
    outstr = writeHeader(outstr)

    for acb in acbList:
        #print(acb.name)
        outstr = writeCueSheetHeader(outstr,assetoutpath,acb.name)
        
        for cueCategoryKey in acb.cueCategories.keys():
            cueCategory = acb.cueCategories[cueCategoryKey]
            print("CueCategoryName " + cueCategory.name)
            if cueCategory.private is True:    #プライベート時は読み飛ばす
                continue

            
            #print("\t" +cueCategory.name)
            outstr = writeCueCategoryHeader(outstr,cueCategory.name)
            
            for cue in cueCategory.cues:
                if cue.omit is True:	#Omit時は読み飛ばす
                    continue
                else:
                                
                    #print("\t\t" +cue.name)
                    outstr = writeCueHeader(outstr,cue.name)

                    outstr += "\t\t\t\t\t\tcueData.UserData = @\"" + cue.userData + "\";\n"
                    outstr += "\t\t\t\t\t\tcueData.Comment = @\"" + cue.comment + "\";\n"

                    aisacNames = list()
                    # 予約（プログラム指定のAISACコントロールを非表示にする）
                    aisacNames.append("Distance")
                    aisacNames.append("GlobalOcclusion")
                    aisacNames.append("Pan")
                    aisacNames.append("Elevation")
                    for aisacName in cue.aisacNames:
                        if aisacName not in aisacNames:          	
                            outstr += "\t\t\t\t\t\tcueData.AisacNames.Add (\"" + aisacName + "\");\n"
                            aisacNames.append(aisacName)
                        
                    for blockName in cue.blockNames:
                        outstr += "\t\t\t\t\t\tcueData.BlockNames.Add (\"" + blockName + "\");\n"

                    blockStartPos = 0
                    for blockEndPos in cue.blockEndPoses:
                        outstr += "\t\t\t\t\t\tcueData.BlockEndPoses.Add (" + str(round(blockStartPos + float(blockEndPos))) + ");\n"
                        blockStartPos += float(blockEndPos)
                    
                    outstr = writeCueFooter(outstr)

            
            outstr = writeCueCategoryFooter(outstr)

        outstr = writeCueSheetFooter(outstr)

    
    outstr = writeFooter(assetoutpath,outstr)
    
    #print(outstr)
    with open(outpath,"w",encoding="utf-8") as f:
        f.write(outstr)

#ScriptableObject生成コード生成と、情報収集
conv(selected_CueSheets,
     "C:/MyDearest/github/Megalith/Assets/MyDearest/Sound/Editor/AcbDataCreator.cs",
     "Assets/MyDearest/Megalith/Data/Sound/")

```

# ScriptableObject側のクラス

``` ADXAcbData.cs
using System;
using System.Linq;
using System.Threading;
using System.Collections.Generic;
using UniRx;
using UniRx.Async;
using UnityEngine;
using Common;

namespace MyDearest {
	[CreateAssetMenu (menuName = "Sound/ADX Acb Data")]
	public class ADXAcbData
		: ScriptableObject {
		[Serializable]
		public class CueData {
			public string Name = "";
			public List<string> BlockNames = new List<string> ();
			public List<int> BlockEndPoses = new List<int> ();
			public List<string> AisacNames = new List<string> ();
			public string Comment = "";
			public string UserData = "";
		}
		[Serializable]
		public class CueCategory {
			public string Name = "";    //	カテゴリー名（Craftでは上位フォルダ名）
			public List<CueData> Cues = new List<CueData> ();

			/// <summary>Indexでアクセス</summary>
			public CueData GetData (int index) => Cues[index];

			/// <summary>名前検索</summary>
			public CueData Find (string name) {
				int count = Cues.Count;
				for (int i = 0; i < count; ++i) {
					if (Cues[i].Name == name) {
						return Cues[i];
					}
				}
				return null;
			}

			public int FindIndex (string name) => Cues.FindIndex (d => d.Name == name);

			private List<string> _cueNames = new List<string> ();
			/// <summary>キュー名リストを返す</summary>
			public string[] CueNames () {
				if (_cueNames.Count > 0) return _cueNames.ToArray ();
				foreach (CueData cuedata in Cues) {
					_cueNames.Add (cuedata.Name);
				}
				return _cueNames.ToArray ();
			}
			
			/// <summary>名前キャッシュをクリアする(次回アクセス時に再生成される)</summary>
			public void Refresh () {
				_cueNames.Clear ();
			}
		}

		public class IndexData
		{
			public int Category = 0;
			public int Index = 0;
		}

		/// <summary>ACB名</summary>
		public string Name = "";
		public List<CueCategory> CueCategories = new List<CueCategory> ();


		/// <summary>Cueすべての数</summary>
		public int CuesCount () {
			int cueCount = 0;
			int count = CueCategories.Count;
			for (int i = 0; i < count; ++i) {
				cueCount += CueCategories[i].Cues.Count;
			}
			return cueCount;
		}


		/// <summary>Indexでアクセス</summary>
		public CueData GetData (int categoryIndex, int index) {

			return CueCategories[categoryIndex].Cues[index];
		}

		/// <summary>Indexでアクセス</summary>
		public CueData GetData (int index) => Cues (index);

		/// <summary>CueすべてからIndexアクセス</summary>
		public CueData Cues (int index) {
			int searchIndex = index;
			//	全カテゴリーから探す
			int count = CueCategories.Count;
			for (int i = 0; i < count; ++i) {
				int cueCount = CueCategories[i].Cues.Count;
				if (searchIndex >= cueCount) {
					searchIndex -= cueCount;
					continue;
				}
				return CueCategories[i].Cues[searchIndex];
			}
			return null;
		}

		/// <summary>名前検索(すべてのサブカテゴリーから名前で検索）</summary>
		public CueData FindAll (string name) {
			//	全カテゴリーから探す
			int count = CueCategories.Count;
			for (int i = 0; i < count; ++i) {
				CueData retData = CueCategories[i].Find (name);
				if (retData != null) return retData;
			}
			return null;
		}

		/// <summary>名前検索(すべてのサブカテゴリーから名前で検索）</summary>
		public CueData FindAll (string name, IndexData outputIndexData)
		{
			//	全カテゴリーから探す
			int count = CueCategories.Count;
			for (int i = 0; i < count; ++i)
			{
				int index = CueCategories[i].FindIndex (name);
				if (index >= 0)
				{
					outputIndexData.Category = i;
					outputIndexData.Index = index;					
					return CueCategories[i].GetData (index);
				}
			}
			return null;
		}

		private List<string> _cueNames = new List<string> ();
		/// <summary>キュー名リストを返す(すべてのサブカテゴリー合わせたリスト）</summary>
		public string[] CueNames () {
			if (_cueNames.Count > 0) return _cueNames.ToArray ();
			foreach (CueCategory cueCategory in CueCategories) {
				_cueNames.AddRange (cueCategory.CueNames ());
			}
			return _cueNames.ToArray ();
		}

		private List<string> _categoryNames = new List<string> ();
		/// <summary>サブカテゴリー名一覧</summary>
		public string[] SubCategoryNames () {
			int count = CueCategories.Count;
			if (_categoryNames.Count == count) return _categoryNames.ToArray ();

			for (int i = 0; i < count; ++i) {
				_categoryNames.Add (CueCategories[i].Name);
			}
			return _categoryNames.ToArray ();
		}

		/// <summary>キュー名リストを返す(カテゴリインデックス指定）</summary>
		public string[] CueNames (int categoryIndex) {
			return CueCategories[categoryIndex].CueNames ();
		}


		/// <summary>名前キャッシュをクリアする</summary>
		public void Refresh () {
			Common.CommonDebug.Log ("<b>[MyDSound]</b>[ADXAcbData] 名前キャッシュクリア");
			_categoryNames.Clear ();
			_cueNames.Clear ();

			int count = CueCategories.Count;

			for (int i = 0; i < count; ++i) {
				CueCategories[i].Refresh ();
			}
		}

	}
}

```


こちらはUnity上のScriptableObjectの定義
[前回](https://qiita.com/tatmos/items/33543bddede4fee80c6b)より進化しているのは

- キューフォルダーをキューカテゴリ（独自）として取り込む
- ブロックの長さとかもとれるようになっている

## キューシート内キューのアクセスの独自実装部分について

今回のデータはキューシートにキューが数千入る場合もあるので、一つのメニューからの選択が困難になりました。
そこで、
このキューカテゴリは独自実装ですが、キューシートの中をキューフォルダーで分けたうえで、
メニューなどで表示しやすいように1階層だけ追加しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96808/cfbbd3fa-e2ca-5a32-d6cc-b1f1873e614d.png)

例えば、
###BGMなら
OP,ED,ADV,Battle,Liveなどのサブカテゴリーを持っているような感じで、選択しやすくしています。

###VOICEなら
VOICEは数値で管理しているので、数値の上位桁ごとにフォルダを分けています。
ボイスについては「[ADX2ロボット機能で大量波形から任意の波形をコピーする](https://qiita.com/tatmos/items/727972d4af29e524e057)」でも少し触れています。

###SEなら
ADV,Battle,SE種別(Systen,Ambient,Charactor,CutSecene,Uta etc...)などで分けています。

あとカテゴリ内から検索できるように関数も用意してあります。

# 移植してみてわかった利点として
###良い点：
- xml出力を経由しないので、時間短縮になる (xml出力は結構時間がかかる数分～数十分)
- xmlではたどりにくいLinkAisacの情報が比較的簡単に取り出せる（XML解析の部分が省略できる）
- ADX2側のメニューなどで処理できるので便利。![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96808/a6416aba-662f-f79e-3f86-3358495e4bd9.png)

###おしい点：

現状（Ver3.44.18)ではコメント内に「",tab,改行」が含まれるものは、コメント取得しようとしても空文字が帰ってくるようです。

#おわりに

ADX2のメニューから選ぶだけで動かせるのはとてもメリットがあって、セットアップさえできていれば、属人化を下げてくれます。
あと、ADX2のデータに直接アクセスできているので、
この時点で、設定の命名ミスなどをチェックするといったことにも使えそうです。（チェックしつつ修正してしまうとかも可能になる）

あと、ゲーム独自のパラメータとかを持たせたい時にも、この形ならいろいろ拡張ができそうです。