Posted at
FlashDay 19

ゲームにおけるガベージコレクションとの付き合い方

More than 3 years have passed since last update.

プログラムでメモリリークは怖いですよね。

どんどんとメモリ使用率が上がっていき最終的にフリーズ&クラッシュ。そんなプログラムはかきたく無いものです。

それを解消してくれるガベージコレクション(以後 GC )のお話が 18日目にありましたが、そのお話に少し付け加えて、「じゃあどうやってその GC と仲良くしていけばよいのか?」のひとつの答えとして、ゲームにおける GC との付き合い方の一例を紹介したいと思います。


基本的にはメモリは開放していくもの

基本的にはメモリにゴミが残らないようにし、GC はにはちゃんと仕事をしてもらうことが望ましいです。

そうしないと冒頭で書いたとおり、フリーズ&クラッシュな未来が待っています。

18日目の記事で GC を動かす際に

System.gc();

と強制的に GC を実行させていました。

記事にもありましたがこの方法はあくまでデバッグ用で、FlashPlayer であればデバッグプレイヤー、AIR であれば adl での実行中や、インストールされているアプリケーションでもアプリケーションサンドボックスのコンテンツなどでしか動きません。

基本的には勝手に動くのが GC で、 メモリが足りなくなってきたときシステムが何もしてないとき に動きます。

そこでどうするかですが、メモリ強制的に足りなくすれば GC を動かすことができたりします。

new BitmapData(512, 512).dispose();

このように BitmapData などでメモリを圧迫して、すぐに削除してやることで動いたりします。

強制的に動かす手段として覚えておくとよいでしょう。


GC のデメリット

GC のメリットは言わずもがな 自動的にいらないメモリを開放してくれる事 です。

ではデメリットは何でしょうか?

実は GC が動くこと がデメリットです。

矛盾していますね。

GC が動かなければメモリリークを起こす危険性があるのに、そもそも GC が動くことがデメリットとはどういうことなのか?

それを知るためにまずは GC が何をしているか説明しましょう。


GC の仕事

GC の仕事はいくつかの方式があり、よく使われるのは「マーク&スイープ」と呼ばれる方式です。

簡単に説明すると「マーク&スイープ」ではまずメモリ内のオブジェクトで「これはいらない(参照がない)」ものに対して「削除予定」のしるしを付けます。それが「マーク&スイープ」の「マーク」です。

そして、マークされたオブジェクトはそのうち削除します。それが「マーク&スイープ」の「スイープ」になります。

その二つが GC の仕事です。

参照がないということを探るにはすべてのオブジェクトを走査してマークし、マークしたものをすべて削除する。その際に走査するオブジェクトが多かったり、削除するオブジェクトが多かったりすると CPU の占有率を高め画面のカクツキとして現れます。

GC の処理は意外と重いのです。

アクションゲームでゲーム中にカクッと止まるゲームをしたいと思いますか?

そんなゲームは絶対気持ちよくないですよね?

ではどうするのがよいのでしょう?


プレイ中は GC を動かさないようにする

走査や削除が重いなら最初から動かないようにしてしまえばいいのです。

具体的に言えば 既に作ったオブジェクトを使いまわす です。

例としてシューティングゲームを考えてみましょう。

出てくるものは Player(自機)、Enemy(敵機)、Bullet(弾丸)、の3つの最小限のものにします。

自機は基本的に1体なので普通に new します。


Main.as

player = new Player();


しかし Enemy や Bullet は常にいるわけでもありませんのでその都度作ります。


Main.as

// 敵が現れたとき

var enemy: Enemy = new Enemy(x, y);
enemyList.push(enemy);

// 画面外に行った時
if (outScreen(enemy))
{
// 配列から削除して Enemy オブジェクトを破棄する
enemyList.splice(enemyList.indexOf(enemy), 1);
enemy.dispose();
}


大体こんな感じでしょうか。

そしてこの enemy がメモリ上から削除されるのは GC が働いた時です。

GC が働かない間はメモリに残り続けます。

そして enemy は1体だけではないでしょうし、 Bullet のインスタンスも同じような処理でメモリに残っていきます。

その大量に溜まったゴミを GC が削除すると、先ほど説明した「画面のカクツキ」が起こります。

なので Enemy をリサイクルするように作り変えます。

オブジェクトプールと呼ばれる配列を用意してそこから受け取るようにしましょう。


Main.as

// 敵が現れたとき

var enemy: Enemy = objectPool.getEnemy();
enemy.init(x, y);
enemyList.push(enemy);

// 画面外に行った時
if (outScreen(enemy))
{
// 配列から削除して Enemy オブジェクトを破棄する。
enemyList.splice(enemyList.indexOf(enemy), 1);
enemy.dispose();
objectPool.returnEnemy(enemy);
}


このようにいちいち new をせず、オブジェクトプールから使っていないオブジェクトを受け取り、使い終わったらオブジェクトプールに返すように作ります。こうすることで、常に objectPool 内で参照を持つので GC 対象になりませんし、一定数のオブジェクトがプールされていれば新たに new することは無いのでメモリを圧迫することもありません。


最後に

GC は便利な仕組みです。しかし便利であるからこそデメリットもあったりします。

デメリットを知っているからこそオブジェクトを安易に new したり破棄したりしないほうがいいときもあることを知ることができました。

Flash は便利な仕組みを無意識に使える良いプラットフォームでありながら、「知らずとも使えてしまう」という諸刃の剣を装備してしまいました。

その諸刃の剣を我が物として自由に扱えるかどうかを決めるのは、多くの「知らずとも使えてしまう」事を知ることだと思います。

そして「おまじない」として覚えるだけでなく、どうなっているのか、何故なのか探ってみてください。

知らなかった仕組みを知るのは、自分の武器になるだけでなくとても楽しいことだと思います。