Unity のフォルダ整理を半自動で行うアセットを無料で公開してみる

今回のテーマは
「Unity のフォルダ整理を半自動で行うアセットを無料で公開してみる」
です。

フォルダ整理をしてくれるアセットが
ネット上に見つからなかったため
存在しない理由が何かあるのかもしれませんが
ここは思い切って作成してみました。

フォルダ分けのルールに関しては
ネット上の記事を参考にしています。

フォルダ整理に自信がない方や
他の人が作成したプロジェクトを見る状況にいる方は
使用すると管理や閲覧が楽になるかもしれません。

本番環境に導入したい場合は
必ずプロジェクトを複製して
気に入るか試してから
バグが発生しないかも試してから
本番環境で使用して下さい。

またこのアセットを使用することで
テスト環境で見つからなかったバグが発生するなど
利用者が不利益を被ることが万が一あった場合に
我々は責任を負わないものとさせて下さい。
何卒よろしくお願いします。

***

こんにちは uni928 です。
皆さんは Unity のフォルダ整理に自信がありますか？

Scripts フォルダの中に SceneA フォルダを作って Human.cs を作成した
→ SceneB でもそのスクリプトを使ってしまった
→ 面倒なのでそのままにした
といった経験は誰にでもあるのではないでしょうか？
SceneA フォルダに SceneB で使うスクリプトが混在すると混乱してしまいますが
2 個以上のシーンで使うスクリプトをまとめるフォルダに移動させるのは
それはそれで面倒なものです。

また人によっては
他の人が作成したプロジェクトを確認しようとしたら
乱雑に配置されていて確認するのが大変だった
という経験もあるかもしれません。

そこで Unity のフォルダ整理を自動で行うアセットを作って
そういった場面に対応できないかと考えました。

フォルダ整理を自動で行うアセットがネット上に存在しないというのは
何か理由があるのかもしれません。
フォルダを移動することはバグの原因になりえるかもしれません。
ですがここは思い切って作成してみることにしました。

繰り返しにはなりますが
フォルダ整理のアセットはバグの原因になりやすい類の物です。
ですのでこのアセットを使用することで
テスト環境で見つからなかったバグが発生するなど
利用者が不利益を被ることが万が一あるかもしれません。
そういった場合に我々は責任を負わないものとさせて下さい。
何卒よろしくお願いします。

また、本番環境に導入したい場合は
必ずプロジェクトを複製して
気に入るか試してから
バグが発生しないかも試してから
本番環境で使用して下さい。

***

配布先

[ver1.1.4 はこちら (最新版)](https://drive.google.com/file/d/1EuwCiYTttfgwJ6yMBZW2p-GcQmuzC728/view?usp=sharing)

ver1.1.3 alpha の公開は終了しました

ver1.1.2 alpha の公開は終了しました

[ver1.1.1 はこちら(ver1.1.4 で動作しない場合はお試し下さい](https://drive.google.com/file/d/1PFtPQ4UFihe17DxLkI5K16K-TlBYyVz8/view?usp=sharing)

[ver1.1.0 はこちら](https://drive.google.com/file/d/18AIN6SAS9dXlhcrTGcRgF8ERtMShPYoX/view?usp=sharing)

[ver1.0.0 はこちら](https://drive.google.com/file/d/1m2d5Za5siWAqQh2ppI2wWsyizQG_7IRJ/view?usp=sharing)

***

アセットの説明

Unity のフォルダ整理を半自動で行う機能です。

この機能をインポートした上で
特定のフォルダに仕分けしたいファイル・フォルダを入れて
Unity 上部のメニューバーから実行することで
後は自動で仕分けされます。

フォルダ整理に自信がない方や
他の人が作成したプロジェクトを見る状況にいる方は
使用すると管理や閲覧が楽になるかもしれません。

この機能で仕分けをした後に
開発を進めて行くとフォルダが乱雑になっていくと思います。
なので定期的に再仕分けすることを推奨します。

シーンファイルの依存関係のみを確認して使用しているか確認しているため
Resources.Load で読み込むなど依存関係が存在しない読み込みに関しては
使用していると判定されません。
なので使用中の判定はかなり甘いかもしれません。
ご了承下さい。

***

フォルダのルール

フォルダ構成は
Assets/Project フォルダの中に

「Prefabs」
「Scripts」
「Animations」
「Materials」
「PhysicsMaterials」
「Fonts」
「Textures」
「Audio」
「Models」
「Shaders」
「Resources」
「Editor」
「Plugins」
「Other」

フォルダを作成して
それぞれの中に
"2個以上のシーンで依存関係がある"
"1個のシーンで依存関係がある"
"0個のシーンで依存関係がある"
フォルダが作成されます。(もちろんフォルダ名は短い英語です)

Project フォルダを作成する理由は
アセットを導入した時に Asset 直下に配置されるため
アセットと非アセットが分かりやすくなるからです。

"1個のシーンで依存関係がある" フォルダの中には
シーン名のフォルダが作成されて
格納されていきます。

***

How To Use

1. 仕分けしたいプロジェクトを複製します。
**「2」～「6」の作業は複製したプロジェクトで行います。
複製しない場合は、毎回何かしらの方法でバックアップを取って下さい。**
2. アセットをインポートします。
3. 仕分けしたいフォルダ・ファイルを全て
　　Assets/SephirothOrganizeFolders/OrganizeFolders フォルダに移動します。
　　(シーンファイルは必ず全て移動して下さい)
4. Unity 上部のメニューバーから
　　Tools/ExecSephirothOrganizeFolders を実行します。
5. 後は自動で仕分けされるため、仕分け結果が気に入るか確認します。
6. 実行してバグが発生しないか確認します。
7. 「5」「6」を確認して OK ならば本番環境で同じ作業を行います。

***

あとがき

今回作成したフォルダ整理のアセットは
いかがでしたでしょうか？

フォルダ整理は
ついサボりがちになるかもしれません。
そういった場合にも是非役立てて頂けると
開発した身として非常に嬉しいです。

皆さんの開発の助けになれますように。
閲覧ありがとうございました。

***

追記

2024年4月24日に
実行すると自動でバックアップを取るよう
バージョンアップしました。
ver1.1.0 以降が、そのバージョンです。
ver1.1.0 以降の方が安全です。

isAutoSave (デフォルトは false) が
false の場合は
途中でエラーが出ずに完了した場合に
バックアップが削除されます。

true の場合は
途中でエラーが出ずに完了した場合も
バックアップは残ります。

true の場合も false の場合も
エラーが出た時はバックアップが残ります。

バックアップは unitypackage で保存されます。

***

追記

内容を確認できるよう
コードを置いておきます。

ver1.1.1 以降は
ファイル数が多いため
ver1.1.0 の紹介になります。

地道に処理しているため
長いコードになってしまっている点は
申し訳ありません。

仕分けフォルダの階層は
厳守する必要がありますので
導入する場合は
unitypackage から導入する事を
推奨します。

```Csharp:仕分けを行っているコード
using System.Collections.Generic;
using UnityEngine;

namespace SephirothTools
{
    [UnityEditor.InitializeOnLoad]
    public class SephirothOrganizeFolders
    {
        public const bool isAutoSave = false;

        [UnityEditor.MenuItem("Tools/ExecSephirothOrganizeFolders")]
        private static void ExecSephirothOrganizeFolders()
        {
            System.DateTime nowDateTime = System.DateTime.Now;

            TakeBackUP(nowDateTime);

            List<string> subDirectoryList = GetSubDirectories("Assets/SephirothOrganizeFolders/OrganizeFolders");
            subDirectoryList.Add(AssetsToAbsolutePath("Assets/SephirothOrganizeFolders/OrganizeFolders"));

            List<string> sceneFilePathList = new();

            foreach (string oneDirectorie in subDirectoryList)
            {
                foreach (string oneFile in System.IO.Directory.GetFiles(AssetsToAbsolutePath(oneDirectorie)))
                {
                    if (oneFile.EndsWith(".unity") && System.IO.File.Exists(oneFile))
                    {
                        sceneFilePathList.Add(oneFile.Replace('\\', '/'));
                    }
                }
            }

            CreateDirectories(sceneFilePathList);

            foreach (string oneDirectorie in subDirectoryList)
            {
                foreach (string oneFile in System.IO.Directory.GetFiles(AssetsToAbsolutePath(oneDirectorie)))
                {
                    string changeOneFile = oneFile.Replace('\\', '/');
                    if (changeOneFile.Contains("SephirothOrganizeFolders/OrganizeFolders/HowToSephirothOrganizeFolders"))
                    {
                        continue;
                    }
                    if(System.IO.Path.GetFileName(changeOneFile).StartsWith("."))
                    {
                        continue;
                    }
                    if (changeOneFile.EndsWith(".meta"))
                    {
                        continue;
                    }
                    if(changeOneFile.EndsWith(".unity"))
                    {
                        continue;
                    }

                    string upFolderName = changeOneFile.Substring(0, changeOneFile.LastIndexOf("/"));
                    upFolderName = upFolderName.Substring(upFolderName.LastIndexOf("/") + 1);

                    string guid = null;
                    foreach(string oneLine in System.IO.File.ReadAllText(AssetsToAbsolutePath(changeOneFile + ".meta")).Split('\n'))
                    {
                        if(oneLine.StartsWith("guid"))
                        {
                            guid = oneLine.Substring(6);
                            break;
                        }
                    }
                    
                    List<string> findSceneNameList = new();
                    if (guid != null)
                    {
                        foreach (string oneScene in sceneFilePathList)
                        {
                            string readText = System.IO.File.ReadAllText(AssetsToAbsolutePath(oneScene));
                            if (readText.Contains(guid))
                            {
                                findSceneNameList.Add(oneScene);
                            }
                        }
                    }

                    string genreName = null;
                    if (changeOneFile.Contains("/Resources/"))
                    {
                        genreName = "Resources";
                    }
                    else if (changeOneFile.Contains("/Editor/"))
                    {
                        genreName = "Editor";
                    }
                    else if (changeOneFile.Contains("/Plugins/"))
                    {
                        genreName = "Plugins";
                    }
                    else if (changeOneFile.EndsWith(".cs") || changeOneFile.EndsWith(".js"))
                    {
                        genreName = "Scripts";
                    }
                    else if (changeOneFile.EndsWith(".prefab"))
                    {
                        genreName = "Prefabs";
                    }
                    else if (changeOneFile.EndsWith(".anim") || changeOneFile.EndsWith(".controller"))
                    {
                        genreName = "Animations";
                    }
                    else if (changeOneFile.EndsWith(".mat") || changeOneFile.EndsWith(".material"))
                    {
                        genreName = "Materials";
                    }
                    else if (changeOneFile.EndsWith(".physicmaterial"))
                    {
                        genreName = "PhysicsMaterials";
                    }
                    else if (changeOneFile.EndsWith(".ttf") || changeOneFile.EndsWith(".otf") || changeOneFile.EndsWith(".dfont"))
                    {
                           genreName = "Fonts";
                    }
                    else if (changeOneFile.EndsWith(".fbx") || changeOneFile.EndsWith(".dae") || changeOneFile.EndsWith(".dxf") || changeOneFile.EndsWith(".obj") || changeOneFile.EndsWith(".max") || changeOneFile.EndsWith(".blend"))
                    {
                        genreName = "Models";
                    }
                    else if (changeOneFile.EndsWith(".shader"))
                    {
                        genreName = "Shaders";
                    }
                    else if (changeOneFile.EndsWith(".png") || changeOneFile.EndsWith(".bmp") || changeOneFile.EndsWith(".exr") || changeOneFile.EndsWith(".gif") || changeOneFile.EndsWith(".hdr") || changeOneFile.EndsWith("iff") || changeOneFile.EndsWith(".jpeg") || changeOneFile.EndsWith(".jpg") || changeOneFile.EndsWith(".pict") || changeOneFile.EndsWith(".psd") || changeOneFile.EndsWith(".tga") || changeOneFile.EndsWith(".tiff") || changeOneFile.EndsWith(".tif"))
                    {
                        genreName = "Textures";
                    }
                    else if (changeOneFile.EndsWith(".mp3") || changeOneFile.EndsWith(".ogg") || changeOneFile.EndsWith(".wav") || changeOneFile.EndsWith(".aiff") || changeOneFile.EndsWith(".aif") || changeOneFile.EndsWith(".mod") || changeOneFile.EndsWith(".it") || changeOneFile.EndsWith(".s3m") || changeOneFile.EndsWith(".xm"))
                    {
                        genreName = "Audio";
                    }
                    else
                    {
                        genreName = "Other";
                    }

                    if (findSceneNameList.Count == 0)
                    {
                        string targetFile1 = AssetsToAbsolutePath("Assets/Projects/" + genreName + "/NoUse" + genreName + "/" + upFolderName + "/" + changeOneFile.Substring(changeOneFile.LastIndexOf("/") + 1) + ".meta");
                        string targetFile2 = AssetsToAbsolutePath("Assets/Projects/" + genreName + "/NoUse" + genreName + "/" + upFolderName + "/" + changeOneFile.Substring(changeOneFile.LastIndexOf("/") + 1));
                        {
                            string[] folderSplit = targetFile1.Split("/");
                            string nowFolder = folderSplit[0];
                            for(int i = 1; i < folderSplit.Length - 1; i++)
                            {
                                nowFolder += "/" + folderSplit[i];
                                if(!System.IO.Directory.Exists(nowFolder))
                                {
                                    System.IO.Directory.CreateDirectory(nowFolder);
                                }
                            }
                        }

                        if (System.IO.File.Exists(targetFile1))
                        {
                            System.IO.File.Delete(targetFile1);
                        }
                        System.IO.File.Move(AssetsToAbsolutePath((changeOneFile + ".meta")), targetFile1);

                        if (System.IO.File.Exists(targetFile2))
                        {
                            System.IO.File.Delete(targetFile2);
                        }
                        System.IO.File.Move(AssetsToAbsolutePath(changeOneFile), targetFile2);
                    }
                    else if(1 < findSceneNameList.Count)
                    {
                        string targetFile1 = AssetsToAbsolutePath("Assets/Projects/" + genreName + "/Common" + genreName + "/" + upFolderName + "/" + changeOneFile.Substring(changeOneFile.LastIndexOf("/") + 1) + ".meta");
                        string targetFile2 = AssetsToAbsolutePath("Assets/Projects/" + genreName + "/Common" + genreName + "/" + upFolderName + "/" + changeOneFile.Substring(changeOneFile.LastIndexOf("/") + 1));
                        {
                            string[] folderSplit = targetFile1.Split("/");
                            string nowFolder = folderSplit[0];
                            for (int i = 1; i < folderSplit.Length - 1; i++)
                            {
                                nowFolder += "/" + folderSplit[i];
                                if (!System.IO.Directory.Exists(nowFolder))
                                {
                                    System.IO.Directory.CreateDirectory(nowFolder);
                                }
                            }
                        }

                        if (System.IO.File.Exists(targetFile1))
                        {
                            System.IO.File.Delete(targetFile1);
                        }
                        System.IO.File.Move(AssetsToAbsolutePath((changeOneFile + ".meta")), targetFile1);

                        if (System.IO.File.Exists(targetFile2))
                        {
                            System.IO.File.Delete(targetFile2);
                        }
                        System.IO.File.Move(AssetsToAbsolutePath(changeOneFile), targetFile2);
                    }
                    else
                    {

                        string sceneName = findSceneNameList[0].Substring(findSceneNameList[0].LastIndexOf("/") + 1);
                        sceneName = sceneName.Substring(0, sceneName.Length - 6);

                        string targetFile1 = AssetsToAbsolutePath("Assets/Projects/" + genreName + "/Scene" + genreName + "/" + sceneName + "/" + upFolderName + "/" + changeOneFile.Substring(changeOneFile.LastIndexOf("/") + 1) + ".meta");
                        string targetFile2 = AssetsToAbsolutePath("Assets/Projects/" + genreName + "/Scene" + genreName + "/" + sceneName + "/" + upFolderName + "/" + changeOneFile.Substring(changeOneFile.LastIndexOf("/") + 1));
                        {
                            string[] folderSplit = targetFile1.Split("/");
                            string nowFolder = folderSplit[0];
                            for (int i = 1; i < folderSplit.Length - 1; i++)
                            {
                                nowFolder += "/" + folderSplit[i];
                                if (!System.IO.Directory.Exists(nowFolder))
                                {
                                    System.IO.Directory.CreateDirectory(nowFolder);
                                }
                            }
                        }

                        if (System.IO.File.Exists(targetFile1))
                        {
                            System.IO.File.Delete(targetFile1);
                        }
                        System.IO.File.Move(AssetsToAbsolutePath((changeOneFile + ".meta")), targetFile1);


                        if (System.IO.File.Exists(targetFile2))
                        {
                            System.IO.File.Delete(targetFile2);
                        }
                        System.IO.File.Move(AssetsToAbsolutePath(changeOneFile), targetFile2);
                    }
                }
            }
            foreach(string oneScene in sceneFilePathList)
            {
                string resultFileName = AssetsToAbsolutePath("Assets/Projects/Scenes/" + oneScene.Substring(oneScene.LastIndexOf("/") + 1));
                if(oneScene.Equals(resultFileName))
                {
                    continue;
                }
                if (System.IO.File.Exists(resultFileName))
                {
                    System.IO.File.Delete(resultFileName);
                }
                System.IO.File.Move(oneScene, resultFileName);
            }
            foreach(string oneDirectory in System.IO.Directory.GetDirectories(AssetsToAbsolutePath("Assets/SephirothOrganizeFolders/OrganizeFolders")))
            {
                UnityEditor.FileUtil.DeleteFileOrDirectory(AssetsToAbsolutePath(oneDirectory));
                System.IO.File.Delete(AssetsToAbsolutePath(oneDirectory + ".meta"));
            }

            UnityEditor.AssetDatabase.Refresh();

            if (!isAutoSave)
            {
#pragma warning disable CS0162 // Unreachable code detected
                System.IO.File.Delete("Assets/SephirothOrganizeFolders/OrganizeFolders/." + nowDateTime.ToString("yyyy_MM_dd_HH_mm_ss") + ".unitypackage");
#pragma warning restore CS0162 // Unreachable code detected
            }

            UnityEditor.AssetDatabase.Refresh();
            Debug.Log("Move Finished...");
        }

        private static void CreateDirectories(List<string> sceneFilePathList)
        {
            List<string> genreNameList = new();
            genreNameList.Add("Scripts");
            genreNameList.Add("Prefabs");
            genreNameList.Add("Animations");
            genreNameList.Add("Materials");
            genreNameList.Add("PhysicsMaterials");
            genreNameList.Add("Fonts");
            genreNameList.Add("Textures");
            genreNameList.Add("Audio");
            genreNameList.Add("Models");
            genreNameList.Add("Shaders");
            genreNameList.Add("Resources");
            genreNameList.Add("Editor");
            genreNameList.Add("Plugins");
            genreNameList.Add("Other");

            List<string> createList = new();
            createList.Add("Assets/Projects");
            createList.Add("Assets/Projects/Scenes");

            foreach (string oneGenreName in genreNameList)
            {
                createList.Add("Assets/Projects/" + oneGenreName);
                createList.Add("Assets/Projects/" + oneGenreName + "/Common" + oneGenreName);
                createList.Add("Assets/Projects/" + oneGenreName + "/NoUse" + oneGenreName);
                createList.Add("Assets/Projects/" + oneGenreName + "/Scene" + oneGenreName);
            }

            for(int i = 0; i < createList.Count; i++)
            {
                if(!System.IO.Directory.Exists(AssetsToAbsolutePath(createList[i])))
                {
                    System.IO.Directory.CreateDirectory(AssetsToAbsolutePath(createList[i]));
                }
            }
        }

        private static List<string> GetSubDirectories(string path)
        {
            string[] directoriesArr = System.IO.Directory.GetDirectories(AssetsToAbsolutePath(path));
            List<string> result = new List<string>(directoriesArr);
            foreach (string innerPath in directoriesArr)
            {
                result.AddRange(GetSubDirectories(innerPath));
            }
            return result;
        }

        private static string AssetsToAbsolutePath(string self)
        {
            if (self.StartsWith("Assets"))
            {
                self = Application.dataPath + self.Substring(6);
            }
            return self;
        }

	    private static void TakeBackUP(System.DateTime nowDateTime)
        {
            if (!System.IO.Directory.Exists(AssetsToAbsolutePath("Assets/SephirothOrganizeFolders/OrganizeFolders"))) System.IO.Directory.CreateDirectory(AssetsToAbsolutePath("Assets/SephirothOrganizeFolders/OrganizeFolders"));
            UnityEditor.AssetDatabase.ExportPackage("Assets", AssetsToAbsolutePath("Assets/SephirothOrganizeFolders/OrganizeFolders/." + nowDateTime.ToString("yyyy_MM_dd_HH_mm_ss") + ".unitypackage"), UnityEditor.ExportPackageOptions.Recurse);
        }
    }
}
```