無料・軽快な 2D / 3D 用のゲームエンジン Godot Engine 4 で、全ての継承元である Object と、そこから派生した参照カウントを持つ RefCounted をそれぞれ継承したインナークラスのオブジェクトを生成して、Array (配列) に格納した後、配列を clear で消去した際に、要素のオブジェクトが解放されるか、メモリリークはないかをテストした結果を紹介します。

※ GodotEngine 4.3 を使用しています。.NET 版ではありません。
※スクリプトは自己責任でご使用ください。
※メモリリークの有無の判断は、調べた結果からの筆者の個人的な見解です。
Godot にガベージコレクションはない?
以下は「ゲームプログラミング:メモリリーク – YouTube」のトークをテキストに起こしたサイトの抜粋です。
※この動画は4年前(2021 年)にアップロードされたものなので、今のバージョンとは異なるかもしれません。
Godot ではガベージコレクションの意図しないタイミングでの解放処理などで処理が突然重くなることを防ぐことなどを理由として、ガベージコレクションではなく手動で解放処理をするように設計されているようです。
Godot doesn’t have garbage collection. Godot makes you handle it manually and these are three reasons I believe that Godot chose not to go this route. In a sense I’m not sure. However one big reason – And we’ll take a look at this in the next slide – Is that your game could potentially freeze.
Godot にはガベージ コレクションがありません。 Godot はそれを手動で処理させますが、これら 3 つの理由が Godot がこの道を選択しなかったと私が信じています。ある意味、よくわかりません。ただし、大きな理由の 1 つは、次のスライドで説明しますが、ゲームがフリーズする可能性があることです。Memory Leaks (Super Basic) | Godot Basics Tutorial | Ep 21 | Godot Tutorials
デバッガーのモニターでメモリ使用量などを計測
※エディタを一度閉じるとチェックが全て解除されていたので、モニターで計測する際は、エディタを起動したあとに一度確認するようにしましょう。

作成したメモリリーク確認用のシーンとその実行・計測
メニュー「シーン」→「新規シーン」を選んだ後に「シーン」ドックで 2D シーンを選択します。
そのルートノード( 2D シーンを選択すると 2D ノードになります)に、後述するスクリプトを割り当てて、異なる3つのスクリプトでメモリリークをしないかを検証します。
ルートノードの名前 (CamelCase) を変更して、右上の+ボタンから、スクリプトを割り当て、Ctrl + S などでシーンを保存します。
※例では TestObjectArrayMemoryLeak として、シーン名はその snake_case 表記にしました。

ルートノードに割り当てた、スクリプトには、これからテストで用いるスクリプトで上書きして保存します。
その後、F6 キーでそのシーンを実行して、「デバッガー」下パネルの「モニター」タブで計測が行われている(計測項目にチェックされている)ことを確認してから、実行したウィンドウをアクティブにして Enter キーを押すとテストが実行されます。
※ Enter キーを押してテストが実行されるのは、そのようにスクリプトを書いたからです。

Object 派生でメモリリークする例
以下は、Object 派生のインナークラスを 10,000 個生成して配列に格納して、その配列要素を消去する際に、その要素は解放されるのかを確認するためのテストコードです。
このテストでは、メモリリークが確認されました。
※詳細はスクリプト内のコメントを参照してください。
extends Node2D
## new クラス関数による生成時、このオブジェクトが消去される際に
## 関数がどのタイミングで呼び出されるかを print 出力で確認するためのインナークラスです。
## test_study_inner_class1.gd で GUT によるテストでメモリリークがないかを確認します。
class InnerClassA extends Object:
## インナークラスを作るために、メンバ変数を適当に持たせます。
var id: int
## コンストラクタの仮想関数です。
func _init() -> void:
print("InnerClassA:_init called")
return
## オブジェクトが消去される直前などに通知してくるイベント関数です。
func _notification(what: int) -> void:
print("InnerClassA:_notification called what = ", what)
if what == NOTIFICATION_PREDELETE: # 実質的なデストラクタ
print("InnerClassA:_notification what = NOTIFICATION_PREDELETE")
elif what == NOTIFICATION_POSTINITIALIZE:
print("InnerClassA:_notification what = NOTIFICATION_POSTINITIALIZE")
elif what == NOTIFICATION_EXTENSION_RELOADED:
print("InnerClassA:_notification what = NOTIFICATION_EXTENSION_RELOADED")
else:
print("InnerClassA:_notification what = unkonwn...")
return
## InnerClassA のオブジェクトを複数保持する配列です。
var inner_class_a_objects: Array[InnerClassA] = []
## Object 派生のインナークラスを生成して格納した配列を消去した際にメモリーリークが発生しないことを検証するための関数です。
## 本関数を呼び出して実行を終了した後に、プロファイラのメモリ使用量の変化を確認します。
## print_orphan_nodes は Node 派生クラスのオブジェクトのリークを調べられそう。
func test_object_array_memory_leak_1() -> void:
print_debug("test_object_array_memory_leak_1 called")
# すでに配列に要素があれば実行しません。
if inner_class_a_objects.size() > 0:
print_debug("test_object_array_memory_leak_1 : ner_class_a_objects.size() > 0")
return
# 生成するオブジェクトの個数を設定します。
var numof_objects: int = 10000
inner_class_a_objects.resize(numof_objects)
# オブジェクトを生成して配列に格納します。
for i in range(numof_objects):
var obj: InnerClassA = InnerClassA.new()
obj.id = i
inner_class_a_objects[i] = obj
# オブジェクトのインスタンスのデータを確認します。(最適化で作られない可能性を防ぐため)
for i in range(numof_objects):
print("inner_class_a_objects[", str(i), "].id = ", str(inner_class_a_objects[i].id))
# 配列の要素を消去します。
inner_class_a_objects.clear() # メモリリークが発生するかどうかを確認するメイン部分
return
# Called when the node enters the scene tree for the first time.
func _ready():
print("実行画面で Enter / Space キーを1度押してメモリーリークについてテストを行います。")
print("モニタータブのメモリーや Object の要素をチェックして、初期に比べ終了時の使用量が増えていないか確認してください。")
return
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(_delta):
# Enter / Space キーが押された場合、1回テストを行います。
if Input.is_action_just_released("ui_accept"):
test_object_array_memory_leak_1()
return
実行時のモニターの計測結果です。
Static Max 項目では、実行時に記録されたの最大のメモリ使用量を計測しています。
例えば 1, 9, 6, 1 とメモリ使用量が変化した場合、 1, 9, 9 ,9 とグラフが表示されます。
この変化により、最大使用量の変化がわかるので、テストで 10,000 個のオブジェクトを生成したタイミングがわかります。
Objects 項目では、使用されている Object の個数が確認できます。
こちらは、リアルタイムの使用個数なので、 1, 9, 6, 1 と変化する場合、 1, 9, 6, 1 とグラフも変化します。
結果として、 Objects のグラフを見るとテストで 10,000 個のオブジェクトを生成した後、その同じ関数内で配列の全要素を消去しましたが、増えたオブジェクト個数が元の数まで減少しないことから、メモリリークが発生したことが推測されます。
-1024x358.png)
次に、スクリプトの numof_objects を 10000 から 3 に変更して、出力の上限(output overflow, print less text!)を超えないようにして、テストを実行すると「出力」下パネルの結果を確認します。
その結果、オブジェクトの消去前に通知される NOTIFICATION_PREDELETE メッセージは _notification 関数に通知されていないことがわかりました。
※テスト関数の呼び出し、インナークラスのコンストラクタ、そのオブジェクトに割り当てた ID を表示する print 文は出力されています。
-1024x404.png)
Godot Engine v4.3.stable.official.77dcf97d8 – https://godotengine.org
OpenGL API 3.3.0 NVIDIA 536.67 – Compatibility – Using Device: NVIDIA – NVIDIA GeForce RTX 4060
実行画面で Enter / Space キーを1度押してメモリーリークについてテストを行います。
モニタータブのメモリーや Object の要素をチェックして、初期に比べ終了時の使用量が増えていないか確認してください。
test_object_array_memory_leak_1 called
At: res://test_object_array_memory_leak.gd:35:test_object_array_memory_leak_1()
InnerClassA:_init called
InnerClassA:_init called
InnerClassA:_init called
inner_class_a_objects[0].id = 0
inner_class_a_objects[1].id = 1
inner_class_a_objects[2].id = 2
— Debugging process stopped —
対処法1 配列の clear 前に要素の free 関数を呼び出す
先ほどのスクリプトの配列の全要素を消去する clear 関数呼び出しの前に、各要素ごとに Object の free() 関数を呼び出してオブジェクトごとに解放する処理を加えました。
※詳細はスクリプト内のコメントを参照してください。
extends Node2D
## new クラス関数による生成時、このオブジェクトが消去される際に
## 関数がどのタイミングで呼び出されるかを print 出力で確認するためのインナークラスです。
## test_study_inner_class1.gd で GUT によるテストでメモリリークがないかを確認します。
class InnerClassA extends Object:
## インナークラスを作るために、メンバ変数を適当に持たせます。
var id: int
## コンストラクタの仮想関数です。
func _init() -> void:
print("InnerClassA:_init called")
return
## オブジェクトが消去される直前などに通知してくるイベント関数です。
func _notification(what: int) -> void:
print("InnerClassA:_notification called what = ", what)
if what == NOTIFICATION_PREDELETE: # 実質的なデストラクタ
print("InnerClassA:_notification what = NOTIFICATION_PREDELETE")
elif what == NOTIFICATION_POSTINITIALIZE:
print("InnerClassA:_notification what = NOTIFICATION_POSTINITIALIZE")
elif what == NOTIFICATION_EXTENSION_RELOADED:
print("InnerClassA:_notification what = NOTIFICATION_EXTENSION_RELOADED")
else:
print("InnerClassA:_notification what = unkonwn...")
return
## InnerClassA のオブジェクトを複数保持する配列です。
var inner_class_a_objects: Array[InnerClassA] = []
## Object 派生のインナークラスを生成して格納した配列を消去した際にメモリーリークが発生しないことを検証するための関数です。
## 本関数を呼び出して実行を終了した後に、プロファイラのメモリ使用量の変化を確認します。
## print_orphan_nodes は Node 派生クラスのオブジェクトのリークを調べられそう。
func test_object_array_memory_leak_1() -> void:
print_debug("test_object_array_memory_leak_1 called")
# すでに配列に要素があれば実行しません。
if inner_class_a_objects.size() > 0:
print_debug("test_object_array_memory_leak_1 : ner_class_a_objects.size() > 0")
return
# 生成するオブジェクトの個数を設定します。
var numof_objects: int = 10000
inner_class_a_objects.resize(numof_objects)
# オブジェクトを生成して配列に格納します。
for i in range(numof_objects):
var obj: InnerClassA = InnerClassA.new()
obj.id = i
inner_class_a_objects[i] = obj
# オブジェクトのインスタンスのデータを確認します。(最適化で作られない可能性を防ぐため)
for i in range(numof_objects):
print("inner_class_a_objects[", str(i), "].id = ", str(inner_class_a_objects[i].id))
# 配列の要素を消去する前に、その要素ごとに解放処理を行います。
for i in range(numof_objects):
inner_class_a_objects[i].free()
# 配列の要素を消去します。
inner_class_a_objects.clear() # メモリリークが発生するかどうかを確認するメイン部分
return
# Called when the node enters the scene tree for the first time.
func _ready():
print("実行画面で Enter / Space キーを1度押してメモリーリークについてテストを行います。")
print("モニタータブのメモリーや Object の要素をチェックして、初期に比べ終了時の使用量が増えていないか確認してください。")
return
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(_delta):
# Enter / Space キーが押された場合、1回テストを行います。
if Input.is_action_just_released("ui_accept"):
test_object_array_memory_leak_1()
return
結果として、Static Max の増加からテストが行われたことを確認でき、 Objects の個数は初期の 1290 から変動していないことから、解放処理が行われ、メモリリークが起きなくなったことが確認できました。
# Objects のグラフが変動しないのは、1フレーム内で生成と解放をしたためだと思います。
-1024x354.png)
次に、スクリプトの numof_objects を 10000 から 3 に変更して、出力の上限(output overflow, print less text!)を超えないようにして、テストを実行すると「出力」下パネルの結果を確認します。
その結果、オブジェクトの消去前に通知される NOTIFICATION_PREDELETE メッセージは _notification 関数に通知されたことがわかりました。
※テスト関数の呼び出し、インナークラスのコンストラクタ、そのオブジェクトに割り当てた ID を表示する print 文も出力されています。
-1024x395.png)
Godot Engine v4.3.stable.official.77dcf97d8 – https://godotengine.org
OpenGL API 3.3.0 NVIDIA 536.67 – Compatibility – Using Device: NVIDIA – NVIDIA GeForce RTX 4060
実行画面で Enter / Space キーを1度押してメモリーリークについてテストを行います。
モニタータブのメモリーや Object の要素をチェックして、初期に比べ終了時の使用量が増えていないか確認してください。
test_object_array_memory_leak_1 called
At: res://test_object_array_memory_leak.gd:35:test_object_array_memory_leak_1()
InnerClassA:_init called
InnerClassA:_init called
InnerClassA:_init called
inner_class_a_objects[0].id = 0
inner_class_a_objects[1].id = 1
inner_class_a_objects[2].id = 2
InnerClassA:_notification called what = 1
InnerClassA:_notification what = NOTIFICATION_PREDELETE
InnerClassA:_notification called what = 3
InnerClassA:_notification what = unkonwn…
InnerClassA:_notification called what = 1
InnerClassA:_notification what = NOTIFICATION_PREDELETE
InnerClassA:_notification called what = 3
InnerClassA:_notification what = unkonwn…
InnerClassA:_notification called what = 1
InnerClassA:_notification what = NOTIFICATION_PREDELETE
InnerClassA:_notification called what = 3
InnerClassA:_notification what = unkonwn…
— Debugging process stopped —
対処法2 Object のかわりに RefCounted を継承させる
しかし、先ほどの方法では配列の要素を消去 (clear) する前に free 関数を呼び出す必要があり、メモリリークの発生リスクが高いと思います。
そこで Object から派生された公式のクラス RefCounted に、インナークラスの継承元を変更します。
RefCounted クラスは参照カウントで、自身が参照されている個数を管理していて、参照カウントが 0 になりどこからも参照されなくなった時点で自身を解放するため、 free 関数を呼ぶ必要がありません。
Base class for any object that keeps a reference count. Resource and many other helper objects inherit this class.
Unlike other Object types, RefCounteds keep an internal reference counter so that they are automatically released when no longer in use, and only then. RefCounteds therefore do not need to be freed manually with Object.free.
参照カウントを保持するオブジェクトの基本クラス。リソースおよび他の多くのヘルパー オブジェクトはこのクラスを継承します。 他のオブジェクト型とは異なり、RefCounted は内部参照カウンターを保持しているため、使用されなくなったときにのみ自動的に解放されます。したがって、RefCounted は Object.free を使用して手動で解放する必要はありません。RefCounted — Godot Engine (4.x)の日本語のドキュメント と Google 翻訳
以下は、インナークラスの継承元クラスを Object → RefCounted に変更し、 free 関数の呼び出し部分をコメントアウトした、 RefCounted 派生のインナークラスのオブジェクトを配列に入れた後に配列の全要素を消去した際に解放処理が呼ばれ、メモリリークが発生しないことを確認するテストコードです。
※詳細はスクリプト内のコメントを参照してください。
extends Node2D
## new クラス関数による生成時、このオブジェクトが消去される際に
## 関数がどのタイミングで呼び出されるかを print 出力で確認するためのインナークラスです。
## test_study_inner_class1.gd で GUT によるテストでメモリリークがないかを確認します。
class InnerClassA extends RefCounted:
## インナークラスを作るために、メンバ変数を適当に持たせます。
var id: int
## コンストラクタの仮想関数です。
func _init() -> void:
print("InnerClassA:_init called")
return
## オブジェクトが消去される直前などに通知してくるイベント関数です。
func _notification(what: int) -> void:
print("InnerClassA:_notification called what = ", what)
if what == NOTIFICATION_PREDELETE: # 実質的なデストラクタ
print("InnerClassA:_notification what = NOTIFICATION_PREDELETE")
elif what == NOTIFICATION_POSTINITIALIZE:
print("InnerClassA:_notification what = NOTIFICATION_POSTINITIALIZE")
elif what == NOTIFICATION_EXTENSION_RELOADED:
print("InnerClassA:_notification what = NOTIFICATION_EXTENSION_RELOADED")
else:
print("InnerClassA:_notification what = unkonwn...")
return
## InnerClassA のオブジェクトを複数保持する配列です。
var inner_class_a_objects: Array[InnerClassA] = []
## Object 派生のインナークラスを生成して格納した配列を消去した際にメモリーリークが発生しないことを検証するための関数です。
## 本関数を呼び出して実行を終了した後に、プロファイラのメモリ使用量の変化を確認します。
## print_orphan_nodes は Node 派生クラスのオブジェクトのリークを調べられそう。
func test_object_array_memory_leak_1() -> void:
print_debug("test_object_array_memory_leak_1 called")
# すでに配列に要素があれば実行しません。
if inner_class_a_objects.size() > 0:
print_debug("test_object_array_memory_leak_1 : ner_class_a_objects.size() > 0")
return
# 生成するオブジェクトの個数を設定します。
var numof_objects: int = 10000
inner_class_a_objects.resize(numof_objects)
# オブジェクトを生成して配列に格納します。
for i in range(numof_objects):
var obj: InnerClassA = InnerClassA.new()
obj.id = i
inner_class_a_objects[i] = obj
# オブジェクトのインスタンスのデータを確認します。(最適化で作られない可能性を防ぐため)
for i in range(numof_objects):
print("inner_class_a_objects[", str(i), "].id = ", str(inner_class_a_objects[i].id))
# 配列の要素を消去する前に、その要素ごとに解放処理を行います。
#for i in range(numof_objects):
#inner_class_a_objects[i].free() # RefCounted 派生の際に呼ぶとエラーになります。
# 配列の要素を消去します。
inner_class_a_objects.clear() # メモリリークが発生するかどうかを確認するメイン部分
return
# Called when the node enters the scene tree for the first time.
func _ready():
print("実行画面で Enter / Space キーを1度押してメモリーリークについてテストを行います。")
print("モニタータブのメモリーや Object の要素をチェックして、初期に比べ終了時の使用量が増えていないか確認してください。")
return
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(_delta):
# Enter / Space キーが押された場合、1回テストを行います。
if Input.is_action_just_released("ui_accept"):
test_object_array_memory_leak_1()
return
ちなみに free 関数を呼ぶ for ループのコメントを解除して、free 関数を呼び出してしまうと「Can’t free a RefCounted object.」とエラーになります。
E 0:00:03:0821 test_object_array_memory_leak.gd:56 @ test_object_array_memory_leak_1(): Can’t free a RefCounted object.
Method/function failed. Returning: Variant()
core/object/object.cpp:766 @ callp()
<スタックトレース> test_object_array_memory_leak.gd:56 @ test_object_array_memory_leak_1()
test_object_array_memory_leak.gd:72 @ _process()
結果として、Static Max の増加からテストが行われたことを確認でき、 Objects の個数は初期の 1290 から変動していないことから、解放処理が行われ、メモリリークが起きなくなったことが確認できました。
# Objects のグラフが変動しないのは、1フレーム内で生成と解放をしたためだと思います。
-1024x401.png)
次に、スクリプトの numof_objects を 10000 から 3 に変更して、出力の上限(output overflow, print less text!)を超えないようにして、テストを実行すると「出力」下パネルの結果を確認します。
その結果、オブジェクトの消去前に通知される NOTIFICATION_PREDELETE メッセージは _notification 関数に通知されたことがわかりました。
※テスト関数の呼び出し、インナークラスのコンストラクタ、そのオブジェクトに割り当てた ID を表示する print 文も出力されています。
-1024x400.png)
Godot Engine v4.3.stable.official.77dcf97d8 – https://godotengine.org
OpenGL API 3.3.0 NVIDIA 536.67 – Compatibility – Using Device: NVIDIA – NVIDIA GeForce RTX 4060
実行画面で Enter / Space キーを1度押してメモリーリークについてテストを行います。
モニタータブのメモリーや Object の要素をチェックして、初期に比べ終了時の使用量が増えていないか確認してください。
test_object_array_memory_leak_1 called
At: res://test_object_array_memory_leak.gd:35:test_object_array_memory_leak_1()
InnerClassA:_init called
InnerClassA:_init called
InnerClassA:_init called
inner_class_a_objects[0].id = 0
inner_class_a_objects[1].id = 1
inner_class_a_objects[2].id = 2
InnerClassA:_notification called what = 1
InnerClassA:_notification what = NOTIFICATION_PREDELETE
InnerClassA:_notification called what = 3
InnerClassA:_notification what = unkonwn…
InnerClassA:_notification called what = 1
InnerClassA:_notification what = NOTIFICATION_PREDELETE
InnerClassA:_notification called what = 3
InnerClassA:_notification what = unkonwn…
InnerClassA:_notification called what = 1
InnerClassA:_notification what = NOTIFICATION_PREDELETE
InnerClassA:_notification called what = 3
InnerClassA:_notification what = unkonwn…
— Debugging process stopped —
Object 派生と RefCounted 派生のそれぞれの特徴
今回の検証で
- Object は、軽量だけれども自前で解放処理を行わないとメモリリークの恐れがある
- RefCounted は参照カウントの処理の分のコストは増えるけれど、メモリリークの心配が非常に少ない
ことがわかりました。
#個人的には参照カウントのわずかなコストよりも、メモリリークを予防してくれる RefCounted を使いたいと思いました。
まとめ
今回は、無料・軽快な 2D / 3D 用のゲームエンジン Godot Engine 4 で、全ての継承元である Object と、そこから派生した参照カウントを持つ RefCounted をそれぞれ継承したインナークラスのオブジェクトを生成して、Array (配列) に格納した後、配列を clear で消去した際に、要素のオブジェクトが解放されるか、メモリリークはないかをテストした結果を紹介しました。
参照サイト Thank You!
- Godot Engine – Free and open source 2D and 3D game engine
- デバッガーパネル — Godot Engine (4.x)の日本語のドキュメント
- Object — Godot Engine (4.x)の日本語のドキュメント
- RefCounted — Godot Engine (4.x)の日本語のドキュメント
- Array — Godot Engine (stable) documentation in English
記事一覧 → Compota-Soft-Press
コメント