Godot 4でアクションゲームやローグライクを作っていると、一時的なステータス変化(バフ/デバフ)って頻出しますよね。特に「一定時間だけ移動速度アップ」は、ダッシュスキル、スピードポーション、トラップ回避ギミックなど、どこにでも出てきます。
ただ、これを素直に実装しようとすると:
- プレイヤーのスクリプトに「速度アップ用の変数」「タイマー処理」「終了処理」などがどんどん増える
- 敵にも同じロジックを入れたくなる → コピペ地獄 or 継承ツリーが肥大化
- 「移動コンポーネント」と「バフロジック」が密結合になり、後から差し替えにくい
Godot標準の「プレイヤーシーンを継承してバフ付きプレイヤーを作る」みたいなやり方だと、シーンやスクリプトのバリエーションが増えすぎて、管理がつらくなりがちです。
そこで今回は、どのキャラにもポン付けできる「速度バフ専用コンポーネント」として SpeedBuff を用意して、継承ではなく合成(Composition)で解決していきましょう。
【Godot 4】一時的にスピード1.5倍!「SpeedBuff」コンポーネント
このコンポーネントは、
- 親ノードが持っている「移動系コンポーネント」の
speed変数を一定時間だけ倍率アップ - 時間が来たら、自動で元の値に戻す
- バフ中にもう一度発動したら、残り時間を延長 or 上書きできる
というシンプルな役割だけを担います。
「速度をどう使うか(移動処理)」は別のコンポーネントに任せて、SpeedBuff はただ speed をいじるだけにしておくのがポイントですね。
GDScript フルコード
extends Node
class_name SpeedBuff
## 一定時間、親の移動系コンポーネントの `speed` を倍率アップするコンポーネント。
##
## 想定する親構成:
## - 親ノードに「移動系コンポーネント」(例:Move2D, CharacterMover など)がアタッチされており、
## そのスクリプトに `var speed: float` が定義されていること。
##
## このコンポーネントは、その `speed` を一定時間だけ倍率アップし、終了時に元に戻します。
## --- 設定パラメータ(インスペクタから編集可能) ---
@export var target_path: NodePath = NodePath("Move2D")
## バフ対象となる「移動コンポーネント」へのパス。
## 例: 親が Player の場合、Player 内の Move2D コンポーネントを指定する想定です。
## 空のままにすると、自動で「親ノード自身」を対象にしようとします。
@export var speed_property_name: StringName = "speed"
## 倍率を掛けるプロパティ名。
## 通常は "speed" を想定していますが、"move_speed" など別名を使っている場合に対応できます。
@export var multiplier: float = 1.5:
set(value):
multiplier = max(value, 0.0) # 負値は防ぐ(0 なら停止バフにもできる)
## バフの継続時間(秒)
@export var duration: float = 3.0:
set(value):
duration = max(value, 0.0)
## バフ中に再度 apply() されたときの挙動
enum RefreshMode {
RESET_TIME, ## 残り時間をリセット(毎回 duration に戻す)
EXTEND_TIME, ## 残り時間に duration を加算(延長方式)
IGNORE ## すでにバフ中なら無視
}
@export var refresh_mode: RefreshMode = RefreshMode.RESET_TIME
## デバッグログを出すかどうか
@export var debug_log: bool = false
## --- 内部状態 ---
var _target: Object = null ## 実際に speed を持っているオブジェクト
var _original_speed: float = 0.0 ## バフ適用前の speed 値
var _remaining_time: float = 0.0 ## 残り時間(秒)
var _is_active: bool = false ## バフ中かどうか
func _ready() -> void:
# 親ノードから対象を解決する
_resolve_target()
set_process(true)
func _process(delta: float) -> void:
if not _is_active:
return
if duration <= 0.0:
# duration 0 の場合、次フレームで即終了させる
_end_buff()
return
_remaining_time -= delta
if _remaining_time <= 0.0:
_end_buff()
## バフを適用するメイン API。
## 例: ポーション取得時やスキル発動時に `speed_buff.apply()` を呼ぶだけでOK。
func apply() -> void:
if _target == null:
_resolve_target()
if _target == null:
if debug_log:
push_warning("[SpeedBuff] Target not found. Check target_path or parent setup.")
return
# すでにバフ中の場合の挙動を制御
if _is_active:
match refresh_mode:
RefreshMode.IGNORE:
if debug_log:
print("[SpeedBuff] Already active. IGNORE mode, so do nothing.")
return
RefreshMode.RESET_TIME:
_remaining_time = duration
if debug_log:
print("[SpeedBuff] Already active. RESET_TIME mode, reset remaining_time to %s" % duration)
return
RefreshMode.EXTEND_TIME:
_remaining_time += duration
if debug_log:
print("[SpeedBuff] Already active. EXTEND_TIME mode, extend remaining_time by %s" % duration)
return
# ここから新規バフ開始
if not _has_speed_property(_target):
if debug_log:
push_warning("[SpeedBuff] Target does not have property '%s'." % speed_property_name)
return
_original_speed = _get_speed(_target)
var new_speed := _original_speed * multiplier
_set_speed(_target, new_speed)
_remaining_time = duration
_is_active = true
if debug_log:
print("[SpeedBuff] Buff applied. speed: %s -> %s (duration: %s)" %
[_original_speed, new_speed, duration])
## 外部から強制的にバフを解除したい場合に呼ぶ。
## 例: シーン切り替えやデス時にリセットしたい場合。
func cancel() -> void:
if not _is_active:
return
_end_buff()
## --- 内部ヘルパー ---
func _resolve_target() -> void:
# target_path が空なら、親ノード自身を対象に試みる
if target_path == NodePath(""):
_target = get_parent()
else:
var parent := get_parent()
if parent:
_target = parent.get_node_or_null(target_path)
else:
_target = null
if debug_log:
if _target:
print("[SpeedBuff] Target resolved: ", _target)
else:
print("[SpeedBuff] Failed to resolve target. target_path = '%s'" % str(target_path))
func _end_buff() -> void:
if _target != null and _has_speed_property(_target):
_set_speed(_target, _original_speed)
if debug_log:
print("[SpeedBuff] Buff ended. speed restored to %s" % _original_speed)
else:
if debug_log:
push_warning("[SpeedBuff] Buff ended but target/property missing; cannot restore speed.")
_is_active = false
_remaining_time = 0.0
func _has_speed_property(obj: Object) -> bool:
# Godot 4 では `has_method` や `obj.get_property_list()` なども使えるが、
# シンプルに `obj.has_meta` ではなく `obj.get` のエラーハンドリングで判定するのが手軽。
return obj != null and obj.has_method("get") and obj.has_method("set") and obj.has_property(speed_property_name)
func _get_speed(obj: Object) -> float:
# 型安全ではないが、ゲーム内では float を想定。
return float(obj.get(speed_property_name))
func _set_speed(obj: Object, value: float) -> void:
obj.set(speed_property_name, value)
使い方の手順
ここでは 2D の例として、CharacterBody2D + 独立した移動コンポーネント + SpeedBuff という構成で説明します。
前提:シンプルな移動コンポーネント例
まず、親が持つ「移動コンポーネント」の例を簡単に示しておきます。
(すでに自作の Move コンポーネントがあるなら、それを使ってOKです)
extends Node
class_name Move2D
## 親の CharacterBody2D を左右入力で動かすだけのシンプルな移動コンポーネント
@export var speed: float = 200.0
var _body: CharacterBody2D
func _ready() -> void:
_body = get_parent() as CharacterBody2D
if _body == null:
push_warning("[Move2D] Parent is not CharacterBody2D.")
func _physics_process(delta: float) -> void:
if _body == null:
return
var input_dir := Input.get_axis("ui_left", "ui_right")
var velocity := _body.velocity
velocity.x = input_dir * speed
_body.velocity = velocity
_body.move_and_slide()
この Move2D が speed を持っているので、SpeedBuff はここに倍率を掛けます。
手順①:Player シーンにコンポーネントをアタッチ
Player シーン構成例:
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── Move2D (Node) # 移動コンポーネント └── SpeedBuff (Node) # 今回の速度バフコンポーネント
Player (CharacterBody2D)シーンを開くMove2Dコンポーネント(上記サンプル)を追加SpeedBuffノードを追加し、上記のSpeedBuff.gdをアタッチ
このとき、インスペクタで SpeedBuff のパラメータを設定します:
- target_path:
"Move2D"(またはエディタからドラッグして指定) - speed_property_name:
"speed"(Move2D の変数名に合わせる) - multiplier:
1.5(1.5倍速) - duration:
3.0(3秒間有効) - refresh_mode: 好みで
RESET_TIME: 3秒バフ中に再取得したら、また3秒にリセットEXTEND_TIME: 3秒バフ中に再取得したら、さらに3秒延長(合計6秒)IGNORE: バフ中は無視(重複取得しても意味なし)
手順②:アイテムやスキルから apply() を呼ぶ
例えば「スピードポーション」的なアイテムシーン:
SpeedPotion (Area2D) ├── Sprite2D └── CollisionShape2D
このアイテムが Player に触れたら、Player の SpeedBuff を呼び出すようにします。
extends Area2D
func _on_body_entered(body: Node) -> void:
# Player かどうか判定(タグやグループで判定するのがオススメ)
if body.is_in_group("player"):
# Player シーン内の SpeedBuff を取得して apply()
var buff := body.get_node_or_null("SpeedBuff") as SpeedBuff
if buff:
buff.apply()
queue_free() # アイテムを消す
これで、プレイヤーがポーションに触れるたびに speed が 1.5倍になり、3秒後に元に戻ります。
手順③:敵や動く床にもそのまま再利用
同じ構成を敵キャラや動く床にも適用できます。例:
Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── EnemyMove2D (Node) # 敵専用の移動コンポーネント(speed を持つ) └── SpeedBuff (Node) # 同じ SpeedBuff をアタッチ
EnemyMove2D も var speed を持っていれば、SpeedBuff の target_path を "EnemyMove2D" に変えるだけで OK。
「敵をスロウにする罠」「味方だけ速くなるバフエリア」なども、同じ apply() を呼ぶだけで実現できます。
手順④:ノード構成図まとめ
プレイヤー・敵・動く床の例をまとめると:
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── Move2D (Node) └── SpeedBuff (Node) Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── EnemyMove2D (Node) └── SpeedBuff (Node) MovingPlatform (Node2D) ├── Sprite2D ├── PlatformMover (Node) # speed を持つ移動コンポーネント └── SpeedBuff (Node)
どのオブジェクトも「移動処理」と「バフ処理」が完全に分離されているので、シーンを見ただけで役割が一目瞭然になります。
メリットと応用
SpeedBuff をコンポーネントとして分離することで、次のようなメリットがあります。
- シーン構造がスッキリ:プレイヤーや敵のスクリプトに「一時的なバフ処理」を書かなくてよい
- 再利用性が高い:どのキャラにも同じ
SpeedBuffをポン付けするだけ - テストしやすい:
SpeedBuff単体で動作確認できる(デバッグログも用意) - 責務が明確:
- 移動コンポーネント:「どう動くか」
- SpeedBuff:「speed を何倍にするか、いつ戻すか」
- 将来の拡張が簡単:同じパターンで AttackBuff, DefenseBuff, GravityBuff などを量産できる
特に「継承で PlayerWithSpeedBuff, EnemyWithSpeedBuff…」みたいなシーンを増やす必要がなく、既存のシーンにコンポーネントを追加するだけで機能拡張できるのが、Composition らしい気持ちよさですね。
改造案:バフ開始/終了時にシグナルを飛ばす
例えば、バフ開始時にエフェクトを出したり、終了時に音を鳴らしたい場合、SpeedBuff にシグナルを追加すると便利です。
signal buff_started(new_speed: float, duration: float)
signal buff_ended(restored_speed: float)
# 既存の apply() の中で、バフ開始直後に emit:
func apply() -> void:
# ... 省略(前述の処理) ...
_original_speed = _get_speed(_target)
var new_speed := _original_speed * multiplier
_set_speed(_target, new_speed)
_remaining_time = duration
_is_active = true
emit_signal("buff_started", new_speed, duration)
# _end_buff() の最後で emit:
func _end_buff() -> void:
if _target != null and _has_speed_property(_target):
_set_speed(_target, _original_speed)
emit_signal("buff_ended", _original_speed)
# ... 残りは同じ ...
これで、外側から SpeedBuff のシグナルに接続して:
- 開始時:足元にエフェクトを出す
- 終了時:バフアイコンを消す
といった「見た目側の処理」も、継承に頼らずにコンポーネント同士の連携だけで組んでいけます。
こうやって「状態変化はコンポーネント」「見た目は別コンポーネント」と分けていくと、プロジェクト全体がどんどん見通し良くなっていきますね。ぜひ、自分のプロジェクト用にカスタマイズしてみてください。
