Godotでメモリリークを防ぎたかった話【ゲーム制作日記】
こんにちはtozicaです。
今日は金曜日!
カナデエスケイプ
昨日は「カナデエスケイプ」の制作を進めました。
一昨日の夜にタコイカさんにテストプレイをお願いしたら、ありがたいことにわたしが寝てる間にバグ報告やらフィードバックやら色々くれたので、昨日はそれに対応する作業をしていました。
バグ修正したり、バランス調整したり。
特にね〜、長時間遊んでると画像読み込みを失敗し始めて、そのうち急に落ちる……みたいな話がありまして。
これは間違いなくメモリリークか何かが起きてるやつだなーって思って、昨日の午前中のほぼ全てをその修正に費やしました。
それでまぁ、いざ調べてみたら、そもそもわたしがGodotにおけるメモリ管理の基本的なところをだいぶ勘違いしてたことが分かりまして。
もはやメモリリークしてるしてないどころの話じゃなくて、メモリリークせずにちゃんと解放できてるデータが存在してなかったのが判明したので、流石に笑っちゃった。
下の画像は修正作業を開始して2時間くらいした時のプロファイラのスクショで、Godot内に保持されているオブジェクトの数を表しています。
ニューゲームとタイトルバックを繰り返してるだけでオブジェクトの数がどんどこ増えていく様子から、思いっきりメモリリークが起こってるのがよく分かると思うんですけど。
実はこれでもまだだいぶマシな方で、修正作業を始める前に計測した時はタイトルに戻ってもオブジェクトの数が全く減ってなかったんですよね。
つまり、メモリに読み込んだデータを全く解放できておらず、ニューゲームするたびにものすごい量のゴミデータがメモリに溜まっていってたということです。
うーん、無知って恐ろしい…。
その後、なんやかんやで色んなところを修正して、正午までにはメモリリークをだいたい修正することができました。
これなら、長時間遊んでもゲームが落ちたりはしない…はず。
やったね。
せっかくなので、わたしが今まで何を勘違いしていたのかを一応書いておこうと思うんですけど。
Godotにおける画像や音声などのリソースは、RefCounted クラスという、いわゆる参照カウントに基づいてメモリ管理を行うクラスの派生クラスとして定義されています。
つまり、そのリソースを参照している他のオブジェクトの数を管理していて、それがゼロになったら「そのリソースはもう使われていない」ということなのでメモリから削除する……という感じ。
これ自体はC++における std::shared_ptr とかと同じ原理なので、個人的にはだいぶ理解しやすいですね。
それで、Godotにおける様々なゲーム内オブジェクトは一般にノードと呼ばれてるわけですが、どうもこのノード達は参照カウントに基づく管理がされてないっぽいんですよね。
つまり、生成されたノードは参照カウントとか見てないので、ちゃんと自分でメモリから解放してあげないと、ずっとメモリ上に残ることになるわけです。
それをわたしは、てっきりノードも含めた Godot の全てのデータは参照カウントでメモリ管理されてるものと勘違いしてたので、ノード間での循環参照などが起こらないようには注意してたんですけど、本来必要だったメモリ解放処理は全く行っていなかったのでした。
これが勘違いその一。
そして、ゲーム上に存在するノードはそれぞれ、ツリーと呼ばれる構造体の上に載っています。
Unityで言うところのヒエラルキーと同じ感じですね。
ツリーから取り除かれたノードは画面にも表示されないし、フレーム更新の対象にもなりません。
それでわたしは、てっきりツリーから取り除くことがそのままメモリから解放することと等価だと思ってたんですけど、これが違うんですよね。
ノードをツリーから取り除くだけだとメモリ上に残ったままであり、ちゃんとメモリから取り除くためには free() や queue_free() などのメソッドを呼び出してあげる必要があったわけです。
なので、今回修正する前までは、プレイ中に作られたデータはタイトルに戻る時にツリーから取り除かれてはいたものの、そのデータ自体はずっとメモリに載りっぱなしだったので、ゲームを遊び続けてるとどんどんメモリ負荷が溜まってしまってたんですね。
これが勘違いその二。
そんなこんなで、勘違いによってメモリリークをめちゃくちゃ起こしちゃってたんですけど、最終的には(たぶん)全部直せたし、Godotに対する理解も深められたので、良かったですね。
とはいえ、本当はこういうのも最初に作る段階で気付いてるべきなので、その意味ではわたしもエンジニアとしてまだまだスキル不足だなぁ…と思ったり。
次回作もたぶんGodotで作ることになるだろうから、次作る時はここらへんもちゃんと気を付けて作ろうね。
おしまい。