【Unity】ScriptableObjectのAssetファイルを自動で出力生成するエディタ拡張


※この方法だと値がリセットされてしまうため、回避方法を模索中です。

概要

置いておくだけでScriptableObjectのAssetファイルを自動で出力・更新してくれるエディタ拡張を作成しました。

ScriptableObjectの生成について

UnityでScriptableObjectのスクリプトを作ったとき、[CreateAssetMenu]属性などでAssetファイルを手動出力する必要があります。

(例:Data.cs→CreateAsset→Data.asset)

これが1回ならいいのですが、パラメータ用の変数追加等で再出力するとなると何回も出力作業をするしかないため非常に億劫です。
(しかも、名前が「new Data1.asset」みたいになったりする! やだやだ!)
自作ウィンドウやコンテキストメニュー等、色んな所に出力手段を作っている記事はありますが、やっぱりどうしても面倒です。

自動で出力したい……!

機能

というわけで、特定の入力ディレクトリと出力ディレクトリを指定し

  • Unity起動時
  • フォルダやスクリプト生成、素材インポートなどのアセット更新時
  • メニューから手動で実行時

にプロジェクト内のScriptableObjectのAssetを一括で正しく配置(生成・上書き・削除)してくれるエディタ拡張を作りました。

"Assets/Scripts/Data"
にスクリプトを作っておくと

"Assets/Resources/Data"
に同じフォルダ構成でアセットが自動で作られる!

みたいな感じです。

使い方

  • ScriptableObjectCreator.csをAssets/Editorに入れてください
  • その時点でアセットの更新が入るので、ScriptableObjectのAssetが必要に応じて出力ディレクトリに配置されます
  • あとは置いておけば、起動時、アセット更新時(ファイル作成や更新時)にScriptableObjectのAssetが整理されます
  • 手動で再配置を実行したいときは、Unityのメニューから「Tool」→「Create ScriptableObject」を選択してください

余計なアセット(対応する.csがないやつ)があれば削除、必要なところにはアセットを生成してくれます。
常に整理がなされるため、常に適切な箇所にアセットが置かれている状態を保つことができます。

ScriptableObjectCreator.cs
using System.IO;
using UnityEditor;
using UnityEngine;

// 起動時に実行.
[InitializeOnLoad]
public class ScriptableObjectCreator : EditorWindow
{
    // スクリプトの入力パス.
    private static string inputPath = "Assets/Scripts/Data";
    // アセットの出力パス.
    private static string outputPath = "Assets/Resources/Data";

    // スクリプトの拡張子.
    public static readonly string scriptExtension = ".cs";
    // アセットの拡張子.
    public static readonly string assetExtension = ".asset";

    // コンストラクタ(起動時に呼び出される).
    static ScriptableObjectCreator()
    {
        // 処理を呼び出す.
        UpdateAssets();
    }

    // アセット更新時に実行.
    public static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetsPath)
    {
        // 処理を呼び出す.
        UpdateAssets();
    }

    // メニューにアイテムを追加.
    [MenuItem("Tools/Create ScriptableObject")]
    // 呼び出す関数.
    public static void UpdateAssets()
    {
        // アセットを削除する.
        DeleteAll(outputPath);

        // アセットを生成する.
        CreateAll(inputPath);

        // データベースをリフレッシュする.
        AssetDatabase.Refresh();
    }

    // 出力先をチェック.
    public static void DeleteAll(string targetPath)
    {
        // ディレクトリパスの配列.
        string[] directories = Directory.GetDirectories(targetPath);
        // ファイルパスの配列.
        string[] files = Directory.GetFiles(targetPath);

        // ファイルを削除.
        foreach (var file in files)
        {
            DeleteAsset(file);
        }

        // さらに深い階層を探索.
        foreach (var directory in directories)
        {
            DeleteAll(directory);
        }
    }

    // 入力元をチェック.
    public static void CreateAll(string targetPath)
    {
        // ディレクトリパスの配列.
        string[] directories = Directory.GetDirectories(targetPath);
        // ファイルパスの配列.
        string[] files = Directory.GetFiles(targetPath);

        // ファイルを生成.
        foreach (var file in files)
        {
            CreateAsset(file);
        }

        // さらに深い階層を探索.
        foreach (var directory in directories)
        {
            CreateAll(directory);
        }
    }

    // アセットを削除.
    public static void DeleteAsset(string targetPath)
    {
        // ファイル情報.
        string directoryName = Path.GetDirectoryName(targetPath);
        string fileName = Path.GetFileName(targetPath);
        string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(targetPath);
        string extension = Path.GetExtension(targetPath);

        // スクリプトのパス.
        string scriptPath = inputPath.Replace("/", "\\");
        // アセットのパス.
        string assetPath = outputPath.Replace("/", "\\");

        // アセットファイルである.
        if (extension == assetExtension)
        {
            // アセットのパス.
            string filePath = targetPath.Replace("/", "\\");
            // スクリプトのパス.
            string checkPath = Path.Combine(directoryName.Replace(assetPath, scriptPath), fileNameWithoutExtension + scriptExtension);

            // 対応するファイルが存在しない.
            if (!File.Exists(checkPath))
            {
                // 削除.
                AssetDatabase.DeleteAsset(filePath);

                Debug.Log("ScriptableObject Deleteed : " + filePath);
            }
        }
    }

    // アセットを生成.
    public static void CreateAsset(string targetPath)
    {
        // ファイル情報.
        string directoryName = Path.GetDirectoryName(targetPath);
        string fileName = Path.GetFileName(targetPath);
        string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(targetPath);
        string extension = Path.GetExtension(targetPath);

        // スクリプトのパス.
        string scriptPath = inputPath.Replace("/", "\\");
        // アセットのパス.
        string assetPath = outputPath.Replace("/", "\\");

        // スクリプトファイルである.
        if (extension == scriptExtension)
        {
            // 生成.
            var obj = CreateInstance(fileNameWithoutExtension);
            string filePath = Path.Combine(directoryName.Replace(scriptPath, assetPath), fileNameWithoutExtension + assetExtension);
            CreateAssetWithOverwrite(obj, filePath);

            Debug.Log("ScriptableObject Created : " + filePath);
        }
    }

    // アセットを上書きで作成する(metaデータはそのまま).
    public static void CreateAssetWithOverwrite(UnityEngine.Object asset, string exportPath)
    {
        // アセットが存在しない場合はそのまま作成(metaファイルも新規作成).
        if (!File.Exists(exportPath))
        {
            AssetDatabase.CreateAsset(asset, exportPath);
            return;
        }

        // 仮ファイルを作るためのディレクトリを作成.
        var fileName = Path.GetFileName(exportPath);
        var tmpDirectoryPath = Path.Combine(exportPath.Replace(fileName, ""), "tmp");
        Directory.CreateDirectory(tmpDirectoryPath);

        // 仮ファイルを保存.
        var tmpFilePath = Path.Combine(tmpDirectoryPath, fileName);
        AssetDatabase.CreateAsset(asset, tmpFilePath);

        // 仮ファイルを既存のファイルに上書き(metaデータはそのまま).
        FileUtil.ReplaceFile(tmpFilePath, exportPath);

        // 仮ディレクトリとファイルを削除.
        AssetDatabase.DeleteAsset(tmpDirectoryPath);

        // データ変更をUnityに伝えるためインポートしなおし.
        AssetDatabase.ImportAsset(exportPath);
    }
}

これですっきり。
ご拝読、ありがとうございました。

こちらを参考にさせていただきました
https://qiita.com/JGS_Developer/items/2b7e9cc91b0113c477fc
https://kan-kikuchi.hatenablog.com/entry/CreateAssetWithOverwrite