Godot 4 でアクションゲームやスキル制のゲームを作っていると、「このスキル、今使っていいの? まだクールダウン中?」という判定をあちこちで書くことになりますよね。

ありがちな実装としては…

  • プレイヤーのスクリプトに var can_cast = truevar cooldown = 1.5 を直接持たせる
  • Timer ノードをプレイヤー配下に生やして、timeout シグナルでフラグを戻す
  • スキルごとに似たようなクールダウン処理をコピペ

…みたいな感じになりがちです。

このやり方だと、

  • プレイヤーや敵のスクリプトがどんどん肥大化する
  • 「クールダウンの仕組み」を共通化しづらく、スキルごとに微妙に違うコードが増える
  • Timer ノードを何個も生やして階層が深くなりがち

といった問題が出てきます。

そこで今回は、「継承より合成」の考え方で、どのキャラにもポン付けできる汎用コンポーネント、CooldownTimer を用意してみましょう。
ノードにアタッチするだけで、「今スキル使っていい?」を bool で返してくれるクールダウン管理が手に入ります。

【Godot 4】スキルのクールタイム管理を丸投げ!「CooldownTimer」コンポーネント

コンポーネントのコンセプト

CooldownTimer は、

  • 「クールダウン中かどうか」を is_ready() で問い合わせできる
  • スキルを使ったタイミングで trigger() を呼ぶだけ
  • 内部では Timer を使わず、_process でカウントダウンするシンプル構造
  • 敵・プレイヤー・ギミックなど、どのノードにもアタッチして使い回せる

という「超シンプルなクールダウン管理コンポーネント」です。

フルコード

以下をそのまま CooldownTimer.gd として保存すれば OK です。
必ずルートは Node にアタッチできる前提で書いています)


extends Node
class_name CooldownTimer
## シンプルなクールダウン管理コンポーネント。
## trigger() を呼ぶとクールダウンが開始され、
## is_ready() で「使用可能かどうか」を bool で問い合わせできます。

## --- 設定パラメータ -------------------------------------------------

@export var cooldown_time: float = 1.0:
	set(value):
		cooldown_time = max(value, 0.0)
		# クールダウン時間を変更したとき、
		# すでにクールダウン中なら残り時間も補正したい場合はここで調整する。
		# 今回はシンプルに「次回から反映」としている。

@export var start_ready: bool = true
## true の場合、シーン開始時点では「使用可能」状態からスタートします。
## false の場合、ロード直後からクールダウンが走っている状態になります。

@export var auto_start_on_ready: bool = false
## true にすると、クールダウン終了時に自動で trigger() し直して
## ループ的に動作させることができます(周期的な発射などに応用可能)。

@export var use_process: bool = true
## true: _process(delta) でカウントダウンします(フレーム単位・ゲーム内時間)。
## false: _physics_process(delta) でカウントダウンします(物理フレーム基準)。
## 物理挙動に連動させたい場合は false を推奨。

## --- 状態 -----------------------------------------------------------

var _remaining: float = 0.0
var _is_ready: bool = true

## クールダウン完了時に発火するシグナル。
## 例: スキル UI の点灯、敵の行動フェーズ切り替えなどに利用できます。
signal cooldown_finished

func _ready() -> void:
	# 初期状態を設定
	if start_ready:
		_is_ready = true
		_remaining = 0.0
	else:
		_is_ready = false
		_remaining = cooldown_time

	set_process(use_process)
	set_physics_process(not use_process)

func _process(delta: float) -> void:
	if use_process:
		_update_cooldown(delta)

func _physics_process(delta: float) -> void:
	if not use_process:
		_update_cooldown(delta)

## 内部的なクールダウン更新処理
func _update_cooldown(delta: float) -> void:
	if _is_ready:
		return

	_remaining -= delta
	if _remaining <= 0.0:
		_remaining = 0.0
		_is_ready = true
		emit_signal("cooldown_finished")

		# 自動リスタート機能(周期的なクールダウンにしたい場合)
		if auto_start_on_ready and cooldown_time > 0.0:
			trigger()

## 現在スキルが使用可能かどうかを返す
func is_ready() -> bool:
	return _is_ready

## クールダウンを開始します。
## 通常は「スキルを発動できたタイミング」で呼び出します。
func trigger(force: bool = false) -> void:
	# すでにクールダウン中で、かつ force=false なら何もしない
	if not _is_ready and not force:
		return

	if cooldown_time <= 0.0:
		# 0秒クールダウンの場合は即 ready に戻す
		_is_ready = true
		_remaining = 0.0
		return

	_is_ready = false
	_remaining = cooldown_time

## クールダウンを強制的に完了させます。
## 例: バフ効果などでクールダウンをリセットしたいときに使います。
func reset() -> void:
	_is_ready = true
	_remaining = 0.0

## クールダウンの残り時間を取得します(秒)。
func get_remaining_time() -> float:
	return _remaining

## クールダウンの進捗率(0.0~1.0)を返します。
## 0.0 = クールダウン完了(使用可能)、1.0 = クールダウン開始直後。
func get_progress() -> float:
	if cooldown_time <= 0.0:
		return 0.0
	return clamp((_remaining / cooldown_time), 0.0, 1.0)

## UI 表示などで「あと何秒か」を丸めて表示したいとき用のヘルパー。
func get_remaining_time_rounded(decimals: int = 1) -> float:
	var factor := pow(10.0, decimals)
	return round(_remaining * factor) / factor

使い方の手順

コンポーネントスクリプトを用意
上記のコードを res://components/CooldownTimer.gd などに保存します。
class_name CooldownTimer を定義しているので、スクリプトをどこかに置くだけでエディタから直接選べます。

ノードにアタッチする
例として、プレイヤーのスキル用クールダウンを管理するケースを考えます。
プレイヤーシーン構成はこんな感じにします:

Player (CharacterBody2D)
├── Sprite2D
├── CollisionShape2D
└── CooldownTimer (Node)

Player の子として Node を追加し、名前を CooldownTimer に変更

そのノードに CooldownTimer.gd をアタッチ

インスペクタで cooldown_time を例えば 1.5 秒などに設定

プレイヤースクリプトから問い合わせる
プレイヤーのスクリプト側では、「攻撃ボタンが押された時」に is_ready() を見て、OKならスキル発動+trigger() を呼ぶ、という流れにします。

extends CharacterBody2D

@onready var cooldown: CooldownTimer = $CooldownTimer

func _process(delta: float) -> void:
# 例: 左クリックで攻撃スキル
if Input.is_action_just_pressed("attack"):
_try_cast_attack()

func _try_cast_attack() -> void:
if not cooldown.is_ready():
# まだクールダウン中なので何もしない or SE 再生など
print("Skill is on cooldown. Remaining: ", cooldown.get_remaining_time())
return

# ここでスキル発動処理を書く
_perform_attack()

# スキル発動に成功したのでクールダウン開始
cooldown.trigger()

func _perform_attack() -> void:
print("Attack!")
# 実際には弾を出したり、アニメ再生したり

敵やギミックにも再利用する
同じ CooldownTimer コンポーネントは、敵 AI や自動砲台、動く床のパターン制御などにもそのまま使えます。

例えば「一定間隔で弾を撃つ砲台」を作りたい場合:


Turret (Node2D)
├── Sprite2D
├── CooldownTimer (Node)
└── Muzzle (Marker2D)



extends Node2D

@onready var cooldown: CooldownTimer = $CooldownTimer
@onready var muzzle: Node2D = $Muzzle

func _ready() -> void:
# クールダウン完了ごとに自動で弾を撃ちたいので、
# auto_start_on_ready は true にしておくとループさせやすい。
cooldown.cooldown_time = 0.8
cooldown.auto_start_on_ready = true
cooldown.start_ready = false # すぐに撃ち始めたい場合
cooldown.trigger(force = true)

cooldown.cooldown_finished.connect(_on_cooldown_finished)

func _on_cooldown_finished() -> void:
_shoot()

func _shoot() -> void:
print("Turret shoots from: ", muzzle.global_position)
# 実際にはここで弾シーンをインスタンス化して発射

メリットと応用

1. スキルのクールダウンロジックを完全に分離できる
プレイヤーや敵のスクリプトからは、「今撃てる?」と「撃ったのでクールダウン開始して」だけを意識すればよくなります。

  • 条件分岐やタイマー管理がコンポーネント側に閉じる
  • スクリプトの責務がはっきり分かれる(入力処理 / 行動ロジック / クールダウン)
  • 「継承してスキルごとにクールダウンを実装」みたいな階層が不要になる

2. シーン構造がフラットで見通しが良くなる
Godot 標準だと、Timer ノードをスキルごとに生やしたりしがちですが、CooldownTimer を 1 つ置いておけば、「このノードはクールダウンを持っているんだな」と一目で分かります。

3. スキルごとのカスタマイズが簡単
同じプレイヤーシーンの中に、スキル A 用・スキル B 用の CooldownTimer を複数置いても OK です。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── CooldownTimer_Attack (Node)
 └── CooldownTimer_Dash   (Node)

それぞれに別の cooldown_time を設定しておけば、$CooldownTimer_Attack$CooldownTimer_Dash を別々に参照するだけで、スキルごとのクールタイムが簡単に管理できます。

4. UI 連携もしやすい
get_progress()cooldown_finished シグナルを使えば、

  • スキルアイコンのクールタイム表示(ゲージや円形マスク)
  • クールダウン完了時の点滅/SE 再生

なども簡単に実装できます。

改造案:UI 用のシンプルなバインディング関数を追加する

例えば、UI の TextureProgressBar に直接バインドするためのヘルパーを CooldownTimer に追加してみるのもアリですね:


## ProgressBar / TextureProgressBar などに直接反映するヘルパー
func bind_to_progress_bar(bar: Range) -> void:
	# Range は ProgressBar / TextureProgressBar の親クラス
	bar.min_value = 0.0
	bar.max_value = 1.0
	# 初期値
	bar.value = get_progress()

	# _process 内から呼ばれるようにしてもいいし、
	# 呼び出し側で毎フレーム更新してもよい。
	bar.value = get_progress()

あるいは、_process 側で「バインドされた UI を自動更新」するような仕組みを付け足してもいいですね。
こうやって 「クールダウンのことは CooldownTimer に任せる」 という設計に寄せていくと、プロジェクト全体がかなりスッキリしてきます。