リフレクションは、プログラムが実行時にプログラム自身を検査する機能です。これはとてつもなく便利な機能であり、アンリアル・エンジン の基礎的なテクノロジーとして、エディタ内の詳細パネルや、シリアライゼーション、ガーベジコレクション、ネットワークのレプリケーション、ブループリント/C++ の通信といった多くのシステムを支えています。ただし、C++ は本来リフレクションがサポートされていません。そのため Unreal には、C++ のクラスや構造体、関数、メンバー変数、列挙体に関する情報を取り込み、問い合わせ、操作するための独自のシステムが備わりました。通常私たちはリフレクションのことをプロパティ システムとみなします。リフレクションはグラフィックスの用語でもあるのですから。
このリフレクション システムは、選択式です。ユーザーは、リフレクション システムから見えるようにする型やプロパティにアノテーションを付ける必要があります。そうすることによって、プロジェクトをコンパイルする際に、Unreal Header Tool (UHT) がその情報が取り込めるようになります。
マークアップ
ヘッダにリフレクションされている型が含まれていることを示すマークを付けるために、特別な include をファイルの先頭に置きます。これによって、UHT は、このファイルを評価の対象にすることができます。また、これはシステムの実装のためにも必要です (詳しくは「舞台裏をのぞく」のセクションをご覧ください)。
#include "FileName.generated.h"
これで、UENUM()、UCLASS()、USTRUCT()、UFUNCTION()、UPROPERTY() を使って、ヘッダの中でさまざまな型やメンバー変数にアノテーションを付けることができるようになります。これらのマクロはそれぞれ、型またはメンバーの宣言よりも先に置かれ、付加的な指定子キーワードを含むことができます。実際のワールドのサンプルを見てみましょう (StrategyGame からのものです)。
//////////////////////////////////////////////////////////////////////////
// Base class for mobile units (soldiers)
#include "StrategyTypes.h"
#include "StrategyChar.generated.h"
UCLASS(Abstract)
class AStrategyChar : public ACharacter, public IStrategyTeamInterface
{
GENERATED_UCLASS_BODY()
/** How many resources this pawn is worth when it dies. */
UPROPERTY(EditAnywhere, Category=Pawn)
int32 ResourcesToGather;
/** set attachment for weapon slot */
UFUNCTION(BlueprintCallable, Category=Attachment)
void SetWeaponAttachment(class UStrategyAttachment* Weapon);
UFUNCTION(BlueprintCallable, Category=Attachment)
bool IsWeaponAttached();
protected:
/** melee anim */
UPROPERTY(EditDefaultsOnly, Category=Pawn)
UAnimMontage* MeleeAnim;
/** Armor attachment slot */
UPROPERTY()
UStrategyAttachment* ArmorSlot;
/** team number */
uint8 MyTeamNum;
[以下のコードは省略します]
};
このヘッダでは、ACharacter から派生した新たなクラス AStrategyChar が宣言されています。UCLASS() が使われることによって、そのクラスがリフレクションされるように指定されています。また、C++ の定義の内部でマクロの GENERATED_UCLASS_BODY() にも関連づけられています。GENERATED_UCLASS_BODY() / GENERATED_USTRUCT_BODY() マクロは、追加の関数と typedef をクラス本体に注入するので、リフレクションされるクラスまたは構造体の中で必要となります。
最初に表示されているプロパティは、ResourcesToGather です。これは、EditAnywhere と Category=Pawn によってアノテーションされています。それによって、このプロパティがエディタのどの詳細パネルでも変更できることになるとともに、Pawn のカテゴリに表示されることになります。アノテーションされている関数で BlueprintCallable とカテゴリが付いたものが 2 つありますが、これは、ブループリントから呼び出して使うことができるという意味です。
MyTeamNum の宣言から分かるように、リフレクションされるプロパティとリフレクションされないプロパティを同じクラスで混在させても構いません。ただし、リフレクションに依存するどのシステムからも、リフレクションされていないプロパティは見えないので注意してください (例: リフレクションされていない UObject の生のポインタを保存することは、ガベージ コレクターによって参照が認識されないため、たいていの場合危険です)。
指定子キーワード (EditAnywhere や BlueprintCallable など) はそれぞれ、意味と使用法に関する短いコメントが付けられて、ObjectBase.h の中で説明されています。キーワードの動作が不明な場合は、Alt+G を押すことによって、ObjectBase.h の定義が開かれることになっています。(これらのキーワードは、正式な C++ のキーワードではありませんが、インテリセンスまたは VAX はその違いを無視するか理解しないようです。)
より詳しい情報については、「Gameplay Programming Reference」(ゲームプレイ プログラミング レファレンス) をご覧ください。
制約
UHT は、正式な C++ のパーサーではありません。UHT は、この言語のかなりのサブセットを理解し、リフレクションされている型や関数、プロパティにのみ注意を払いながら、スキップ可能なテキストはどんどんスキップしようとします。ただし、まだ UHT が混乱するものがあるため、既存のヘッダにリフレクションされた型を追加する場合に、#if CPP / #endif の中で、書き換えたりラップしなければならない場合があります。また、アノテーションされたプロパティまたは関数に #if/#ifdef を使用しないようにしてください (ただし、WITH_EDITOR と WITH_EDITORONLY_DATA についてはその限りではありません)。その理由は、生成されるコードがそれらを参照して、その define が true ではない設定の場合にコンパイルエラーを引き起こすからです。
たいていの一般的な型は、期待どおりに動作します。しかし、このプロパティ システムがあらゆる C++ の型を処理できるとは限りません (とりわけ、TArray や TSubclassOf といったテンプレート型は、ごくわずかしかサポートされていません。また、それらのテンプレート パラメータは、ネストされた型にはできません)。実行時に処理されない型がアノテーションされると、UHT は、記述エラーのメッセージを吐きます。
リフレクション データを利用する
たいていのゲームコードでは、実行時にこのプロパティ システムを無視して、このシステムが支えている他のシステムを利用できます。しかし、ツールのコードを書く場合やゲームプレイのシステムを構築する場合には、このシステムが役立つことがあるかもしれません。
このプロパティ システムの型の階層は次のようになっています。
UField
UStruct
UClass (C++ クラス)
UScriptStruct (C++ 構造体)
UFunction (C++ 関数)
UEnum (C++ 列挙体)
UProperty (C++ メンバー変数または関数の引数)
(さまざまな型の多数のサブクラス)
UStruct は、基本的な集合体 (C++ のクラスや構造体、関数などのような、他のメンバーを含むもの) です。これは、C++ の構造体 (UScriptStruct が該当します) と混同してはいけません。UClass は、関数またはプロパティをそれらの子として含めることができます。一方、UFunction と UScriptStruct は、プロパティだけに限定されます。
リフレクションされた C++ の型のための UClass または UScriptStruct を得るには、UTypeName::StaticClass() または FTypeName::StaticStruct() とします。UObject インスタンスのための型を得るには、Instance->GetClass() を使います。(構造体のために必要なストレージまたは一般的な基本クラスがないため、構造体のインスタンスの型を得ることはできません。)
UStruct のすべてのメンバーをイテレートするには、TFieldIterator を使います。
for (TFieldIterator<UProperty> PropIt(GetClass()); PropIt; ++PropIt)
{
UProperty* Property = *PropIt;
// プロパティについて何らかの処理を行います。
}
TFieldIterator へのテンプレート引数は、フィルタとして使用されます (UField を使用してプロパティと関数の両方、またはどちらか一方を見ることができます)。イテレータ コンストラクタへの 2 番目の引数は、指定されたクラス/構造体に導入されたフィールドのみが必要なのか、それとも親クラス/構造体のフィールドも必要とする (デフォルト) のかを指示します。関数には効果がありません。
各型には、ユニークなフラグのセット (EClassFlags + HasAnyClassFlags など) と、UField から継承された汎用のメタデータ ストレージ システムが備わっています。キーワード指定子は、通常、フラグまたはメタデータのどちらかとして保存されます。どちらになるかは、実行時のゲームで必要とされるのか、それともエディタ機能のためにだけに必要なのかということに依存します。これによって、エディタ用のみのメタデータは削除されてメモリの節約となりますが、実行時のフラグは常に使用できます。
リフレクション データを使ってさまざまなことができます (プロパティの列挙、データ主導による値の取得またはセット、リフレクションされた関数の呼び出し、新たなオブジェクトの生成)。ここでどれかを掘り下げるよりも、UnrealType.h と Class.h に目を通し、あなたがやろうとしていることに近いコードを探すほうがおそらく簡単に行くはずです。
舞台裏をのぞく
このプロパティ システムを使用するだけでよいのであれば、このセクションは飛ばしても構いません。ただし、システムがどのように動作するかということが分かれば、リフレクションされる型を含むヘッダについて的確に判断し制約に対処しやすくなる場合があるでしょう。
Unreal Build Tool (UBT) と Unreal Header Tool (UHT) は、協力して、実行時のリフレクションを支えるのに必要なデータを生成します。UBT は、その仕事を実行するためにヘッダをスキャンします。リフレクションされる型を少なくとも 1 個入っているヘッダを含むあらゆるモジュールを記憶します。これらのヘッダのどれかが、最後のコンパイル以降に変更になった場合は、UHT が呼び出されて、リフレクション データを取り込み更新します。UHT は、ヘッダをパースし、リフレクション データ一式を作り上げ、リフレクション データ (モジュール別 generated.inl に寄与する) を含む C++ コード、さらに各種ヘルパー関数およびサンク関数 (ヘッダ別 .generated.h) を生成します。
リフレクション データを、生成された C++ コードとして保存することには、大きなメリットがあります。その 1 つは、バイナリと同期することが保証されるということです。古かったり期限切れしているリフレクション データをロードすることはあり得ません。これは、残りのエンジンコードとともにコンパイルされるからです。さらに、特定のプラットフォーム/コンパイラ/オプティマイゼーションのコンボによるパッキングの挙動がリバース・エンジニアリングされることなく、メンバーのオフセットなどが C++ の式を使用して起動時に計算されるようになります。また、UHT は、生成されるヘッダをまったく使わないスタンドアロンのプログラムとして作られているため、UE3 のスクリプト コンパイラに関してありがちな卵が先か鶏が先かという問題が回避されます。
生成される関数には、その型のリフレクション データを取得しやすくする StaticClass() / StaticStruct() などのようなものが含まれます。また、ブループリントやネットワーク レプリケーションから C++ の関数を呼び出すために使用されるサンクも生成される関数に含まれます。これらは、クラスまたは構造体の一部として宣言されなければなりません。リフレクションされた型が GENERATED_UCLASS_BODY() または GENERATED_USTRUCT_BODY() マクロを含む理由はここにあります。これらマクロを定義する #include "TypeName.generated.h" についても同様です。