【Godot4】Tween中のNodeをqueue_free()したらゲームが止まった話
2026-09-17
ゲーム内で敵を左右に動かすためにTweenを使用して座標を直接操作していました。 ところが、敵を攻撃して削除した時、ゲームの動作が止まってしまいました。
削除される可能性のあるNodeをTweenで操作するとき、Tweenのバインド先によっては、Node削除後もTweenが動作し続けることがあります。
本記事では実際の再現例と、Node.create_tween()を使った対処方法を紹介します。
環境
- Godot4.7
- GDScript
具体例
まずは、実際に発生した問題を簡略化して再現してみます。
- Control
- Button1
- Button2
今回はControlの子Nodeとして、Button1とButton2の2つのボタンを配置しました。
それぞれのボタンはTweenで座標を操作し、左右に移動し続けます。また、ボタンを押すとqueue_free()が呼ばれ、押したボタンが削除されるようにしています。
extends Control
@onready var button1 := $Button1
@onready var button2 := $Button2
var tween1
var tween2
func _ready() -> void:
tween1 = create_tween().set_loops()
tween1.tween_property(button1, "position", Vector2(800,200), 1).from(Vector2(200,200))
tween1.tween_property(button1, "position", Vector2(200,200), 1)
tween2 = create_tween().set_loops()
tween2.tween_property(button2, "position", Vector2(800,400), 1).from(Vector2(200,400))
tween2.tween_property(button2, "position", Vector2(200,400), 1)
func _on_button_1_pressed() -> void:
button1.queue_free()
func _on_button_2_pressed() -> void:
button2.queue_free()
実行する方法によって結果が変わりました。 その結果を以下に示します。
| 実行方法 | 結果 |
|---|---|
| プロジェクトを実行 | エラー検出・停止しない |
| ブラウザで実行 | エラー検出・停止しない |
| Release Export | エラー表示なし・ゲーム停止 |
まず、ゲームエンジンから「プロジェクトを実行」で確認します。
エラーで無限ループを検出するがゲームは止まりません。
続いて、ゲームエンジンから「ブラウザで実行」した場合も確認します。
結果は同じで、エラーで無限ループを検出するがゲームは止まりません。
ここまでは、どちらも無限ループを検出するエラーが表示されるものの、ゲーム自体は停止しませんでした。
一方、リリースビルドでエクスポートしたHTMLファイルでは結果が変わります。
エラー表示はなしで、ゲームが停止してしまいました。
ゲームエンジンから実行した時と、エクスポートしたゲームの動きが違うことがわかります。
スマホで、エクスポートしたHTMLファイルを実行し、ゲームが停止した状態でしばらく放置したところ、スマホが発熱しました。処理負荷が高くなっていた可能性があります。
そもそもゲーム自体が停止するので重大な欠陥です。
問題点
今回のコードでは、create_tween()をControlのスクリプト内で呼び出しているため、作成されたTweenはControl(self)にバインドされています。
ここで注意したいのが、Tweenが操作しているNodeと、TweenがバインドされているNodeは別という点です。
今回Tweenで座標を操作しているのはButtonですが、Tween自体がバインドされているのはControlです。
そのため、Buttonをqueue_free()で削除しても、Controlが残っている限りTweenは一緒に削除されません。
TweenのButtonを操作する命令は、操作対象のButtonがないので即座に命令完了になります。 即座に終了する命令を無限回繰り返すので同じフレームで何回も呼び出されて負荷がかかります。 その結果、ゲームのほかの処理が実行できなくなって止まってしまいました。
ゲームエンジンから実行した場合は、デバッグビルドでの実行なので無限ループ検出の安全装置が有効になっているのでゲームは止まりませんでした。
解決方法
では、今回の問題をどのように解決すればよいのでしょうか。 対策は、Tweenを操作対象のNodeにバインドすることです。 これにより、対象のNodeが解放されたときに、そのNodeにバインドされているTweenも自動的に終了します。
Tweenをノードにバインドする方法は以下の二つがあります。
var tween = create_tween().bind_node(node)
var tween = node.create_tween()
この二つは意味としては同じなので好きな方で書けばいいです。
Godot公式ドキュメントでも、Tweenの寿命が特定のNodeに依存する場合はbind_node()を使用するよう案内されています。
修正したコードは以下のようになります。
extends Control
@onready var button1 := $Button1
@onready var button2 := $Button2
var tween1
var tween2
func _ready() -> void:
tween1 = button1.create_tween().set_loops()
tween1.tween_property(button1, "position", Vector2(800,200), 1).from(Vector2(200,200))
tween1.tween_property(button1, "position", Vector2(200,200), 1)
tween2 = button2.create_tween().set_loops()
tween2.tween_property(button2, "position", Vector2(800,400), 1).from(Vector2(200,400))
tween2.tween_property(button2, "position", Vector2(200,400), 1)
func _on_button_1_pressed() -> void:
button1.queue_free()
func _on_button_2_pressed() -> void:
button2.queue_free()
それぞれのボタンにバインドしたので以下のようになります。
それでは、修正したコードでもう一度動作を確認してみます。
まずは、ゲームエンジンから「プロジェクトを実行」した場合です。
無限ループを検出したエラーは消えています。
続いて、先ほどゲームが停止したリリースビルドでエクスポートしたHTMLファイルでも確認します。
エラーが出ることもなく、ゲームが停止することもなく、正常に動作します。
まとめ
削除される可能性のあるNodeをTweenで操作するときは、そのNodeにTweenをバインドしておくと安全💯