はじめに
※確認UEバージョン:5.0.3
ポインタとして扱っている構造体のUObjectメンバ変数がGCで解放されないようにする手順を解説します。
構造体ポインタとして扱いたいが、GCを防ぐ目的でUObject継承クラス化したくない方はぜひ最後までご覧ください!
構造体のポインタはUPROPERTY化できない
下記のように、ポインタで構造体を扱うと構造体ポインタにUPROPERTYは付けられないのでエラーが出てしまいます。
USTRUCT()
struct FMyStruct {
GENERATED_BODY()
UPROPERTY(Transient)
TObjectPtr<UObject> MyHogeObject;
};
UCLASS(Blueprintable)
class MYTESTPROJECT_API UMyObject : public UObject {
GENERATED_BODY()
public:
// エラー内容:Inappropriate '*' on variable of type 'FMyStruct', cannot have an exposed pointer to this type. [UnrealHeaderTool Error]
// 訳:FMyStruct' 型の変数に不適切な '*' があり、この型への公開ポインタを持つことができません。
// UPROPERTY(Transient)
// FMyStruct* MyStruct;
};
そのためUPROPERTYではないポインタとして扱う必要があり、構造体内のUObjectは参照できずGCで消されてしまいます。
以下の項目で対応方法をいくつか紹介します。
方法1:構造体ポインタを持っているUObjectクラス内でGCリファレンスを追加する
構造体ポインタを所持しているUObjectクラスにAddReferencedObjects関数を追加して、構造体をリファレンスオブジェクトとして登録する方法です。
手順
UObjectを継承したクラス宣言内で下記関数を宣言してください。
// Begin UObject Interface
static void AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector);
// End UObject Interface
この関数はクラス情報のUProperty登録にないオブジェクトを手動で登録できる関数になります。
virtualではなく、static関数になっていますが宣言するだけで関数呼び出しがありますので、問題ありません。
関数定義は以下のようにします。
void UMyObject::AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector)
{
const UMyObject* pThisObject = CastChecked<UMyObject>(InThis);
if(pThisObject->MyStruct != nullptr)
{
const UScriptStruct* pStructClass = FMyStruct::StaticStruct();
Collector.AddReferencedObjects(pStructClass, pThisObject->MyStruct);
}
Super::AddReferencedObjects(InThis, Collector);
}
手順だけだとわかりにくいので、下記で解説していきます。
const UMyObject* pThisObject = CastChecked<UMyObject>(InThis);
UObject*のInThisを自クラス型にキャストして、pThisObject に格納しています。
この関数がstatic関数なので、直接メンバ変数を記述してもコンパイルエラーになるためです。
また、Castではなく、CastCheckedにしている理由は、InThisが自クラスオブジェクトか継承クラス以外の型を渡されることがないからです。(nullだった場合はCastChecked内でアサートが出ます。そのためこれ以降の行ではpThisObjectのnullチェックをしていません。)
if(pThisObject->MyStruct != nullptr)
MyStructのnullチェックです。
nullを渡してもAddReferencedObjects関数内で読み取ろうとしてエラーが出てしまうため追加しています。
Collector.AddReferencedObjects(pStructClass, pThisObject->MyStruct);
構造体ポインタ内のPropertyをリファレンスオブジェクトとして登録します。
AddReferencedObjectsはたくさんのオーバーロード候補があるのですが、今回は下記を使用しています。
void AddReferencedObjects(
const class UScriptStruct*& ScriptStruct,
void* StructMemory,
const UObject* ReferencingObject = nullptr,
const FProperty* ReferencingProperty = nullptr);
第1引数には構造体のStaticStruct()を、第2引数には構造体ポインタを入れてください。
方法2:構造体ポインタ型を独自に作成する
構造体ポインタの代わりに独自に作成したUSTRUCTの型を持たせて、作成した構造体内で構造体ポインタを管理する方法です。
手順
まず、専用のポインタをもつUSTURCT構造体を作成します。
USTRUCT()
struct FMyStructPtr {
GENERATED_BODY()
public:
FMyStruct* Pointer = nullptr;
public:
void AddStructReferencedObjects(class FReferenceCollector& Collector);
};
template<>
struct TStructOpsTypeTraits<FMyStructPtr> : public TStructOpsTypeTraitsBase2<FMyStructPtr>
{
enum
{
WithAddStructReferencedObjects = true,
};
};
作成した構造体にAddStructReferencedObjects関数を追加したので、処理を書き込みます。
void FMyStructPtr::AddStructReferencedObjects(FReferenceCollector& Collector)
{
const UScriptStruct* Struct = FMyStruct::StaticStruct();
if (Struct != nullptr && Pointer != nullptr)
{
Collector.AddReferencedObjects(Struct, Pointer);
}
}
構造体ポインタ型をメンバ変数宣言していた箇所を、作成した構造体型にしたうえでUPROPERTYにします。
UPROPERTY(Transient)
FMyStructPtr MyStructPtr;
手順だけだとわかりにくいので、下記で解説していきます。
TStructOpsTypeTraits<>
FMyStructPtrの下で宣言しているTStructOpsTypeTraitsは、USTRUCTの型特性を設定できるテンプレート構造体になります。
TStructOpsTypeTraitsBase2 を継承していて、TStructOpsTypeTraitsBase2 の宣言を確認するとデフォルトの型特性フラグ情報を確認することができます。
今回、WithAddStructReferencedObjectsをtrueにしていますが、このフラグをtrueにすることでGC回収時にAddStructReferencedObjectsが呼ばれるようになり、UPROPERTYになっていないUObjectのGCを防ぐことができます。
詳しくは下記をご確認ください。
AddStructReferencedObjectsの処理については、方法1とほぼ同じですので、解説は省略します。
方法3:プラグイン「StructUtils」を使用する
UEに標準で入っているプラグイン StructUtls を使用する方法です。
StructUtls内のInstancedStruct構造体を使用することで、方法2と同じような形でGCを防ぐことができます。
さらに、GCを防ぐだけでなくUEエディタで値を編集・アセットに構造体のデータを保存することができるようです。
手順
プラグイン「StructUtls」を有効にします。
このプラグインはデフォルトで有効になっていませんので、UE側で有効にする必要があります。
UEメインウィンドウから[編集]タブを開き、プラグインウィンドウを出してください。
プラグイン検索で「Struct」と入力すると下のほうにStruct Utlsプラグインが表示されます。
左側にあるチェックボックスにチェックを入れてください。
チェックをすると再起動を促されるので、再起動しましょう。
プラグイン有効後、自プロジェクトで使用するためにBuild.csを編集します。
プロジェクト名.Build.csを開いてください。
開いたら、 PublicDependencyModuleNames に”StructUtls”を追加してください。
以上の作業で、自プロジェクトでStructUtlsが使用できるようになりました。
構造体ポインタ型をメンバ変数宣言していた箇所を、FInstancedStruct型にしたうえでUPROPERTYにします。( “InstancedStruct.h”をインクルードしてください。)
UPROPERTY(Transient)
FInstancedStruct MyStructPtr;
FInstancedStructの扱いについて以降の項目で解説していきます。
FInstancedStruct に構造体のポインタを設定
ポインタを設定する方法は3つあります。
引数付きコンストラクタで構造体を生成したい場合は、InitializeAs 関数か Make 関数を使用する必要があります。
コンストラクタで設定する方法
UMyTestObject::UMyTestObject()
: MyStructPtr(FMyStruct::StaticStruct())
{
}
InitializeAs 関数を使用する方法
void UMyTestObject::Init()
{
MyStructPtr.InitializeAs(FInstancedStruct::StaticStruct());
// 引数付きコンストラクタで構造体を生成する場合は
//MyStructPtr.InitializeAs<FInstancedStruct>(/* ここにコンストラクタ引数を入れてください。 */);
}
Make 関数を使用する方法
Make関数はstaticなのでUObjectのコンストラクタでもよぶことも可能です。
UMyTestObject::UMyTestObject()
: MyStructPtr(FInstancedStruct::Make(FInstancedStruct::StaticStruct()))
// 引数付きコンストラクタで構造体を生成する場合は
//: MyStructPtr(FInstancedStruct::Make<FInstancedStruct>(/* ここにコンストラクタ引数を入れてください。 */))
{
}
FInstancedStruct 内の構造体データを取得
constの有無、参照かポインタ型かで呼び出す関数が異なります。
// const参照型を取得
const FMyStruct& ConstantMyStructRef = MyStructPtr.Get<FMyStruct>();
// 非const参照型を取得
FMyStruct& MyStructRef = MyStructPtr.GetMutable<FMyStruct>();
// constポインタ型を取得
const FMyStruct* ConstantMyStructPointer = MyStructPtr.GetPtr<FMyStruct>();
// 非constポインタ型を取得
FMyStruct* MyStructPointer = MyStructPtr.GetMutablePtr<FMyStruct>();
まとめ
いかがでしたでしょうか。
筆者は方法3で対応しましたが、方法1・2にもメリットはありますので、ケーズバイケースで対応方法を検討していくことをお勧めします。(構造体をポインタで持たせるケースは少ないですが…)
最後までご覧いただき、ありがとうございました!