C++ ときどき ごはん、わりとてぃーぶれいく☆

USAGI.NETWORKのなかのひとのブログ。主にC++。

UE4: コンポーネントをアタッチする状況により潜む罠

今回紹介する「コンポーネントのアタッチ状況により潜む罠」は3つ以上のコンポーネントを"縦"にアタッチ(†)する場合に生じる可能性のある罠です。

例として、2つの USceneComponent と1つの UStaticMeshComponent を"縦"にアタッチする状況を幾つか挙げます。この様な状況は1つの AActorRootComponent(=USceneComponent)と他の USceneComponentUAudioComponent などでも同様です。

Note: この記事では煩雑を避ける目的でコード例から一般的な実装では組み込むがこの記事については直接関係しないエラーチェックなどを省きます。また、一般的には .cpp へ実装する cotr の実装も .h へ直接記述しています。

Case 1: すべて1つの ctor で CreateDefaultSubobject で生成する場合

次のコード例のように関連するコンポーネントがすべて1つの ctor で生成されるような基礎的な実装では「罠」は生じません。

UCLASS()
class AMyActor: AActor
{
  GENERATED_BODY()
public:
  AActor()
  {
    // Create; S2, M1
    S2 = CreateDefaultSubobject< USceneComponent >( "S2" );
    M1 = CreateDefaultSubobject< UStaticMeshComponent >( "M1" );
    static ConstructorHelpers::FObjectFinder< UStaticMesh > m( TEXT( "/Game/MyMesh.MyMesh" ) );
    M1->SetStaticMesh( m.Object );
    // Attach; RootComponent, S2, M1
    S2->SetupAttachment( RootComponent );
    M1->SetupAttachment( S2 );
  }
  USceneComponent* S2 = nullptr;
  UStaticMeshComponent* M1 = nullptr;
};

ちなみに、 RootComponentS2 をアタッチしてから S2M1 をアタッチしようと、あるいは S2M1 をアタッチしてから RootComponentS2 をアタッチしようと、コンポーネントSetStaticMesh など準備を先に済ませてからアタッチしようと、アタッチした後でコンポーネントの準備を行おうと、何れも ctor を抜けた後の動作には変わりは生じません。それは罠にならないので1つ安心しましょう。

Case 2: すべて ctor 以外で NewObject で生成する場合

次のコードのような場合も「罠」は"まだ"生じません。

// AMyActor.h
UCLASS()
class AMyActor: AActor
{
  GENERATED_BODY()
protected:
  void Tick( float ) override;
public:
  USceneComponent* S2 = nullptr;
  UStaticMeshComponent* M1 = nullptr;
};
// AMyActor.cpp
void AMyActor::Tick( float )
{
  if ( something_trigger )
  {
    // NewObject; S2, M1
    S2 = NewObject< USceneComponent >( this );
    S2->RegisterComponent();
    M1 = NewObject< UStaticMeshComponent >( this );
    M1->RegisterComponent();
    M1->SetStaticMesh( Cast< UStaticMesh >( StaticLoadObject( UStaticMesh::StaticClass(), nullptr, TEXT( "/Game/MyMesh.MyMesh" ) ) ) );
    // Attach; RootComponent, S2, M1
    S2->SetupAttachment( RootComponent );
    M1->SetupAttachment( S2 );
  }
};

Case 3: NewObjectCreateDefaultSubobject するコンポーネントを扱う場合

次の状況で「罠」が発生します。

  1. ctor で CreateDefaultSubobject により UStaticMeshComponent を生成し自らへアタッチする USceneComponent の派生クラス UMyScene のようなものがある。
  2. ctor 以外で NewObject により UMyScene のオブジェクト S2 を生成し、 S2RegisterComponent し、 RootComponent へアタッチする AActor の派生クラス AMyActor のようなものがある。

言葉で書くとやや混乱するかもしれませんが、わりと簡単/単純で "ありそう" な状況です。

// UMyScene.h
UCLASS()
class UMyScene: USceneComponent
{
  GENERATED_BODY()
  UMyScene()
  {
    // Create; M1
    M1 = CreateDefaultSubobject< UStaticMeshComponent >( "M1" );
    static ConstructorHelpers::FObjectFinder< UStaticMesh > m( TEXT( "/Game/MyMesh.MyMesh" ) );
    M1->SetStaticMesh( m.Object );
    // Attach; M1 -> this(≃S2)
    M1->SetupAttachment( this );
  } // Create; this(≃S2)
public:
  UStaticMeshComponent* M1 = nullptr;
};
// UMyActor.h
UCLASS()
class AMyActor: AActor
{
  GENERATED_BODY()
protected:
  void Tick( float ) override;
public:
  UMyScene* S2 = nullptr;
  UStaticMeshComponent* M1 = nullptr;
};
// ****** TRAPPED ******
// AMyActor.cpp;
void AMyActor::Tick( float )
{
  if ( something_trigger )
  {
    // NewObject; S2, M1
    S2 = NewObject< UMyScene >( this );
    S2->RegisterComponent();
    // Attach; RootComponent, S2, M1
    S2->SetupAttachment( RootComponent );
  }
};

このように実装した AMyActor の動作を確認すると、見えるはず、と意図したであろう UStaticMeshComponent は見えません。存在するのに存在しません。

この罠を抜け出すための"ヒント"を示します。

// ****** AVOID TRAP ( DIRTY ) ******
// AMyActor.cpp;
void AMyActor::Tick( float )
{
  if ( something_trigger )
  {
    // NewObject; S2, M1
    S2 = NewObject< UMyScene >( this );
    S2->RegisterComponent();
    // Attach; RootComponent, S2, M1
    S2->SetupAttachment( RootComponent );
    // ****** DIRTY IMPLEMENTATION ******
    S2->M1->RegisterComponent();
  }
};

状況と必要な対応がわかりやすいように "DIRTY" な応急対処を示しました。

  1. NewObject で生成されるオブジェクト S2 が、
  2. その S2 の ctor において CreateDefaultSubobject で生成したオブジェクト M1 がある場合、
  3. M1RegisterComponent されていない。(ので、そうならない設計にするか、対処可能な実装を施す必要が生じる事がある)

という「罠」です・x・

もう少し複雑な状況でうっかりはまって気付かない事があると精神がすり減る事もありそうです。

Appendix

(†): 「"縦"にアタッチ」は Component0 へ Component1 をアタッチ、 Component1 へ Component2 をアタッチするような、1つのコンポーネントへ複数のコンポーネントをアタッチする状況を"横"、1つのコンポーネントへ1つのコンポーネントをアタッチし、アタッチしたコンポーネントへまた1つのコンポーネントをアタッチする状況を"縦"と模して表現したものです。

References