Godot 4 でアクションゲームやスキル制のゲームを作っていると、「このスキル、今使っていいの? まだクールダウン中?」という判定をあちこちで書くことになりますよね。
ありがちな実装としては…
- プレイヤーのスクリプトに
var can_cast = trueやvar 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 に任せる」 という設計に寄せていくと、プロジェクト全体がかなりスッキリしてきます。
