Unity+ADX2環境でエディタからプロジェクト設定情報(acf)を確認する

# acfファイルの中身をエディタで表示＆クリップボードにコピー

.acfファイルはADX2から出力される、ADX2のプロジェクト設定が入ったファイルです。
Atom Craftでは「プロジェクトツリー」の「全体設定」以下で設定する項目となっており、ゲームプロジェクト全体で使用するADX2の設定情報になります。Atom Craftを開けばどんな設定になっているかすぐに確認できます。

しかしながら、複数人で作業している場合や、今作業している環境にAtom Craftやプロジェクトファイルがない状態のときは、出力されたacfファイルの中身がどんなものであるかはわかりません。そこで、Unity Editor上でacfの中身を取り出しておき、作業者がAtom Craftを開かずとも設定が簡単に確認できることを目指します。


今回は「カテゴリーの情報」と「AISACコントロールの情報」の２つを表示するウィンドウをエディタ拡張で作成します。

![ADX2AcfViewer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19010/27479d99-7073-19bb-4773-c44a7f186617.png)

### ソースコードとunitypackage

Unity ADX2 Acf Viewer
https://github.com/TakaakiIchijo/UnityADX2AcfViewer

### 確認環境

Unity 2019.4.1f1
ADX2 LE SDK 2.10.05

## Scriptable Objectでacf情報を保存する

ADX2 LEでは、エディタで一度ゲームを実行するとacfファイルの中身をADX2のランタイムが読み込む仕組みになっています。
これから作成するACF　Viewerもゲームを実行してからデータ読み込み、という手続きになりますが、エディターを起動するたびに読み込むのは面倒です。そこで、一回読み込んだacfのデータはScriptable Objectとして保存しておき、次回起動時にはこのScriptableObjectからデータを読み込むことにします。

「カテゴリー」と「AISACコントロール」のプロパティを保存するScriptable Object
は次の通りです。

```AcfDataSo.cs

using System.Collections.Generic;
using UnityEngine;
using ACFDataClass;

public class AcfDataSo : ScriptableObject
{
    public List<CategoryInfo> categoryInfoList = new List<CategoryInfo>();
    public List<AisacControlInfo> aisacControlInfoList = new List<AisacControlInfo>();
}

namespace ACFDataClass
{
    [System.Serializable]
    public class CategoryInfo
    {
        public string name;
        public uint groupNo;
        public uint id;
        public uint numCueLimits;
        public float volume;

        public CategoryInfo(uint groupNo, uint id, string name, uint numCueLimits, float volume)
        {
            this.groupNo = groupNo;
            this.id = id;
            this.name = name;
            this.numCueLimits = numCueLimits;
            this.volume = volume;
        }
    }
    
    [System.Serializable]
    public class AisacControlInfo
    {
        public string name;
        public uint id;

        public AisacControlInfo(string name, uint id)
        {
            this.name = name;
            this.id = id;
        }
    }
}

```

## ウィンドウが有効になったらScriptable Objectを作成する

次に、保存用のScriptableObjectのファイルをエディタ上で生成します。
EditorWindowのOnEnableメソッドで、指定のパスにScriptableObjectが存在するか確認し、なければ新しく作ります。

```CriAtomAcfViewerWindow.cs

using System;
using System.Collections.Generic;
using System.Linq;
using ACFDataClass;
using UnityEditor;
using UnityEngine;

public class CriAtomAcfViewerWindow : EditorWindow
{
    const string acfScriptableObjectPath = "Assets/Editor/CriWare/CriAtom/acfDataSo.asset";
    
    private AcfDataSo acfDataSo;
    
    [MenuItem("Window/CRIWARE/Open Acf Viewer Window", false, 100)]
    static void OpenWindow()
    {
        GetWindow<CriAtomAcfViewerWindow>("Acf Viewer");
    }

    private void OnEnable()
    {
        acfDataSo = AssetDatabase.LoadAssetAtPath<AcfDataSo>(acfScriptableObjectPath);

        if (acfDataSo == null)
        {
            acfDataSo = CreateInstance<AcfDataSo>();
            AssetDatabase.CreateAsset(acfDataSo, acfScriptableObjectPath);
            AssetDatabase.Refresh();
        }
    }

//以下略

```

## ADX2のランタイムからacf情報を読み込む

ランタイムからのacf情報読み込みは、CriAtomExAcfDebugクラスを経由して行います。
カテゴリー情報はGetNumCategoriesでカテゴリーの数を取得し、GetCategoryInfoByIndexでカテゴリの情報を取得できます。

```CriAtomAcfViewerWindow.cs
    private List<CategoryInfo> LoadCategory()
    {
        List<CategoryInfo> categoryInfoList = new List<CategoryInfo>();
        
        for (ushort i = 0; i < CriAtomExAcfDebug.GetNumCategories(); i++)
        {
            CriAtomExAcfDebug.GetCategoryInfoByIndex(i, out var categoryInfo);
            
            categoryInfoList.Add(new CategoryInfo(
                categoryInfo.groupNo,
                categoryInfo.id, 
                categoryInfo.name,
                categoryInfo.numCueLimits,
                categoryInfo.volume));
        }

        return categoryInfoList;
    }
```

AISACコントロール情報も全く同様に、GetNumAisacControlsで数を取得し、GetAisacControlInfoで情報を取得できます。

```CriAtomAcfViewerWindow.cs

    private List<AisacControlInfo> LoadAisacControlList()
    {
        List<AisacControlInfo> aisacControlInfoList =new List<AisacControlInfo>();
        
        for (ushort i = 0; i < CriAtomExAcfDebug.GetNumAisacControls(); i++)
        {
            CriAtomExAcfDebug.GetAisacControlInfo(i, out var aisacControlInfo);

            aisacControlInfoList.Add(new AisacControlInfo(
                aisacControlInfo.name,
                aisacControlInfo.id));
        }

        aisacControlInfoList.Sort((a,b) => (int)a.id - (int)b.id);

        return aisacControlInfoList;
    }

```

## ウィンドウの各要素の描画

次に、ウィンドウを構成する各要素の描画処理を書いていきます。

![ADX2AcfViewer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19010/27479d99-7073-19bb-4773-c44a7f186617.png)


### acf読み込みボタンの描画
「Load ACF data from runtime」ボタンは、EditorApplication.isPlayingを使ってゲームが実行中かどうかを判定し、実行中のみacfの読み込み処理を行います。
先ほど作成したScriptableObjectにカテゴリの情報、アイザックの情報を読み込み、保存します。

```CriAtomAcfViewerWindow.cs

    private void DrawLoadButton()
    {
        GUILayout.BeginHorizontal();
        {
            if (GUILayout.Button("Load ACF data from runtime", GUILayout.Width(200), GUILayout.Height(50)))
            {
                if (!EditorApplication.isPlaying)
                {
                    Debug.LogError("Please load acf data in editor playing");
                }
                else
                {
                    acfDataSo = AssetDatabase.LoadAssetAtPath<AcfDataSo>(acfScriptableObjectPath);
                    
                    acfDataSo.categoryInfoList = LoadCategory();
                    acfDataSo.aisacControlInfoList = LoadAisacControlList();
                    
                    EditorUtility.SetDirty(acfDataSo);
                    AssetDatabase.SaveAssets();
                }
            }
            //中略
        }
        GUILayout.EndHorizontal();
    }

```

### カテゴリー情報の項目名を描画

カテゴリー情報を表示する箇所の、「項目名」部分を描画します。
これらはボタンで構成されており、クリックすることでそれぞれの項目の昇順で並び替えを行うことができます。

```CriAtomAcfViewerWindow.cs

    private void DrawCategoryItemNames()
    {
        List<CategoryInfo> categoryInfoList = acfDataSo.categoryInfoList;
        
        GUILayout.BeginHorizontal();
        {
            GUIStyle style = new GUIStyle(EditorStyles.miniButtonMid);
            style.alignment = TextAnchor.LowerLeft;
            
           if(GUILayout.Button("Category Name", style))
           {
               categoryInfoList.Sort((a,b) => String.CompareOrdinal(a.name, b.name));
           }

           if(GUILayout.Button("Volume", style, GUILayout.Width(80)))
            {
                categoryInfoList.Sort((a,b) => (int)a.volume - (int)b.volume);
            }
            
            if(GUILayout.Button("Cue Limit", style, GUILayout.Width(80)))
            {
                categoryInfoList.Sort((a,b) => (int)a.numCueLimits - (int)b.numCueLimits);
            }

            if(GUILayout.Button("Id", style, GUILayout.Width(80)))
            {
                categoryInfoList.Sort((a,b) => (int)a.id - (int)b.id);
            }
            
            if(GUILayout.Button("Group No", style, GUILayout.Width(80)))
            {
                categoryInfoList.Sort((a,b) => (int)a.groupNo - (int)b.groupNo);
            }
        }
        GUILayout.EndHorizontal();
    }

```

### カテゴリーのリストを描画

カテゴリー情報のリストを順番に描画します。「カテゴリ名」の部分はボタンになっておおり、クリックするとそのカテゴリ名をクリップボードにコピします。
EditorGUIUtility.systemCopyBuffer を使うことで、簡単にクリップボードにアクセスできます。
ラジオボタンにしている理由は、見た目がリストっぽくなるためです。
クリックされた行はインデックスを保持しておき、黄色で表示します。

```CriAtomAcfViewerWindow.cs

    private int selectedInfoIndex;

//中略

    private void DrawCategoryList()
    {
        List<CategoryInfo> categoryInfoList = acfDataSo.categoryInfoList;
        
        for(int i = 0; i < categoryInfoList.Count; ++i) {
            EditorGUILayout.BeginHorizontal();
            if (this.selectedInfoIndex == i) {
                GUI.color = Color.yellow;
            } else {
                GUI.color = Color.white;
            }

            var categoryName = categoryInfoList[i].name;
            
            if (GUILayout.Button(categoryName, EditorStyles.radioButton)) {
                
                EditorGUIUtility.systemCopyBuffer = categoryName;
                this.selectedInfoIndex = i;

                Debug.Log("Saved to clipboard " + "\""+categoryName+ "\"");
            }
            
            GUILayout.Label(categoryInfoList[i].volume.ToString(), GUILayout.Width(75));

            string numCueLimits = categoryInfoList[i].numCueLimits == UInt32.MaxValue
                ? "Unlimited"
                : categoryInfoList[i].numCueLimits.ToString();
            
            GUILayout.Label(numCueLimits, GUILayout.Width(75));

            GUILayout.Label(categoryInfoList[i].id.ToString(), GUILayout.Width(75));
            GUILayout.Label(categoryInfoList[i].groupNo.ToString(), GUILayout.Width(70));
            
            EditorGUILayout.EndHorizontal();
            
        }
        GUI.color = Color.white;
    }
```

キューリミットの値については、リミットを設定していない場合はuintの最大値（	4,294,967,295）が入るため、これと一致した場合は「Unlimited」と表示するようにしています。

### AISACコントロール情報の項目名を描画

AISACコントロールについてもカテゴリと同様です。項目名をボタンで表示し、クリック時にソートを行います。

```CriAtomAcfViewerWindow.cs

    private void DrawAisacControlItemNames()
    {
        List<AisacControlInfo> aisacControlInfoList = acfDataSo.aisacControlInfoList;
        
        GUILayout.BeginHorizontal();
        {
            GUIStyle style = new GUIStyle(EditorStyles.miniButtonMid);
            style.alignment = TextAnchor.LowerLeft;

            if (GUILayout.Button("Aisac Control Name", style))
            {
                aisacControlInfoList.Sort((a,b) => String.CompareOrdinal(a.name, b.name));
            }

            if (GUILayout.Button("ID", style, GUILayout.Width(80)))
            {
                aisacControlInfoList.Sort((a,b) => (int)a.id - (int)b.id);
            }
        }
        GUILayout.EndHorizontal();
    }

```

### AISACコントロールのリストを描画

AISACコントロールの表示にはオプションとして「デフォルトのAISAC値を表示するかどうか」というチェックボックスを設けます。「AisacControl_00」のフォーマットでデフォルトでは16個用意されているため、これをリスト上で見せるか見せないかを切り替えます。

```CriAtomAcfViewerWindow.cs

    bool hideDefaultAisacControls = true;

//中略

    private void DrawAisacControlList()
    {
        List<AisacControlInfo> list = acfDataSo.aisacControlInfoList;
        
        if (hideDefaultAisacControls)
        {
            list = list.Where(a => a.name.Contains("AisacControl_") == false).ToList();
        }
        
        for(int i = 0; i < list.Count; ++i) {
            EditorGUILayout.BeginHorizontal();
            if (this.selectedInfoIndex == i) {
                GUI.color = Color.yellow;
            } else {
                GUI.color = Color.white;
            }

            var aisacName = list[i].name;
            
            if (GUILayout.Button(aisacName, EditorStyles.radioButton)) {
                
                EditorGUIUtility.systemCopyBuffer = aisacName;
                this.selectedInfoIndex = i;

                Debug.Log("Saved to clipboard " + "\""+aisacName+ "\"");
            }
            
            GUILayout.Label(list[i].id.ToString(), GUILayout.Width(70));
            
            EditorGUILayout.EndHorizontal();
        }
        GUI.color = Color.white;
    }

```

### OnGUIで各描画パーツを呼んでウィンドウを描画する

最後にOnGUIの中でDrawLoadButton、DrawCategoryItemNames、DrawCategoryList、DrawAisacControlItemNames、DrawAisacControlListをスクロールビューで囲みながら順番に呼びます。

```CriAtomAcfViewerWindow.cs
    private Vector2 scrollPos_Window, scrollPosCategory, scroppPosAisacControl;

//中略

    public void OnGUI()
    { 
        DrawLoadButton();

        if (acfDataSo == null) return;
        
        this.scrollPos_Window = GUILayout.BeginScrollView(this.scrollPos_Window);
        {
            DrawCategoryItemNames();

            float categoryHeight = this.position.height - 300.0f;
            if (categoryHeight < 100.0f) categoryHeight = 100.0f;
            scrollPosCategory = EditorGUILayout.BeginScrollView(scrollPosCategory, GUILayout.Height(categoryHeight));
            
            DrawCategoryList();
            
            EditorGUILayout.EndScrollView();
            
            DrawAisacControlItemNames();

            float aisacControlHeight = this.position.height - 300.0f;
            if (aisacControlHeight < 100.0f) aisacControlHeight = 100.0f;
            
            scroppPosAisacControl = EditorGUILayout.BeginScrollView(scroppPosAisacControl);
            
            DrawAisacControlList();
            
            EditorGUILayout.EndScrollView();
        }
        
        GUILayout.Box("", GUILayout.ExpandWidth(true), GUILayout.Height(2));

        hideDefaultAisacControls = GUILayout.Toggle(hideDefaultAisacControls, "Hide default name AISAC controls");

        GUILayout.Space(10);
        EditorGUILayout.EndScrollView();
    }

```

一番下でGUILayout.Toggleを使って、デフォルトのAISACコントロール名を表示するかしないかのオプションを描画しています。

# 拡張性

今回は「カテゴリー」「AISACコントロール」のみを表示しましたが、acfにはほかの情報も含まれています。

![全体設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19010/598a2ce2-5af0-8ee7-0494-a171d5387ee3.png)

DSPバス設定やゲーム変数、セレクタなどが取得できますが、データの取得と描画方法は全く一緒です。プロジェクトに応じて必要な情報を見られるウィンドウを作ってみましょう。

また、保存したScriptable ObjectはEditor以外の位置に配置すればランタイムでも利用できます。