Godotで「バフ(強化状態)」を実装しようとすると、だいたいこんな構成になりがちですよね。
- プレイヤーのスクリプトに「攻撃力アップ」「移動速度アップ」などのロジックを書き足す
- さらに「残り時間のカウント」「UIバーの更新」「時間切れで元に戻す」まで同じスクリプトに押し込む
- 敵や味方NPCにもバフを付けたくなって、コピペ or 継承地獄…
結果として、1つのスクリプトが「移動」「攻撃」「UI更新」「バフ管理」全部入りの巨大クラスになりがちです。
Godotはノードと継承でサクッと作れるのが魅力ですが、そのまま進めると「深いノード階層+肥大化したスクリプト」という構造になりやすいんですよね。
そこで今回は、「バフ時間の管理」と「UIバー表示」をまるごとコンポーネント化して、
どのキャラクターにもポン付けできる BuffTimer コンポーネントを用意してみましょう。
【Godot 4】バフ管理をまるごと外出し!「BuffTimer」コンポーネント
BuffTimer は、ざっくり言うとこんな役割を持つコンポーネントです。
- バフの持続時間をカウントダウンする
- 残り時間を UIバー(ProgressBar / TextureProgressBar など)に反映する
- 開始時と終了時に 対象ノードへコールバック(メソッド呼び出し)を行う
- バフが切れたらステータスを元に戻す処理を、呼び出し先に任せる(=BuffTimerは汎用)
つまり、「時間管理+UI更新」だけを担当するコンポーネントです。
「攻撃力を何倍にするか」「どのステータスをいじるか」は、プレイヤー側などのスクリプトに任せます。
これがまさに「継承より合成」の考え方ですね。
フルコード:BuffTimer.gd
extends Node
class_name BuffTimer
## バフの残り時間を管理し、UIバーに反映しつつ
## 対象ノードのコールバックを呼び出すコンポーネント。
##
## 想定:
## - 親ノード(Player, Enemy など)にアタッチして使う
## - 親ノードは on_buff_started(buff_id) / on_buff_ended(buff_id) などを実装する
## - UIバー(ProgressBar / TextureProgressBar)はシーン上の任意のノードを参照する
@export_category("Buff Settings")
## バフの識別子。攻撃力アップなら "atk_up" など。
## 親ノード側で、どのバフかを判定するときに使います。
@export var buff_id: StringName = &"default_buff"
## バフの総時間(秒)。
## start_buff() を呼ぶと、この秒数からカウントダウンします。
@export var duration_seconds: float = 5.0
## バフがスタック(重ね掛け)したときの挙動。
## true の場合:start_buff() を再度呼ぶと残り時間を duration_seconds にリセット。
## false の場合:すでに有効なら何もしない。
@export var reset_on_restart: bool = true
@export_category("Target & Callbacks")
## バフ対象のノード。通常は自分の親ノード(Player, Enemyなど)を指定します。
## 未設定の場合、自動的に get_parent() を対象とみなします。
@export var target_node: NodePath
## バフ開始時に対象へ呼び出すメソッド名。
## 例: "on_buff_started"
@export var callback_on_start: StringName = &"on_buff_started"
## バフ終了時に対象へ呼び出すメソッド名。
## 例: "on_buff_ended"
@export var callback_on_end: StringName = &"on_buff_ended"
@export_category("UI Settings")
## 残り時間を表示する ProgressBar / TextureProgressBar などのノードパス。
## 未設定の場合、UI更新はスキップされます(バフ時間管理だけ行う)。
@export var ui_bar_path: NodePath
## バーの値を 0~100 にするか、0~duration_seconds にするか。
## true: 0~100 のパーセンテージで表示
## false: 0~duration_seconds の実時間で表示
@export var ui_use_percentage: bool = true
## バーの値を 0→100 で増やすか、100→0 で減らすか。
## true: 経過時間に応じて 0→100 に増える
## false: 残り時間に応じて 100→0 に減る(一般的な残り時間バー)
@export var ui_fill_forward: bool = false
@export_category("Debug")
## true のとき、start_buff() / end_buff() などでログを出します。
@export var debug_log: bool = false
## 内部状態
var _elapsed: float = 0.0
var _active: bool = false
var _target_cache: Node = null
var _ui_bar: Range = null ## ProgressBar / TextureProgressBar は Range を継承
func _ready() -> void:
## ターゲットノードの解決
if target_node != NodePath():
_target_cache = get_node_or_null(target_node)
else:
_target_cache = get_parent()
if _target_cache == null and debug_log:
push_warning("[BuffTimer] target_node が解決できませんでした。コールバックは呼び出されません。")
## UIバーの解決
if ui_bar_path != NodePath():
_ui_bar = get_node_or_null(ui_bar_path) as Range
if _ui_bar == null and debug_log:
push_warning("[BuffTimer] ui_bar_path が Range として解決できませんでした。UI更新は行われません。")
## 初期状態のUIをリセット
_update_ui_bar()
func _process(delta: float) -> void:
if not _active:
return
_elapsed += delta
if _elapsed >= duration_seconds:
## 時間切れ
_elapsed = duration_seconds
_update_ui_bar()
_end_buff_internal()
else:
_update_ui_bar()
## 外部から呼び出す:バフを開始する
func start_buff() -> void:
if _active:
if reset_on_restart:
if debug_log:
print("[BuffTimer] Buff restarted: ", buff_id)
_elapsed = 0.0
_update_ui_bar()
## 既にアクティブだが、再スタート時にも開始コールバックを呼びたいならここで呼ぶ
_invoke_callback(callback_on_start)
else:
## すでに有効で、リセットしない設定なら何もしない
if debug_log:
print("[BuffTimer] Buff already active, ignored: ", buff_id)
return
if debug_log:
print("[BuffTimer] Buff started: ", buff_id)
_active = true
_elapsed = 0.0
_update_ui_bar()
_invoke_callback(callback_on_start)
## 外部から呼び出す:バフを強制終了する(時間切れ以外の理由)
func stop_buff() -> void:
if not _active:
return
if debug_log:
print("[BuffTimer] Buff stopped manually: ", buff_id)
_end_buff_internal()
## バフが有効かどうかを返す
func is_active() -> bool:
return _active
## 内部処理:バフ終了共通
func _end_buff_internal() -> void:
if not _active:
return
_active = false
_update_ui_bar()
_invoke_callback(callback_on_end)
## UIバーの更新ロジック
func _update_ui_bar() -> void:
if _ui_bar == null:
return
if duration_seconds <= 0.0:
_ui_bar.value = 0.0
return
var ratio := clamp(_elapsed / duration_seconds, 0.0, 1.0)
var value: float
var max_value: float
if ui_use_percentage:
max_value = 100.0
if ui_fill_forward:
value = ratio * max_value
else:
value = (1.0 - ratio) * max_value
else:
max_value = duration_seconds
if ui_fill_forward:
value = _elapsed
else:
value = duration_seconds - _elapsed
_ui_bar.max_value = max_value
_ui_bar.value = value
## コールバック呼び出しヘルパー
func _invoke_callback(method_name: StringName) -> void:
if _target_cache == null:
return
if not _target_cache.has_method(method_name):
if debug_log:
push_warning("[BuffTimer] Target does not have method '%s'" % method_name)
return
## 引数として buff_id と self(BuffTimer) を渡す
## 例: func on_buff_started(buff_id: StringName, buff_timer: BuffTimer) -> void:
_target_cache.call(method_name, buff_id, self)
使い方の手順
- シーン構成を用意する
- BuffTimer コンポーネントをアタッチする
- 対象側(プレイヤーなど)にコールバックを実装する
- アイテムやスキルから start_buff() を呼ぶ
例1:プレイヤーに攻撃力アップバフを付ける
まず、プレイヤーシーンの構成例です。
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── CanvasLayer │ └── AtkBuffBar (TextureProgressBar) └── BuffTimer (Node / BuffTimer.gd)
1. Player に BuffTimer を追加
- Player(CharacterBody2D)を開く
- 子ノードとして
Nodeを追加し、名前をBuffTimerに変更 BuffTimer.gdをアタッチ(スクリプト)
2. インスペクタで設定
buff_id:"atk_up"duration_seconds: 5.0(5秒の攻撃力アップ)target_node: 空(未設定)にしておけば、自動的にget_parent()= Player を対象にしますcallback_on_start:"on_buff_started"callback_on_end:"on_buff_ended"ui_bar_path:"CanvasLayer/AtkBuffBar"ui_use_percentage: ON(true)ui_fill_forward: OFF(false) → 残り時間が減るバー
3. Player側にコールバックを実装
Player のスクリプト(例:Player.gd)に、バフ開始・終了時の処理を書きます。
extends CharacterBody2D
var base_attack_power: int = 10
var buffed_attack_power: int = 10
## 攻撃力アップ中かどうかのフラグ
var is_atk_buffed: bool = false
func _ready() -> void:
buffed_attack_power = base_attack_power
## BuffTimer から呼ばれる想定のメソッド
func on_buff_started(buff_id: StringName, buff_timer: BuffTimer) -> void:
match buff_id:
&"atk_up":
if not is_atk_buffed:
is_atk_buffed = true
buffed_attack_power = int(base_attack_power * 1.5)
print("攻撃力アップ開始! 現在: ", buffed_attack_power)
_:
## 他のバフIDにも対応したければここに追加
pass
func on_buff_ended(buff_id: StringName, buff_timer: BuffTimer) -> void:
match buff_id:
&"atk_up":
if is_atk_buffed:
is_atk_buffed = false
buffed_attack_power = base_attack_power
print("攻撃力アップ終了。元に戻しました: ", buffed_attack_power)
_:
pass
4. アイテムやスキルから start_buff() を呼ぶ
例えば、フィールド上のアイテム AtkBuffItem を拾ったときに、プレイヤーの BuffTimer を起動します。
AtkBuffItem (Area2D) ├── Sprite2D └── CollisionShape2D
extends Area2D
@export var buff_duration: float = 5.0
func _on_body_entered(body: Node) -> void:
if not body is CharacterBody2D:
return
## プレイヤーに BuffTimer コンポーネントが付いている前提
var buff_timer := body.get_node_or_null("BuffTimer") as BuffTimer
if buff_timer:
buff_timer.duration_seconds = buff_duration
buff_timer.start_buff()
queue_free() # アイテムを消す
これで、攻撃力アップアイテムを拾うと
BuffTimerが 5秒カウントダウン- UIバーに残り時間が表示
- 開始時に
on_buff_started("atk_up")が呼ばれて攻撃力アップ - 終了時に
on_buff_ended("atk_up")が呼ばれて攻撃力が元に戻る
という流れになります。
例2:敵の移動速度アップバフにもそのまま使う
同じ BuffTimer を敵にも付けてみます。
Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── CanvasLayer │ └── SpeedBuffBar (ProgressBar) └── BuffTimer (Node / BuffTimer.gd)
敵のスクリプト側では、buff_id によって挙動を変えるだけです。
extends CharacterBody2D
var base_speed: float = 80.0
var move_speed: float = 80.0
var is_speed_buffed: bool = false
func on_buff_started(buff_id: StringName, buff_timer: BuffTimer) -> void:
match buff_id:
&"speed_up":
if not is_speed_buffed:
is_speed_buffed = true
move_speed = base_speed * 1.5
_:
pass
func on_buff_ended(buff_id: StringName, buff_timer: BuffTimer) -> void:
match buff_id:
&"speed_up":
if is_speed_buffed:
is_speed_buffed = false
move_speed = base_speed
_:
pass
UIバーの挙動は BuffTimer 側が全部やってくれるので、敵側では「数値をいじる」だけでOKです。
メリットと応用
BuffTimer コンポーネントを使うことで、こんなメリットがあります。
- ロジックの責務分離
- キャラクター側:ステータスの変更(攻撃力・移動速度など)
- BuffTimer:時間管理とUI更新
という綺麗な分担になります。
- シーン構造がシンプル
深い継承ツリーを作らずに、BuffTimerを必要なノードに足すだけ。
「バフ持ちプレイヤー」「バフ持ち敵」「バフ持ち動く床」など、全部同じコンポーネントでOKです。 - UIの再利用性が高い
ProgressBar / TextureProgressBar なら何でも参照できるので、
プレイヤーは画面端の大きなバー、敵は頭上の小さなバーといった使い分けも簡単です。 - 複数バフへの拡張が容易
buff_idを変えれば、同じBuffTimerスクリプトを使い回せます。
1キャラに複数のBuffTimerを付ける構成もアリですね。
改造案:シグナルで通知する版
コールバックメソッドではなく、シグナルで通知したい場合は、こんな改造が考えられます。
signal buff_started(buff_id: StringName, buff_timer: BuffTimer)
signal buff_ended(buff_id: StringName, buff_timer: BuffTimer)
func _invoke_callback(method_name: StringName) -> void:
## 既存のメソッド呼び出しに加えてシグナルも発火
match method_name:
&"on_buff_started":
emit_signal("buff_started", buff_id, self)
&"on_buff_ended":
emit_signal("buff_ended", buff_id, self)
if _target_cache and _target_cache.has_method(method_name):
_target_cache.call(method_name, buff_id, self)
こうしておけば、
- シグナル接続で UI やエフェクトを制御
- メソッドコールバックでステータス変更
といった「さらに疎結合な構成」に育てていけます。
継承ではなくコンポーネントを積み上げていくと、「バフ管理」みたいな横断的な機能ほど威力を発揮します。
ぜひ自分のプロジェクト用に BuffTimer をカスタマイズして、バフ周りをスッキリさせてみてください。
