はじめに
UnityEditor用のアセット周りの機能を作っていると、AssetDatabase.FindAssetsを使う機会もそれなりにあるかと思われます。
AssetDatabase.FindAssetsはフィルター文字列の条件を満たすアセットを探してそれらのGUIDを文字列で取得するメソッドです。
ただ、AssetDatabase.FindAssetsは結果(戻り値)としてstring[]を返します。ランダムアクセスしないような逐次処理で使うのには少し無駄があるようにも感じますね。
そのうえ、アセットデータベースが同じ状態かつ同じ検索条件であっても、AssetDatabase.FindAssetsを呼び出すたびに配列が新しく生成されます(ただの配列なので渡された後に要素の差し替えなどされる可能性もあるため)。
ただ、先述の通りシーケンシャルアクセスのみで済むような逐次処理で使う場合にわざわざ配列が生成されるのは好ましくないので、そもそも最初から配列を生成せずにIEnumerableで取得できるようにします。
動作を確認した環境
- Unity
6000.0.60f1 - OS
Windows 11 25H2
実装について
AssetDatabase.FindAssetsはどこで配列を作っているか
これがわからないとどの処理が使えてどの処理が使えないかがわからないため、まずはUnity公式が公開しているC#部分の実装であるUnityCsReferenceを見ていきます。
AssetDatabaseの実装は複数のファイルに分けられて行われていました。FindAssetsはそのうちのEditor/Mono/AssetDatabase/AssetDatabaseSearching.csに実装されています。
公開されている1引数のFindAssets(public static string[] FindAssets(string))は2引数のFindAssets(public static string[] FindAssets(string, string[]))の第2引数にnullを渡す形で実装されています(当該箇所)。
また、2引数の方は、SearchFilterというinternalクラスを生成し、それをinternalなFindAssets(internal static string[] FindAsset(SearchFilter))に渡し、その戻り値をそのまま返すという処理をしています(当該箇所)。
internalな1引数のFindAssetsでは、FindAllAssetsに第1引数を渡し、その戻り値のコレクションからGUIDを取り出し、重複をなくした上で配列にする処理が行われていました(当該箇所)。
FindAllAssetsは、AssetDatabase内に存在するinternalな静的メソッドで、SearchFilterを1つ引数に取り、IEnumerable<HierarchyProperty>を返します。
したがって、GUIDの列挙は配列を生成せずとも行えるということになります。
実装の準備
- フォルダーを作成し、その中にAssembly Definition(asmdef)を作成します
その時のアセット名は「Unity.InternalAPIEditorBridge.0xx(xxには01~24が入る)」とします(別の名前で作成してしまった場合はアセット内のNameを先述のものに変更してください) -
- のasmdefと同じフォルダー内にcsファイルを生成し(今回の例では
AssetDatabaseUtil.cs)、そこに以下のようなコードを記述します
- のasmdefと同じフォルダー内にcsファイルを生成し(今回の例では
- 必要に応じて、1. のasmdefへの参照を設定し、2. で記述したメソッドを呼び出す
public static class AssetDatabaseUtil
{
// フォルダーを指定しない版
public static IEnumerable<string> FindAssets(string filter)
{
// フォルダーを指定せずにアセットを列挙する
return FindAssets(filter, null);
}
// フォルダーを指定する版
public static IEnumerable<string> FindAssets(string filter, string[] searchInFolders)
{
// SearchFilterを準備する
SearchFilter searchFilter = new()
{
searchArea = SearchFilter.SearchArea.AllAssets
};
SearchUtility.ParseSearchString(filter ?? "", searchFilter);
if (searchInFolders != null && searchInFolders.Length > 0)
{
searchFilter.folders = searchInFolders;
searchFilter.searchArea = SearchFilter.SearchArea.SelectedFolders;
}
// アセットを列挙する
return AssetDatabase.FindAllAssets(searchFilter).Select(x => x.guid).Distinct();
}
}
解説
非公開(internalやprivate)のメソッドを使用するための方法はいくつかあり、その中でもリフレクションがもっとも有名で手軽かつ使える範囲の広いものではありますが、今回はメソッドに渡す引数の中に非公開クラスがあるため、今回はUnityEditor.dllに[InternalsVisibleTo]が付与されていることを利用して、この属性で示された名前のアセンブリを作成し、そこから普段通りにアクセスする方法を採用しました。
UnityEditor.dllには、Unity.InternalAPIEditorBridgeから始まる合計29個のアセンブリに対する[InternalsVisibleTo]が付与されています(当該箇所)。
| 接頭辞 | 数値の範囲 |
|---|---|
Unity.InternalAPIEditorBridge. |
001 ~ 024
|
Unity.InternalAPIEditorBridgeDev. |
001 ~ 005
|
これらの名前を持つアセンブリからは[InternalsVisibleTo]が付与されたアセンブリのinternalなもの(型やメソッドなど)を普段通り使うことができます。
ちょっと追加
上記の処理では、プロジェクトのすべてが検索対象になります。
というわけで、Assetsフォルダー以下のみを検索対象とする処理を以下に記載します。
// Assetsフォルダーのみを探す版
public static IEnumerable<string> FindAssetsInAssets(string filter)
{
// SearchFilterを準備する
SearchFilter searchFilter = new()
{
searchArea = SearchFilter.SearchArea.InAssetsOnly
};
SearchUtility.ParseSearchString(filter ?? "", searchFilter);
// アセットを列挙する
return AssetDatabase.FindAllAssets(searchFilter).Select(x => x.guid).Distinct();
}
先述のコードとほとんど同じですが、searchFilter.searchAreaの値をAllAssetsからInAssetsOnlyになっています。
この値はアセットを探すルートフォルダーを決めるもののようで、他にはPackagesフォルダー以下のみを検索対象にするInPackagesOnlyがあります(これについては私自身が使い道を思いつかなかったため今回はコードを例示していません)。