はじめに
この記事は, Unreal Engineでどうしても必要なことがあり, 調べたことについてまとめます.
リフレクション
リフレクション, リフレクティブプログラミングは, プログラムの構造や振る舞いを取得する機能やそのような動作のことをいいます.
主に次のような用途に使われます. 組み合わせ爆発に対応するためであったり, 構造が事前にわからないときに使用します.
- 単体テスト
- シリアライゼーション
- エディタ
- 依存性の注入
フレームワークやライブラリの一部として使われます. アプリケーションプログラムで不用意に使うと, プログラムの構造や意味を変えてしまうため危険です.
Unreal Engineのリフレクション
Unreal Engineでは, 特定のタグをUnreal Header Tool
が解析し, リフレクションやガベージコレクションに必要なコードを生成します. 次のようなコードになります.
#pragma once
#include <CoreMinimal.h>
#include <Containers/Array.h>
#include "ActorA.generated.h"
UENUM()
enum class EEnumA : uint8
{
A,
B,
C,
};
USTRUCT()
struct REFLECTION_API FStructA
{
GENERATED_BODY()
UPROPERTY()
int32 Int_;
};
UCLASS()
class REFLECTION_API AActorA : public AActor
{
GENERATED_BODY()
public:
virtual void BeginPlay() override{}
virtual void Tick(float DeltaTime) override{}
UFUNCTION()
int32 Get() const;
UFUNCTION()
void Set(int32 Int);
//UFUNCTION()
//const int32& operator[](int32 Index) const; // 複雑なものはだめ
int32& operator[](int32 Index);
private:
UPROPERTY()
int32 Int_;
UPROPERTY()
TArray<int32> Array_;
};
#include "ActorA.h"
int32 AActorA::Get() const
{
return Int_;
}
void AActorA::Set(int32 Int)
{
Int_ = Int;
}
int32& AActorA::operator[](int32 Index)
{
return Array_[Index];
}
Unreal Header Tool
は, UCLASS
などを解析し, (プロジェクトディレクトリ)\Intermediate\Build\(プラットフォーム名)\UnrealEditor\Inc\(モジュール名)
などにコードを生成します. などというのはバージョンに依って変更があるからです.
Unreal Engineでは対応するクラスがあり, UCLASS
はUClass
が対応します, UClass
を見れば大体何ができるかがわかります.
例として, FPropertyをみると, 子孫にFMapProperty
があります. これは, Unreal Header Tool
が認識できる, TMap
以外の連想配列は対応できないということです.
余談1:コーディング規約
基本はEpic社のコーディング規約に則っていますが, 私はUnreal Engineが大嫌いなのでアレンジ部分があります.
規約ではメンバ変数はPascalケースですが, 私は_
を接尾語として付けます. これは, 次のようにメンバ名と実引数名が同じとき, In
を仕方なく付けるというプログラムが間抜けに思うからです.
void Hoge::Set(int32 InVar)
{
Var = InVar;
}
メンバ変数
AActorA
について, 次のようなテストをしてみます. 関数呼び出しはWIPで上手くいっていませんが, プロパティについては読み書きができます.
オブジェクトのメタデータのロードは, Class'/Script/(モジュール名)/(接頭辞を除いたオブジェクト名)'
となります.
#include <Misc/AutomationTest.h>
#include "ActorA.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestReflection, "Reflection.TestReflection", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter);
bool FTestReflection::RunTest(const FString& Parameters)
{
UStruct* StructA = Cast<UStruct>(StaticLoadObject(UObject::StaticClass(), nullptr, TEXT("Class'/Script/Reflection.StructA'"), nullptr, LOAD_None, nullptr, true));
{
UE_LOG(LogTemp, Log, TEXT("FStructA:"));
FStructA InstanceA = {123};
for (TFieldIterator<FProperty> It(StructA); It; ++It) {
FProperty* Property = *It;
UE_LOG(LogTemp, Log, TEXT(" %s"), *Property->GetName());
}
FProperty* PropertyInt = StructA->FindPropertyByName(TEXT("Int_"));
int32* Int = PropertyInt->ContainerPtrToValuePtr<int32>(&InstanceA);
UE_LOG(LogTemp, Log, TEXT(" %d"), *Int);
}
UClass* ClassActorA = StaticLoadClass(UObject::StaticClass(), nullptr, TEXT("Class'/Script/Reflection.ActorA'"), nullptr, LOAD_None, nullptr);
{
UE_LOG(LogTemp, Log, TEXT("AActorA:"));
AActor* InstanceA = GWorld->SpawnActor(ClassActorA);
for (TFieldIterator<FProperty> It(ClassActorA); It; ++It) {
FProperty* Property = *It;
UE_LOG(LogTemp, Log, TEXT(" %s"), *Property->GetName());
}
FIntProperty* PropertyInt = CastField<FIntProperty>(ClassActorA->FindPropertyByName(TEXT("Int_")));
int32* Int = PropertyInt->ContainerPtrToValuePtr<int32>(InstanceA);
PropertyInt->SetPropertyValue(Int, 123);
UE_LOG(LogTemp, Log, TEXT(" Get %d"), *Int);
UFunction* Setter = ClassActorA->FindFunctionByName(TEXT("Set"));
{
int32 Value = 1;
InstanceA->ProcessEvent(Setter, &Value);
}
InstanceA->Destroy();
UE_LOG(LogTemp, Log, TEXT(" Get %d"), *Int);
}
return true;
}
悪用して別モジュールのクラスをインスタンシング
やりたかったことはこれです.
Unreal Engineでは, モジュールと呼ばれる仕組みによって, プログラムの機能単位を分けることができます. 実体は動的リンクライブラリです.
例えば次のように, プロジェクトを複数のモジュール(動的リンクライブラリ)に分けて管理することができます. モジュールには依存関係があり, 依存先から依存元のプログラムを参照することはできません. 循環参照を許した場合の動作は保証されません.
さてここで, ReflectionLib
からReflection
のクラスをインスタンシングしたいという, 悪巧み切なる希望があるとします.
こんなことをすると普通に動作します. OpenGLやVulkanのAPIをロードしている感じですね. ただの動的リンクライブラリですからね.
#include <Misc/AutomationTest.h>
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestReflectionLib, "Reflection.TestReflectionLib", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter);
bool FTestReflectionLib::RunTest(const FString& Parameters)
{
UStruct* StructA = Cast<UStruct>(StaticLoadObject(UObject::StaticClass(), nullptr, TEXT("Class'/Script/Reflection.StructA'"), nullptr, LOAD_None, nullptr, true));
{
UE_LOG(LogTemp, Log, TEXT("FStructA:"));
for (TFieldIterator<FProperty> It(StructA); It; ++It) {
FProperty* Property = *It;
UE_LOG(LogTemp, Log, TEXT(" %s"), *Property->GetName());
}
}
UClass* ClassActorA = StaticLoadClass(AActor::StaticClass(), nullptr, TEXT("Class'/Script/Reflection.ActorA'"), nullptr, LOAD_None, nullptr);
{
UE_LOG(LogTemp, Log, TEXT("AActorA:"));
AActor* InstanceA = GWorld->SpawnActor(ClassActorA);
FIntProperty* PropertyInt = CastField<FIntProperty>(ClassActorA->FindPropertyByName(TEXT("Int_")));
int32* Int = PropertyInt->ContainerPtrToValuePtr<int32>(InstanceA);
int32 Value0 = *Int;
PropertyInt->SetPropertyValue(Int, 123);
int32 Value1 = PropertyInt->GetPropertyValue(Int);
UE_LOG(LogTemp, Log, TEXT(" AActorA::_Int %d => %d"), Value0, Value1);
InstanceA->Destroy();
}
return true;
}
まとめ
メソッドについては調査中ですが, プロパティアクセスは可能なことがわかりました. 内部については, C++の未定義ばかりなのでどんな環境でも動く保証はありません.
悪用については, 真似しないでください.