攻撃のクールダウン、敵の出現間隔、ステージの制限時間のように、決まった秒数を待つ処理は Timer ノードで作ります。Timer は、開始すると決めた秒数を数え、数え終わると timeout シグナルを出します。
動き方を知らずに使うと、連打でクールダウンが効かなかったり、制限時間のラベルが最後の1秒だけ飛んだりします。ポーズ中も敵が湧き続けることもあります。
この記事では、クールダウン・出現間隔・制限時間の3本の Timer を並べて、設定の違いと止め方を解説します。
この記事でわかること
- 数え終わる前の
start()は、残りを捨てて最初から数え直すstart(0.5)は待ち時間そのものを書き換える- 開始前と
stop()のあとtime_leftは0。表示はceili()でstop()は残りを捨て、pausedは残りを保ったまま止める
動作確認環境: Godot 4.7
Timer の動き方(timeout と one_shot)
Timer がやることは1つだけです。開始すると決めた秒数を数えはじめ、数え終わったときに timeout シグナルを出します。$Timer.timeout.connect(_on_timeout) のように関数を繋いでおくと、数え終わるたびにその関数が呼ばれます。

数え終わったあとどうするかを決めるのが 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本を並べたほうが早いです。

攻撃のクールダウン(一度使ってから次に使えるまでの待ち時間。ここでは 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.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() が true になるのは3つの場面です。まだ一度も開始していないとき、stop() したあと、one_shot のタイマーが数え終わったあとです。残りを保ったまま paused で止めているあいだは false のままです。0.0 秒の1回が通り、0.2 秒と 0.4 秒は素通りします。次に通るのは、数え終わったあとに押し直した 0.6 秒です。
判定はボタン側に置きます。timeout に繋いだ関数の中で is_stopped() を見ても、one_shot のタイマーはそこへ入った時点でもう止まっています。いつも true になるので、判定の役に立ちません。
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つだけ注意があります。

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(): 残りを捨てて止める。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 にあります。

実践:攻撃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 Time | One Shot | Autostart |
|---|---|---|---|
| AttackCooldown | 0.5 | オン | オフ |
| SpawnInterval | 2.0 | オフ | オン |
| StageLimit | 9.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つです。スペースが効かないときは、ゲームの画面を一度クリックしてください。

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 で秒数を自分で引き算している箇所はありますか。