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()

この Move2Dspeed を持っているので、SpeedBuff はここに倍率を掛けます。

手順①:Player シーンにコンポーネントをアタッチ

Player シーン構成例:

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── Move2D           (Node)  # 移動コンポーネント
 └── SpeedBuff        (Node)  # 今回の速度バフコンポーネント
  1. Player (CharacterBody2D) シーンを開く
  2. Move2D コンポーネント(上記サンプル)を追加
  3. 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 をアタッチ

EnemyMove2Dvar speed を持っていれば、SpeedBufftarget_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 のシグナルに接続して:

  • 開始時:足元にエフェクトを出す
  • 終了時:バフアイコンを消す

といった「見た目側の処理」も、継承に頼らずにコンポーネント同士の連携だけで組んでいけます。

こうやって「状態変化はコンポーネント」「見た目は別コンポーネント」と分けていくと、プロジェクト全体がどんどん見通し良くなっていきますね。ぜひ、自分のプロジェクト用にカスタマイズしてみてください。