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

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

@exportで敵の性能をインスペクタに出し、範囲や選択肢で入力を縛って、インスペクタの値が正しく届くようにする方法を解説します。

同じ敵を性能違いで何体も置くときは、速さや最大HPを @export でインスペクタに出し、コードを開かずに変えられるようにします。数値には範囲を、選ぶものには選択肢を付けておくと、入力の間違いも減ります。

ただ、インスペクタで敵のHPを30にしたのに、動かすと10で倒れることがあります。ゲームで使う値を、シーンに保存された値が届く前に作ってしまっているからです。

この記事では、数値を範囲で縛る書き方と選択肢の作り方、インスペクタの値が届く順番とセッターの使いどころを解説します。

スクリプトの1行がインスペクタのスライダー付きの項目になり、そこからゲームへ値が届くまでが並んでいる

この記事でわかること

  • @export_range の引数は min max step の順。ヒントはその後ろ
  • 選ぶものはコード側の enum にすると、名前のまま比較できる
  • 範囲は入力欄の制限で、実行中の値は守らない
  • 宣言の行では、インスペクタの値はまだ届いていない

動作確認環境: Godot 4.7

Sponsored

数値の範囲(@export_range)

数値をそのまま出すと、いくらでも入ります。@export_range を使うと、スライダーと上下限が付きます。

@export_range(20, 180, 1, "suffix:px/s") var speed: float = 60.0
@export_rangeの引数は、min・max・stepの3つが位置で決まり、その後ろに引用符付きのヒント文字列が並ぶ

引数の位置が決まっています。前から min、max、step で、ヒントの文字列はその後ろです。3番目にいきなり "or_greater" と書くと step の位置なので通りません。

使えるヒントは次のとおりです。

  • "or_greater" と "or_less": スライダーの外の値も入力できるようにする
  • "hide_slider": スライダーを隠して、入力欄だけにする
  • "suffix:px/s": 欄の後ろに単位を表示する

もう1つ、書き方で見落としやすいところがあります。@export と @export_range を両方書いてはいけません。@export_range だけでエクスポート済みです。重ねると解析エラー(実行する前、エディタがスクリプトを読んだ時点で出るエラー)になり、そのスクリプトの項目がまるごとインスペクタから消えます。

型も明示してください。@export_range(0, 10, 0.1) var speed = 3 と書くと、初期値の 3 から整数の欄になり、0.1 刻みのスライダーになりません。var speed: float = 3.0 と書けば小数で刻めます。

他のエンジンでいうと: @export_range は、Unity で [SerializeField] に [Range] を添えた形、Unreal Engine の UPROPERTY(EditAnywhere, meta=(ClampMin, ClampMax)) にあたります。

選択肢(enum)

「歩く」「跳ねる」のように選ぶ値は、数値の欄にせず選択肢にします。コード側で enum(名前を付けた整数の並び)を定義して、そのまま出すのがいちばん扱いやすい形です。

enum Motion { WALK, HOP }

@export var motion: Motion = Motion.WALK
enumのエクスポートの図。コードのenum��定義がインスペクタのドロップダウンになり、保存されるのは0や1の整数であることを示している

インスペクタには、Walk・Hop のように読みやすく表示されたドロップダウンが出ます。コードからは motion == Motion.HOP と名前で比べられます。

文字列だけを並べる @export_enum("Walk", "Hop") という書き方もあります。こちらは変数の型が int なら 0 や 1 が入るので、motion == "Hop" とは比べられません。型が違うので、その行を書いた時点で解析エラーになります。

保存されているのが整数だという点は、あとで効いてきます。並びの途中に選択肢を足すと、すでに保存されている数字の意味がずれます。WALK と HOP のあいだに IDLE を入れると、HOP を選んでいたノードが IDLE になります。途中に足す予定があるなら enum Motion { WALK = 0, HOP = 1 } のように数字を固定し、足すときも IDLE = 2 のように新しい数字を付けます。番号を付けずに WALK と HOP のあいだへ書くと、直前の WALK の次の 1 が振られて HOP と重なります。

範囲が効く場所

@export_range を付けても、ゲームの中では守られません。

範囲が守られない2つの場面の図。左はコードから500を代入してスライダーの外に出��ている様子、右はあとから範囲を付けても保存済みの200が残っている様子
@export_range(0, 100) var speed: float = 50.0

func _ready() -> void:
	speed = 500.0    # そのまま入る

もう1つ、すでに置いてあるノードにも効きません。speed に 200 を保存したあとで @export_range(0, 100) を付け足しても、そのノードに保存された 200 は残ります。

範囲は、あとから自分やチームが触るときに、入力を間違えないための制限です。値そのものを守りたいなら、clampf()(小数を範囲に収める関数。整数なら clampi())を通す行を自分で書きます。

Sponsored

インスペクタの値が届く順番

ここがこの記事でいちばん事故の多いところです。

@export var max_hp := 10
var hp := max_hp        # インスペクタで30にしても、ここは10

インスペクタで max_hp を 30 にしたのに、hp は 10 のままになります。値が戻っているのではなく、まだ届いていない値を写しているからです。

値が届く順番の図。宣言の初期化と_initが先に走り、そのあとでシーンに保存された値が@export変数にだけ代入され、hpが取り残される4段が並んでいる

順番はこうです。

  1. インスタンスが作られるとき、宣言の初期化が上から順に走る。この時点の max_hp はスクリプトに書いた 10。hp はその 10 を写す
  2. _init()(インスタンスが生まれたときに1回呼ばれる関数)が呼ばれる。ここもまだ 10
  3. シーンに保存された値が代入される。スクリプトの変数のうち保存されるのは @export を付けたものだけなので、届くのは max_hp で、30 になる。hp は保存されていないので対象外で、10 のまま
  4. @onready と _ready() の時点では、max_hp はもう 30

直し方は1語です。@onready を付けて、代入を _ready() の直前まで遅らせます。

@export var max_hp := 10
@onready var hp := max_hp    # ここでは 30 が入る

_ready() の中で hp = max_hp と書いても同じです。ほかの @export から作る値は、宣言の行で作らない と覚えておけば足ります。値がいつ届くかは ライフサイクル入門 にまとめてあります。

セッター(set)の使いどころ

値が変わったときに何かしたいなら、セッター(代入のときに割り込んで走る処理)を書きます。宣言の末尾に : を付けて、下に set(value): を書くと、代入のたびに中が走ります。中で motion = value と自分で入れないと、値は入りません。ただ、呼ばれるタイミングに癖があります。

セッターは宣言時の初期化では呼ばれず、シーンの値の適用と実行中の代入では呼ばれ、エディタではtool付きだけ
タイミング呼ばれるか
宣言時の初期値の代入呼ばれない(@onready も同じ)
シーンに保存された値の適用呼ばれる(既定値から変えて保存された値だけ)
エディタのインスペクタで変えたとき@tool(エディタの中でも動かす印)を付けたスクリプトだけ呼ばれる
実行中にコードから代入したとき呼ばれる
@export var motion: Motion = Motion.WALK:
	set(value):
		motion = value
		if is_node_ready():
			_apply_motion()

is_node_ready() は、このノード自身の準備(_ready())が済んだかを返します。これで守っているのは、シーンに保存された値が届く時点では、子ノードがまだそろっているとは決まっていないからです。守っただけだと初回の反映が飛びます。既定値のままのノードではセッター自体が呼ばれないので、_ready() からもう一度 _apply_motion() を呼びます。

セッターに向くのは、もとの値から計算して作る値のほうです。速さからアニメの再生速度を決める、巡回の半幅から左右の折り返し位置を計算する、といった処理です。現在HPを最大HPから毎回写すような使い方をすると、戦闘中に最大HPを変えたときに全回復してしまいます。

Sponsored

実践:同じスライムを3体、インスペクタだけで変える

見下ろしARPGの雑魚、横スクロールの巡回敵、タワーディフェンスのウェーブ——同じ敵を性能違いで並べるのは、ジャンルを問わず出てきます。1つのシーンを3体置いて、コードを開かずに3体とも違う動きにします。

① 置くもの

まず敵のシーンを作ります。ルートは Node2D で、名前は Slime です。子に Sprite2D(Texture に res://icon.svg、Scale (0.3, 0.3))と Label を置いて HpLabel にし、res://slime.tscn として保存します。

# slime.gd(slime.tscn のルートに付ける)
class_name Slime
extends Node2D

enum Motion { WALK, HOP }

@export_range(20, 180, 1, "suffix:px/s") var speed: float = 60.0
@export var motion: Motion = Motion.WALK
@export_range(20, 200, 10, "suffix:px") var patrol_half_width: float = 60.0
@export var max_hp: int = 10

var hp := max_hp                       # この1行があとで効いてくる

@onready var label: Label = $HpLabel
@onready var origin := position

var direction := 1.0
var bob := 0.0

func _ready() -> void:
	_update_label()

func _physics_process(delta: float) -> void:
	position.x += direction * speed * delta
	if absf(position.x - origin.x) >= patrol_half_width:
		direction = -direction
	if motion == Motion.HOP:
		bob += delta * 8.0
		position.y = origin.y - absf(sin(bob)) * 24.0
	else:
		position.y = origin.y

func take_damage(amount: int) -> void:
	hp = maxi(hp - amount, 0)
	_update_label()

func _update_label() -> void:
	label.text = "%d / %d" % [hp, max_hp]

次にステージです。新しいシーンを main.tscn として保存し、Project Settings の Application → Run → Main Scene に指定します。そこへ slime.tscn を3つ置きます。

Main (Node2D)            main.gd
└── Slimes (Node2D)
    ├── Slime (slime.tscn のインスタンス)   位置 (200, 150)
    ├── Slime2 (同上)                       位置 (200, 300)
    └── Slime3 (同上)                       位置 (200, 450)
# main.gd
extends Node2D

func _unhandled_input(event: InputEvent) -> void:
	if event.is_action_pressed("ui_accept"):
		for child in $Slimes.get_children():
			var slime := child as Slime   # class_name Slime の型として受け取る。違えば null
			slime.take_damage(10)

② インスペクタだけで3体を変える

3体を1つずつ選んで、インスペクタの値を変えます。コードは触りません。

ノードSpeedMotionPatrol Half WidthMax Hp
Slime40Walk4010
Slime2150Hop18010
Slime360Walk6030
3体のインスペクタの値を並べた図。速さ・動き・巡回半幅・最大HPの4項目が3体で違う値になっていて、Speedにはpx/sの単位が付いている

Speed の欄にスライダーと px/s が付いていること、Motion がドロップダウンになっていることを見ておいてください。ここまでが @export_range と enum の仕事です。

③ 動かして確かめる

F5 で実行します。見るのは3つです。

1つ目は動きです。3体が違う速さで左右に動き、2体目だけ跳ねます。コードは同じで、違うのはインスペクタの値だけです。

2つ目はラベルです。

Slime    10 / 10
Slime2   10 / 10
Slime3   10 / 30
直す前のラベルの図。3体目だけ最大HPが30なのに現在HPは10のままになっている

3体目がおかしくなっています。Max Hp は 30 と表示されているのに、現在HPは 10 です。宣言の行の var hp := max_hp が、まだ 30 が届く前の 10 を写したからです。

スペースキーを押すと、3体とも 0 になります。最大HPを増やしたはずの3体目が、他と同じ1回で 0 です。スペースが効かないときは、ゲームの画面を一度クリックしてください。

3つ目は直したあとです。

ここで slime.gd の1行に @onready を足します。

@onready var hp := max_hp

保存して、実行中のゲームを止めてから、もう一度 F5 を押してください。

Slime    10 / 10
Slime2   10 / 10
Slime3   30 / 30
直したあとのラベルの図。3体目が30/30で始まり、スペースを1回押すと20/30になって残っている

3体目が 30 / 30 で始まります。スペースを1回押すと 0 / 10、0 / 10、20 / 30 になり、3体目だけ 20 が残ります。変えたのは1語だけです。

ポイントは2つです。

  • ほかの @export から作る値は、宣言の行で作らない: 宣言の初期化はシーンの値より先に走ります。@onready を付けるか、_ready() の中で写してください
  • 欄の形は、あとで自分を助ける: Speed に px/s が付いていれば単位を迷いません。Motion がドロップダウンなら打ち間違いが起きません。項目が増えて種類ごとにまとまってきたら、カスタムResource へ切り出す番です

うまくいかないときは

症状原因直し方
コードで出した敵だけ、渡した最大HPが効かないadd_child() の中で子の _ready() まで走り、@onready の写しが先に済む値は add_child() の前に入れる
セッターの中で子ノードに触ったら null のエラーシーンの値が届く時点では、子がまだそろっていないis_node_ready() で抜け、_ready() からもう一度呼ぶ
@export var items: Array の要素を足すたびに型を選ばされる配列の要素の型を書いていないArray[Texture2D] のように要素の型を書く
@export_file で選んだ画像が表示されない中身はパスの文字列で、画像そのものではないload() で読み込むか、@export var tex: Texture2D で受ける

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

  • 複数選びたいなら @export_flags: @export_flags("Fire", "Ice", "Wind") は、選んだものを、1つの整数の中にオン・オフの並びとしてまとめて持ちます。1つだけ選ぶ enum とは別物です
  • ファイルの欄は文字列: @export_file("*.png") var path: String = "" の中身はパスで、画像そのものではありません。テクスチャとして持ちたいなら @export var tex: Texture2D にします
  • フィルタの書き方: @export_file のフィルタは "*.png" の形です。".png" や "png" では意図した絞り込みになりません
  • ボタンも置ける: @tool のスクリプトで @export_tool_button を使うと、インスペクタに押せるボタンが出ます。Godot 4.4 からの機能です
  • @export_subgroup は入れ子にするためのもの: 先に、項目を見出しでまとめる @export_group があると、その内側に入ります。入れ子にしないなら @export_group だけで足ります
  • 次に読むもの: @export の基礎と、スクリプトの既定値とシーンの保存値の関係は カスタムノード にあります

まとめ

  • @export_range は min max step の順: ヒント文字列はその後ろ。@export と重ねると項目が出なくなる
  • 選ぶものは enum: Motion.HOP と名前で比べられる。保存されるのは整数なので、途中に足すと意味がずれる
  • 範囲は入力欄だけの制限: コードからの代入も、保存済みの値も守らない
  • 宣言の行ではまだ届いていない: シーンに保存された値は、宣言のあとで入る
  • セッターは、計算して作る値を作り直す場所: 宣言時の初期化では呼ばれない

var ◯◯ = 別の@export変数 という行には、インスペクタの値はまだ届いていません。いま書いているスクリプトに、その形の行はありますか。

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

シグナルによるノード間の連携

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