【UE5】UNiagaraComponentのインスタンスをコンテナで保持するときの注意点
今回はUNiagaraComponentのインスタンスをコンテナで保持するときの注意点についてまとめます。
- 検証環境バージョン:Unreal Engine “5.5.4”
UNiagaraComponentを保持したいケース
だいぶ状況は限られますが、Niagaraエフェクトのインスタンスを管理したり、破棄タイミングを制御したいときがあります。
- [ケース1] 常駐や再利用前提のエフェクト:Niagara生成のコストを抑えてインスタンスを使い回す
- [ケース2] 大量発生するエフェクト生成数をコントロールしたい:例えば最大32までに抑える。または超えたときは古いエフェクトを消すなど
「ケース1」については、生成や破棄タイミングはある程度限られるため、管理はあまり難しくなく、ポインタを直接管理するなどある程度雑な実装でも問題なく動作するかもしれません。
ですが「ケース2」では生成や破棄を厳密に管理しないと、不正なポインタアクセスや場合によってはメモリ破壊を引き起こす可能性があるので、今回紹介する方法が役に立つのではないかと思っています。
ポインタ管理には TWeakObjectPtr (弱参照ポインタ) を使う
ポインタを管理する場合は、そのまま扱うのではなく TWeakObjectPtr (弱参照ポインタ) をポインタに被せると比較的安全に扱えます。
// ☓:生ポインタはおすすめできない.
UNiagaraComponent* pNiagaraComp = {};
// ◯:TWeakObjectPtrを被せる.
TWeakObjectPtr<UNiagaraComponent> NiagaraCompPtr = {};これにより、TWeakObjectPtr::IsValid() で無効かどうか判定したり、TWeakObjectPtr::Get() でアドレスが有効な場合のみポインタを返す (それ以外は nullptr) ようになるためポインタのアドレスが「ある程度」安全であることが保証されます。
TWeakObjectPtr<UNiagaraComponent> EffectCompPtr;
if(auto LComp = EffectCompPtr.Get()) {
// 無効なアドレスでないことが保証される.
// 何らかの UNiagaraComponent に対する処理を行う.
}TMapのキーで管理する場合は FObjectKey を使う
たくさんのオブジェクトのアドレスを管理したい場合があるとします。そこで例えば TMap のキーとしてオブジェクトのアドレスをキーにするとします。しかしアドレス値は実行環境依存で、予期せぬ問題を引き起こす可能性があります。
安全にオブジェクトをキーとして扱うには「FObjectKey」を使うのをおすすめします。これは “UObject” が持つ “GetUniqueID()” をもとに作られるキーであり、エンジンが保証する値であり、かつある程度ユニーク (一意) であることが保証されているキーです。
ということで、TMapでオブジェクトをポインタを管理する場合には以下の構成にすると良いです。
- キー:FObjectKey (高速な検索キー生成用)
- 値:TWeakObjectPtr (安全な参照用)
FObjectKeyの基本的な使い方
#include "UObject/ObjectKey.h" // FObjectKeyを使うために必要.
// TMapの定義.
TMap<FObjectKey, TWeakObjectPtr<UNiagaraComponent>> m_Map;
// 要素の追加.
void AddEntry(UNiagaraComponent* pComp)
{
// キー作成.
FObjectKey LKey(pComp);
// 値作成.
TWeakObjectPtr<UNiagaraComponent> LEntry = pComp;
// 要素を追加.
m_Map.Add( LKey, LEntry );
}FObjectKeyの使い方は簡単で、コンストラクタに UObject (≒ UNiagaraComponent) のポインタを渡すだけです。
ただ、生成と破棄を繰り返すUObjectを扱うときには注意が必要です。それは「無効なアドレス・オブジェクト」「破棄フラグが立っている」場合、それをFObjectKeyに渡すと check() により停止します。
安全にFObjectKeyを使うには
無効なアドレスかどうかだけであれば IsValid() で判定できますが、「破棄フラグが立っているかどうか」は以下の記述で判定します。
bool IsValidNiagaraComponent(UNiagaraComponent* pComp) const
{
if(IsValid(pComp) == false) {
return false;
}
// 追加安全性チェック.
if(!pComp->IsRegistered()) {
return false; // 登録されていない.
};
if(pComp->HasAnyFlags(RF_BeginDestroyed | RF_FinishDestroyed)) {
return false; // 破棄中.
}
return true;
}まず、IsRegistered() でオブジェクトツリーに含まれているかどうか (Worldに所属、またはUnregisterしていないか) を判定し、HasAnyFlags() に “RF_BeginDestroyed” と “RF_FinishDestroyed” を指定することで、破棄対象になっているかどうかがチェックできます。
上記関数を定義しておくと、以下の記述で FObjectKey が生成可能かどうかをチェックできます。
// 指定のエフェクトを内包しているかどうか.
bool ContainEffect(UNiagaraComponent* pComp) const
{
if(IsValidNiagaraComponent(pComp) == false) {
return false; // そもそも無効なエフェクト.
}
// キーの作成.
FObjectKey(pComp)
return m_Map.Contains(LKey);
}
// Entryの追加.
bool AddEntry(UNiagaraComponent* pComp)
{
if(IsValidNiagaraComponent(pComp) == false) {
return false; // そもそも無効なエフェクト.
}
if(ContainEffect(pComp)) {
return false; // すでに含まれている場合は何もしない.
}
// キー作成.
FObjectKey LKey(pComp);
// 値作成.
TWeakObjectPtr<UNiagaraComponent> LEntry = pComp;
// 要素を追加.
m_Map.Add(LKey, LEntry);
return true; // 追加成功.
}UNiagaraComponentに破棄フラグが立っているかどうかのチェックが必要なタイミング
FObjectKeyを生成する以外でも、UNiagaraComponentに破棄フラグが立っているかどうかのチェックが必要なタイミングはいくつか存在します。
- コンテナからの削除 (クリーンアップ) が必要かどうかを判定するタイミング
- USceneComponent::GetAttachParentActor() で UNiagaraComponentがアタッチしている親のアクターを取得するタイミング
他にも考えられるタイミングはありそうですが、私が遭遇した問題のみを挙げておきました。
コンテナのクリーンアップ
TArrayやTMapでポインタを管理する場合、オブジェクトの生成と破棄を繰り返すと、無効なアドレスでコンテナが埋まってしまいパフォーマンスが低下し、格納可能な最大数を超えると停止する場合もあります。また無効な FObjectKey のキーが存在し続けると、まれに「キーの重複」も起こり得ます。
そのため、無効になっているオブジェクトをコンテナから削除する定期的なクリーンアップが必要です。
以下、クリーンアップのサンプルコードです。
// 無効なエフェクトをクリーンアップする.
void CleanupInvalidEffects()
{
TArray< FObjectKey > LRemoveKeys{}; // 削除キーリスト.
for(const auto& LParam : m_Map) {
TWeakObjectPtr<UNiagaraComponent> pCompPtr = LParam.Value;
if(pCompPtr.IsValid() == false) {
// 無効なエフェクト.
LRemoveKeys.Add(LParam.Key);
continue;
}
if(IsValidNiagaraComponent(pCompPtr.Get()) == false) {
// 無効なエフェクト.
LRemoveKeys.Add(LParam.Key);
continue;
}
}
// 削除実行.
for(auto LKey : LRemoveKeys) {
m_Map.Remove(LKey);
}
}補足
クリーンアップは可能であれば即時除去
上記例ではタイマーイベントによるクリーンアップとしましたが、可能であればエフェクト側に破棄タイミングのコールバックを (デリゲードなどで) 仕込めるようにして、即時除去できると確実です。
ただこの場合でも、保険として定期的なクリーンアップ処理は残しておくのが良いです。
検索が不要であれば “TArray+リングバッファ” も有効
例えば生成数の上限を決めて古いエフェクトを消すといった仕様 (検索が不要) であれば、TMap を使わずに TArrayでリングバッファを作り、先頭を削除して末尾に追加、とすると実装もシンプルになって良いです。
おしまい
以上、UNiagaraComponentのインスタンスを保持するときの注意点でした。
簡単なまとめとしては、
- TWeakObjectPtrを使うと安全なポインタ参照が実装しやすい
- オブジェクトをキーにして検索したい場合はTMap + FObjectKey を使う (検索が不要であれば TArrayを検討)
- FObjectKeyが生成や破棄を繰り返すオブジェクトの場合は、破棄フラグのチェックやコンテナのクリーンアップなどの工夫が必要
- コンテナに TWeakObjectPtr を入れても無効なオブジェクトは自動で消えないので、定期的なクリーンアップが必要。可能であれば即時破棄の仕組みがあると良い
特にNiagaraComponentは、常駐エフェクトでない限り破棄タイミングがNiagaraのシステムに依存するため、このような管理をしておくと制御しやすいのではないかと思います。
Niagaraのインスタンス管理を作るときのお役に立てれば何よりです。

