概要
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()
がついているメンバだけアクセス可能です。
構造体
以下のような構造体にアクセスしてみます。
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
クラス
以下のようなクラスにアクセスしてみます。
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
を使って取得することができます。以下サンプルコード。
// メンバ変数 Param1Float のFPropertyを取得する
FProperty* _Property = FindField<FProperty>(UTestMyClass::StaticClass(), TEXT("Param1Float"));
文字列指定で関数を探して実行する1
文字列指定で関数を探して、その関数を実行してみます。
実行には ProcessEvent
を使います。引数がない場合は第2引数は nullptr
でOKです。
テストコード
実行されるメソッドには UFUNCTION()
をつけないと呼び出しができません。
UCLASS()
class MYPROJECT_API AMyActor : public AStaticMeshActor
{
GENERATED_BODY()
public:
// ...省略...
// 実行されるメソッド
UFUNCTION()
void Func(int32 _ArgInt, float _ArgFloat, FString _ArgString);
};
// 実行
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です。
テストコード
// 実行
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++定義クラス
インターフェイスを実装した以下のようなクラスを想定します。
// インターフェイス定義
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!")) ); }
};
// インターフェイスのコンストラクタ
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"
となります。
実行コード
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
からパス指定をしてロードしてオブジェクトを取得します。
実行コード
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エディタにてプロパティを変更したときにリフレクション機能を使って変数名を調べて任意の処理を行うことができます。
以下サンプルコード。
UCLASS()
class MYPROJECT_API UMyActorComponent : public UActorComponent
{
GENERATED_BODY()
// ...省略...
#if WITH_EDITOR
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
#endif
UPROPERTY(EditAnywhere)
int32 MyVariable = 0;
};
// エディタ上で変数変更があった時の処理
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#でリフレクションを利用してクラス生成する場合は以下のような感じになるでしょうか。ネームスペース.クラス名
で指定ができます。
public interface IPerson
{
string Greeting();
}
public class Jananese : IPerson
{
public string Greeting() { return "こんにちは!"; }
}
public class American : IPerson
{
public string Greeting() { return "Hello!"; }
}
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#のほうが簡潔に書けます。
まとめ
UnrealC++のリフレクションはBPのための実装の側面が大きい感じです。
とはいっても有効な使い方が他にもあるかと思いますので、どなたか教えて下さい。