Godot 4でゲームパッドの振動(ランブル)をきちんと扱おうとすると、意外と面倒ですよね。
「ダメージを受けたらちょっとだけ震えたい」「着地したらドンっと強く振動させたい」と思っても、

  • 毎回 Input.start_joy_vibration() を直接呼んで条件分岐を書く
  • 敵・プレイヤー・ギミックごとにランブル処理をコピペしがち
  • ゲームパッドがない環境では何もしないように気をつける必要がある

…といった「小さいけどダルい処理」が散らばってしまいがちです。
しかも、Godotは「深いノード階層+継承」で書き始めると、いつの間にか Player.gd の中に「移動・アニメーション・ランブル・エフェクト…」と何でも入りの巨大スクリプトが出来上がってしまいます。

そこで今回は、「継承より合成」の考え方で、どのシーンにも後付けでポン付けできる「RumbleManager」コンポーネントを用意してみましょう。
ダメージ、着地、スキル発動など、イベント側は「RumbleManager に合図を送るだけ」にして、ランブルの強さや時間はコンポーネント側で一元管理するスタイルです。

【Godot 4】イベントを投げるだけでランブル!「RumbleManager」コンポーネント

以下が今回のフルコードです。
ゲームパッド1台(index 0)をターゲットに、複数のランブル要求をキューとして管理するシンプルな実装になっています。


extends Node
class_name RumbleManager
"""
RumbleManager
--------------
ダメージや着地など、ゲーム内イベントから呼び出して
ゲームパッドの振動(ランブル)を一元管理するコンポーネント。

・基本的な考え方
  - 各イベント(ダメージ・着地など)は RumbleManager のメソッドを呼ぶだけ
  - 実際の Input.start_joy_vibration は RumbleManager に集約
  - 「継承」ではなく「コンポーネントとしてアタッチ」して使う

・制約 / 注意点
  - Godot 4 のゲームパッド振動APIを使用
  - 現在は 1つの joypad_index(デフォルト: 0)のみを対象
"""

@export_category("Target Gamepad")
@export var joypad_index: int = 0:
	set(value):
		joypad_index = max(value, 0)

@export_category("Default Patterns")
@export_range(0.0, 1.0, 0.01)
var default_damage_strength: float = 0.6

@export_range(0.0, 1.0, 0.01)
var default_damage_weak_strength: float = 0.3

@export_range(0.0, 5.0, 0.01, "or_greater")
var default_damage_duration: float = 0.25

@export_range(0.0, 1.0, 0.01)
var default_land_strength: float = 0.8

@export_range(0.0, 5.0, 0.01, "or_greater")
var default_land_duration: float = 0.2

@export_category("Global Settings")
@export var enable_rumble: bool = true

@export_range(0.0, 2.0, 0.01, "or_greater")
var global_strength_scale: float = 1.0

@export_range(0.0, 2.0, 0.01, "or_greater")
var global_duration_scale: float = 1.0

# 内部キュー: 複数のランブル要求を順番に処理する
var _queue: Array[RumbleRequest] = []
var _current_request: RumbleRequest = null
var _current_time: float = 0.0


# ランブル要求を表す小さなデータクラス
class RumbleRequest:
	var strong: float
	var weak: float
	var duration: float

	func _init(strong: float, weak: float, duration: float) -> void:
		self.strong = strong
		self.weak = weak
		self.duration = duration


func _ready() -> void:
	# シーンから削除されたときに振動を止めるために
	# tree_exiting シグナルを接続しておく
	connect("tree_exiting", Callable(self, "_on_tree_exiting"))


func _process(delta: float) -> void:
	if not enable_rumble:
		# 無効化されている場合は常に振動停止
		_stop_vibration()
		_queue.clear()
		_current_request = null
		_current_time = 0.0
		return

	if _current_request == null:
		# 現在処理中のリクエストがなければ、キューから取り出す
		if _queue.is_empty():
			return
		_current_request = _queue.pop_front()
		_current_time = 0.0
		_start_vibration(_current_request)
	else:
		# 現在のリクエストの経過時間を更新
		_current_time += delta
		var scaled_duration := _current_request.duration * global_duration_scale
		if _current_time >= scaled_duration:
			# 終了時間を過ぎたら振動停止して次へ
			_stop_vibration()
			_current_request = null


# --- 公開API -------------------------------------------------------------

## 任意の強さ・時間でランブルさせる汎用メソッド
func rumble_custom(strong: float, weak: float, duration_sec: float) -> void:
	"""
	任意の強さと時間でランブルさせたいときに使う汎用メソッド。
	強さは 0.0 ~ 1.0 の範囲で指定。
	"""
	if not enable_rumble:
		return

	# スケールを適用
	var s := clamp(strong * global_strength_scale, 0.0, 1.0)
	var w := clamp(weak * global_strength_scale, 0.0, 1.0)
	var d := max(duration_sec, 0.0)

	var req := RumbleRequest.new(s, w, d)
	_queue.push_back(req)


## ダメージ時用のショートカット
func rumble_damage() -> void:
	"""
	ダメージを受けたときに呼ぶことを想定したショートカット。
	デフォルト設定はインスペクタから調整可能。
	"""
	rumble_custom(default_damage_strength, default_damage_weak_strength, default_damage_duration)


## 着地時用のショートカット
func rumble_land() -> void:
	"""
	高いところから落下して着地したときなどに呼ぶショートカット。
	"""
	rumble_custom(default_land_strength, default_land_strength * 0.5, default_land_duration)


## 即座にすべてのランブルを停止する
func stop_all() -> void:
	"""
	シーン遷移やポーズなどで、すぐに振動を止めたいときに呼ぶ。
	"""
	_stop_vibration()
	_queue.clear()
	_current_request = null
	_current_time = 0.0


# --- 内部処理 -----------------------------------------------------------

func _start_vibration(request: RumbleRequest) -> void:
	if not enable_rumble:
		return
	if not Input.is_joy_known(joypad_index):
		# ゲームパッドが接続されていないなら何もしない
		return

	var strong := clamp(request.strong * global_strength_scale, 0.0, 1.0)
	var weak := clamp(request.weak * global_strength_scale, 0.0, 1.0)
	var duration := max(request.duration * global_duration_scale, 0.0)

	# Godot 4: Input.start_joy_vibration(joypad, weak_magnitude, strong_magnitude, duration_sec)
	# ※エンジンバージョンによって引数順が異なる場合は公式ドキュメントを確認してください。
	Input.start_joy_vibration(joypad_index, weak, strong, duration)


func _stop_vibration() -> void:
	if not Input.is_joy_known(joypad_index):
		return
	Input.stop_joy_vibration(joypad_index)


func _on_tree_exiting() -> void:
	# シーンから抜けるときに振動が残らないようにする
	_stop_vibration()

使い方の手順

このコンポーネントは「どこか1か所に置いて、ゲーム全体のランブルを担当させる」か、
「プレイヤーごとに1つずつ持たせる」か、どちらでもOKです。ここでは典型的なプレイヤー例で説明します。

手順①: シーンに RumbleManager を追加する

プレイヤーシーンにコンポーネントとしてアタッチします。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── RumbleManager (Node)
  1. Player シーンを開く
  2. Player の子として Node を追加し、スクリプトに上記 RumbleManager.gd をアタッチ
  3. スクリプト上部の class_name RumbleManager が効いていれば、ノード追加時に「RumbleManager」という独自ノードとして選べます

インスペクタで以下を設定しておきましょう。

  • joypad_index: 0(1Pコントローラー)
  • enable_rumble: true
  • default_damage_strength / duration: お好みで調整
  • default_land_strength / duration: 着地の「ドンッ」とした感じに合わせて調整

手順②: プレイヤースクリプトから呼び出す(ダメージ時)

Player.gd の中で、ダメージを受けたタイミングで RumbleManager に通知します。


extends CharacterBody2D

@onready var rumble: RumbleManager = $RumbleManager

var hp: int = 100

func take_damage(amount: int) -> void:
	hp -= amount
	if hp <= 0:
		die()
	else:
		# ダメージ時のランブル
		if rumble:
			rumble.rumble_damage()
		# ここに点滅・ノックバックなども書く

これで Player 側は「ダメージ時に rumble_damage() を呼ぶ」ことだけに集中できます。
ランブルの強さや時間は、RumbleManager コンポーネント側で一元管理されます。

手順③: 着地時にランブルさせる

落下速度が一定以上のときだけ、着地ランブルを入れる例です。


var was_on_floor: bool = false

func _physics_process(delta: float) -> void:
	var is_on_floor_now := is_on_floor()

	# 着地判定: 前フレームは空中、今フレームは床の上
	if not was_on_floor and is_on_floor_now:
		# 一定以上の落下速度なら着地ランブル
		if velocity.y < -300.0:
			if rumble:
				rumble.rumble_land()

	was_on_floor = is_on_floor_now

「落下速度のしきい値」や「ランブルの強さ」は、コードではなく RumbleManager のパラメータで調整できるので、
レベルデザインの調整がかなり楽になります。

手順④: 敵やギミックにも使い回す

同じコンポーネントを敵やギミックにも使い回せます。たとえば、巨大な動く床が衝突したときにランブルさせる例:

MovingPlatform (StaticBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── RumbleManager (Node)

extends StaticBody2D

@onready var rumble: RumbleManager = $RumbleManager

func on_heavy_hit() -> void:
	# デフォルトでは物足りないなら、カスタム値で上書き
	if rumble:
		rumble.rumble_custom(0.9, 0.7, 0.35)

プレイヤーに限らず、「何かのイベントが起きたらランブルしたい」ノードに RumbleManager をアタッチして、
イベント発生時にメソッドを1行呼ぶだけ、というパターンを徹底できます。

メリットと応用

この RumbleManager コンポーネントを使うことで、次のようなメリットがあります。

  • 巨大な Player.gd からランブル処理を追い出せる
    振動APIの呼び出しや、ゲームパッド接続チェックなどの雑務をコンポーネント側にまとめられます。
  • シーン構造がスッキリする
    「ランブルは RumbleManager が全部見る」というルールにすると、イベント側は rumble_damage() などの呼び出しだけで済みます。
  • パラメータ調整がしやすい
    インスペクタから強さ・時間・グローバルスケールをいじれるので、「このステージだけちょっと強め」なども簡単です。
  • ゲームパッド非接続時のハンドリングを1か所に集約
    各所で Input.is_joy_known() を書かずに済み、テストもしやすくなります。

「継承より合成」の観点でいうと、プレイヤーは「動く人」であり、ランブルは「演出用の付属品」です。
演出はコンポーネントに切り出して、必要なノードにだけアタッチしていく方が、長期的に見て保守しやすい構造になりますね。

最後に、ちょっとした「改造案」として、クリティカルヒット専用のランブルを足す例を載せておきます。
RumbleManager に以下のメソッドを追加してみましょう。


func rumble_critical_hit() -> void:
	# 通常より強く・長く振動させる例
	# 強いモーターを最大、弱いモーターもそこそこ動かす
	var strong := clamp(default_damage_strength * 1.5, 0.0, 1.0)
	var weak := clamp(default_damage_weak_strength * 1.2, 0.0, 1.0)
	var duration := default_damage_duration * 1.8

	rumble_custom(strong, weak, duration)

あとは、攻撃処理側で「クリティカルが出たら rumble_critical_hit() を呼ぶ」だけです。
このように、ゲームごとのこだわりパターンをどんどんメソッドとして足していけるのも、コンポーネント化の良いところですね。