Unityを触り始めた頃は、つい Update() に「移動」「攻撃」「エフェクト」「UI更新」「振動処理」などを全部詰め込んでしまいがちですよね。
とりあえず動くので満足してしまうのですが、時間が経つと「どこで何をしているのか分からない巨大スクリプト(Godクラス)」になり、バグ修正や機能追加のたびに地獄を見ることになります。
特にゲームパッドの振動(Rumble)は、Update() に直接 Gamepad.current.SetMotorSpeeds() を書き散らかすと、
- ダメージ時の振動
- 着地時の振動
- 必殺技発動時の振動
などが全部バラバラの場所に書かれてしまい、「どこかで振動が止まらない」「別の処理が振動を上書きしてしまう」といった問題が起きやすくなります。
そこで今回は、「振動だけ」を責務に持つ小さなコンポーネントとして、RumbleManager を用意して、
「ダメージを受けた」「高いところから着地した」といったイベント側からは、
RumbleManager に「振動して!」と依頼するだけの形に分離してみましょう。
【Unity】イベント駆動で気持ちよく振動!「RumbleManager」コンポーネント
以下は Unity6(新Input System前提)で、ダメージ・着地などから簡単に呼び出せる汎用的な振動制御コンポーネントです。
1つのゲームオブジェクトにアタッチしておき、他のスクリプトから PlayRumble() / PlayDamageRumble() などを呼び出して使います。
フルコード
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem; // 新Input System用
/// <summary>
/// ゲームパッド振動を一元管理するコンポーネント。
/// - 複数の「振動リクエスト」をキューとして管理
/// - 時間経過で自動停止
/// - ダメージ/着地など、用途別のプリセットAPIを用意
///
/// 想定環境:
/// - Unity 6
/// - Input System パッケージ使用
/// - 使用前に「Edit > Project Settings > Input System Package」設定を済ませておくこと
/// </summary>
public class RumbleManager : MonoBehaviour
{
// ==============================
// 設定項目(インスペクターで調整)
// ==============================
[Header("デフォルト振動パラメータ")]
[Tooltip("特に指定がないときに使う振動の長さ(秒)")]
[SerializeField] private float defaultDuration = 0.3f;
[Tooltip("特に指定がないときの低周波モーター強度(0〜1)")]
[SerializeField, Range(0f, 1f)] private float defaultLowFrequency = 0.5f;
[Tooltip("特に指定がないときの高周波モーター強度(0〜1)")]
[SerializeField, Range(0f, 1f)] private float defaultHighFrequency = 0.5f;
[Header("プリセット:ダメージ時")]
[SerializeField] private float damageDuration = 0.25f;
[SerializeField, Range(0f, 1f)] private float damageLowFrequency = 0.7f;
[SerializeField, Range(0f, 1f)] private float damageHighFrequency = 1.0f;
[Header("プリセット:着地時")]
[SerializeField] private float landingDuration = 0.18f;
[SerializeField, Range(0f, 1f)] private float landingLowFrequency = 0.9f;
[SerializeField, Range(0f, 1f)] private float landingHighFrequency = 0.3f;
[Header("その他設定")]
[Tooltip("シーン内に複数存在する場合、どれか1つをグローバルに参照できるようにするか")]
[SerializeField] private bool useAsSingleton = true;
[Tooltip("ゲームパッドが見つからないときに警告ログを出すか")]
[SerializeField] private bool logWarningIfNoGamepad = true;
// ==============================
// 内部状態
// ==============================
/// <summary>
/// 1つの振動リクエストを表す構造体
/// </summary>
private struct RumbleRequest
{
public float remainingTime; // 残り時間(秒)
public float lowFrequency; // 低周波モーター強度
public float highFrequency; // 高周波モーター強度;
public float priority; // 優先度(大きいほど優先)
}
// 現在アクティブな振動リクエストたち
private readonly List<RumbleRequest> _requests = new List<RumbleRequest>();
// シングルトン的に参照したいとき用
public static RumbleManager Instance { get; private set; }
// 直近のフレームで適用したモーター値(無駄なSetMotorSpeeds呼び出しを避ける)
private float _appliedLow;
private float _appliedHigh;
// キャッシュ用:現在のGamepad
private Gamepad CurrentGamepad => Gamepad.current;
// ==============================
// ライフサイクル
// ==============================
private void Awake()
{
if (useAsSingleton)
{
if (Instance != null && Instance != this)
{
// 複数存在した場合は警告を出して、後から生成された方を破棄
Debug.LogWarning(
$"[RumbleManager] すでにInstanceが存在します。重複したRumbleManagerを削除します。({gameObject.name})");
Destroy(this);
return;
}
Instance = this;
}
}
private void OnDisable()
{
// コンポーネントが無効化されたら、振動も止めておく
StopAllRumble();
}
private void OnDestroy()
{
if (Instance == this)
{
Instance = null;
}
// オブジェクト破棄時にも念のため停止
StopAllRumble();
}
private void Update()
{
// ゲームパッドが接続されていなければ、なにもしない
var gamepad = CurrentGamepad;
if (gamepad == null)
{
if (logWarningIfNoGamepad && _requests.Count > 0)
{
Debug.LogWarning("[RumbleManager] Gamepad が見つかりません。振動リクエストは無視されます。");
}
// リクエストだけは時間経過で削除しておく
UpdateRequestsWithoutOutput();
return;
}
// リクエストを時間経過で更新しつつ、最終的な出力値を決定
UpdateRequestsAndApplyTo(gamepad);
}
// ==============================
// パブリックAPI:汎用
// ==============================
/// <summary>
/// 任意のパラメータで振動させる基本API。
/// duration 秒間、指定した強度で振動させます。
/// </summary>
/// <param name="duration">振動時間(秒)</param>
/// <param name="lowFrequency">低周波モーター強度 0〜1</param>
/// <param name="highFrequency">高周波モーター強度 0〜1</param>
/// <param name="priority">
/// 優先度。値が大きいほど優先されます。
/// 例: 通常 0, ダメージ 10, 必殺技 100 など。
/// </param>
public void PlayRumble(
float duration,
float lowFrequency,
float highFrequency,
float priority = 0f)
{
if (duration <= 0f)
{
return;
}
var clampedLow = Mathf.Clamp01(lowFrequency);
var clampedHigh = Mathf.Clamp01(highFrequency);
var request = new RumbleRequest
{
remainingTime = duration,
lowFrequency = clampedLow,
highFrequency = clampedHigh,
priority = priority
};
_requests.Add(request);
}
/// <summary>
/// デフォルト設定で振動させる簡易API。
/// </summary>
public void PlayDefaultRumble(float priority = 0f)
{
PlayRumble(defaultDuration, defaultLowFrequency, defaultHighFrequency, priority);
}
// ==============================
// パブリックAPI:用途別プリセット
// ==============================
/// <summary>
/// ダメージを受けたときに呼ぶ想定の振動。
/// 強め・短めのビリッとした振動。
/// </summary>
public void PlayDamageRumble()
{
// ダメージはやや高めの優先度にしておく
const float damagePriority = 10f;
PlayRumble(damageDuration, damageLowFrequency, damageHighFrequency, damagePriority);
}
/// <summary>
/// 高いところから着地したときなどに呼ぶ想定の振動。
/// 低周波寄りで「ドスッ」とくる感じ。
/// </summary>
public void PlayLandingRumble()
{
const float landingPriority = 5f;
PlayRumble(landingDuration, landingLowFrequency, landingHighFrequency, landingPriority);
}
// ==============================
// パブリックAPI:停止系
// ==============================
/// <summary>
/// すべての振動リクエストを破棄し、即座に振動を止める。
/// </summary>
public void StopAllRumble()
{
_requests.Clear();
ApplyMotorSpeeds(0f, 0f);
}
// ==============================
// 内部処理:Update用
// ==============================
/// <summary>
/// Gamepadが存在しない場合用。リクエストの時間だけ減らして削除する。
/// </summary>
private void UpdateRequestsWithoutOutput()
{
if (_requests.Count == 0)
{
return;
}
float deltaTime = Time.unscaledDeltaTime;
for (int i = _requests.Count - 1; i >= 0; i--)
{
var r = _requests[i];
r.remainingTime -= deltaTime;
if (r.remainingTime <= 0f)
{
_requests.RemoveAt(i);
}
else
{
_requests[i] = r;
}
}
// 出力は常に0
_appliedLow = 0f;
_appliedHigh = 0f;
}
/// <summary>
/// リクエストを更新し、最終的な振動値をGamepadに適用する。
/// </summary>
private void UpdateRequestsAndApplyTo(Gamepad gamepad)
{
if (_requests.Count == 0)
{
// リクエストがなければ振動を止める
ApplyMotorSpeeds(0f, 0f);
return;
}
float deltaTime = Time.unscaledDeltaTime;
// 1. 時間経過でリクエストを減衰・削除
for (int i = _requests.Count - 1; i >= 0; i--)
{
var r = _requests[i];
r.remainingTime -= deltaTime;
if (r.remainingTime <= 0f)
{
_requests.RemoveAt(i);
}
else
{
_requests[i] = r;
}
}
if (_requests.Count == 0)
{
ApplyMotorSpeeds(0f, 0f);
return;
}
// 2. 優先度の高いリクエストほど強く影響するように合成
float finalLow = 0f;
float finalHigh = 0f;
float maxPriority = 0f;
foreach (var r in _requests)
{
// 優先度に応じて重み付け
float weight = Mathf.Max(0f, r.priority);
if (weight == 0f)
{
// 優先度0は「通常扱い」としてそのまま加算
finalLow = Mathf.Max(finalLow, r.lowFrequency);
finalHigh = Mathf.Max(finalHigh, r.highFrequency);
continue;
}
// 優先度が高いほど上書きしやすくする
if (r.priority >= maxPriority)
{
maxPriority = r.priority;
finalLow = Mathf.Max(finalLow, r.lowFrequency);
finalHigh = Mathf.Max(finalHigh, r.highFrequency);
}
}
finalLow = Mathf.Clamp01(finalLow);
finalHigh = Mathf.Clamp01(finalHigh);
ApplyMotorSpeeds(finalLow, finalHigh);
}
/// <summary>
/// 実際にGamepadにモーター値を適用する。
/// 同じ値で何度も呼ばないように最適化。
/// </summary>
private void ApplyMotorSpeeds(float low, float high)
{
// すでに同じ値が適用されているなら何もしない
if (Mathf.Approximately(_appliedLow, low) && Mathf.Approximately(_appliedHigh, high))
{
return;
}
_appliedLow = low;
_appliedHigh = high;
var gamepad = CurrentGamepad;
if (gamepad == null)
{
return;
}
gamepad.SetMotorSpeeds(low, high);
}
}
使い方の手順
-
シーンに RumbleManager を配置する
- 空の GameObject を作成して名前を
RumbleManagerにする。 - 上のスクリプトを
RumbleManager.csとして保存し、その GameObject にアタッチ。 - インスペクターで「ダメージ」「着地」のプリセット値をお好みで調整。
- 「Use As Singleton」にチェックを入れておくと、どこからでも
RumbleManager.Instanceで呼び出せます。
- 空の GameObject を作成して名前を
-
プレイヤーのダメージ処理から呼び出す
例として、プレイヤーが敵に当たったときに振動させるコードはこんな感じです。using UnityEngine; public class PlayerHealth : MonoBehaviour { [SerializeField] private int maxHp = 100; private int _currentHp; private void Awake() { _currentHp = maxHp; } public void TakeDamage(int amount) { _currentHp -= amount; if (_currentHp <= 0) { _currentHp = 0; // TODO: 死亡処理 } // ダメージを受けたので振動させる if (RumbleManager.Instance != null) { RumbleManager.Instance.PlayDamageRumble(); } } }振動の具体的なパラメータは
RumbleManager側に閉じ込めているので、
ダメージ処理は「ダメージを受けた」ことだけに集中できます。 -
着地判定から呼び出す(2D/3Dどちらでも応用可)
高いところから落ちたときにだけ「ドスッ」と振動させる例です。using UnityEngine; [RequireComponent(typeof(Rigidbody))] public class LandingDetector : MonoBehaviour { [SerializeField] private float minImpactVelocity = 5f; private Rigidbody _rb; private bool _wasGrounded; private void Awake() { _rb = GetComponent<Rigidbody>(); } private void OnCollisionEnter(Collision collision) { // 下方向にある程度の速度でぶつかったら着地とみなす if (Vector3.Dot(_rb.velocity.normalized, Vector3.down) < 0.5f) { return; } if (_rb.velocity.magnitude >= minImpactVelocity) { if (RumbleManager.Instance != null) { RumbleManager.Instance.PlayLandingRumble(); } } } }こうしておくと、「着地のロジック」と「振動のロジック」がきれいに分離されて保守しやすくなります。
-
任意のイベントからカスタム振動を鳴らす
必殺技ボタンを押したときに、長め・強めの振動を鳴らしたい場合は、
どこからでも以下のように書けます。if (RumbleManager.Instance != null) { // duration: 0.8秒, low: 0.8, high: 0.8, priority: 100(かなり高い) RumbleManager.Instance.PlayRumble(0.8f, 0.8f, 0.8f, 100f); }プレイヤー、敵、ギミック(動く床やトラップ)など、
「ここで振動させたい」と思った場所から同じAPIを呼ぶだけで統一した挙動になります。
メリットと応用
RumbleManager のように振動を専用コンポーネントに切り出しておくと、次のようなメリットがあります。
- 巨大な Update 地獄からの脱出
プレイヤー制御スクリプトの中にGamepad.current.SetMotorSpeeds()を直接書かないので、
移動や攻撃ロジックと振動ロジックが混ざらず、読みやすくなります。 - プレハブの再利用性が上がる
プレイヤー、敵、ギミックなどのプレハブは「自分のイベントが起きたら RumbleManager に通知する」だけにしておけば、
振動の強さやパターンを変えたくなったときはRumbleManagerのインスペクターをいじるだけで済みます。
いちいち全プレハブのスクリプトを書き換える必要がありません。 - レベルデザインが楽になる
「このトラップはもう少し弱い振動にしたい」「ボス戦の必殺技だけ強烈にしたい」といった調整も、
レベルデザイナーがインスペクターの数値を触るだけで完結します。
コードに手を入れなくてもフィードバック調整ができるのは大きな利点ですね。 - 優先度制御で「気持ちよさ」を崩さない
強い演出(必殺技など)は高い priority を設定しておくことで、
その最中に小さなダメージを受けても、ビリビリ演出が中途半端に上書きされないようにできます。
さらに、ちょっとした改造で「フェードアウト」や「パルス振動」などにも対応できます。
例えば、現在の振動を一瞬だけ強くしてからスッと落とす「ショック」演出を追加したい場合、
以下のようなヘルパーメソッドを RumbleManager に足すのもありです。
/// <summary>
/// 現在の振動に「ショック」を重ねる例。
/// 強い振動を短時間鳴らし、その後少し弱くして余韻を残す。
/// </summary>
public void PlayShockWave(float baseDuration = 0.15f)
{
// 一瞬だけ強く
PlayRumble(baseDuration * 0.4f, 1.0f, 1.0f, priority: 50f);
// そのあと少し弱く余韻
PlayRumble(baseDuration * 0.6f, 0.4f, 0.4f, priority: 20f);
}
このように、振動の知識は RumbleManager に集約しておき、
各コンポーネントは「どのタイミングでどのプリセットを鳴らすか」だけを考えるようにすると、
プロジェクト全体がすっきりしたコンポーネント指向の構成になります。
