LoginSignup
8
6

UE4 UnrealC++リフレクションについてのメモ

Last updated at Posted at 2020-11-09

概要

UnrealEngine のリフレクション関連についてのメモ書きです。
リフレクション機能を使ったコード例を書いています。

更新履歴

日付 内容
2020/12/22 FPropety取得についての補足追加
2023/06/08 PostEditChangePropertyでの使用例を追記

環境

Windows10
Visual Studio 2017
UnrealEngine 4.25

参考

以下を参考にさせて頂きました、ありがとうございます。
UnrealEngine4 公式
UnrealEngine4 公式:アンリアルのプロパティシステム(リフレクション)
UE4 Unreal C++のリフレクションを使って文字列で関数を呼び出す方法について

リフレクションとは

Wikiによると、
情報工学においてリフレクション とは、プログラムの実行過程でプログラム自身の構造を読み取ったり書き換えたりする技術のことを指す。
という定義のようです。

UE4では UCLASS() USTRUCT() UENUM() などのメタ情報を付与することでリフレクション情報を生成し、MyObject.generated.h などといったヘッダファイルに出力しているようです。

メンバにアクセスする

構造体とクラスのメンバにアクセスをしてみます。リフレクション機能を使っているので UPROPERTY()がついているメンバだけアクセス可能です。

構造体

以下のような構造体にアクセスしてみます。

TestStruct.cpp
USTRUCT(BlueprintType)
struct FTestDataTable : public FTableRowBase
{
	GENERATED_USTRUCT_BODY()

public:
	FTestDataTable()
	:Name0(NAME_None)
	,Param0Int(0)
	,Param0Float(0.0f)
	{}


public:
	UPROPERTY(Category = "TestDataTable", EditAnywhere)
	FName			Name0;

	UPROPERTY(Category = "TestDataTable", EditAnywhere)
	int32			Param0Int;

	UPROPERTY(Category = "TestDataTable", EditAnywhere)
	float			Param0Float;
};

UScriptStruct に対してイテレーションします。

UScriptStruct* _Struct = FTestDataTable::StaticStruct();
for (TFieldIterator<FProperty> PropIt(_Struct); PropIt; ++PropIt)
{
	FProperty* _Property = *PropIt;
	// プロパティを表示
	UE_LOG(LogTemp, Log, TEXT("%s"), *_Property->GetNameCPP());
}

結果は以下の様になります。

アウトプットログ
LogTemp: Name0
LogTemp: Param0Int
LogTemp: Param0Float

クラス

以下のようなクラスにアクセスしてみます。

TestClass.cpp
UCLASS(BlueprintType)
class UTestMyClass : public UObject
{
	GENERATED_BODY()
	
public:
	UTestMyClass()
	:Name1(NAME_None)
	,Param1Int(0)
	,Param1Float(0.0f)
	{}


public:
	UPROPERTY(Category = "UTestMyClass", EditAnywhere)
	FName			Name1;

	UPROPERTY(Category = "UTestMyClass", EditAnywhere)
	int32			Param1Int;

	UPROPERTY(Category = "UTestMyClass", EditAnywhere)
	float			Param1Float;
};

UClassに対してイテレーションをします。

UClass* _Class = UTestMyClass::StaticClass();
for (TFieldIterator<FProperty> PropIt(_Class); PropIt; ++PropIt)
{
	FProperty* _Property = *PropIt;
	// プロパティを表示
	UE_LOG(LogTemp, Log, TEXT("%s"), *_Property->GetNameCPP());
}

結果は以下の様になります。

アウトプットログ
LogTemp: Name1
LogTemp: Param1Int
LogTemp: Param1Float

補足:特定のクラスメンバのFPropertyの取得

FindField を使って取得することができます。以下サンプルコード。

.cpp
// メンバ変数 Param1Float のFPropertyを取得する
FProperty* _Property = FindField<FProperty>(UTestMyClass::StaticClass(), TEXT("Param1Float"));

文字列指定で関数を探して実行する1

文字列指定で関数を探して、その関数を実行してみます。
実行には ProcessEvent を使います。引数がない場合は第2引数は nullptr でOKです。

テストコード

実行されるメソッドには UFUNCTION() をつけないと呼び出しができません。

.h

UCLASS()
class MYPROJECT_API AMyActor : public AStaticMeshActor
{
	GENERATED_BODY()

public:
	// ...省略...

	// 実行されるメソッド
	UFUNCTION()
	void Func(int32 _ArgInt, float _ArgFloat, FString _ArgString);

};


.cpp
// 実行
void AMyActor::ExecFind()
{
	// 文字列指定で "Func" という関数を探す
	UFunction* _FindFunc = FindFunction(TEXT("Func"));
	// FindFunction の代わりに FindFunctionChecked を使うと見つからない場合エラーになる	

	if(::IsValid(_FindFunc) ){
		// 引数指定用
		struct MyFunc_Parms
		{
			int32 _ArgInt;
			float _ArgFloat;
			FString _ArgString;
		};

		// 引数をセット
		MyFunc_Parms _Parms;
		_Parms._ArgInt = 123;
		_Parms._ArgFloat = 45.6f;
		_Parms._ArgString = FString(TEXT("7,8,9"));

		// 実行する
		ProcessEvent(_FindFunc, &_Parms);
	}
}

// 実行されるメソッド
void AMyActor::Func(int32 _ArgInt, float _ArgFloat, FString _ArgString)
{
	UE_LOG(LogTemp, Log, TEXT("Func(%d, %f, %s)"), _ArgInt, _ArgFloat, *_ArgString);
}

実行結果

Funcが実行されて以下の様になります。

アウトプットログ
> Func(123, 45.599998, 789)

文字列指定で関数を探して実行する2

別な方法の文字列指定で関数を実行してみます。
実行には CallFunctionByNameWithArguments を使ってみます。引数がない場合は CallFunctionByName でOKです。

テストコード

.cpp
// 実行
void AMyActor::ExecFunc()
{
	// 文字列指定で "Func" という関数を探す
	FString _FuncName = FString(TEXT("Func"));
	
	int32	_Arg1 = 11;
	float	_Arg2 = 2.2f;
	FString _Arg3 = GetName();
		
	// 関数呼び出しコマンドの文字列を作成する
	const FString _FuncCommand = FString::Printf(TEXT("%s %d %f %s"), *_FuncName, _Arg1, _Arg2, *_Arg3);
	FOutputDeviceNull _Null;

	// Func関数を呼び出す
	CallFunctionByNameWithArguments(*_FuncCommand, _Null, this, true);
}

// 実行されるメソッド
void AMyActor::Func(int32 _ArgInt, float _ArgFloat, FString _ArgString)
{
	UE_LOG(LogTemp, Log, TEXT("Func(%d, %f, %s)"), _ArgInt, _ArgFloat, *_ArgString);
}

実行結果

Funcが実行されて以下の様になります。

アウトプットログ
> Func(11, 2.2, MyActor)

C++定義クラスのメソッド実行

C++定義クラス

インターフェイスを実装した以下のようなクラスを想定します。

MyClass.h
// インターフェイス定義
UINTERFACE(BlueprintType, meta = (CannotImplementInterfaceInBlueprint))
class MYPROJECT2_API UPerson : public UInterface
{
	GENERATED_UINTERFACE_BODY()
};

class MYPROJECT2_API IPerson
{
	GENERATED_IINTERFACE_BODY()

public:
	// 挨拶
	virtual FString		Greeting();
};

// インターフェイスを実装したクラス1
UCLASS()
class MYPROJECT2_API UJapanese : public UObject, public IPerson
{
	GENERATED_BODY()
	
public:
	virtual FString		Greeting() override { return( FString::Printf(TEXT("こんにちは!")) ); }
	
};

// インターフェイスを実装したクラス2
UCLASS()
class MYPROJECT2_API UAmerican : public UObject, public IPerson
{
	GENERATED_BODY()
	
public:
	virtual FString		Greeting() override { return( FString::Printf(TEXT("Hello!")) ); }
	
};
MyClass.cpp
// インターフェイスのコンストラクタ
UPerson::UPerson(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
}
// インターフェイスで実装されたメソッド
FString IPerson::Greeting()
{
	return( FString(TEXT("")) );
}

オブジェクト検索からクラスを取得して実行

FindObjectを使ってクラスを取得します。C++オブジェクトのパス指定は /Script/モジュール名.クラス名 となります。(接頭辞は不要)

アクセスするのがBPクラスの場合は、パスに _C をつける必要があります。
(例)
"/Game/ThirdPersonBP/AMyActorBP.AMyActorBP_C"
もしくは
"/Game/ThirdPersonBP/AMyActorBP"
となります。

実行コード

.cpp

UClass* _Class = FindObject<UClass>(ANY_PACKAGE, TEXT("Class'/Script/MyProject.American'"));
if (_Class) {
	auto _ClassPerson = Cast<IPerson>(_Class->GetDefaultObject());
	if( _ClassPerson){
		UE_LOG(LogTemp, Log, TEXT("Name:%s - %s"), *_Class->GetFName().ToString(), *_ClassPerson->Greeting() );
	}
}

実行結果

インターフェイスでキャストして実行します。

アウトプットログ
> Name:American - Hello!

オブジェクトをロードしてクラス実行

FindObjectを使う方法は、エディタ環境は問題ないのですがランタイムなどではオブジェクトのロード問題がでるためオブジェクトをロードして実行してみます。

FSoftObjectPathからパス指定をしてロードしてオブジェクトを取得します。

実行コード

.cpp
UClass* _Class = TSoftObjectPtr<UClass>(FSoftObjectPath(TEXT("Class'/Script/MyProject.Japanese'"))).LoadSynchronous();
if (_Class) {
	auto _ClassPerson = Cast<IPerson>(_Class->GetDefaultObject());
	if (_ClassPerson) {
		UE_LOG(LogTemp, Log, TEXT("Name:%s - %s"), *_Class->GetFName().ToString(), *_ClassPerson->Greeting());
	}
}

実行結果

インターフェイスでキャストして実行します。

アウトプットログ
> Name:Japanese - こんにちは!

エディタ上でのプロパティ変更時に変数名を調べて処理する

UEエディタにてプロパティを変更したときにリフレクション機能を使って変数名を調べて任意の処理を行うことができます。
以下サンプルコード。

.h
UCLASS()
class MYPROJECT_API UMyActorComponent : public UActorComponent
{
	GENERATED_BODY()
// ...省略...

#if WITH_EDITOR
	virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
#endif
	
	UPROPERTY(EditAnywhere)
		int32 MyVariable = 0;
};
.cpp
// エディタ上で変数変更があった時の処理
void UMyActorComponent::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
	if( PropertyChangedEvent.Property){
		const FName PropertyName = PropertyChangedEvent.GetPropertyName();
		// 変数名を調べる
		if( PropertyName == GET_MEMBER_NAME_CHECKED(UMyActorComponent, MyVariable) ){
			UE_LOG(LogTemp, Log, TEXT("MyVariable の値がエディタで変更された!"));
		}
	}
	Super::PostEditChangeProperty(PropertyChangedEvent);
}

備考:C#の時のコード例

C#でリフレクションを利用してクラス生成する場合は以下のような感じになるでしょうか。ネームスペース.クラス名で指定ができます。

MyClass.cs

public interface IPerson
{
	string Greeting();
}

public class Jananese : IPerson
{
	public string Greeting() { return "こんにちは!"; }
}

public class American : IPerson
{
	public string Greeting() { return "Hello!"; }
}
Program.cs
public class PersonFactory
{
	public IPerson CreatePerson(string className)
	{
		// クラスを生成
		Type type = Type.GetType(className);
		if (type == null)
		{
			throw new ArgumentException();
		}
		return (IPerson)Activator.CreateInstance(type);
	}
}

class Program
{
	static void Main(string[] args)
	{
		var _PersonFactory = new PersonFactory();
		Console.WriteLine(_PersonFactory.CreatePerson("reflection_test.American").Greeting());
	}
}

C#のほうが簡潔に書けます。:smiley:

まとめ

UnrealC++のリフレクションはBPのための実装の側面が大きい感じです。
とはいっても有効な使い方が他にもあるかと思いますので、どなたか教えて下さい。

8
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
6