【Godot】Godotのノード参照入門:HUDの階層を動かしても壊れないHPバーの掴み方

作成: 2026-09-22最終更新: 2026-10-10

$・%・@exportでノードを掴む方法の違いを、HUDの階層を動かしながら確かめ、壊れないHPバーの参照にする方法を解説します。

スクリプトから別のノードを使うには、そのノードを掴む(参照を取る)必要があります。Godot では、道順で書く $、名前で探す %、外から渡してもらう @export の3通りを使い分けます。

使い分けずに $ の道順だけで書くと、ノードを1つ別の階層へ移しただけでパスが切れます。UIを CanvasLayer の下へ入れた途端に、それまで動いていたコードが落ちます。

この記事では、HPバーを持つHUDを動かしながら、3通りの掴み方がどこまで届くのかを確かめ、壊れない掴み方にする方法を解説します。

ノードの掴み方のイメージ。同じシーンの中を指す矢印と、シーンの境界で止まる矢印、境界をまたいで根へ渡される矢印が並んでいる

この記事でわかること

  • $ はその場からの道順。階層が動くと切れる
  • % は固有名を登録したノードだけに届く。探す範囲は自分のシーンまで
  • 枝を別シーンにすると所有者が変わり、% では届かなくなる
  • シーンをまたぐときは、中を取りに行かず根のメソッドを呼ぶ

動作確認環境: Godot 4.7

Sponsored

$(その場からの道順)

$ は get_node() の短縮で、書いたスクリプトが付いているノードから見た道順です。

@onready var bar: ProgressBar = $HUD/Margin/HPBar
@onready var sprite: Sprite2D = $Sprite2D
@onready var sibling: Node2D = $"../Enemy"     # 上へ1つ戻ってから隣へ
$は、スクリプトが付いているLevelを起点に、HUD・Margin・HPBarと1段ずつ下る道順

名前に空白が入るときは $"HP Bar" のようにパス全体を引用符で囲みます。

@onready を付けているのは、変数の初期化が走る時点では、まだ子ノードがそろっていないからです。代入を _ready() の直前まで遅らせれば、$ の先にノードがいます。付けずにメンバー変数へ $ を書くと、4.7 では書いた時点で The default value uses "$" which won't return nodes in the scene tree before "_ready()" is called. のエラーになります。詳しくは ライフサイクル入門 にあります。

道順なので、途中のノードを1つ挟むだけで切れます。カメラが動いても画面に貼り付けておくために、UIを CanvasLayer の下へ入れる。整えるために、中身の周りに余白を付ける MarginContainer を1枚足す。よくある作業のたびに、書いたパスが合わなくなります。

%(固有名で探す)

%HPBar と書くと、道順ではなく名前で取りにいけます。取れるのは、固有名として登録したノードだけです。

シーンドックでノードを右クリックし、固有名でアクセスする項目をオンにします。英語UIでは「Access as Unique Name」です。オンにすると、シーンドックのノード名の横に % が付きます。

固有名の登録の図。シーンドックのHPBarを右クリックしてメニューの項目をオンにすると、ノード名の横に%マークが付く様子
@onready var bar: ProgressBar = %HPBar

これで、HUDを CanvasLayer の下へ移しても、MarginContainer を1枚足しても、この行は書き換えずに済みます。コードから登録するなら unique_name_in_owner = true です。

探す先は、登録したノードに限られます。登録していなければ、名前が一致していても取れません。

% が探す範囲

ここが分かりにくいところです。% は次の順に探します。

  1. 自分が持ち主になっている固有名(自分がシーンのルートなら、そのシーンの固有名)
  2. 無ければ、自分の owner(自分が属するシーンのルート)が持っている固有名
%はまず自分の固有名を探し、無ければ持ち主であるLevelの固有名から%HPBarを見つける

owner は「このノードはどのシーンの持ち物か」を示す印です。ひとつの .tscn に置いたノードは、そのシーンが持ち主になります。だから同じシーンの中なら、何段深くても % で届きます。

変わるのは、枝を別のシーンとして保存したときです。HUD 以下を hud.tscn に切り出すと、HPBar の持ち主が Level から HUD へ移ります。Level のスクリプトから %HPBar と書いても、もう見つかりません。届かなくなるのは % のほうだけです。道順で書く $ は持ち主を見ずにツリーの名前をたどるので、切り出したあとでも通ります。

実行中にシーンドックの上に出る Remote タブでも、HPBar は Level の下にちゃんと見えています。見えているのに取れない。これが % でいちばん驚くところです。

Sponsored

シーンをまたぐ参照(@export)

では外から触りたいときはどうするか。中のノードを取りにいくのをやめて、シーンの根(いちばん上のノード)に頼む形にします。

# hud.gd(hud.tscn のルートに付ける)
class_name HUD   # このスクリプトに HUD という型の名前を付ける
extends Control

@onready var bar: ProgressBar = %HPBar

func set_hp(value: int) -> void:
	bar.value = value
# level.gd
extends Node2D

@export var hud: HUD        # インスペクタでHUDをドロップする

func _ready() -> void:
	hud.set_hp(70)
根を渡してもらう図。Levelの矢印がHUDの根で止まり、そこから中のHPBarへはHUD自身の矢印が伸びている

Level が知っているのは「HUDには set_hp() がある」ことだけです。中で MarginContainer を挟もうと、バーを2本に増やそうと、level.gd は1文字も変わりません。

他のエンジンでいうと: @export でノードを受け取るのは、Unity で [SerializeField] の欄に相手をドラッグするのと同じ形です。Unreal Engine なら UPROPERTY(EditAnywhere) で参照を持ちます。

@export var hud: HUD の欄には、編集中のシーンに置いてあるノードをドロップできます。別シーンとしてインスタンス化した根も、型が合っていればそのままドロップできます。実行中に instantiate() して足した HUD はこの欄に入らないので、足したときにコードで hud = … と入れます。

get_parent() で上へたどる形

get_parent() で上へ辿るのは、いちばん壊れやすい掴み方です。

# Margin に付けたスクリプト
func _ready() -> void:
	var level = get_parent().get_parent()    # HUD の外に何があるかを決め打ちしている
MarginからHUDを経て2段上のLevelに届いていた道順が、CanvasLayerを1枚挟むと別のノードに当たる

CanvasLayer を1枚挟まれた瞬間に、この2段は別のノードを指します。そのシーンを F6(いま開いているシーンだけを実行するキー)で単体再生したときも、外側の構成は読み込まれません。そのシーンのルートの親は、実行中のツリーのいちばん上にある /root(ゲームのウィンドウ自身)です。想定していた相手はどこにもいません。

シーンの中の親子関係を使うのは構いません。避けるのは、外側に何があるかを前提にする ことです。外の相手が要るなら、@export で渡してもらうか、シグナルで知らせます。

null と解放済みの参照

最後に、失敗の形が2つあります。取れないことと、取ったあとに消えることです。

最初から無い相手にはget_node_or_nullがnullを返し、queue_freeで消えた相手には参照だけが残る

必ずあるはずの相手には $(get_node())を使ってください。見つからないときにエラーが出るので、パスの打ち間違いにすぐ気づけます。無いことがありうる相手には get_node_or_null() を使い、null かどうかを自分で確かめます。

一度掴んだノードが queue_free() されたときは、変数が null に書き換わりません。無効になった参照がそのまま残ります。is_instance_valid(node) で確かめてください。ただし queue_free() を呼んだそのフレームはまだ true なので、消える予約をした相手も外したいときは is_instance_valid(node) and not node.is_queued_for_deletion() の順で確かめます。解放されたあとの参照に is_queued_for_deletion() を先に呼ぶと、そこでエラーになります。

Sponsored

実践:HUDを動かしても、別シーンにしても壊れないHPバー

アクションゲームの体力バー、シューティングの残機表示、パズルの手数カウンター——UIを整える過程で階層を組み替えるのは、ジャンルを問わず起きます。同じHPバーを4段階で動かして、どこで何が切れるのかを見ます。

① 置くもの

新しいシーンを1つ作り、level.tscn として保存してメインシーンに指定します。

Level (Node2D)                   level.gd
└── HUD (Control)
    └── Margin (MarginContainer)
        └── HPBar (ProgressBar)

HPBar は、値の割合を横棒で見せる ProgressBar です。Max Value を 100、Value を 100、Layout の Custom Minimum Size を (240, 24) にします。幅を与えないとバーが細すぎて変化が見えません。Margin は Theme Overrides の Constants で Margin Left と Margin Top を有効にし、40 を入れておくと画面の角から離れます。

あわせて HPBar を右クリックして固有名を登録しておきます。名前の横に % が付けば登録できています。

# level.gd
extends Node2D

@onready var bar: ProgressBar = $HUD/Margin/HPBar

func _ready() -> void:
	bar.value -= 30
	print("% でも同じノード: ", %HPBar == bar)

F5 で実行すると、バーが70%になり、出力に true が出ます。この時点では $ と % のどちらでも同じノードに届きます。ここが出発点です。

② 階層を動かして、掴み方を替えていく

  1. Level の下に CanvasLayer を追加し、HUD をその下へドラッグして移す。F5 で実行する
  2. level.gd の1行を @onready var bar: ProgressBar = %HPBar に書き換えて、F5 で実行する
  3. HUD を右クリックして Save Branch as Scene を選び、res://hud.tscn として保存する。F5 で実行する
  4. 下の2つのスクリプトに書き換える。hud.gd は hud.tscn のルートの HUD に付ける。Level を選び、インスペクタの Hud の欄に、シーンの中の HUD をドラッグして入れ、F5 で実行する
# hud.gd(hud.tscn のルートに付ける)
class_name HUD
extends Control

@onready var bar: ProgressBar = %HPBar

func set_hp(value: int) -> void:
	bar.value = value
# level.gd
extends Node2D

@export var hud: HUD

func _ready() -> void:
	hud.set_hp(70)

class_name HUD を書いて保存すると、エディタが HUD を型として取り込みます。取り込まれてから、Level のインスペクタに Hud の欄が出ます。

③ 動かして確かめる

手順ごとに F5 で動かします。見るのは3つです。

1つ目は手順1です。$HUD/Margin/HPBar が見つからず、出力にエラーが出ます。HUD はもう Level の直下にいないので、道順が途中で切れました。手順2で %HPBar に替えると、またバーが70%になります。HPBar の持ち主は level.tscn のままなので、何段下がっても % で届きます。

CanvasLayerを挟むと$のパスは途中で切れるが、%HPBarは同じシーンの中なので届いている

2つ目は手順3です。今度は %HPBar が見つかりません。HPBar の持ち主が level.tscn から hud.tscn へ移ったからです。切り出したあとのシーンドックでは、HUD の中身が畳まれて見えません。Remote タブを開くと、HPBar は Level の下に見えています。見えているのに取れない、というのがこの手順です。

3つ目は手順4です。hud.set_hp(70) でバーが70%に戻ります。level.gd が知っているのは set_hp() があることだけです。hud.tscn の中の階層をどう組み替えても、この行は変わりません。

4段階で、$はCanvasLayerを挟むと切れ、%は切り出すと切れ、set_hp()は切り出したあとも通る

図の最後の行は、HUD を Level の直下へ戻した場合です。$HUD/Margin/HPBar の道順はまた届きます。表の3行目で $ が切れていたのは、切り出したせいではなく、間に CanvasLayer が挟まったままだったからです。$ は持ち主を見ずに、ツリーの名前をたどるだけです。

ポイントは2つです。

  • 名前で届くのは、自分のシーンが持っているものまで: % を使うかどうかは深さでは決まりません。枝を別シーンに切り出した瞬間に持ち主が変わる、という1点だけを覚えてください
  • 外から中を取りにいかない: 根を @export で受けてメソッドを呼べば、中の階層は自由に組み替えられます。相手から知らせてほしいときは シグナル を使います

うまくいかないときは

症状原因直し方
@export と @onready を付けた行が赤くなる@onready の代入が、インスペクタで入れた値をあとから上書きする。4.7 では既定でエラーどちらか一方にする。ノードを渡してもらうなら @export だけ
F6 で単体再生すると落ちる外側の親を決め打ちしている。単体再生では、上へ辿った先が /root のウィンドウなど別のノードになるget_parent() で辿るのをやめて @export で受け取る。単体再生では欄が空なので、null なら何もしないように確かめる
2つ目のノードに同じ固有名を付けたら、出力に already claimed の警告が出る1つのシーンで同じ固有名は1つだけ。あとから付けたほうの登録が外れる名前を変えてから登録する
%HPBar と書いたのに、別のシーンの HPBar が取れない% が探すのは自分と自分の owner のシーンだけそのシーンの根を @export で受け、根のメソッドを呼ぶ

おまけ:先に知っておくと良いこと

  • @export var hud: HUD の欄には HUD しか入らない: hud.gd を付けたノードだけをドロップできます。ProgressBar のような別の型の欄に HUD を入れることもできません
  • 固有名はコードからも付けられる: 子として足したあとに、owner に登録先にしたいシーンの根を入れてから unique_name_in_owner = true にします。owner が空のままだと、登録先のシーンが決まりません
  • 根を経由すれば外からでも取れる: $HUD.get_node("%HPBar") のように、いったん根を掴めば中の固有名にも届きます。この書き方だと外が中の名前を知ることになるので、set_hp() のようなメソッドを用意するほうが壊れません
  • 次に読むもの: いつ掴めるようになるかは ライフサイクル入門 、相手から知らせてもらう形は シグナル にあります

まとめ

  • $ は道順: get_node() の短縮で、その場からの道順。途中に1枚挟まると切れる
  • % は登録したノードを名前で探す: 自分の固有名、次に自分の owner の固有名の順
  • 切り出すと % は届かない: 持ち主が変わるから。$ は道順が同じなら届く
  • シーンをまたぐなら @export: 根を受け取って、メソッドを呼ぶ
  • いないかもしれない相手: 無いことがあるなら get_node_or_null、消えたかもしれないなら is_instance_valid

切れる $ は固有名に、別のシーンの相手は根のメソッドを呼ぶ形にします。いま書いているスクリプトの $ のうち、途中に CanvasLayer を1枚挟んだら切れるのはどれですか。

次に読む学習パスの次の記事
GDScript

Godotの@export入門:数値をスライダーと選択肢で縛り、インスペクタの値が消えないようにする

この記事を読む
Godot このセクションのノート104