やりたいこと
上記はUE4ぷちコン応募作品のフォルダ構造。
- 「PanicFarm」がプロジェクト本体のフォルダ。
- その他のフォルダは、UE4 Marketplaceなどから入手したアセット。
これをこのままパッケージングすると膨大なファイルサイズになる。なので、使用していないアセットは消去してしまいたい。だが、うっかり使っているものを消してしまうと、とんでもなく面倒くさいことになる。
UE4 には、Reference Viewer などの依存関係を調べるツールがあるし、削除時には関係しているアセットを列挙してくれるが、それでもうっかりミスをしないように気を付けて作業するのは、ひたすら面倒くさい。
ということで、
「プロジェクトのメインフォルダにあるアセットを参照している、Marketplaceアセットをずらっと列挙する」
ようなツールがほしい …がないので作る。
こういう時に、Editor Utility Widget や Editor Utility Blueprint (いわゆる Blutility)は非常に便利である。
IAssetRegistry::GetReferencers()
IAssetRegistry::GetReferencers() - UE4 C++ API Reference
引数に渡したアセットを参照しているアセットを列挙する関数。
Reference Viewer で使われているものと同じ(はず)。
Reference Viewer で、左側から中央のアセットに矢印が向かっているアセット群が Referencer に相当する。
この Referencer は、中央のアセットがないと動かない。
つまり、今回は、
- Marketplaceのフォルダ内のアセットが、
- プロジェクトのメインフォルダの中にあるアセットを「参照している」場合、
- そのMarketplaceアセットは使用中なので消してはいけない。
- プロジェクトのメインフォルダの中にあるアセットを「参照していない」場合、
- そのMarketplaceアセットは未使用なので消してよい。
- プロジェクトのメインフォルダの中にあるアセットを「参照している」場合、
という方針で作りたい。つまり(横長で見づらいが)、
…で言うところの「Marketplaceフォルダのアセットはプロジェクトフォルダ内のアセットを参照しているか?」の関数を用意すればよい。
なお、今回は使わないが、Reference Viewer の右側にあるアセット群は "Dependency" になる。
この Dependency を取得する関数が、IAssetRegistry::GetDependencies()。※ GetReferencer も GetDependencies も、同じものが Blutility にも用意されている。
循環参照でハマる
一つのアセットに対する Referencer は複数存在するし、依存関係は1階層では終わらずに、何階層にも続く。
そうすると、一つのアセットに対して、再帰的に Referencer をたどって、Referencer がいなくなるまで検索していく必要がある。
再帰的な検索は、関数の再帰呼び出しなり、スタックを用意するなりして解決できる。
ところが困ったことに、アセットの依存関係は複雑で、普通に循環参照が存在する。
上の画像では、PFBP_Buddy が PFBP_BuddyMarkers を参照し、PFBP_BuddyMarkers が PFBP_Buddy を参照している。
つまり、再帰検索ループで PFBP_Buddy → PFBP_BuddyMarkers → PFBP_Buddy → … と延々同じところを回ってしまう。要するに無限ループである。
これを回避するには「一度調べたアセットはもう調べない」ような仕組みを用意する必要がある。
本来は、依存関係が必ず一方向になるような設計にすべきである。
その辺が上手な人のプログラムは、非常にすっきりしている。
まあ、最初から全体像が見えてないと、なかなか難しいのだが。
ソースコード
class ReferencerNames
指定したアセット(単体)が参照しているアセット群を取得するクラス。
意味合い的には IAssetRegistry::GetReferencers()
のラッパ。
namespace assetutils
{
class ReferencerNames
{
public:
ReferencerNames(FAssetRegistryModule& InAssetRegistryModule, const FName& InPackageName)
{
ReferencerNameArray.Empty();
TArray<FName> hardReferences;
bool successFlag = InAssetRegistryModule.Get().GetReferencers(InPackageName, hardReferences,
EAssetRegistryDependencyType::Hard);
if (successFlag)
{
ReferencerNameArray.Append(hardReferences);
}
TArray<FName> softReferencers;
successFlag = InAssetRegistryModule.Get().GetReferencers(InPackageName, softReferencers,
EAssetRegistryDependencyType::Soft);
if (successFlag)
{
ReferencerNameArray.Append(softReferencers);
}
}
public:
const TArray<FName>& GetArray() const { return ReferencerNameArray; }
private:
TArray<FName> ReferencerNameArray;
}; // class ReferencerNames
} // namespace assetutils
class NameStack
配列(TArray)をスタックとして使用するためのラッパ。
namespace assetutils
{
class NameStack
{
public:
NameStack() {}
NameStack(const TArray<FName>& InInitialNames) : NameArray(InInitialNames) {}
public:
void Push(const FName& InName) { NameArray.Push(InName); }
void Push(const TArray<FName>& InNameArray) { NameArray.Append(InNameArray); }
bool Pop(FName& OutName)
{
if (NameArray.Num() > 0)
{
OutName = NameArray.Pop(/*bAllowShrinking=*/false);
return true;
}
return false;
}
const FName& Peek() const { return NameArray.Last(0); }
bool IsEmpty() const { return NameArray.Num() == 0; }
private:
TArray<FName> NameArray;
}; // class NameStack
} // assetutils
関数 HasReferencersInFolder()
本体「このアセットは、あのフォルダのアセットを参照しているか?」
- スタックを使って再帰検索を行っている。
- 「一度調べたアセットは調べない」周りは、Set を使っている。
意外とシンプルに組めて、ホッとしている。
bool URinderonEditorFunctionLibrary::HasReferencersInFolder(
const FAssetData& InAssetData, const FString& InFolderPath)
{
FAssetRegistryModule& assetRegistryModule
= FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
assetutils::ReferencerNames referencers(assetRegistryModule, InAssetData.PackageName);
assetutils::NameStack packageNameStack(referencers.GetArray());
TSet<FName> alreadyCheckedSet;
bool FoundFlag = false;
while (!packageNameStack.IsEmpty())
{
FName referencerPackageName;
if (packageNameStack.Pop(referencerPackageName))
{
// asset referencing graph may be looping each other,
// to avoid infinite loop, ignores the referencer already handled.
if (!alreadyCheckedSet.Contains(referencerPackageName))
{
FString strPackageName = referencerPackageName.ToString();
FString strAssetPath = FPaths::GetPath(strPackageName);
if (strAssetPath.StartsWith(InFolderPath))
{
FoundFlag = true;
break;
}
// gets referencers of "referencerPackageName" and add to stack (to perform recursive search)
assetutils::ReferencerNames subReferencers(assetRegistryModule, referencerPackageName);
packageNameStack.Push(subReferencers.GetArray());
// now, we mark this "referencerPackageName" as already handled
alreadyCheckedSet.Emplace(referencerPackageName);
}
}
}
return FoundFlag;
}
使用例
Editor Utility Widget にボタンをつくり、その応答で動くようにした例はこちら。
- Asset Registry の Get Assets By Path で、/Game/StarterContent/Textures 内にあるアセット群を取得。
- For Each Loop で、その /Game/StarterContent/Textures 内のアセット一個一個を調査。
- Has Referencers In Folder (今回作った関数) で、そのアセットが /Game/PanicFarm (プロジェクトのメインフォルダ) 内のアセットを参照しているかどうかをチェック。
- 結果を Print String で表示。
「使用中」と「未使用」が正しく取得できているようだ。
これで、赤で表示されたアセット群は消して大丈夫なことが分かる。
例として見やすいように、あらかじめ StarterContent/Textures のアセット群の数を減らしてあるので注意
最終的にこうした
Editor Utility Widget を使って、アセット一覧を見つつ、不要なアセットを検証できるようにした。
- 左側:プロジェクトのメインフォルダのアセット一覧 (画像では /Game/PanicFarm)
- 右側上:調べたいフォルダ(画像では/Game/StarterContent)の中で、プロジェクトのメインフォルダ(画像では /Game/PanicFarm)で使われているアセットの一覧。この一覧にあるアセットは消してはいけない。
- 右側下:調べたいフォルダの中で、プロジェクトのメインフォルダで「使われていない」アセットの一覧。この一覧にあるアセットは消してよい。
なお、「Move All Independents To Other Folder」ボタンは、「使われていないアセットを全部、別のフォルダに移動する」ボタン。これを作ったことで、不要アセットの整理が非常に楽になった。
なお、なぜ「使われていないアセットを全部消すボタン」にしなかったかというと:
- UE4 標準の削除確認ダイアログで、本当に消してよいかのを「自分の目で」最終確認したかったから。
- 一気に大量のアセットを消すと、UE4 Editor が非常に重くなり、正しく処理が動いているのか、ハングしているのかが分からない。精神衛生上、小さなフォルダから順番に、小分けで消したかったから。
このツールのおかげで、不要ファイル群(=たくさんGB)を消すのに 15分くらいで済んだ。
グッジョブ、自分。
なお、2つある Browse Folder ボタンについては、前回の記事をご参照ください。