ADX2ビルド時にPythonでScriptableObjectを作る

#はじめに
UnityでEditorと実行時両方で
ADX2の情報にいろいろアクセスしたく作ってみました。

この記事はおそらく、ちょっと難易度高い（説明不足）です。

なおキュー名だけとかなら前回の記事
[ADX2ビルド時にUnityにコピーするなど便利なスクリプト紹介](https://qiita.com/tatmos/items/9ede96ed4b21ad8cfbb5)
もどうぞ。

#用語
## ScriptableObjectについて

kan_kikuchiさんのページ
[ScriptableObjectとは【Unity】【ScriptableObject】](http://kan-kikuchi.hatenablog.com/entry/What%27s_Scriptableobject)

## ADX2
サウンドミドルウェア　[ADX2LE](https://game.criware.jp/products/adx2-le/)なら無料で使える（条件あり）

## ptyhon
[ここ](https://www.python.org/)

#誰向けの情報？

・ScriptableObjectを生成するコードを作りたい人
・ADX2のxml出力をなんとか利用したい人
・pythonでxml解析してc#コード生成したい人


#なぜ作った？
###ADX2標準の方法ではちょっと取り出しにくい情報を得る

実行時にAcbの関数でとれる情報や、ビルド時に出力したｃｓファイル経由などあるのですが、
必要な情報だけを一か所で管理したかった。

###直接的に得ることのできない情報

- キューのコメント情報
- キューが利用しているブロック名をリストで
- キューが利用しているAISACコントロール名をリストで

などいろいろと何かに使えそうな情報を取り出したかった。

（ここでいう「何か」とは、社内ツールだったりします。）

#何ができるのか？
ADX2でビルド後、
Unity上でメニューから実行すると

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96808/c5eac867-800f-3e82-19d4-805829d1d0d8.png)

といった感じの.assetsファイルを生成します。
キューシートの数だけ作られます。

##プログラムからの利用が簡単に 
この生成したScriptableObjectをUnityのオブジェクトから参照しておけば、あらゆる場面でADX2のACB情報を扱えます。
あと、ビルド時に自動更新（メニュー操作は必要ですが　※）するので、情報伝達ミスが減らせます。

(ここでいう「あらゆる場面」とは実行時、エディター時、ビルド時などを指す）

※メニュー操作忘れるのでこれも自動化したい

##コメントの活用
あと、コメントとかに、Unityだけで触っている人向けに「コメント」がキューごとにつけられます。
エディター拡張とかで見れるので、いろいろコメントを利用すると良さそう

例えば、「仮データです」　だとか、　「更新日付とか」　　（ADX2 -> Unityへの片方コメントなので双方向ではないが・・・）

#xmlをpythonで解析

xmlの解析にpythonを使います。
pythonで必要な情報を選別し、
ScriptableObjectを作るコードを生成します。


#AISACについて
AISACコントロール名が知りたいのですが、グローバルAISACを参照している場合、XMLに出力されないので、
命名規則として、
名前＠コントロール名
というものを採用

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96808/9cdc950d-0d79-ec61-a5cd-228840465102.png)

#ADX2のAtomCraftでのビルド設定
- "acb_info"XML出力をチェック
- 階層化出力をオフ

ポストプロセス処理でpythonを呼び出しています
（ここではちょっとファイル名違いますが同じような感じで呼びます）


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96808/e9e04f45-3501-7b49-1307-66cdb684a591.png)


#ScriptableObjectのクラス
必要な情報を付け足していけば、XMLからの情報をUnityで利用できて便利になるはず。

```　ADXAcbData.cs
using System;
using System.Collections.Generic;
using UnityEngine;

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<string> AisacNames = new List<string> ();
			public string Comment = "";
			public string UserData = "";
		}
		public string Name = "";
		public List<CueData> Cues = new List<CueData> ();

		private List<string> _cueNames = new List<string> ();
		/// <summary>
		/// キュー名リストを返す
		/// </summary>
		/// <returns></returns>
		public string[] CueNames () {
			if (_cueNames.Count > 0) return _cueNames.ToArray ();
			foreach (CueData cuedata in Cues) {
				_cueNames.Add (cuedata.Name);
			}
			return _cueNames.ToArray ();
		}
	}
}

```



#ScriptableObjectを作るコード

データの数だけ長くなるので、ここではキュー一個だけで省略してます。

``` AcbDataCreators.cs
using UnityEngine;
using UnityEditor;
namespace MyDearest {
	public static class AcbDataCreator {
		[MenuItem ("Sound/CreateAcbData")]
		private static void Create () {
			{
				ADXAcbData acb = ScriptableObject.CreateInstance<ADXAcbData> ();
				acb.Name = "BGM";
				{
					ADXAcbData.CueData cueData = new ADXAcbData.CueData ();
					cueData.Name = "Chronos";
					acb.Cues.Add (cueData);
				}
				AssetDatabase.CreateAsset (acb, "Assets/MyDearest/Chronos/Data/Sound/BGM.asset");
			}
		}
	}
}

```


#pythonでUnityのCSファイルを生成

###UnityのScriptableObjectを作る方法
直接pythonからScriptableObjectを作れればよかったのですが、UnityのYAMLの情報がちょっとわからなかったので、
unityでScriptObjectを作るメニューのソースコードを作っています。

解析と同時にコード生成しているので、ちょっと見づらいですが、何しているかは理解しやすいかと。

path名とかは適宜修正してください。

``` adx2xml_to_str_cs.py
#print("ADX2のxml出力からScriptableObjectを作るC#コードを生成する")

import xml.etree.ElementTree as ET
import os

g_currentCueName = ""  #キュー名
g_currentCueSheetName = "" #キューシート名


def writeHeader(outstr):
    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 += "		private static void Create () {\n"

    return outstr

def writeFooter(assetoutpath,outstr):
    outstr += "\t\t\t\t\tacb.Cues.Add (cueData);\n"
    outstr += "\t\t\t\t}\n"
    outstr += "\t\t\t\tEditorUtility.SetDirty (acb);\n"
    outstr += "\t\t\t}\n"
    outstr += "\t\t}\n"
    outstr += "\t}\n"
    outstr += "}\n"

    return outstr
        
def printOrcaName(nest,child,xmlpath,outpath,assetoutpath,outstr):
    global g_currentCueName,g_currentCueSheetName
    nestspacestr = ""
    for i in range(nest):
        nestspacestr +=" "
    
    if(child.get("OrcaType") == "CriMw.CriAtomCraft.AcCore.AcOoCueSheet"):        
        print("キューシート名 " + child.get("OrcaName"))   #キューシート

        if(g_currentCueSheetName != "" and g_currentCueSheetName != child.get("OrcaName")):   #キューシートが変わった時のみ
            outstr += "\t\t\t\t\tacb.Cues.Add (cueData);\n"
            outstr += "\t\t\t\t}\n"
            outstr += "\t\t\t\tEditorUtility.SetDirty (acb);\n"
            outstr += "\t\t\t}\n"


        g_currentCueSheetName = child.get("OrcaName")
        g_currentCueName = ""
        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        acb = ScriptableObject.CreateInstance<ADXAcbData> ();	//	無い時は作る\n"
        outstr += "\t\t\t\t        AssetDatabase.CreateAsset (acb, \"" + assetoutpath + g_currentCueSheetName + ".asset\");\n"
        outstr += "\t\t\t\t        acb = (ADXAcbData)AssetDatabase.LoadAssetAtPath (\"" + assetoutpath + g_currentCueSheetName + ".asset\", typeof (ADXAcbData));\n"
        outstr += "\t\t\t\t}\n"
        outstr += "\t\t\t\tacb.Cues.Clear ();\n"
        outstr += "\t\t\t\tacb.Name = \"" + g_currentCueSheetName + "\";\n"
    if(child.get("OrcaType") == "CriMw.CriAtomCraft.AcCore.AcOoCueFolder"):
        print(nestspacestr + "キューフォルダ名 " + child.get("OrcaName"))
    if(child.get("OrcaType") == "CriMw.CriAtomCraft.AcCore.AcOoCueSynthCue"): #キュー
        print(nestspacestr + "キュー名 " + child.get("OrcaName"))

        if(g_currentCueName != "" and g_currentCueName != child.get("OrcaName")):   #キューが変わった時に閉じる
            outstr += "					acb.Cues.Add (cueData);\n"
            outstr += "				}\n"

        g_currentCueName = child.get("OrcaName")
        outstr += "				{\n"
        outstr += "					ADXAcbData.CueData cueData = new ADXAcbData.CueData ();\n"
        outstr += "					cueData.Name = \"" + g_currentCueName + "\";\n"
        
        if 'UserData' in child.attrib:
            outstr += "					cueData.UserData = @\"" + child.get("UserData") + "\";\n"
        if 'Comment' in child.attrib:
            outstr += "					cueData.Comment = @\"" + child.get("Comment") + "\";\n"
            
    if(child.get("OrcaType") == "CriMw.CriAtomCraft.AcCore.AcOoAisac"):
        print(nestspacestr + "AISACコントロール名 " + os.path.basename(child.get("AisacControl")))
        outstr += "					cueData.AisacNames.Add (\"" + os.path.basename(child.get("AisacControl")) + "\");\n"
    if(child.get("OrcaType") == "CriMw.CriAtomCraft.AcCore.AcOoAisacLink"):
        print(nestspacestr + "AISACコントロール名 " + os.path.basename(child.get("LinkAisac")).split('@')[1])  # Distance@Distance のような命名規則で@の後ろがコントロール名 (LinkAisacにAisacControlが無いので）
        outstr += "					cueData.AisacNames.Add (\"" + os.path.basename(child.get("LinkAisac")).split('@')[1] + "\");\n"
    if(child.get("OrcaType") == "CriMw.CriAtomCraft.AcCore.AcOoBlock"):
        print(nestspacestr + "Block名 " + child.get("OrcaName"))
        outstr += "					cueData.BlockNames.Add (\"" + child.get("OrcaName") + "\");\n"

    return outstr

# XML解析してscriptableobject生成コードを生成する
def conv(xmlpaths,outpath,assetoutpath):
    outstr = "";    
    outstr = writeHeader(outstr)

    for xmlpath in xmlpaths:
        tree = ET.parse(xmlpath) 
        root = tree.getroot()
        for child in root:
            for child1 in child:
                for child2 in child1:
                    for child3 in child2:
                        outstr = printOrcaName(0,child3,xmlpath,outpath,assetoutpath,outstr)
                        for child4 in child3:
                            outstr = printOrcaName(1,child4,xmlpath,outpath,assetoutpath,outstr)
                            for child5 in child4:
                                outstr = printOrcaName(2,child5,xmlpath,outpath,assetoutpath,outstr)
                                for child6 in child5:
                                    outstr = printOrcaName(3,child6,xmlpath,outpath,assetoutpath,outstr)
                                    for child7 in child6:
                                        outstr = printOrcaName(4,child7,xmlpath,outpath,assetoutpath,outstr)


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

#ビルド出力先（階層化出力はしない）
adx2outputpath = "C:/MyDearest/CraftData/OculusAdxTest/PC/"
#解析対象のxmlリスト
cuesheetXmlNames = [adx2outputpath + "BGM_acb_info.xml",
                 adx2outputpath + "SE_acb_info.xml",
                 adx2outputpath + "VOICE_acb_info.xml"]

#実際の変換、生成するcsファイルの場所と、生成したファイルが生成するassetsの場所を指定        
conv(cuesheetXmlNames,
     "C:/MyDearest/github/Chronos/Assets/MyDearest/Sound/Editor/AcbDataCreator.cs",
     "Assets/MyDearest/Chronos/Data/Sound/")
```

すみません
xmlの再帰的な解析がちょっと残念＆雑なコードですが、データの複雑度に合わせて付け足してください。



#おわりに

応用としては
xml解析してテキストファイルとか作るのは簡単なので、
チェック用途とか、バージョン管理やciツールとの連携にも使えるかもしれません。

ここにあるコードでは未対応ですが、例えば

- キューのフォルダ階層情報（Craftでの管理上のパス）　
- シーケンス上の配置時刻
- ビート情報
- ブロックの長さ

とか取り出せそうです。
ただ、やみくもに全部取り出すとそれはそれで膨大になって扱いづらい＆データ肥大しそうなので、
必要な情報だけ取り出せるようにしておけば良いと思います。


ビルド時にデータを差し替えたりとか、デバッグ情報を削ったりとかいった用途にも将来的には使えるかも（まだ試せていないけど）

あと　ビル時に情報をクラウドに上げてそっちから更新チェックして自動生成とかもありかもしれない。