BLOGブログ

2017.05.31UE4UE/ C++

[UE4] ObjectInitializerでコンポーネント生成を制御する

髭キャラが定着してきました。今回はプログラマ向けの記事です。
C++で UObject 継承のクラスを作る時に、コンストラクタの引数に const FObjectInitializer& ObjectInitializer を与えたり、与えなかったりすることがあるかと思います。
今回はこの謎の ObjectInitializer について調べてみました。

 

ObjectInitializer の有無による挙動の違い

なぜコンストラクタの書き方を変えられるのか、どのような呼び出し方をされるのか、どちらを使えば良いのか、気になっている方はいると思います。
結論から先に言うと、ObjectInitializer をコンストラクタで使いたい場合は定義する。それ以外で挙動の違いはほぼ無し です。(当たり前ですが)

 

UE4はビルドシーケンスの中で XXX.generated.h というヘッダを自動生成します。
ユーザー定義されたコンストラクタの呼び出しコードはこのヘッダ内に書かれます。
例えば AInitializerTest1 という ObjectInitializer をコンストラクタの引数に与えない クラスを定義した場合、InitializerTest1.generated.h には下記コードが生成されます。

 

これに対して AInitializerTest2 という ObjectInitializer をコンストラクタの引数に与える クラスを定義した場合、InitializerTest2.generated.h には下記コードが生成されます。

 

DEFINE_DEFAULT_CONSTRUCTOR_CALL というマクロを使うか、DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL というマクロを使うかという部分で違いが出ていますね。
これらのマクロは ObjectMacros.h に定義されており、内容は以下の通りとなっています。

 

つまりはビルドシーケンスの中で静的に分岐するようになっており、ランタイムで分岐するようにはなっていません。
両方のコンストラクタが定義された場合は ObjectInitializer を引数に与えたコンストラクタが呼び出されるようです。
また、ObjectInitializer 自体は引数に与えられようがいまいがインスタンス自体は生成されているので、挙動の違いはほぼ無し と言えます。

 

ObjectInitializer を使う時はどんな時?

ではそもそも ObjectInitializer とは何者で、どんな時に使えるのか。
ObjectInitializer の定義は UObjectGlobals.h にあります。
提供されるAPIはいくつかありますが、その中で最も多く利用されるであろう関数は CreateDefaultSubobject です。
CreateDefaultSubobject はコンストラクタ内でコンポーネントを作成するための関数で、C++で UObject を継承したクラスを定義する時にはよく使います。

 

この関数の存在だけで、ObjectInitializer を引数として受け取ることはほぼ必須だと思われるかもしれません。
しかしエディタ上から新規にC++クラスを作成する場合は ObjectInitializer は引数として与えられていない状態で作られます。
これには理由があり、実は CreateDefaultSubobject をラップした同名の関数が UObject から提供されており、そちらを用いることでより簡潔に記述できます。

 

つまり、内部的には同じものを使っているので、あえて ObjectInitializer を利用する必要はない といったところです。(関数オーバーヘッドは無視できるものとしています)
また、CreateDefaultSubobject の派生として以下の関数がありますが、これらも UObject でラップされています。

 

CreateEditorOnlyDefaultSubobject エディタのみで有効なコンポーネントを作成する
CreateOptionalDefaultSubobject 作られなくても良い(動作には必ずしも必要無い)コンポーネントを作成する
※ これは後述する関数と組み合わせて利用します
CreateAbstractDefaultSubobject Abstract 属性のクラスは CreateDefaultSubobject を使うと作成できないようになっているので、必要な場合は明示的にこの関数を利用する

 

これらのみを利用する場合は ObjectInitializer を利用する必要性はありません。
逆に言えば、これら以外の ObjectInitializer が提供するAPIを利用したい時は引数に与える必要性がある ということです。
UObject でラップされていない、ObjectInitializer が提供する関数は以下のものがあります。

 

SetDefaultSubobjectClass 親クラスがコンポーネントを作成する時、そのコンポーネントのクラスを名前指定で上書きする
※ 親クラスが作成するコンポーネントから派生したコンポーネントのみが指定できます
DoNotCreateDefaultSubobject CreateOptionalDefaultSubobject でコンポーネントを作成する時、名前指定でコンポーネントの作成を無効化できる

 

局所的にしか使わないものばかりですが、特に SetDefaultSubobjectClass はコンポーネント指向の設計においてとても役に立ちます。
例えばエンジンが提供する ACharacter を使いたいが、移動制御には独自の処理を入れたい(素の UCharacterMovementComponent を使いたくない)といった機会は少なからずあります。
もちろんエンジンに手を加えて UCharacterMovementComponent 自体を改造するということもできますが、SetDefaultSubobjectClass を用いることで、このような時も継承先で振る舞いを変更することが可能になります。

 

SetDefaultSubobjectClass を使ってみる

先程の例に挙げた通り、ACharacter 派生クラス(AMyCharacter)から、UCharacterMovementComponent 派生クラス(UMyCharacterMovementComponent)を扱えるようにしてみます。
それぞれの派生クラスを作る部分は省略しますが、AMyCharacter クラスのコンストラクタで以下の記述をします。

 

これにより CharMoveComp という名前で追加されるコンポーネントは、UMyCharacterMovementComponent を使うように設定されます。
ちなみに SetDefaultSubobjectClass 及び DoNotCreateDefaultSubobject の戻り値は FObjectInitializer のコピーなので、メソッドチェーンで記述することが可能です。
そしてお馴染み ThirdPersonCharacter の親クラスを ACharacter ではなく、AMyCharacter に変更することで、UMyCharacterMovementComponent に置き換わることを確認できます。

 

 

ぜひ活用していきたいところです。