Programming Serendipity

プログラミングを中心に種々雑多に書き留めます

フォルダを再帰的に生成する

この記事はさいたまげーむす Advent Calendar 2016 16日目の記事です。

AssetDatabse.CreateAsset()でScriptableObjectを作ろうと思い、パスの途中のフォルダは存在しなければ自動的に作られるかと思ったら、フォルダは明示的に作る必要があることに気づく!しかし面倒くさい!ならばいっぺんに作るメソッドを作ろう。ということで作りました。

C#のメリットをふんだんに生かしました。実行時は(パフォーマンス的に)Cっぽいコードを強いられるケースもありますが、エディターなので奇麗なコードが書けます。

using UnityEngine;
using System.Linq;
using UnityEditor;

public class EditorScript 
{
    /// <summary>
    /// 複数階層のフォルダを作成する
    /// </summary>
    /// <param name="path">一番子供のフォルダまでのパスe.g.)Assets/Resources/Sound/</param>
    /// <remarks>パスは"Assets/"で始まっている必要があります。Splitなので最後のスラッシュ(/)は不要です</remarks>
    public static void CreateFolderRecursively(string path)
    {
        Debug.Assert(path.StartsWith("Assets/"), "arg `path` of CreateFolderRecursively doesn't starts with `Assets/`");

        // もう存在すれば処理は不要
        if (AssetDatabase.IsValidFolder(path)) return;

        // スラッシュで終わっていたら除去
        if (path[path.Length - 1] == '/')
        {
            path = path.Substring(0, path.Length - 1);
        }

        var names = path.Split('/');
        for (int i = 1; i < names.Length; i++)
        {
            var parent = string.Join("/", names.Take(i).ToArray());
            var target = string.Join("/", names.Take(i + 1).ToArray());
            var child = names[i];
            if (!AssetDatabase.IsValidFolder(target))
            {
                AssetDatabase.CreateFolder(parent, child);
            }
        }
    }
}

前回の記事のコードと合わせて使ってみましょう。

    class ObjA : ScriptableObject
    {
        public int blah1;
        public bool blah2;
        public string blah3;
    }

    class ObjB : ScriptableObject
    {
        public List<ObjA> objs = new List<ObjA>();
    }


    [MenuItem("Tools/Create SO")]
    public static void CreateScriptableObject()
    {
        // こーんなパスでも大丈夫b
        const string path = "Assets/a/b/c/d/e/f/g/h/i/j/k/l/m/n/B.asset";

        var b = ScriptableObject.CreateInstance<ObjB>();
        for (int i = 0; i < 10; ++i)
        {
            var a = ScriptableObject.CreateInstance<ObjA>();
            // プロジェクトビューで表示される名前
            a.name = string.Format("a[{0}]", i);
            b.objs.Add(a);
        }

        // ここでアセット作成前にフォルダ作成
        CreateFolderRecursively(path.Substring(0, path.LastIndexOf('/')));
        AssetDatabase.CreateAsset(b, path);

        // アセットとして作成済みのものに対して追加する
        for (int i = 0; i < 10; ++i)
        {
            AssetDatabase.AddObjectToAsset(b.objs[i], path);
        }

        // アセットを再インポートして反映させる(これをしない場合、プロジェクトを保存するまで反映されない)
        AssetDatabase.ImportAsset(path);
    }

f:id:q7z:20161216002249p:plain らくちん!

追記

もっと簡単な方法を教えてもらいました。

// using directives
using System.IO;

// in CreateScriptableObject()
        const string dirPath = "Assets/a/b/c/d/e/f/g/h/i/j/k/l/m/n/";
        if (!Directory.Exists(dirPath))
        {
            Directory.CreateDirectory(dirPath);
        }

.metaファイル周りの動作がセットになるのでフォルダ周りはUnityの提供する関数を使ったほうがいいかなと思っていましたが、フォルダを認識し次第自動生成されますから生成に関してはC#の標準ライブラリで問題ないようです。灯台下暗しでした。