【Godot4】@onready変数がnullになる原因|instantiate後はadd_childしてからアクセスする

2026-06-04

Godot4で@onreadyを使用しているプログラムがアタッチされているノードを複製して操作しようとしたところ、@onready 変数がinstantiate()時点では初期化されないため、エラーとなり正常に動作しませんでした。

この記事では@onready の基本的な説明と初期化のタイミングについて詳しく説明します。

環境

  • Godot4.5
  • GDScript

@onreadyとは

次のシーンを例に考えます。Rankingノードにスクリプトがアタッチされています。

各順位の情報を格納する、VBoxContainerを取得するには、次のコードを使用します。

var rank_list_container
func _ready() -> void:
	rank_list_container = $ScrollContainer/VBoxContainer

この時、変数宣言の前に@onreadyアノテーションを置くと、_ready関数が呼び出される直前に初期化されます。_ready関数はそのノードが初めてシーンツリーに追加されたときに呼び出されます。

@onready var rank_list_container = $ScrollContainer/VBoxContainer

具体例

各順位の情報を表示するシーンを作成し、そのシーンを順位の数だけ複製してランキング画面を作成します。

ここでは、ランキング一覧を作るために、順位ごとのUIシーンをinstantiate()して追加する例で説明します。 この例では、RankEntry.gd内の@onready変数が初期化される前にset_label()を呼んでしまうため、エラーが発生します。

RankEntry.gd

extends Control

@onready var rank_label = $RankLabel
@onready var name_label = $NameLabel
@onready var distance_label = $DistanceLabel
@onready var score_label = $ScoreLabel

func set_label(rank:int, user_name:String, distance:int, score:int):
	rank_label.text = str(rank)
	name_label.text = user_name
	distance_label.text = str(distance)
	score_label.text = str(score)

このシーンを複製した後に外部からset_label関数を呼び出すことで内容を書き換えます。

Ranking.gd

extends Control

# 各順位の情報を表示するシーン
const RANK_ENTRY_SCENE = preload("res://Scenes/UI/rank_entry.tscn")

@onready var rank_list_container := $ScrollContainer/VBoxContainer

func _ready() -> void:
	set_ranking()

# ランキングを作成する
func set_ranking():
	# ランキング情報を取得する
	var ranking = await Ranking.get_ranking()

	for row in ranking:
		var rank_ui := RANK_ENTRY_SCENE.instantiate()
		rank_ui.set_label(row["rank"], row["name"], row["distance"], row["score"])
		rank_list_container.add_child(rank_ui)

_ready関数が呼ばれたときに取得したランキング情報に従い、先ほどのシーンを複製して適切な値にセットしてシーンツリーに追加するプログラムです。

このままでは正常に動作せずに、エラーが発生してしまいます。 エラー例: set_label: Invalid assignment of property or key 'text' with value of type 'String' on a base object of type 'Nil'.

@onready_ready関数の直前に実行されます。なのでRANK_ENTRY_SCENE.instantiate()で複製しただけではシーンツリーに追加されていないので_ready関数は呼び出されず、@onreadyが書かれた変数(rank_label, name_label, distance_label, score_label)も初期化されていません。

その状態でset_label関数を呼び出して、@onreadyが書かれた変数(rank_label, name_label, distance_label, score_label)を編集しようとしても中身が設定されていないのでエラーになります。

修正案

Ranking.gd

extends Control

# 各順位の情報を表示するシーン
const RANK_ENTRY_SCENE = preload("res://Scenes/UI/rank_entry.tscn")

@onready var rank_list_container := $ScrollContainer/VBoxContainer

func _ready() -> void:
	set_ranking()

# ランキングを作成する
func set_ranking():
	# ランキング情報を取得する
	var ranking = await Ranking.get_ranking()

	for row in ranking:
		var rank_ui := RANK_ENTRY_SCENE.instantiate()
		rank_list_container.add_child(rank_ui)
		rank_ui.set_label(row["rank"], row["name"], row["distance"], row["score"])

解決方法は単純で、set_label関数の前に_ready関数が呼び出されるように先に、add_childを実行します。add_childすることでシーンツリーに追加され、rank_entryの_ready関数が呼び出されるので@onreadyが書かれた変数も初期化されます。

別解

RankEntry.gd

extends Control

func set_label(rank:int, user_name:String, distance:int, score:int):
	$RankLabel.text = str(rank)
	$NameLabel.text = user_name
	$DistanceLabel.text = str(distance)
	$ScoreLabel.text = str(score)

思い切って@onreadyの変数を使用しないプログラムにするとこうなります。 この方法ならRanking.gdを変更する必要はありませんが、ノードの要素を直接指定しています。そのため、複数個所で使用する場合は指している要素の名前やパスが変わった時に記述されているすべてを編集しなければならないので注意しましょう。

まとめ

複製したシーンは始めにadd_childするべきである。

add_childすることで@onreadyの変数が初期化される。💯

ブログゲーム開発Godot