【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がないので即座に命令完了になります。 即座に終了する命令を無限回繰り返すので同じフレームで何回も呼び出されて負荷がかかります。 その結果、ゲームのほかの処理が実行できなくなって止まってしまいました。

graph TD Buttonが削除 --> Tweenは残る --> C["Tweenの命令が即終了"] --> 無限回繰り返す --> 同一フレームで処理され続ける --> ゲーム停止

ゲームエンジンから実行した場合は、デバッグビルドでの実行なので無限ループ検出の安全装置が有効になっているのでゲームは止まりませんでした。

解決方法

では、今回の問題をどのように解決すればよいのでしょうか。 対策は、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()

それぞれのボタンにバインドしたので以下のようになります。

graph TD Buttonを削除 --> ButtonにバインドされたTweenも終了 --> 正常動作

それでは、修正したコードでもう一度動作を確認してみます。

まずは、ゲームエンジンから「プロジェクトを実行」した場合です。

無限ループを検出したエラーは消えています。

続いて、先ほどゲームが停止したリリースビルドでエクスポートしたHTMLファイルでも確認します。

エラーが出ることもなく、ゲームが停止することもなく、正常に動作します。

まとめ

削除される可能性のあるNodeをTweenで操作するときは、そのNodeにTweenをバインドしておくと安全💯

ブログゲーム開発Godot