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)

使い方の手順

  1. シーン構成を用意する
  2. BuffTimer コンポーネントをアタッチする
  3. 対象側(プレイヤーなど)にコールバックを実装する
  4. アイテムやスキルから 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 をカスタマイズして、バフ周りをスッキリさせてみてください。