こんにちは。プログラマのヨルンです。
ヘキサドライブでは活発に勉強会が開催されています! 今日は、最近僕が主催したUE4のUObjectについての勉強会の一部を紹介したいと思います。
早速ですが、UObjectは素晴らしいです。少ないコードで安全・簡潔にリソースを管理できるためです。しかしその利点を享受するには、PendingKillという状態への理解が不可欠です。今日はこれを解説してみようと思います。
UObjectとGC
UObjectは、UE4においてnewやdeleteを置き換える概念と考えると分かりやすいかもしれません。 UE4で定義される多くのクラスはUObjectを継承し、直接または間接にNewObject関数で生成します。
一方、UObjecctの解放は必ずGCによって行われます。プログラマは自分でUObjectを解放することはできません。その代わりに、ポインタにUPROPERTYを付ける事によりオブジェクトを参照中であることをGCに伝えることになります。UObjectへの全ての参照が無くなるといずれGCにより解放されます。
GCによるメモリ管理の恩恵として、循環参照やメモリリーク等のメモリ系のバグが入りにくく便利なのですが、GCによる解放はいつ行われるか予想しにくいですね。
例えばゲームプログラムでは、オブジェクトが任意のタイミングで消滅してほしいことがあります。例えば既に倒して画面上に存在しない敵がいたとします。その敵のオブジェクトがGCで解放される前だったので、味方AIがその存在しない敵を認識し続けてしまったとしたらどうでしょう。
原因がわかりにくいバグの温床になりそうですし、GCのタイミングがゲームに影響するのはナンセンスですね。GCを待たずに「存在しないモノ」に出来ると便利です。
PendingKillの意義
UObjectを「存在しないモノ」とする印が、PendingKillです。 UObject::MarkPendingKillを呼ぶことで、対象UObjectへの参照の有無とは関係なくGCの回収対象になります。そして、解放までの間そのオブジェクトは既に存在しないかのように振る舞います。アクタならAActor:: Destroy、コンポーネントならUActorComponent:: DestroyComponentが内部でUObject::MarkPendingKillを呼びます。つまり、PendingKillという状態を設けることで、GCに解放されるタイミングとは別に任意のタイミングでUObjectを「存在しないモノ」に出来るわけです。
そして、UObjectがPendingKillであるかを判定するためのIsValid関数が用意されています。
ここで、UE4のGCの大事な仕事の一つとして、GCが解放したUObjectを参照する全てのUPROPERTYにnullptrを代入する事に触れたいと思います。
これにはいくつかの利点があります。まず、ダングリングポインタの発生を防いでくれます。そして、IsValid関数の利便性を担保してくれることです。実はIsValid関数でUObjectの有効性を確認する際nullptrとPendingKill双方に対しfalseを返してくれるので、2つの「無効」な状態を区別なく一度に判定可能です。
IsValidとPendingKillの仕組みにより、上のAIの例は解決します。任意タイミングで「存在しないモノ」とする事を実現できるためです。すると、プログラマはGCの存在すら意識しなくて済むようになりますね!
ここまでC++を前提に書きましたが、BPからはPendingKillとnullptrの違いは隠蔽されており、BPからはGCの存在を意識することはないように設計されています。
コーディングのポイントとまとめ
C++からUObjectを管理する上で気をつけるポイントは2点です。
・UObjectへのポインタにはUPROPERTYをつける
・UObjectの有効性チェックは、nullptrとの比較ではなくIsValid
特別な意図が無い限り、この2点を心がけてコーディングするのがおすすめです。
実際に書いてみるとわかりますが、UObjectを使うと、直接のnew/delete、あるいはAddRef/Release等の参照カウンタ方式で起こりがちなリソース管理の難しさを感じず、コードが簡潔になる事に気づくと思います。UObjectはゲームプログラミングにおけるリソース管理の困難からプログラマを解放し、ゲーム制作の本質に集中できるようにしてくれます!