【Godot】GodotのTimerノード入門:クールダウン・出現間隔・制限時間を3本のTimerで作り分ける

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

クールダウン・出現間隔・制限時間の3本のTimerを作り分け、開始と止め方、残り時間の表示を解説します。

攻撃のクールダウン、敵の出現間隔、ステージの制限時間のように、決まった秒数を待つ処理は Timer ノードで作ります。Timer は、開始すると決めた秒数を数え、数え終わると timeout シグナルを出します。

動き方を知らずに使うと、連打でクールダウンが効かなかったり、制限時間のラベルが最後の1秒だけ飛んだりします。ポーズ中も敵が湧き続けることもあります。

この記事では、クールダウン・出現間隔・制限時間の3本の Timer を並べて、設定の違いと止め方を解説します。

攻撃・出現・制限時間の3本のTimerが、それぞれの残り時間を数えながら同時に動いているイメージ

この記事でわかること

  • 数え終わる前の start() は、残りを捨てて最初から数え直す
  • start(0.5) は待ち時間そのものを書き換える
  • 開始前と stop() のあと time_left は 0。表示は ceili() で
  • stop() は残りを捨て、paused は残りを保ったまま止める

動作確認環境: Godot 4.7

Sponsored

Timer の動き方(timeout と one_shot)

Timer がやることは1つだけです。開始すると決めた秒数を数えはじめ、数え終わったときに timeout シグナルを出します。$Timer.timeout.connect(_on_timeout) のように関数を繋いでおくと、数え終わるたびにその関数が呼ばれます。

開始すると残り時間が減り、数え終わるとtimeoutが出る。one_shotオンは止まり、オフはまた最初から数える

数え終わったあとどうするかを決めるのが one_shot です。オンなら、そこで止まります。オフなら、また最初から数えはじめます。

他のエンジンでいうと: Unreal Engine の Set Timer by Event(Looping をオンにすると繰り返す)に近い役割です。Unity で Invoke() やコルーチンを使って書く待ち時間を、ノードとして置いたものと考えられます。

1回だけ待って処理を続けたいなら、Timer ノードを置かなくても await get_tree().create_timer(1.0).timeout で足ります。その使い分けは awaitとコルーチン入門 にまとめてあります。ここから先は、待っている途中の様子をボタンやラベルから使う場合の話です。残りを読む、途中で止める、押されたかどうかで分岐する。この3つが要るなら Timer ノードを置きます。

3本の Timer の設定

プロパティを一覧で覚えるより、使い道の違う3本を並べたほうが早いです。

攻撃と制限時間はone_shotオンで1回きり、出現だけone_shotオフで繰り返し、autostartも出現だけオン

攻撃のクールダウン(一度使ってから次に使えるまでの待ち時間。ここでは 0.5 秒)と制限時間(9 秒)は one_shot をオンにして、1回数え終わったら止めます。出現間隔(2 秒)だけはオフにして、何度でも数え直させます。同じ timeout でも、意味が3本で違います。攻撃は「次を受け付けられる」、出現は「次の1体」、制限時間は「終わり」です。

autostart はシーンに入った時点で自動的に開始するスイッチです。出現のように「置かれたら動き出す」ものは、これで足ります。制限時間をコードから始めているのは、ステージの開始を1行として持っておきたいからです。始まりがシーンの読み込みと同じでよいなら、こちらも autostart で構いません。

途中で start() を呼んだとき

攻撃のクールダウンを、押すたびに攻撃して start() する形で書いてみます。

# UI が使わなかった入力だけが届く。ui_accept は最初から Space と Enter に割り当て済み
func _unhandled_input(event: InputEvent) -> void:
	if event.is_action_pressed("ui_accept"):
		attack_label.text = "斬"
		attack_cooldown.start()     # 押すたびに最初から数え直す
0.2秒おきに押し続けると、そのたびに残りが0.5秒へ戻り、timeoutが一度も出ない連打のタイムライン

連打すると、押すたびに 斬 が出ます。クールダウンがまったく効いていません。しかも 0.0 秒、0.2 秒、0.4 秒と押し続けるあいだ、残りは押すたびに 0.5 秒へ戻ります。数え終わらないので、timeout がいつまでも出ません。AttackLabel を ... に戻す合図も来ないままです。

止めているのはボタン側です。空いているときだけ振る、という判定を入れます。

func _unhandled_input(event: InputEvent) -> void:
	if event.is_action_pressed("ui_accept") and attack_cooldown.is_stopped():
		attack_label.text = "斬"
		attack_cooldown.start()
is_stopped()のときだけ攻撃すると、0.0秒の1回が通り、0.2と0.4秒は素通りして0.6秒でまた通る

is_stopped() が true になるのは3つの場面です。まだ一度も開始していないとき、stop() したあと、one_shot のタイマーが数え終わったあとです。残りを保ったまま paused で止めているあいだは false のままです。0.0 秒の1回が通り、0.2 秒と 0.4 秒は素通りします。次に通るのは、数え終わったあとに押し直した 0.6 秒です。

判定はボタン側に置きます。timeout に繋いだ関数の中で is_stopped() を見ても、one_shot のタイマーはそこへ入った時点でもう止まっています。いつも true になるので、判定の役に立ちません。

Sponsored

start() の引数(wait_time の上書き)

start() には秒数を渡せます。これは足し算ではなく、長さの上書きです。

attack_cooldown.start(0.5)     # wait_time が 0.5 に書き換わる
attack_cooldown.start()        # 以降も 0.5 秒。インスペクタの値には戻らない

インスペクタに 2.0 と入れてあっても、一度 start(0.5) を通すと、そのノードの wait_time は 0.5 になります。インスペクタの値で回したいなら、最初から引数を付けないでください。

逆に、走っている途中で wait_time を書き換えても、いま数えている残りは変わりません。変わるのは次に数えはじめるときの長さです。

残り時間の表示(time_left)

残り時間は time_left で読めます。表示するときに1つだけ注意があります。

開始直後の8.98秒をintは8、ceiliは9と表示し、最後の0.9秒をintは0、ceiliは1と表示する
func _process(_delta: float) -> void:
	time_label.text = "のこり %d" % ceili(stage_limit.time_left)

int(time_left) は切り捨てなので、開始直後の 8.98 は 8、最後の 0.9 は 0 と表示されます。9 はほとんど見えず、最後の1秒で 0 に飛びます。ceili()(小数を切り上げて整数にする関数)なら、9 から 1 まで数えられます。

もう1つ、開始する前と stop() のあと、time_left は 0 を返します。開始前のラベルも 0 になるので、ステージが始まってから表示を回してください。

終わりの判定は timeout に任せます。_process の中で time_left <= 0 を見ると、開始前にも、終わったあとの毎フレームにも当たってしまいます。

止め方(stop・paused・ポーズ)

止め方は3通りあり、残り時間の扱いが違います。

stop()は残りを捨てて0に、pausedは残りを保って止まり、ツリーのポーズでは周りごと止まる
  • stop(): 残りを捨てて止める。time_left は 0 になり、timeout は出ない
  • paused = true: 残りを保ったまま止める。false に戻すと続きから数える
  • get_tree().paused = true: ツリーごと止める。Timer も止まる

ポーズ中も進めたいノードは、インスペクタの Process → Mode を Always にします。コードでは process_mode = Node.PROCESS_MODE_ALWAYS です。process_callback は、残り時間を毎フレームの処理で減らすか、物理の処理で減らすかの切り替えです。Physics に変えても、ポーズでは止まります。この2つは別のスイッチです。

ポーズ中も敵が湧き続けるなら、原因はたいてい2つのどちらかです。湧きを get_tree().create_timer() で回しているか、Timer の process_mode が、親から受け継いだものも含めて Always になっています。create_timer() の使い捨てのタイマーは、既定でポーズ中も進みます。止めたいなら create_timer(秒, false) と2つ目の引数を渡すか、周期は Timer ノードに任せます。ポーズそのものの作り方は ポーズとProcess Mode にあります。

ツリーのポーズ中、Timerノードは止まり、create_timer()は既定で数え続けて敵が湧く
Sponsored

実践:攻撃0.5秒・出現2秒・制限9秒のステージ

アクションゲームの攻撃間隔、タワーディフェンスの湧き、タイムアタックの残り時間——待ち時間の管理はジャンルを問わず出てきます。3本を同時に動かして、設定の違いを目で見ます。

① 置くもの

新しいシーンを1つ作り、main.tscn として保存します。Project Settings の Application → Run → Main Scene に、このシーンを指定します。

Main (Node2D)                 main.gd
├── AttackCooldown (Timer)
├── SpawnInterval (Timer)
├── StageLimit (Timer)
└── Hud (VBoxContainer)       位置 (40, 40)。子を縦に並べる入れ物
    ├── TimeLabel (Label)
    ├── SpawnLabel (Label)
    └── AttackLabel (Label)

3本の Timer は、インスペクタで次のように設定します。名前を Timer Timer2 Timer3 のままにすると、どれがどれだか分からなくなります。

ノードWait TimeOne ShotAutostart
AttackCooldown0.5オンオフ
SpawnInterval2.0オフオン
StageLimit9.0オンオフ
# main.gd
extends Node2D

@onready var attack_cooldown: Timer = $AttackCooldown
@onready var spawn_interval: Timer = $SpawnInterval
@onready var stage_limit: Timer = $StageLimit
@onready var time_label: Label = $Hud/TimeLabel
@onready var spawn_label: Label = $Hud/SpawnLabel
@onready var attack_label: Label = $Hud/AttackLabel

var spawned := 0
var finished := false

func _ready() -> void:
	attack_cooldown.timeout.connect(_on_attack_ready)
	spawn_interval.timeout.connect(_on_spawn)
	stage_limit.timeout.connect(_on_stage_end)
	spawn_label.text = "敵 0"
	attack_label.text = "..."
	stage_limit.start()                # ここでステージが始まる

func _process(_delta: float) -> void:
	if not finished:
		time_label.text = "のこり %d" % ceili(stage_limit.time_left)

func _unhandled_input(event: InputEvent) -> void:
	if finished:
		return
	if event.is_action_pressed("ui_accept") and attack_cooldown.is_stopped():
		attack_label.text = "斬"
		attack_cooldown.start()        # 空くまで次は受け付けない

func _on_attack_ready() -> void:
	attack_label.text = "..."

func _on_spawn() -> void:
	spawned += 1
	spawn_label.text = "敵 %d" % spawned

func _on_stage_end() -> void:
	finished = true
	spawn_interval.stop()              # 終わったら湧きも止める
	attack_cooldown.stop()             # stop() は timeout を出さないので、表示はここで変える
	time_label.text = "のこり 0"
	attack_label.text = "しゅうりょう"

② 3本がどう重なるかを見る

F5 を押す前に、9秒間に何が起きるかを確かめておきます。

SpawnInterval は autostart なので、シーンに入った瞬間から数えはじめます。最初の1体が出るのは 2 秒後です。開始と同時に1体出したいなら、_ready() で _on_spawn() を1回呼んでから任せます。

StageLimit は _ready() の start() で始まり、9 秒で timeout を出します。そこで _on_stage_end() が湧きとクールダウンを止め、入力も弾きます。

③ 動かして確かめる

F5 で実行します。見るのは3つです。スペースが効かないときは、ゲームの画面を一度クリックしてください。

起動直後はのこり9・敵0、連打中は斬、9秒後はのこり0・敵4・しゅうりょうに変わる

1つ目は制限時間です。起動直後は のこり 9 敵 0 ... で、数字がおよそ1秒ごとに減ります。int() で表示していると、開始直後に 8 へ変わります。

2つ目は攻撃です。一度 斬 になると、約0.5秒は何度押しても素通りします。is_stopped() の判定を外して試すと、押すたびに攻撃が通ります。0.5秒たつ前に押し続けているあいだは、斬 のまま ... に戻りません。数え終わる前に、最初からの数え直しを続けているからです。

3つ目は出現と終わりです。約 2 4 6 8 秒で 敵 の数が増え、約 9 秒で のこり 0 しゅうりょう になります。敵は 4 で止まり、そのあとスペースを押しても何も起きません。3本の Timer が、それぞれの長さと止まり方で最後まで動きました。

ポイントは2つです。

  • 待ちの数だけ Timer を置く: 1本で数えて「4回目に出現、18回目に終了」とすると、攻撃の速さを変えた瞬間に出現も終了も壊れます。名前を付けて分けておけば、あとでインスペクタの数字を変えるだけで済みます
  • 終わりは1か所から配る: _on_stage_end() で finished を立て、湧きとクールダウンを止め、入力を弾いています。クールダウンを stop() すると is_stopped() は true に戻るので、入力を止めているのは finished のほうです

うまくいかないときは

症状原因直し方
timeout が一度も来ない_process() の中で毎フレーム start() している。連打と同じで、毎回最初から数え直す開始は _ready() やボタンの処理など、きっかけの場所で1回だけ
コードで作った Timer が動かず、エラーが出るadd_child() する前に start() している。ツリーの外では開始できないシーンツリーに入っている親へ add_child() してから start()
途中で止めたら、timeout に書いた後片付けが走らないstop() は timeout を出さない止める側でも、同じ後片付けの関数を呼ぶ
クールダウンが最初の1回しか効かず、2回目から攻撃できないAttackCooldown の One Shot がオフ。数え終わっても止まらないので、is_stopped() が true に戻らないOne Shot をオンにする(既定はオフ)

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

  • autostart は開始したら降りる: 自動で始まったあと、実行中の値は false に戻ります。シーンに保存した設定はそのままなので、次にシーンを開いたときはまた自動で始まります
  • start(0) はすぐには鳴らない: 0 以下を渡すと引数は無視され、いまの wait_time で開始します。wait_time そのものも 0 にはできず、インスペクタの下限は 0.001 秒です
  • 数えるだけなら自前でもよい: _process で delta(前のフレームからの経過秒数)を足していけば、秒数は数えられます。Timer はそこに停止と残り時間と timeout を付けたものだと思ってください
  • 持ち主は使う側に寄せる: 実践では3本を並べて見るために Main の下に置きました。攻撃のクールダウンは、本当は攻撃する側の子にしておくと持ち運べます

まとめ

  • Timer は決めた秒数を数える: 数え終わると timeout を出す。one_shot をオフにすると、また最初から数える
  • 途中の start() は最初から数え直す: 残りを捨てる。判定は呼ぶ側に置く
  • start(0.5) は wait_time を書き換える: 次に引数なしで start() しても、その長さになる
  • 止まっているとき time_left は 0: 開始前と stop() のあと。表示は ceili() で切り上げる
  • 捨てるか、保つか: stop() は残りを捨て、paused は残りを保つ。ポーズ中も進めるなら Process → Mode を Always に

「途中で止めたい」「残りを見せたい」が出てきたら、Timer を1本置く合図です。いま _process で秒数を自分で引き算している箇所はありますか。

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

Autoloadによるシーンをまたぐデータ管理

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