【UE5】UNiagaraComponentのインスタンスをコンテナで保持するときの注意点

今回はUNiagaraComponentのインスタンスをコンテナで保持するときの注意点についてまとめます。

  • 検証環境バージョン:Unreal Engine “5.5.4”

UNiagaraComponentを保持したいケース

だいぶ状況は限られますが、Niagaraエフェクトのインスタンスを管理したり、破棄タイミングを制御したいときがあります。

  • [ケース1] 常駐や再利用前提のエフェクト:Niagara生成のコストを抑えてインスタンスを使い回す
  • [ケース2] 大量発生するエフェクト生成数をコントロールしたい:例えば最大32までに抑える。または超えたときは古いエフェクトを消すなど

「ケース1」については、生成や破棄タイミングはある程度限られるため、管理はあまり難しくなく、ポインタを直接管理するなどある程度雑な実装でも問題なく動作するかもしれません。

ですが「ケース2」では生成や破棄を厳密に管理しないと、不正なポインタアクセスや場合によってはメモリ破壊を引き起こす可能性があるので、今回紹介する方法が役に立つのではないかと思っています。

Information

ただ、”ポインタ直接管理” は予期しない不具合を招きやすいので、「ケース1」のように生成や破棄が決まっていてもポインタを直接保持するのではなく、今回紹介する “TWeakObjectPtr” や “FObjectKey” による管理をおすすめします。

というのも、nullptrでなくても安全なポインタとは限らず、特にC++言語では、不正なポインタを間違って使うとメモリ破壊など重大な問題を引き起こす可能性があるためです。

ポインタ管理には TWeakObjectPtr (弱参照ポインタ) を使う

ポインタを管理する場合は、そのまま扱うのではなく TWeakObjectPtr (弱参照ポインタ) をポインタに被せると比較的安全に扱えます。

// ☓:生ポインタはおすすめできない.
UNiagaraComponent* pNiagaraComp = {};
// ◯:TWeakObjectPtrを被せる.
TWeakObjectPtr<UNiagaraComponent> NiagaraCompPtr = {};

これにより、TWeakObjectPtr::IsValid() で無効かどうか判定したり、TWeakObjectPtr::Get() でアドレスが有効な場合のみポインタを返す (それ以外は nullptr) ようになるためポインタのアドレスが「ある程度」安全であることが保証されます。

TWeakObjectPtr<UNiagaraComponent> EffectCompPtr;
if(auto LComp = EffectCompPtr.Get()) {
  // 無効なアドレスでないことが保証される.
  // 何らかの UNiagaraComponent に対する処理を行う.
}
Information

厳密には TWeakObjectPtr は UObjectのGCによるアドレス無効化をチェックする弱参照です。これにより破棄された UObject にアクセスする事故を回避できます。

ただし、レアケースとして、チェックした直後に別スレッドで破棄された場合はチェックできません。別スレッドで破棄される可能性がある場合はセマフォを使う必要があります。

TMapのキーで管理する場合は FObjectKey を使う

たくさんのオブジェクトのアドレスを管理したい場合があるとします。そこで例えば TMap のキーとしてオブジェクトのアドレスをキーにするとします。しかしアドレス値は実行環境依存で、予期せぬ問題を引き起こす可能性があります。

安全にオブジェクトをキーとして扱うには「FObjectKey」を使うのをおすすめします。これは “UObject” が持つ “GetUniqueID()” をもとに作られるキーであり、エンジンが保証する値であり、かつある程度ユニーク (一意) であることが保証されているキーです。

ということで、TMapでオブジェクトをポインタを管理する場合には以下の構成にすると良いです。

  • キー:FObjectKey (高速な検索キー生成用)
  • 値:TWeakObjectPtr (安全な参照用)
FObjectKeyは万能ではない

厳密には FObjectKeyの値は、完全にユニークなIDではなくインスタンスの生成・破棄によって稀に重複する可能性があるため、万能のキーではありません

そのため、無効となった FObjectKey は速やかにコンテナから削除する必要があります(後述の「コンテナのクリーンアップ」を参照)

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” を指定することで、破棄対象になっているかどうかがチェックできます。

破棄の判定コードは一例です

「IsRegistered() == false」 の判定は、常駐エフェクトで一時的にオブジェクトツリーから外す場合には不要です。
用途に合わせて上記関数は修正することをおすすめします。

上記関数を定義しておくと、以下の記述で 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に破棄フラグが立っているかどうかのチェックが必要なタイミングはいくつか存在します。

  1. コンテナからの削除 (クリーンアップ) が必要かどうかを判定するタイミング
  2. 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);
  }
}
クリーンアップのタイミングは?

コンテナ側からはエフェクトの破棄タイミングは掴めないので、このクリーンアップ関数は、例えば1秒に1度発生するタイマーイベントから呼び出すといった実装にすると良いと思います。(生成・破棄の発生頻度によって要調整)

補足

クリーンアップは可能であれば即時除去

上記例ではタイマーイベントによるクリーンアップとしましたが、可能であればエフェクト側に破棄タイミングのコールバックを (デリゲードなどで) 仕込めるようにして、即時除去できると確実です。

ただこの場合でも、保険として定期的なクリーンアップ処理は残しておくのが良いです。

検索が不要であれば “TArray+リングバッファ” も有効

例えば生成数の上限を決めて古いエフェクトを消すといった仕様 (検索が不要) であれば、TMap を使わずに TArrayでリングバッファを作り、先頭を削除して末尾に追加、とすると実装もシンプルになって良いです。

おしまい

以上、UNiagaraComponentのインスタンスを保持するときの注意点でした。

簡単なまとめとしては、

  • TWeakObjectPtrを使うと安全なポインタ参照が実装しやすい
  • オブジェクトをキーにして検索したい場合はTMap + FObjectKey を使う (検索が不要であれば TArrayを検討)
  • FObjectKeyが生成や破棄を繰り返すオブジェクトの場合は、破棄フラグのチェックやコンテナのクリーンアップなどの工夫が必要
  • コンテナに TWeakObjectPtr を入れても無効なオブジェクトは自動で消えないので、定期的なクリーンアップが必要。可能であれば即時破棄の仕組みがあると良い

特にNiagaraComponentは、常駐エフェクトでない限り破棄タイミングがNiagaraのシステムに依存するため、このような管理をしておくと制御しやすいのではないかと思います。

Niagaraのインスタンス管理を作るときのお役に立てれば何よりです。

\ 最新情報をチェック /