DestroyされたGameObjectのインスタンスの寿命とか
GameObjectのインスタンスを別の場所にコピーしたまま、先にGameObjectをDestroyしたら?
リファレンスの明記が見つけられなかったので、この日記の内容は普段より怪しさ2割増し(どころか寝言)くらいで受け取ってもらえるとうれしいのですが実験レポ。
Destroy後のインスタンスについて。
- Destroyによって破壊されたメンバークラスへのアクセスは、nullを返さず、ほぼ全てExceptionを発生し、事実上インスタンスだけ生きてても何も出来ない状態になっている。
- 注意が要るのは、Destroyされたインスタンスを等号で比較すると「nullに等しい振る舞い」をオーバーライドされている点(キモイ!)。
なるほど。GameObjectはあくまでも各コンポーネントクラスの参照ホルダーのような振る舞いにしてあるのですね。しかし一応インスタンスだけ残っているので、わずかではありますがある種のリーク状態ではあります(機能の実態であり、主にメモリを消費するコンポーネント側はキレイに削除されていますが、ホルダーであるGameObjectクラスのインスタンスが抜け殻状態のままリークしちゃっている)。その状況で太字の特徴は親切ですがネガでもあります。「これが本当にnullの初期値状態なのか、Destroy後であるのか」を判断するのが難しい。かえってこんな感じになりかねないのです。
案1
/* objがGameObjectインスタンスのコピーだったとして、ともかくDestroyされてないか疑ってクリアしておく */ if ( obj == null ) obj = null;
しかしこれはどう見たってTYPOにしか見えません。「nullだったらnull代入」とか一見してシュール。しかし毎回丁寧にコメントをつけるのも現実的とは言い難い。これをやるくらいなら(インスペクタとかに出さなくて良いんであれば)リンクリストとかにして「基本的にnullなヤツは配列に持たないロジックにする」「nullなヤツを見つけ次第エラートラップとして配列から消す」ような作りにしておく方がまだ自然でしょうか。しかし配列でなく一個だけコピーしておきたい場合は…それは後でもうちょっと掘り下げますが、先にこの結論に至った実験内容と結果を。
実験
以下のソースにブレーク張って実行、Update()の1秒判定内部でobjやらtransやらコピーしといたインスタンスをウォッチング。
public class LifetimeTest : MonoBehaviour { public GameObject obj; public Transform trans; private float t = 0f; // Use this for initialization void Start () { obj = new GameObject("sample1"); trans = obj.transform; GameObject.Destroy(obj); } // Update is called once per frame void Update () { t += Time.deltaTime; if ( 1f < t ) { t = 0f; } } }
結果
- コピーされたGameObjectインスタンス自体はDestroy後もC#の文法上存在しているので、インスタンスを参照する事自体はExceptionもなくブレーク張ってメンバのウォッチも可能。
- しかし各メンバクラスは参照先がほぼMissingReferenceExceptionで全滅。これはtry/catchしないと処理が中断して大変。
- なおこの状態でコピーしておいたGameObjectインスタンスを等号などで評価するとnullとして振舞うようになる(ウォッチからも普通にメンバをプルダウン出来るんだから実際のインスタンスがnullになったワケじゃなく、演算子オーバーライド的な振る舞い)。
- GameObject.gameObjectだけは例外を返さず、普通に自分自身を返す。自分自身なのでnullに見えつつ実態も存在する。(ただしComponent.gameObjectとかはダメで、Exceptionが返る)。
考察
見事にいろいろ怪しい。それと最後に出てきた「GameObject.gameObjectだけはExceotionでなく取得できる」のはちょっと変わっています。多分「バックアップされてるGameObjectインスタンスによってGameObjectインスタンス自体は存続している」「評価する演算子はオーバーライドによりnullを返す」の二点からでしょうか。しかしそれにしてはComponent.gameObjectの振る舞いはちょっとおかしい。残っていればとにかく取得できるようにというワケでも無さそう。では、GameObjectのインスタンスもComponentのインスタンスも残っているとしても、相互の関連性は途絶した状態であるべきだから、あえてComponent.gameObject側はカットしてあると考えれば良いのかな?確かに、破棄されたはずのComponentからGameObjectがたどれちゃ色々マズいけど、GameObject.gameObjectは自分自身なんだから隠す必要はない。とは言えともかく、こんな詳細仕様の将来にわたる恒常性は微妙なのでGameObject.gameobjectの仕様を利用するのは意見が分かれそうです。そのへんも含め、削除済みのオブジェクトかを判定する作戦第2案をメモ。
案2
bool IsDestroiedObject( GameObject obj ) { try { return (obj.gameObject == null); } catch (System.NullReferenceException) { return false; } catch (UnityEngine.MissingReferenceException) { return true; } }
もうありきたりですが困った時は力技に限ります。Exception仕様までフラフラ変わるようでは泣くしかありませんが、しかしこれならIsDestroiedObject( Component obj )としても判定できると思います。
ただこういった判定が出来るだけ必要ない構造にしておくのが一番です。
どっちかと言うと、配列で返すようなFindObjectsOfType()あたりが全オブジェクト総当りになって重いので(子系列に絞った一個のコンポーネント検索程度ならまだしもシーン中の全オブジェクトを処理の度に全て検索対象にするのはなんぼなんでもキツい)、それに比べれば予めインスタンスをコピーしといた方がコストが安い!でもDestroyされちゃったらどーなんの?といったケースにぶち当たった時は、今回の記事が糸口になればなぁと思います。
*1:C#のようなクラスをインスタンスで管理する言語の場合、Create/Destroyタイプで設計したくとも、コピーなんていくらでも存在し得るインスタンスそのものは、Destroy的メソッドで破壊することが出来ません。そもそもインスタンスの概念は、あくまでも逆ポーランド記法とかスコープのスタックから発展してきた「利用者の居る限り存続し、居なくなり次第消える」のが基本の文法です。インスタンスは原則として全てのコピーを消す(消えるとはつまりインスタンス変数がスコープ寿命を迎えてしまうか、インスタンス変数にnullが代入されるか)以外の方法では削除できません。とは言え、ゲームキャラの寿命に限らずファイルリソースへのアクセスとかは寿命の最初と最後を順に一箇所ずつに決めた方がやりやすいケースもあります。インスタンス方式はこういったCreate/Destroy系の設計とどうしても食い合わせが悪く、ガベコレ全盛のいまでも諸問題として残っています。ぶっちゃけ各環境ごとに「ケースバイで俺ルール乱立状態」。この間のコリダーキャンセラーで使ったIDisposableなんかもそのひとつです。