CC2の楽屋裏

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

*

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

      2019/04/01

はじめまして

サイバーコネクトツーでツール系テクニカルアーティストをしております、しばはらです。

弊社がどんなお仕事をしているか、皆様に知っていただくきっかけになればと思い、この度、技術寄りのブログも始めることにいたしました!私自身もこれまでいろいろな開発コミュニティ、他社様の講演資料で勉強させていただいて大きくなってきたので、この場を借りてゲーム開発に悩める皆様のお役に立てればと思っています。

では早速、第一回をはじめます。

フィルターの種類

UE4のコンテンツブラウザのフィルタリング機能、みなさん使っていますか?

  • アセットの絞り込みで、目に入るアセットを限定できる
  • ワンクリックでフィルタリングのOn/Offができる

という便利な機能ですね。

フィルターには大きく2種類ありまして、ひとつはおなじみのアセットの種類によって絞り込みを行うものです。

img01

もうひとつは、アセットの種類以外の条件で絞り込みを行うフィルターです。

UE4では、デフォルトでも色んな絞り込み条件のフィルターが用意されていて、このタイプのフィルターは、一覧の一番下、[Other Filters]の中にまとまっています。

※ どんな機能のフィルターがあるかについては、有志の方が紹介してくださっている記事がありますので、興味のある方は、そちらも是非ご覧になってください。
[UE4]コンテンツブラウザ便利機能「ViewTypeの変更」と「Compare Tags」のご紹介!|株式会社ヒストリア
UE4 レベル上で使用中か未使用かアセットを見分ける方法 – Let’s Enjoy Unreal Engine

この記事では、後者の、アセットの種類以外の条件で絞り込みを行うフィルターをつくっていきます。

フィルターの自作方法

フィルターを追加するにはブループリントではなくC++のソースを記述する必要があります。エンジンのソースの修正はありません。
※この項は、UE4 バージョン 4.19.2, Win64 環境下での動作をもとに書かれています。

方法は以下の4ステップになります。

(1) 実装先のモジュール下に、特定のクラスを継承したソースを配置する
(2) つくったソースに処理を書く
(3) 実装先のモジュールに依存モジュールへの参照を追加する
(4) 動作確認

順番に見ていきましょう。

(1) まずはソースの追加から

フィルター機能はUE4エディター専用の機能なので、ソースはエディターモジュールに追加します。今回は、自分のプロジェクトのエディターモジュール「MyProjectEditor」に配置する想定で話を進めます。モジュールの説明や、エディターモジュールの用意の仕方については、すでに詳しいサイトがありますので、そちらを参考に用意をしてください。

新規にプロジェクトを作成し、「3.エディタモジュールの実装例」の項をなぞると用意できるかと思います。
[UE4] モジュールについて|株式会社ヒストリア

tb001_07

追加することになるファイルは以下の合計4ファイルです。

  • 「FFrontendFilter」を継承するクラスのヘッダーとソース
    MyFilter.h                    MyFilter.cpp

  • 「UContentBrowserFrontEndFilterExtension」を継承するクラスのヘッダーとソース
    MyFilterExtentsion.h          MyFilterExtension.cpp

まずは実装の全体像を把握するため、ヘッダーを見てみましょう。

[MyFilter.h]
#pragma once

#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "FrontendFilterBase.h"

// フィルタークラスの定義
class FMyFilter : public FFrontendFilter
{
public:
	FMyFilter(TSharedPtr InCategory)
		: FFrontendFilter(InCategory)
	{
	}

	// FFrontendFilter implementation
	virtual FLinearColor GetColor() const override;
	virtual FString GetName() const override;
	virtual FText GetDisplayName() const override;
	virtual FText GetToolTipText() const override;
	// End of FFrontendFilter implementation

	// IFilter implementation
	virtual bool PassesFilter(FAssetFilterType InItem) const override;
	// End of IFilter implementation

	//
	// ここから下はフィルター専用の独自処理
	//
protected:
	static const int32 INVALID_FILE_SIZE = -1;

private:
	static FString GetFullPathWithoutExt(const FAssetFilterType& InItem);
	static int32   GetFileSize(const FString& InPath);
};

フィルターの本体にあたるクラスです。

#includeに記述してあるヘッダーは、UE4でお約束的に記述するものと、継承元のクラスに関するもののみです。
16行目 ~ 21行目の override キーワードが入っている関数は、どのような自作フィルターであっても書くことになる関数です。
27行目以降は、今回のフィルターの用途に合わせて記述したメンバになります。

[MyFilterExtentsion.h]
#pragma once

#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "ContentBrowserFrontEndFilterExtension.h"
#include "MyFilterExtention.generated.h"

// フィルターの一覧にMyFilterを登録する
UCLASS()
class UMyFilterExtention : public UContentBrowserFrontEndFilterExtension
{
public:
	GENERATED_BODY()

	// UContentBrowserFrontEndFilterExtension interface
	virtual void AddFrontEndFilterExtensions(TSharedPtr DefaultCategory, TArray< TSharedRef >& InOutFilterList) const override;
	// End of UContentBrowserFrontEndFilterExtension interface
};

フィルターをUE4に登録するためのクラスです。

こちらも#includeに記述してあるヘッダーは、シンプルになっています。関数を見ると、登録処理の呼び出ししか必要ないのが見て取れると思います。なお、このクラスはつくったフィルターごとに1つずつ必要なものではなく、モジュールの中に1つあれば事足ります。

(2) 処理を書いていきましょう

実用性はさておき、今回はPCのディスク上でのファイル容量が1MiB以上のアセットを絞り込むフィルターをつくっていくことにします。

[MyFilter.cpp]
#include "MyProjectEditor.h"
#include "MyFilter.h"
#include <fstream>

#define LOCTEXT_NAMESPACE "MyFilter"

// フィルターを有効にしたときの色を決められる
FLinearColor FMyFilter::GetColor() const
{
	return FLinearColor::Yellow;
}

// フィルターのエンジン内部での識別名を決める。重複不可
FString FMyFilter::GetName() const
{
	return TEXT("MyFilter");
	// リリース後に変更すると、フィルターの設定の記録が消えてしまう
}

// フィルターのコンテンツブラウザ上での表示名を返す
FText FMyFilter::GetDisplayName() const
{
	return LOCTEXT("MyFilterDisplay", "MyFilter");
}

// フィルターの一覧でマウスポインタを合わせたときに表示されるヒント文(ツールチップ)を設定する
FText FMyFilter::GetToolTipText() const
{
	return LOCTEXT("MyDisplayTooltip", "Check asset size is grater than or equal to 1MB.");
}

// フィルターに渡されたパス情報からフルパス(拡張子は除く)を生成する
FString FMyFilter::GetFullPathWithoutExt(const FAssetFilterType& InItem)
{
	FString result;
	FString AssetPath = InItem.PackageName.ToString();
		// PackageName には、例えば 

	
	// プロジェクトのコンテンツ
	if (AssetPath.Contains(TEXT("/Game/")))
	{
		FString Root = FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir());
		result = FPaths::Combine(Root, AssetPath);
		result = result.Replace(TEXT("/Content//Game/"), TEXT("/Content/"));
	}
	// エンジンコンテンツ
	else if (AssetPath.Contains(TEXT("/Engine/")))
	{
		FString Root = FPaths::ConvertRelativePathToFull(FPaths::EngineContentDir());
		result = FPaths::Combine(Root, AssetPath);
		result = result.Replace(TEXT("/Content//Engine/"), TEXT("/Content/"));
	}
	else
	{
		// 想定外のパス
		ensure(false);
	}
	return result;
}

// 指定パス(フルパス)のアセットのファイルのPCディスク上でのサイズ(単位はバイト)を返す
int32 FMyFilter::GetFileSize(const FString& InPath)
{
	std::ifstream ifs(*InPath);
	if (!ifs.is_open()) return INVALID_FILE_SIZE;

	ifs.seekg(0, std::ios_base::end);
	return ifs.tellg();
}

// フィルターのメイン処理。
// コンテンツブラウザで表示しているフォルダー下のファイル一つ一つが、この判定にかけられる
bool FMyFilter::PassesFilter(FAssetFilterType InItem) const
{
	FString basePath = GetFullPathWithoutExt(InItem);
	
	if (basePath.IsEmpty()) return false;

	// 拡張子を uasset として測定
	FString fullpath = basePath + TEXT(".uasset");
	if (InItem.AssetClass == FName("World"))
	{
		// レベルアセットは拡張子を umap として測定
		fullpath = basePath + TEXT(".umap");
	}
	int32 fileSize = GetFileSize(fullpath);
	if (fileSize == INVALID_FILE_SIZE)
	{
		// 想定外の拡張子 or 未保存のアセット
		ensure(false);
	}

	// サイズが一定量以上か
	const int32 ThresholdSize = 1 * 1024 * 1024;
	return (fileSize >= ThresholdSize);
}

#undef LOCTEXT_NAMESPACE

フィルター本体の処理になります。

7行目~30行目は、フィルターの見た目や表示する文字について記述する関数が並んでいます。
唯一、13行目~ のGetName() は、UE4Editor側がこのフィルターの情報を扱う際の識別子という意味合いがあるようです。
要となるのは、72行目~ のPassesFilter() です。これは、コンテンツブラウザでファイルを表示する直前にファイルごとに呼び出され、フィルターの条件を満たす(表示する)ものには true を、そうでないものには false を返す関数です。
サンプルの解説に戻りましょう。
PassesFilter() の引数である“FAssetFilterType”は
Engine\Source\Runtime\AssetRegistry\Public\AssetData.h
で定義されている “FAssetData”がその正体です。 (typedefしてあります)

今回は2つのメンバ変数の情報を使用します。

  • FName PackageName
    UE4上でのアセットのパスが拡張子なしで入っています。
    プロジェクトのContentフォルダ下のアセットは /Game がルートパスになります。
    EngineContentのアセットは、/Engine がルートになるようです。

例:
“/Game/MyAsset/Sphere”
“/Engine/EngineMeshes/Sphere”

  • FName AssetClass
    アセットの種類を表す文字列が入っています

例:
レベル → “World”
スタティックメッシュ → “StaticMesh”

76行目~86行目は、これらを元にファイルのフルパスの文字列をつくっている部分になります。あとはフルパスからデータサイズを取得して、判定を返して関数は終了です。

ひとつ注意すべき点は、この関数は、コンテンツブラウザで開いているフォルダ以下、(サブフォルダ下も含む)全てのファイルに対して一回ずつ呼び出されます。したがって、処理時間が長い処理を書くと、UE4が固まったまま動かない、という問題が起き得ます。
今回のサンプルはファイルアクセス(ifstream)を含む仕様になっているので、そこそこ動作が重たいのを確認頂けるのではないでしょうか。せっかく頑張って実装したのに、みんなから文句を言われると踏んだり蹴ったり、ということになりますので、注意しておきましょう。

[MyFilterExtentsion.cpp]
#include "MyProjectEditor.h"
#include "MyFilterExtention.h"
#include "MyFilter.h"

// フィルターをエンジンに登録する処理
// エンジン起動時にSFilterList::Constructから自動で呼ばれるため、
// エンジン側のソース修正が不要
void UMyFilterExtention::AddFrontEndFilterExtensions(TSharedPtr DefaultCategory, TArray< TSharedRef >& InOutFilterList) const
{
	// Other Filters の一覧に登録
	InOutFilterList.Add(MakeShareable(new FMyFilter(DefaultCategory)));

	// 2つ目以降のフィルターの登録はこのファイルにまとめられる
	//InOutFilterList.Add(MakeShareable(new FMyFilter2(DefaultCategory)));
}

こちらはシンプルで簡単そうですね。

#include に先程作った “MyFilter.h” が書かれているのに気づかれたでしょうか。11行目にあるように、作ったフィルターを登録する際にインスタンスをつくるためです。
UE4を起動すると、6行目のコメントにあるように、UE4が自動でこの登録処理を呼びに来てくれるので、フィルターの登録はエンジン修正が不要になっています。
また、13行目のコメントにもあるように、自作のフィルターを複数作ることになっても、登録処理はこの関数にまとめて書いて構いません。

(3) 最後に、依存モジュールへの参照を追加します

今回の実装では、ContentBrowser モジュールのPublicなクラスを継承して使っているので、実装先のモジュールの Build.cs の PublicDependencyModuleNames に ”ContentBrowser” を追加しておきます。

img04

(4) 動作確認

プロジェクトをビルドしてUE4Editorを起動しましょう。コンテンツブラウザの [Filters] → [Other Filters] につくったフィルターが登録されていることが確認できると思います。

有効化すると、1MiB以上の容量のファイルのみが表示されるようになります。

適用前

適用後

フィルターをつくる意味は?

ここまでで、みなさんもどんどんフィルターをつくっていける準備が整ったわけですが、もう少しだけフィルターについて考えてみましょう。フィルターは便利な機能ですが、見方を変えればアセットの絞り込みまでしかやってくれません。絞り込んだアセットそれぞれに対して行いたい作業が決まっているなら、必要な機能はフィルターではなく、アセットの一括(自動)修正機能なのかもしれません。
つくったフィルターが本当に役に立つのか見極めるには、以下の視点が必要かと思います。

  • 絞り込んだアセットを一覧で見れることに意義がある
  • 絞り込んだアセットに対する作業は人の手で行う必要がある

もしこの要件に当てはまるなら、きっと重宝するフィルターがつくれると思うので、検討してみてください!

参考

フィルターの実装方法を勉強するには、以下のエンジンのソースが参考になります。
Engine\Source\Editor\ContentBrowser\Private\FrontendFilters.cpp

また、今回はプロジェクトのエディターモジュールにフィルターを実装する流れで紹介しましたが、フィルターのコードの汎用性、独立性を高めるという意味では、プラグインとしての提供も考えられるようになりたいところです。
プラグインの作り方については、こちらの記事がおすすめです。
[UE4] プラグインによるエディタ拡張(1) 空のプラグインを追加する|株式会社ヒストリア

おわりに

さて、技術ブログ 第一回、いかがでしたでしょうか。若干外部の記事を紹介しすぎた感もありますが…(いつもお世話になっております。多謝)

ここが分かりにくかった、自分ならこうするよ、次はこういう記事を書いてほしいなどなど、皆さんのご意見、ご要望をお待ちしています。下記 CC2技術ブログのメッセージフォームにて、お気軽にお問い合わせください。

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

 

 - CC2技術ブログ, ブログ , ,

  関連記事

no image
「.hack//Quantum(クワンタム)」公式サイト更新!

「.hack//Quantum(クワンタム)」の公式サイトが更新されていますので …

no image
ワイヤレス頭部受話器

「アドホック・パーティー」に夢中な今日この頃、 ボイスチャットを行う為に「ワイヤ …

120414
ぴろし、パリの大地を駆け抜ける!みんな、ぴろしへのエールを!!

こんにちは! ご存知の方も多いと思いますが… ぴろし社長、パリマラソン出場!! …

no image
第5回「ソラトロボCC2ノート」に寄せられたメッセージ!! #solatorobo

どうもーソラノウチです! サイバーコネクトツー福岡本社&東京スタジオの受付に 大 …

no image
プラチナ殿堂入りありがとうございます!

発売まであと1週間となりました「ナルティメットストーム2」ですが。 本日、開発チ …

no image
PSPがますます便利になったですよ!

(個人的に)待望のサービスがいよいよ開始! PlayStation®Storeに …

140205_18
ゲーム開発会社のデザイン室って何をしているの?デザイン室のお仕事。

こんにちはーたっしーです! いつもグッズ紹介で登場しているデザイン室ですが、 グ …

no image
「ソラトロボ博物館」でいただいた応援コメントをご紹介!(第1回)

どうもー!ヤマノウチです! 4/29~5/1に開催された 「ソラトロボ博物館 そ …

no image
正式タイトル決定!「Solatorobo それからCODAへ」

いままで仮題とさせて頂いてましたが、正式タイトルが決定しました! (C) 201 …

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

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