ゲームプログラミングでは、ガーベージコレクション(以下GCと略す)が最大の敵と言っても過言ではない(過言かもしれないが)。ゲーム中にGCが発生すると、突然ゲームの進行がピタッと止まり、ゲームの操作感が一気に損なわれる。これを如何に防ぐのかというのが、本ブログ記事の内容です。
- GCはなぜ起きるのか
簡単に結論から言うとメモリの断片化を解消しようとして、システム側がその処理だけを行いゲーム進行などの通常タスクを止めてしまうためです。GCをなくすのはメモリ断片化を防ぐのが重要です。このメモリ断片化は、メモリの確保と開放を頻繁に繰り返すことで発生します。
例えばここに16個の入れ物があるとします。赤3個,緑2個,青3個,橙4個を取得して使ったとします。空きは4個です。
赤と青は使い終わったので空けます。これで空きは10個まで増えました。
次の黄5個を確保しようとしますが確保できません。空きが10個あるのに、連続した5個の空間がなくなっているためです。これがメモリ断片化の状態です。

システムはこの問題を解決するためにメモリの空いている部分を集めます。

この集める動作が GC なのです。この GC という動作が入ることにより、黄5個の確保ができるようになります。
この集める動作が GC なのです。この GC という動作が入ることにより、黄5個の確保ができるようになります。
日本マイクロソフト
2026-03-20
※楽天側のほうが安いですがケーブルはありません。ご注意ください。
- これを防ぐには
ゲーム中にメモリの取得と開放を繰り返すことが、GC発生の主原因です。最も多くのメモリの取得と開放を繰り返すのは、おそらく多くのシューティングゲームでは、弾数だと思います。弾幕シューティングに至っては、とんでもない数の弾が画面上に飛び交います。これを全てシステムに対して「メモリをくれー」「メモリーを返すー」なんてやってたら、あっという間にメモリ断片化が出来上がります。
このメモリに対する取得と開放をなくすため、少なくとも私が作っているゲームシステムでは、最初に必要な最大数をまとめて確保しておき、それの割当を自分自身で管理するという手法を取ります。この管理する主管プログラムをマネージャーと呼んでいます。まとめて確保して、まとめて返す。これだけです。
- 具体的な実装例
var bullet_emptys: Array #未使用の弾 var bullet_uses: Array #使用中の弾
まずは未使用配列と使用中の配列を用意します。ステージ開始などの区切りの時点で、未使用の弾配列に必要な数の弾を、実体化して格納しておきます。
#未使用の弾を用意する func initialize() -> void: for i in BULLET_MAX: var blt = bullet_scn.instantiate() #弾シーンを実体化 blt.add_to_group(Common.GROUP_BULLET) #弾グループに設定 blt.visible = false #表示を消す gameField.add_child(blt) #マップに接続 bullet_emptys.append(blt) #未使用リストに登録
これで準備完了です。弾を発生させる時は以下のように呼び出します。
#弾を発射する func Fire(type: TYPE, pos: Vector2, spd: float, dir: EDir): if (bullet_emptys.is_empty() #空きがない? or spd <= 0): #ゼロ速度? return #弾を撃てなかった var blt = bullet_emptys.pop_back() #未使用ストから取得 blt.start(type_table[type], pos, spd, dir) #弾を発射する bullet_uses.append(blt) #使用中リストに登録
blt.start は弾の種類、位置、速度、方向を初期化する呼び出しです。今回はGC防止の観点が主目的なので、ここはスルーします。大事なのは、使う時は bullet_emptys から拾ってきて、使用中の配列 bullet_uses に入れ直していることです。厳密に言えばここでもメモリの増減は発生していますが、中身は仮想アドレスを入れているだけなのと、動的配列は GCの発生がある程度少なくなる工夫が施されているので、これで問題ありません。
弾が画面外または対象に当たって消える時は、以下の関数を呼び出します。
#弾を消す func restore_unused(obj): call_deferred("_defer_disable_bullet", obj) #安全なタイミングで実行予約 bullet_uses.erase(obj) #使用中リストから削除 #弾を消す実際の処理 func _defer_disable_bullet(obj): obj.hide() #表示を消す obj.process_mode = Node.PROCESS_MODE_DISABLED #動きを止める bullet_emptys.append(obj) #未使用リストに戻す
弾を消す際に call_deferred で遅延処理としているのは、未使用に戻す restore_unused の呼び出し元が、_process 等の場合は、例外が発生する可能性があるためです。これを防止するために、遅延呼び出しを行っています。この辺りは皆さんのシステムに作り方で変わると思います。ここで重要なのは、使い終わったら bullet_uses から bullet_emptys に戻すという処理です。これを間違えると、弾のワークが枯渇して、弾が撃てなくなります。
今年発売を予定しているゲームです。本記事のテクニックもそのまま使っています。
よろしければWishlist登録して頂けるととても嬉しいです❤
今年発売を予定しているゲームです。本記事のテクニックもそのまま使っています。
よろしければWishlist登録して頂けるととても嬉しいです❤

コメント