はじめに
クラスのメンバ変数を定義するときにメンバ変数の上にこういうコードを書いたことがあると思います。
UPROPERTY(EditAnywhere)
int32 Health;
今日はUPROPERTY()の大事な役割について話します。これを付け忘れると大変困ったことになるんです。
BPに公開するためだけじゃない
UPROPERTY()は プロパティ指定子 を定義するのに使うことが多いと思います。
EditAnywhere、BlueprintReadWrite
などはよく使うのではないでしょうか?これらの説明はドキュメント見てもらうとして、
今回はポインタ変数に付けるUPROPERTY()について説明します。
UPROPERTY()
AActor* Weapon;
UPROPERTY()
UParticleSystemComponent* Effect;
こういうやつです。
結論から言うと
「UObject継承のクラスのポインタ変数にはUPROPERTY()を付け忘れてはいけない」 です。
付け忘れるとどうなるの?
UPROPERTY()はポインタ変数に付けた場合に以下ような機能があります。
- ポインタの参照カウンタを増やす
- DestroyされたらNullポインタにしてくれる
そのため、
どのクラスにも参照されていないと判断されてGC(Garbage Collect)されてしまいます。
デフォルトだと開始1分後にGCが走るため、ちょうど1分後くらいにクラッシュしたりします。
ポインタの指している先が急になくなるので当然ですね。
しかも1分後というのが発見しずらい、いやらしいバグになります。
あと、ActorなどはDestroy()することで強制的に削除することができます。
その場合にも同じように解放済みのメモリを参照してしまいクラッシュの原因になります。
試しにやってみよう
参照カウンタの確認
意味は無いですがUCanvasをNewObjectで作ってみましょう。
UPROPERTY()を付けないとどうなるか見てみます。
UCanvas* Canvas = nullptr;
Canvas = NewObject<UCanvas>();
"obj gc" コマンドで強制的にGCを走らせます
次に、UPROPERTY()を付けてやってみます
UPROPERTY()
UCanvas* Canvas = nullptr;
Nullにしてくれるか確認
ActorをSpawnActorして1秒後にDestroyしてNullにしてくれるか確認してみましょう。
UPROPERTY()
AActor* Actor = nullptr;
void APropertyTestGameModeBase::BeginPlay()
{
Super::BeginPlay();
Actor = GetWorld()->SpawnActor<AActor>();
FTimerHandle handle;
GetWorldTimerManager().SetTimer(handle, this, &APropertyTestGameModeBase::DestroyObject, 1.0f);
}
void APropertyTestGameModeBase::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
UE_LOG(LogTemp, Log, TEXT("%x"), Actor);
}
実行してログをみてみましょう。
・・・あれ?・・・Nullになりませんね。GCしてみましょう。
Nullになりました!DestroyしてもGCされないと消えない、ポインタも消えるまでNullにはしてくれないようです。
死にかけのActorは参照したくないんだけど
そいうあなたにはIsValid()がおすすめです。
if (Actor)
{
// Destroy後の死にかけの場合も処理されちゃう
}
if (IsValid(Actor))
{
// Destroyされたら処理されない
}
全部IsValidでいいじゃん。
って声が聞こえてきそうですが、IsValid()はnullチェックに比べて多少のオーバーヘッドがあります。
なので自分は基本はnullチェックだけど死にかけのActorで参照しちゃまずい場合にIsValidを使うことにしています。
UPROPERTY()の付け忘れ、実はReSharperは教えてくれる
「オブジェクト メンバー "Weapon" はいつでもガベージ コレクションできます。」
こっそり水色波線で教えてくれてました。ReSharper!便利!素敵!
おちいりやすい罠
構造体のメンバにはUPROPERTY()付けたけどその構造体をメンバ変数として定義するときにUPROPERTY()付けてない場合
こういう場合もアウトです。詳しくは以下のスライドをご参照下さい。
UE5からお作法が変わります
UPROPERTY()
TObjectPtr<USceneComponent> RootComponent;
生ポインタではなくTObjectPtrを使う事が推奨されています。
TObjectPtrって何?
今後実装される機能で
- オブジェクトの依存関係の追跡
- エディターでのオブジェクトの遅延読み込み
などがあり、それに使われるようです。
パッケージなどエディタ以外のビルドでは生ポインタに変換されるそうです。
おわりに
ポインタ変数には UPROPERTY() これだけは忘れないで下さいね。
以上。