UnityからSpineのプロジェクトを指定してエクスポートとプレハブ作成を行う

初投稿です！
分かりづらい、読みにくいなどあると思いますがお手柔らかにお願いします。

最近仕事で触っていた[Spine](http://ja.esotericsoftware.com/)ですが、コマンドラインの使い方については情報が少ないですね
そして[公式のマニュアル](http://ja.esotericsoftware.com/spine-export#%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%83%A9%E3%82%A4%E3%83%B3)ってちょっと分かりにくいですよね
だれかまとめてくれないかなあ...

僕が調べた成果として、
Unityからエクスポートを実行させて、ついでにプレハブを作成する
という自作のエディタ拡張機能をご紹介します

UnityでSpine使うのであれば、いつか役に立つかもしれません

#目的
Unityエディタ上からSpineのファイルを指定して、
あらかじめ用意しておいた設定に基づいてデータを作成・更新する

特定のフォルダ以下のSpineファイルを一気に変換・更新する

#動作環境
インストール手順などは省略します

Windows 10 Pro (Macが手元になくて・・・、パスやコマンドライン実行部分を良い感じに変えてください)
[Unity](https://unity3d.com/jp/get-unity/download) 2019.3.0f6 Personal (自宅用なのでPersonal)
[Spine](http://ja.esotericsoftware.com/) 3.8.84 Pro
[Spine-Unity](http://ja.esotericsoftware.com/spine-unity-download) spine-unity-3.8-2020-02-06.unitypackage

#Spineのパスを確認する
まずはコマンドラインを使うためにSpineのパスを調べます
デスクトップなどにあるSpineのアイコンを右クリックして「ファイルの場所を開く」を選択して、
表示されたエクスプローラのパスをメモしておきましょう

素直にインストールした人はこちらだと思います
windows
`C:\Program Files (x86)\Spine\Spine.com`
mac
`/Applications/Spine/Spine.app/Contents/MacOs/Spine`

#エクスポート設定ファイルの作成
次にSpineのエクスポート設定ファイルを作成します
Spineを起動して左上のロゴをクリックしてエクスポートを選択しましょう

設定はプロジェクトの方針で決めてください　([公式マニュアル-エクスポート](http://ja.esotericsoftware.com/spine-export#%E3%82%A8%E3%82%AF%E3%82%B9%E3%83%9D%E3%83%BC%E3%83%88))
Unityで使う場合には以下の2点に注意してください

- バイナリ形式を使う場合は拡張子を`.skel.bytes`にする
- アトラスファイルの拡張子を`.atlas.txt`にする

設定を変更したら、保存してパスを覚えておいてください
今回はプロジェクト内に`Assets/Editor/SpineUtility/export_settings.json`として保存しました

<details><summary>export_settings.json</summary><div>

```json:export_settings.json
{
"class": "export-binary",
"name": "Binary",
"project": "C:\\Program Files (x86)\\Spine\\examples\\spineboy\\spineboy-pro.spine",
"output": "D:/work",
"open": false,
"extension": ".skel.bytes",
"nonessential": false,
"cleanUp": true,
"packAtlas": {
	"stripWhitespaceX": true,
	"stripWhitespaceY": true,
	"rotation": true,
	"alias": true,
	"ignoreBlankImages": false,
	"alphaThreshold": 3,
	"minWidth": 16,
	"minHeight": 16,
	"maxWidth": 2048,
	"maxHeight": 2048,
	"pot": true,
	"multipleOfFour": false,
	"square": false,
	"outputFormat": "png",
	"jpegQuality": 0.9,
	"premultiplyAlpha": true,
	"bleed": false,
	"scale": [ 1 ],
	"scaleSuffix": [ "" ],
	"scaleResampling": [ "bicubic" ],
	"paddingX": 2,
	"paddingY": 2,
	"edgePadding": true,
	"duplicatePadding": false,
	"filterMin": "Linear",
	"filterMag": "Linear",
	"wrapX": "ClampToEdge",
	"wrapY": "ClampToEdge",
	"format": "RGBA8888",
	"atlasExtension": ".atlas.txt",
	"combineSubdirectories": false,
	"flattenPaths": false,
	"useIndexes": false,
	"debug": false,
	"fast": false,
	"limitMemory": true,
	"currentProject": true,
	"packing": "rectangles",
	"silent": false,
	"ignore": false,
	"bleedIterations": 2
},
"packSource": "attachments",
"packTarget": "perskeleton",
"warnings": true
}
```

</div></details>

#UnityでC#スクリプトを作成

##Assets/Editor/SpineUtility/SpineConfig.cs

Spineのパスやバージョンなどの定義を記載しています

<details><summary>SpineConfig.cs</summary><div>

```csharp:SpineConfig.cs
using UnityEngine;
namespace SpineUtility
{
    public static class SpineConfig
    {
        //Spineのパス、空白が含まれるため使用する際に""で囲む
        public static readonly string SpinePath = "C:/Program Files (x86)/Spine/Spine.com";

        //Spineのバージョン　latestか3.8.84など
        public static readonly string SpineVersion = "latest";

        //Spineエクスポート設定の絶対パス
        public static readonly string ExportSetting = $"{Application.dataPath}/Editor/SpineUtility/export_settings.json";
    }
}

```

</div></details>

##Assets/Editor/SpineUtility/SpineExporterWindow.cs

エディタウィンドウの表示部分です

<details><summary>SpineExporterWindow.cs</summary><div>

```csharp:SpineExporterWindow.cs
using System.IO;
using UnityEngine;
using UnityEditor;

namespace SpineUtility
{
    public class SpineExporterWindow : EditorWindow 
    {
        private Vector2 _scrollPosition;
        private string _spineProjectPath = string.Empty;
        private string _outputDirectory = string.Empty;
        private string _spineProjectsRootDirectory = string.Empty;
        private string _outputRootDirectory = string.Empty;
        private SpineExporter _spineExporter = new SpineExporter();

        [MenuItem("SpineUtility/SpineExporterWindow")]
        private static void ShowWindow() {
            var window = GetWindow<SpineExporterWindow>("SpineExporterWindow");
            window.Show();
        }
    
        private void OnGUI() {
            using (var scroll = new EditorGUILayout.ScrollViewScope(_scrollPosition, GUI.skin.box))
            {
                _scrollPosition = scroll.scrollPosition;

                SelectSpineProject();
                EditorGUILayout.Space();

                SelectOutputDirectory();
                EditorGUILayout.Space();

                ExportButton();
                EditorGUILayout.Space();
                EditorGUILayout.Space();

                EditorGUILayout.LabelField("このフォルダ以下のSpineプロジェクトをエクスポートします");
                SelectSpineProjectsRoot();
                EditorGUILayout.Space();

                EditorGUILayout.LabelField("このフォルダ以下にフォルダ構成に沿って出力します");
                SelectOutputRootDirectory();
                EditorGUILayout.Space();

                ExportAllButton();
                EditorGUILayout.Space();
          }
        }

        /// <summary>
        /// プロジェクトファイル選択GUI
        /// </summary>
        private void SelectSpineProject()
        {
            using(new EditorGUILayout.VerticalScope(GUI.skin.box))
            {
                _spineProjectPath = EditorGUILayout.TextField("Spineプロジェクトファイル", _spineProjectPath);
                if(GUILayout.Button("Spineプロジェクト選択"))
                {
                    string path = EditorUtility.OpenFilePanel("Spineプロジェクト選択", _spineProjectPath, "spine");
                    if(!string.IsNullOrEmpty(path))
                    {
                        _spineProjectPath = path.Replace("\\", "/");
                        Repaint();
                    }
                }
            }
        }

        /// <summary>
        /// 出力先選択GUI
        /// </summary>
        private void SelectOutputDirectory()
        {
            using(new EditorGUILayout.VerticalScope(GUI.skin.box))
            {
                _outputDirectory = EditorGUILayout.TextField("エクスポートフォルダ", _outputDirectory);
                if(GUILayout.Button("エクスポートフォルダ選択"))
                {
                    string path = EditorUtility.OpenFolderPanel("エクスポートフォルダ選択", _outputDirectory, "");
                    if(!string.IsNullOrEmpty(path))
                    {
                        _outputDirectory = path.Replace("\\", "/");
                        Repaint();
                    }
                }
            }
        }

        /// <summary>
        /// エクスポート実行ボタンGUI
        /// </summary>
         private void ExportButton()
        {
            bool disabled = string.IsNullOrEmpty(_spineProjectPath) || string.IsNullOrEmpty(_outputDirectory);
            using(new EditorGUI.DisabledScope(disabled))
            {
                if(GUILayout.Button("エクスポート実行"))
                {
                    _spineExporter.Export(_spineProjectPath, _outputDirectory);
                }
            }
        }


        /// <summary>
        /// プロジェクトファイル選択GUI
        /// </summary>
        private void SelectSpineProjectsRoot()
        {
            using(new EditorGUILayout.VerticalScope(GUI.skin.box))
            {
                _spineProjectsRootDirectory = EditorGUILayout.TextField("Spineプロジェクトルート", _spineProjectsRootDirectory);
                if(GUILayout.Button("Spineプロジェクトルートフォルダ選択"))
                {
                    string path = EditorUtility.OpenFolderPanel("Spineプロジェクトルートフォルダ選択", _spineProjectsRootDirectory, "");
                    if(!string.IsNullOrEmpty(path))
                    {
                        _spineProjectsRootDirectory = path.Replace("\\", "/");
                        Repaint();
                    }
                }
            }
         ;   
        }

        /// <summary>
        /// 出力先選択GUI
        /// </summary>
        private void SelectOutputRootDirectory()
        {
            using(new EditorGUILayout.VerticalScope(GUI.skin.box))
            {
                _outputRootDirectory = EditorGUILayout.TextField("エクスポートフォルダルート", _outputRootDirectory);
                if(GUILayout.Button("エクスポートフォルダルート選択"))
                {
                    string path = EditorUtility.OpenFolderPanel("エクスポートフォルダルート選択", _outputRootDirectory, "");
                    if(!string.IsNullOrEmpty(path))
                    {
                        _outputRootDirectory = path.Replace("\\", "/");
                        Repaint();
                   }
                }
            }
        }

        /// <summary>
        /// エクスポート実行ボタンGUI
        /// </summary>
        private void ExportAllButton()
        {
            bool disabled = string.IsNullOrEmpty(_spineProjectsRootDirectory) || string.IsNullOrEmpty(_outputRootDirectory);
            using(new EditorGUI.DisabledScope(disabled))
            {
                if(GUILayout.Button("全ファイルエクスポート実行"))
                {
                    //  指定ディレクトリ以下の.spineファイルを取得
                    string[] files = System.IO.Directory.GetFiles(_spineProjectsRootDirectory, "*.spine", System.IO.SearchOption.AllDirectories);
                    foreach(var file in files)
                    {
                        //  Windowsではディレクトリの区切り文字が違うので変換して使う
                        var filepath = file.Replace("\\", "/");
                        var dir = Path.GetDirectoryName(filepath).Replace("\\", "/");

                        //  出力先ディレクトリを作成
                        string outputDir = dir.Replace(_spineProjectsRootDirectory, _outputRootDirectory); 
                        if(!Directory.Exists(outputDir))
                        {
                            Directory.CreateDirectory(outputDir);
                        }

                        //  変換実行
                        _spineExporter.Export(filepath, outputDir);
                    }
                }
            }
        }
    }
}

```

</div></details>

##Assets/Editor/SpineUtility/SpineExporter.cs

エクスポートコマンドの呼び出し、プレハブの生成を行います
今回は省略してますが、テクスチャの設定やRendererの設定、初期アニメーションを変更する、
などを変換処理の中に追加しておくといちいち変更しなくてすみます

<details><summary>SpineExporter.cs</summary><div>

```csharp:SpineExporter.cs
using System.IO;
using UnityEngine;
using UnityEditor;
using Spine.Unity;

namespace SpineUtility
{
    public class SpineExporter
    {
        /// <summary>
        /// SpineプロジェクトからUnityで必要なアセットをエクスポートしてプレハブを作成
        /// </summary>
        /// <param name="projectPath">.spineファイル</param>
        /// <param name="outputDirectory">出力先ディレクトリ</param>
        public void Export(string projectPath, string outputDirectory)
        {
            try
            {
                EditorUtility.DisplayProgressBar("SpineExporter", "エクスポート中", 0);

                if(ExportAssets(projectPath, outputDirectory) != 0)
                {
                    EditorUtility.DisplayDialog("エラー", "エクスポートコマンドの実行に失敗しました", "ok");
                    return;
                }

                //  エクスポートファイルがインポートされる
                AssetDatabase.Refresh();
                //  必要があればここでテクスチャの設定を変更したり、Mixの変更をしてりするとよい

                CreatePrefab(projectPath, outputDirectory);
            }
            finally
            {
                EditorUtility.ClearProgressBar();
            }
        }

        /// <summary>
        /// Unityで使用するSpineのアセットを出力
        /// </summary>
        /// <param name="projectPath">.spineファイル</param>
        /// <param name="outputDirectory">出力先ディレクトリ</param>
        /// <returns></returns>
        private int ExportAssets(string projectPath, string outputDirectory)
        {
            string command  = $"\"{SpineConfig.SpinePath}\" "
                            + $"-u \"{SpineConfig.SpineVersion}\" "
                            + $"-i \"{projectPath}\" "
                            + $"-m -o \"{outputDirectory}\" "
                            + $"-e \"{SpineConfig.ExportSetting}\" ";
            return CommandLine.Run(command);    
        }

        /// <summary>
        /// SkeletonAnimationのプレハブを作成する
        /// </summary>
        /// <param name="projectPath">.spineファイル</param>
        /// <param name="outputDirectory">出力先ディレクトリ</param>
        private void CreatePrefab(string projectPath, string outputDirectory)
        {
            //UnityのAPIを呼ぶ際はプロジェクトフォルダからのパスにする
            string outDir = outputDirectory.Replace(Application.dataPath, "Assets");
            string filename = Path.GetFileNameWithoutExtension(projectPath);

            string dataAssetPath = $"{outDir}/{filename}_SkeletonData.asset";
            var dataAsset = AssetDatabase.LoadAssetAtPath<SkeletonDataAsset>(dataAssetPath);
            if(dataAsset == null)
            {
                UnityEngine.Debug.LogError("SkeletonDataAssetが見つかりません");
                return;
            }

            //  すでにプレハブがあるかもしれない
            if(FindPrefab(filename, dataAsset) != null)
            {
                return;
            }

            var skeletonAnimation = SkeletonAnimation.NewSkeletonAnimationGameObject(dataAsset);
            //  必要があればコンポーネントの設定を変更する


            //  プレハブの保存
            bool success = false;
            var prefab = PrefabUtility.SaveAsPrefabAsset(skeletonAnimation.gameObject, $"{outDir}/{filename}.prefab", out success);
            if(success)
            {
                EditorGUIUtility.PingObject(prefab);
            }
            else
            {
                UnityEngine.Debug.LogError("プレハブの作成に失敗しました", dataAsset);
            }
            Object.DestroyImmediate(skeletonAnimation.gameObject);
        }


        /// <summary>
        /// スケルトンが使用されているアセットを探す
        /// </summary>
        /// <param name="filename">名前</param>
        /// <param name="dataAsset">SkeletonDataAsset</param>
        /// <returns>見つかったプレハブ</returns>
        private GameObject FindPrefab(string filename, SkeletonDataAsset dataAsset)
        {
            var guids = AssetDatabase.FindAssets($"t:Prefab {filename}");
            foreach(var guid in guids)
            {
                var path = AssetDatabase.GUIDToAssetPath(guid);
                var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
                var skeletonAnimations = prefab.GetComponentsInChildren<SkeletonAnimation>(true);
                foreach(var skeletonAnimation in skeletonAnimations)
                {
                    if(skeletonAnimation.skeletonDataAsset == dataAsset)
                    {
                        EditorGUIUtility.PingObject(prefab);
                        UnityEngine.Debug.Log("すでにプレハブが作成されていました", prefab);
                        return prefab;
                    }
                }
            }
            return null;
        }
    }
}
```

</div></details>

##Assets/Editor/SpineUtility/CommandLine.cs

コマンド実行を行います
ここはあんまり詳しくないです

<details><summary>CommandLine.cs</summary><div>

```csharp:CommandLine.cs

using System.Diagnostics;

namespace SpineUtility
{
    public static class CommandLine
    {
        /// <summary>
        /// コマンドプロンプトでコマンドを実行する
        /// </summary>
        /// <param name="command">実行コマンド</param>
        /// <returns>終了ステータス</returns>
        public static int Run(string command)
        {
            var process = new Process
            {
                StartInfo =
                {
                    FileName = "cmd.exe",
                    Arguments = $"/c \"{command}\"",
                    CreateNoWindow = true,
                    UseShellExecute = false, 
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                }
            };
            process.Start();
            process.WaitForExit();

            string output = process.StandardOutput.ReadToEnd();
            if(!string.IsNullOrEmpty(output))
            {
                UnityEngine.Debug.Log(output);
            }
            string error = process.StandardError.ReadToEnd();
            if(!string.IsNullOrEmpty(error))
            {
                UnityEngine.Debug.LogError(error);
            }
            int exitCode = process.ExitCode;

            process.Close();
            return exitCode;
        }
    }
}
```

</div></details>


#使い方

メニューの「SpineUtility/SpineExporterWindow」を選択してウィンドウを表示させます

1ファイルの時は上の方の2か所を入力して3番目のボタンを押します

特定フォルダ以下を全部変換する際は下の2か所を入力して一番下のボタンを押します
指定したフォルダの構成に沿ってフォルダを作って出力します

![spine_exporter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298712/e115992f-99e1-f12a-7ee2-f063fcb64332.png)


#最後に

実際のプロジェクトではパラメータ設定やフォルダ構成が異なると思います
キャラはここでこんな設定、ステージはここであんな設定とかとか
エクスポート設定ファイルや出力先の選択ルールが決まっていれば自動化も可能だと思います

ちなみにSpineエディタの起動が一番時間がかかっている部分なので、
複数ファイルでコマンドをまとめて送ると速くなったりします
全ファイルの書き出しが成功すること前提です