CC2の楽屋裏

ゲーム制作会社サイバーコネクトツー公式ブログ

*

第004回 UE4のアセットを一括修正する

      2019/06/07

ツール系テクニカルアーティストのしばはらです。

プロジェクトが進んで、ある程度アセットがそろってきた段階で、新しいアイデアが出て設計を見直したり、バグが見つかったりして、これまで作ったアセットを一斉に直さなくてはいけなくなることがあります。

ひとつひとつアセットを開いて、必要になった修正を手作業でやっていけばいい話ではあるんですが… 数が増えてくると時間もかかるし、人力では修正ミスも出てくる、やっかいな面があります。

今回はアセットを開くことなく一括修正する方法をご紹介していきます!

こんな問題がありまして…

早速ですが、具体例を見ながら一括修正のイメージを持っていただこうと思います。
FBXや画像ファイルなど、UE4にファイルをインポートしてアセット化するタイプのものがあります。
これらのアセットに記録されている元データのパスがメンバーによってばらばらになっている問題を、一括修正で解決してみましょう。

まずは状況設定から。
  • インポート元ファイルは、ProjectDataというフォルダ下に置く、としてファイルの配置ルールを決めました。
  • ProjectData以下の階層は皆ルール通り配置しているものの、ProjectDataフォルダ自体の置き場所がメンバーによって食い違っていることが発覚しました。あるメンバーは D:\MyProject の下に、また別のメンバーは E:\の直下に ProjectData フォルダがあります。
  • ここが統一されていないせいで、UE4のコンテンツブラウザから再インポート操作を行うときに元ファイルが見つからない、ということが起きだしました。
  • 全メンバーで D:\ProjectData というパス下にファイルを置くように修正します。

tb004_09

※D:\ProjectDataへのファイル移動は通常のファイル操作で行っていただいたくことにします。シンボリックリンクも検討できるでしょう。

実装手順

データの一括修正を実装するには、次の4ステップを踏みます。

(1)対象アセットの一覧を取得する
(2)アセットの修正を行う
(3)1,2の手順を呼び出す方法を提供する
(4)動作確認

本稿の内容は、UE4.20.3, Win64 の環境で動作確認を行っています。

(1)対象アセットの一覧を取得する

まずは、プロジェクトのデータの中から対象のアセットを絞り込みます。

こちらのコードは、プロジェクトのデータの中から StaticMesh, Texture2D のアセット情報をピックアップするコードになっています。

FAssetSourcePathChanger.cpp より抜粋
void FAssetSourcePathChanger::PickAssets(TArray<FAssetData>& OutAssetList) const
{
	FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(FName("AssetRegistry"));
	IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();

	FARFilter Filter;
	
	// 検索対象パスの指定
	Filter.PackagePaths.Add(TEXT("/Game/TestAsset"));	// Content/TestAssetフォルダ以下。 Contentフォルダ全体を対象にするなら "/Game/" を指定.
	Filter.bRecursivePaths = true;				// サブフォルダ以下も検索する

	// 検索対象クラスの指定
	Filter.ClassNames.Add(UStaticMesh::StaticClass()->GetFName());
	Filter.ClassNames.Add(UTexture2D::StaticClass()->GetFName());

	AssetRegistry.GetAssets(Filter, OutAssetList);
}

 

条件に沿ったアセットの一覧を取得するには、FAssetRegistry が便利です。
・特定のクラスのアセットを検索する GetAssetsByClass
・指定したパスのアセットを検索する GetAssetsByPath
といった条件ごとの関数も用意されていますが、条件を組み合わせて検索する場合は GetAssets 関数を使います。この関数では、条件の指定を FARFilter を使って行うことができ、サンプルでは、パス、クラスの条件を指定して使っています。

いずれの検索関数も、引数に指定した FAssetDataのTArrayに結果が格納されます。

(2)アセットの修正を行う

続いてFAssetDataからアセットのポインタを取得し、内容を変更する処理です。

FAssetSourcePathChanger.cpp より抜粋
bool ReplaceAssetImportPath(UAssetImportData* AssetImportData)
{
	if (!AssetImportData) return false;

	const FString newPrefix(TEXT("D:/ProjectData/"));
	const FString mustInclude(TEXT("/ProjectData/"));

	// StaticMesh の インポートパスの取得
	FString currentPath = AssetImportData->GetFirstFilename();

	// パスの置換が必要かのチェック
	if (currentPath.Contains(newPrefix)) return false;

	// パスの置換ができそうかのチェック
	FString strRight, strDummy;
	if (!currentPath.Split(mustInclude, &strDummy, &strRight)) return false;

	// 置換パスの成形
	FString replacedPath = newPrefix + strRight;

	// 置換先のファイルが実在するかのチェック
	if (!IFileManager::Get().FileExists(*replacedPath)) return false;

	// パスの置換
	AssetImportData->Update(replacedPath);

	return true;
}

void FAssetSourcePathChanger::ChangeSourcePaths(const TArray<FAssetData>& InAssetList) const
{
	// 抽出した全アセットに対して走査
	for (FAssetData assetData : InAssetList)
	{
		// アセットの種類がマッチすれば
		if (UStaticMesh* StaticMesh = Cast<UStaticMesh>(assetData.GetAsset()))
		{
			// パスの修正を呼び出す
			if (ReplaceAssetImportPath(StaticMesh->AssetImportData))
			{
				StaticMesh->MarkPackageDirty();
			}
		}
		else if (UTexture2D* Texture = Cast<UTexture2D>(assetData.GetAsset()))
		{
			if (ReplaceAssetImportPath(StaticMesh->AssetImportData))
			{
				Texture->MarkPackageDirty();
			}
		}
	}
}

アセットのポインタが取得できたら、UStaticMesh, UTexture2D いずれもAssetImportDataという変数にインポート元の情報が格納されているので、これらのパスを加工していきます。

修正したアセットはコンテンツブラウザで編集中(未保存)であると認識できるよう、MarkPackageDirtyを呼び出しておきます。

アセットの修正処理に関して、注意点が3つあります。

一つ目は、この段階ではアセットは保存されていないということ。

UE4がクラッシュしたり、保存せずに終了してしまうと、修正は無効になります。逆に言えば、保存しない限りは、一度UE4を終了して同じ実験をすることもできます。保存の呼び出しまで行いたい場合は、GEditor-&gt;SavePackage が利用できます。

二つ目は、目的のパラメータの修正方法にたどり着くのが容易ではないケースがあるということ。

FAssetDataから取得したアセットクラスが直接持っている変数を書き換えれば済むなら話は簡単なのですが、アセット内のオブジェクトを深く深くもぐっていかないと、目的のパラメータを保持するオブジェクトにたどり着けないことはままあります。

目的のパラメータまでの到達方法は、基本的にUE4が提供しているコードから対象のアセットへのアクセス処理を調べていく必要があります。対象によっては、エンジンコードの修正も必要になるかもしれません。ここに時間が掛かると、手動で直していっても大差なかったということになりかねませんから、その点の算段は計画初期につけておきましょう。

三つ目はメモリの解放について。

サンプルコードでは、ループ処理で次々アセットのポインタを取得していますが、このとき裏ではアセットのロードが行われています。すなわち、どんどんメモリが消費されていきます。サンプルではループごとにポインタを保持している変数がスコープを外れるので、ガベージコレクションによってメモリは適宜解放されますが、アセットの参照が生き続けるコーディングをするとメモリが占有され続け、やがて足りなくなります。

変数のスコープに意識して処理を書きましょう。

(3)1,2の手順を呼び出す方法を提供する

今回は技術ブログ 第003回で紹介したショートカットキーへ紐づけたコマンドから処理を呼び出すことにしましょう。

まずは、1,2の処理を呼び出す関数を追加します。

AssetSourcePathChanger.cpp
#if WITH_EDITOR

#include "MyProjectEditor.h"
#include "AssetSourcePathChanger.h"
#include "AssetRegistryModule.h"
#include "Editor.h"

#define LOCTEXT_NAMESPACE "AssetSourcePathChanger"
 
 // 処理の呼び出し
void FAssetSourcePathChanger::Call()
{
	TArray<FAssetData> AssetList;

	PickAssets(AssetList);
	ChangeSourcePaths(AssetList);
}

// アセットの絞り込み
void FAssetSourcePathChanger::PickAssets(TArray<FAssetData>& OutAssetList) const { ... }
// 問題のパスの修正
bool ReplaceAssetImportPath(UAssetImportData* AssetImportData) { ... }
// アセットを取得してパスの修正を呼び出し
void FAssetSourcePathChanger::ChangeSourcePaths(const TArray<FAssetData>& InAssetList) const { ... }

#undef LOCTEXT_NAMESPACE
#endif //WITH_EDITOR

冒頭の関数 Call で順に呼び出しを行っていますね。

※これまで紹介してきた関数は略記してあります。

クラスのヘッダーはこんな感じになっています。

AssetSourcePathChanger.h
#pragma once

#if WITH_EDITOR
class FAssetSourcePathChanger
{
public:
	FAssetSourcePathChanger(){};
	void Call();

private:
	void PickAssets(TArray<FAssetData>& OutAssetList) const;
	void ChangeSourcePaths(const TArray<FAssetData>& InAssetList) const;
};
#endif // WITH_EDITOR

続いて、コマンドの紐づけ部分の変更点を見てみましょう。

MyEditorCommandBinder.cpp
// コンストラクタ
FMyEditorCommandBinder::FMyEditorCommandBinder()
	: IsBinded(false)
{
	PathChanger = MakeShareable(new FAssetSourcePathChanger());     // クラスのポインタを保持
}

void FMyEditorCommandBinder::OnEditorLoaded(SWindow& SlateWindow, void* ViewportRHIPtr) { ... }

// コマンドの処理の紐付け
void FMyEditorCommandBinder::BindCommands()
{
	const FMyEditorCommands& Commands = FMyEditorCommands::Get();
	
	if (FPlayWorldCommands::GlobalPlayWorldActions.IsValid())
	{
		// UE4のあらゆるところで呼び出せるコマンドとして紐付け
		FPlayWorldCommands::GlobalPlayWorldActions->MapAction(
			Commands.MyEditorCommand,
			FExecuteAction::CreateSP(PathChanger.ToSharedRef(), &FAssetSourcePathChanger::Call)     // 一括変更呼び出し
		);
	}
}

前回のブログ記事のサンプルでは、ラムダ式でメッセージウィンドウを表示する処理と紐づけを行っていました。今回は、これを修正関数の呼び出しに変更しただけです。

(4)動作確認

まずは修正前の状態です。

StaticMesh Texture
tb004_04 tb004_05

ご覧のとおり、アセットのルートフォルダがばらばらになっています。
※右側のテクスチャはEドライブを指しています。私の環境ではUE4がEドライブ置かれていたため、相対パス表示になっています。

ショートカットキー (サンプル通りならCtrl + F12です) を押してコマンドを実行すると、修正対象となったファイルに編集中(未保存)のマークが付きます。

tb004_10

ファイルを開いて、パス情報が修正されているのを確認します。

StaticMesh Texture
tb004_07 tb004_08

※修正されたのはインポート元のパス情報のみになります。再インポートはしていませんので、パス以外のアセットの内容は処理前と変わっていません。

修正にあたって考えたいこと

さて、一括修正の方法をご紹介しましたが、一括修正には大量のデータが差し替わるリスクが伴いますので、計画を立てる段階こそ重要だと思っています。ここでは、考えるべきポイントについて少しだけ掘り下げたいと思います。

・どこを直すのか

アセットの内容なのか、アセットのデータの持たせ方なのか、プログラム側のデータの扱い方なのか。どこを直す方法もとれる、という状況がほとんどだと思いますので、それぞれのメリット・デメリットを洗い出しましょう。

・いつ、何をきっかけに直すのか

今回は修正処理を明示的に(ショートカットキーで)呼び出して、その場で全データを直す方法を紹介しました。直したことをチームメンバーが認識する必要があるか、目的のアセットすべてが修正されることをどう保証するかを明確にしましょう。

・どう確認するのか

修正後の挙動確認まで自動化できれば幸せなのですが、ケースによっては難しいと思います。人力で確認が必要な場合は、どのような手順で修正を確認していくのか、関係者と相談しておきましょう。また、問題発覚時は速やかにバージョン管理ツールによるロールバックができるようにしておくなど、もしもに備えておきましょう。

・再発は考えなくてよいか

一括修正によって既存のアセットはすべて修正されるとして、その後作成されるアセットでは問題が起きないでしょうか。再発防止の仕組みも考えましょう。また、「一回きりの修正」ではなく、「今後何度も行われる修正」となる場合はその仕組みの維持にどのくらい労力がかかるかも考えておけなければいけません。

参考

今回紹介した以外の一括修正の方法について、ご紹介します。

1.既存の一括修正機能を使う

プロパティマトリックスという同じ種類のアセット複数の特定パラメータを書き換えられる機能があります。これで直せるパラメータであればこれを使う方が断然お手軽です。

今回紹介したのと同じく、作業者が自分の意志とタイミングで修正するタイプの方法ですね。

2.アセットのロード時に行う方法

カスタムバージョンを使う案です。こちらのページがわかりやすいかと思います。

量産されたアセットのプロパティ仕様を変更する – ほげたつブログ

こちらはアセットのロード時に勝手に修正を行うタイプの方法で、作業者が意識することはありません。

修正方法を選択させるような柔軟性は持たせられないのと、重たい処理を書くとクック時間や、ロード時間に影響が出てきますので要注意です。

3.UE4を起動せずに裏で修正を行ってしまう方法

UE4のコンソールアプリケーションを介して処理を行っていく機能、コマンドレットを使う方法です。あまり資料がないため、既存の実装を見ながら実装法を勉強する必要がありますが、多少の動作オプションも持たせつつ、CIツールと連携して定期的に実行するという運用に向いています。

明示的に呼び出すタイプの使い方もできますし、定期実行であまり意識しないタイプの使い方もできるでしょう。

[UE4] ライトビルドをコマンドから実行しよう!|株式会社ヒストリア

おわりに

技術ブログ 第004回、最後まで読んでいただきありがとうございました。

ご意見、ご感想などは、下記CC2技術ブログのメッセージフォームからお気軽にお問い合わせください。

「CC2技術ブログ」へのご意見・ご要望はこちらから

 - CC2技術ブログ , ,

  関連記事

tb002_04
第002回 アクタのトランスフォームの実態

皆さんストラーズドヴィーチェ(ロシアのあいさつ)! 今回の担当は、新人プログラマ …

CC2技術ブログ
第006回 コマンドラインからConfig設定を上書きする時の注意点

皆さん、こんにちは。サイバーコネクトツーでテクニカルサポートのマネージャーをして …

tb004_15
第005回 Tick関数がどう処理されているかUnrealC++を追ってみよう!

皆さんグーテンターク(ドイツのあいさつ)! CC2TechBlog今回の担当は新 …

スクリーンショット (47)
第007回UE4のアセットの参照方法について、そのロードの違い

皆さんボンジュール(フランスのあいさつ)! ランダムのスペルが覚えられないプログ …

第003回 UE4のショートカットキーの仕組み

今回は ツール系テクニカルアーティスト しばはらがお送りします。 突然ですが、P …

第001回 UE4のコンテンツブラウザフィルターを自作する

はじめまして サイバーコネクトツーでツール系テクニカルアーティストをしております …

第008回:ゲームクリエイターは破壊表現が好き。

皆さんボンジョルノ(イタリアのあいさつ)! 乱読派プログラマ親泊です。 現代ゲー …