UnityのUI実装でありがちなのが、ダメージ処理・HP計算・アニメーション・SE再生などを、全部ひとつの Update 関数に書いてしまうパターンです。
最初は動いて見えるのですが、だんだん「どこを触ればいいのか分からない」「他の画面でも同じ揺れを使いたいのにコピペ地獄」という状態になりがちですね。
この記事では、「UIが揺れる」という機能だけに責務を絞ったコンポーネント ShakeUI を作っていきます。
HPバー、ダメージアイコン、警告マークなどにポン付けするだけで、きれいに「ガタガタ揺れる」演出を追加できるようにして、UIロジックと演出ロジックを分離していきましょう。
【Unity】UIを簡単にガタガタ揺らす!「ShakeUI」コンポーネント
以下が、UIを一定時間揺らすための ShakeUI コンポーネントの完全版コードです。
Unity6(Unity 6000系)+新しいInput Systemなどの環境でもそのまま使えます。
using UnityEngine;
using System.Collections;
/// <summary>
/// UI用のシンプルな「揺れ」コンポーネント。
/// RectTransform を揺らして、ダメージや警告などの演出に使う。
///
/// ・Shake() を呼ぶだけで揺れが開始
/// ・揺れ中に再度 Shake() を呼ぶと、設定に応じて上書き or 加算
/// ・UI以外(Transform)にも一応使える設計
/// </summary>
[DisallowMultipleComponent] // 同じオブジェクトに複数アタッチされるのを防ぐ
public class ShakeUI : MonoBehaviour
{
// --- 参照関連 ---
[Header("対象Transform(未指定なら自動取得)")]
[Tooltip("揺らしたい RectTransform。未指定の場合は、このGameObjectの RectTransform を探します。なければ Transform を使います。")]
[SerializeField] private RectTransform targetRectTransform;
[Tooltip("RectTransform が無いオブジェクト向け。未指定なら自身の Transform。")]
[SerializeField] private Transform targetTransform;
// --- 基本設定 ---
[Header("揺れの基本設定")]
[Tooltip("1回のShakeの長さ(秒)")]
[SerializeField] private float duration = 0.3f;
[Tooltip("揺れの強さ(位置のランダム振れ幅)")]
[SerializeField] private float positionStrength = 20f;
[Tooltip("揺れの強さ(回転のランダム振れ幅・度数)")]
[SerializeField] private float rotationStrength = 10f;
[Tooltip("時間経過による減衰カーブ(0~1の時間に対する減衰係数)")]
[SerializeField] private AnimationCurve attenuationCurve = AnimationCurve.EaseInOut(0, 1, 1, 0);
[Header("挙動オプション")]
[Tooltip("揺れ中に再度 Shake() が呼ばれた時、残り時間を上書きするかどうか")]
[SerializeField] private bool overrideWhileShaking = true;
[Tooltip("Time.timeScale の影響を受けるか。false にするとポーズ中でも揺れる")]
[SerializeField] private bool useUnscaledTime = true;
[Tooltip("Play On Awake: シーン開始時に自動で揺らすかどうか(デバッグ用)")]
[SerializeField] private bool playOnAwake = false;
// --- 内部状態 ---
// 元の位置・回転を保持しておく
private Vector3 _defaultLocalPosition;
private Quaternion _defaultLocalRotation;
private Coroutine _shakeCoroutine;
private bool _initialized;
private void Awake()
{
InitializeTarget();
// 元のTransform情報を記録
CacheDefaultTransform();
if (playOnAwake)
{
Shake();
}
}
private void OnEnable()
{
// Disable / Enable で元に戻らない事故を防ぐため、再度キャッシュ
CacheDefaultTransform();
}
private void OnDisable()
{
// 無効化されるときに、変な位置で止まらないように元に戻す
ResetTransform();
}
/// <summary>
/// 対象の RectTransform / Transform を初期化する
/// </summary>
private void InitializeTarget()
{
if (_initialized) return;
// RectTransform が指定されていなければ、自身から探す
if (targetRectTransform == null)
{
targetRectTransform = GetComponent<RectTransform>();
}
// Transform が指定されていなければ、自身の Transform を使う
if (targetTransform == null)
{
targetTransform = transform;
}
_initialized = true;
}
/// <summary>
/// 元のローカル座標・回転をキャッシュする
/// </summary>
private void CacheDefaultTransform()
{
InitializeTarget();
if (targetRectTransform != null)
{
_defaultLocalPosition = targetRectTransform.anchoredPosition3D;
_defaultLocalRotation = targetRectTransform.localRotation;
}
else
{
_defaultLocalPosition = targetTransform.localPosition;
_defaultLocalRotation = targetTransform.localRotation;
}
}
/// <summary>
/// 揺れを開始するための公開メソッド。
/// HPバーやアイコンのダメージイベントから呼び出します。
/// </summary>
public void Shake()
{
// duration が 0 以下なら何もしない
if (duration <= 0f)
{
return;
}
InitializeTarget();
CacheDefaultTransform();
// すでに揺れている場合の挙動
if (_shakeCoroutine != null)
{
if (overrideWhileShaking)
{
// 前の揺れをキャンセルして新しく開始
StopCoroutine(_shakeCoroutine);
ResetTransform();
}
else
{
// 加算揺れにしたい場合は、ここで return せずに
// 「残り時間を延長する」などのロジックを入れてもよい
// ひとまず何もしないで return
return;
}
}
_shakeCoroutine = StartCoroutine(ShakeRoutine());
}
/// <summary>
/// 外部から明示的に揺れを止めたいとき用。
/// (例えば、画面遷移時に強制リセットしたい場合など)
/// </summary>
public void StopShake()
{
if (_shakeCoroutine != null)
{
StopCoroutine(_shakeCoroutine);
_shakeCoroutine = null;
}
ResetTransform();
}
/// <summary>
/// 実際に揺れを制御するコルーチン本体
/// </summary>
private IEnumerator ShakeRoutine()
{
float elapsed = 0f;
while (elapsed < duration)
{
// 0~1 の正規化時間
float t = Mathf.Clamp01(elapsed / duration);
// 減衰係数(カーブから取得)
float attenuation = attenuationCurve.Evaluate(t);
// ランダムなオフセット(-1~1 の範囲)
float offsetX = (Random.value * 2f - 1f) * positionStrength * attenuation;
float offsetY = (Random.value * 2f - 1f) * positionStrength * attenuation;
// ランダムな回転(Z軸のみを回転させる 2D UI 想定)
float angleZ = (Random.value * 2f - 1f) * rotationStrength * attenuation;
ApplyShake(offsetX, offsetY, angleZ);
// 次のフレームまで待機
if (useUnscaledTime)
{
elapsed += Time.unscaledDeltaTime;
}
else
{
elapsed += Time.deltaTime;
}
yield return null;
}
// 終了時に元の位置・回転に戻す
ResetTransform();
_shakeCoroutine = null;
}
/// <summary>
/// 実際に Transform / RectTransform に揺れを適用する処理
/// </summary>
private void ApplyShake(float offsetX, float offsetY, float angleZ)
{
if (targetRectTransform != null)
{
// anchoredPosition3D をベースに XY を揺らす
Vector3 shakenPos = _defaultLocalPosition + new Vector3(offsetX, offsetY, 0f);
targetRectTransform.anchoredPosition3D = shakenPos;
// Z軸回転だけを変更
Quaternion shakenRot = Quaternion.Euler(0f, 0f, angleZ) * _defaultLocalRotation;
targetRectTransform.localRotation = shakenRot;
}
else
{
// 通常の Transform の場合
Vector3 shakenPos = _defaultLocalPosition + new Vector3(offsetX, offsetY, 0f);
targetTransform.localPosition = shakenPos;
Quaternion shakenRot = Quaternion.Euler(0f, 0f, angleZ) * _defaultLocalRotation;
targetTransform.localRotation = shakenRot;
}
}
/// <summary>
/// 位置と回転を「元の状態」にリセットする
/// </summary>
private void ResetTransform()
{
if (targetRectTransform != null)
{
targetRectTransform.anchoredPosition3D = _defaultLocalPosition;
targetRectTransform.localRotation = _defaultLocalRotation;
}
else
{
targetTransform.localPosition = _defaultLocalPosition;
targetTransform.localRotation = _defaultLocalRotation;
}
}
// --- デバッグ用:インスペクターからテストしやすくする ---
#if UNITY_EDITOR
[ContextMenu("Test Shake")]
private void ContextMenuTestShake()
{
Shake();
}
#endif
}
使い方の手順
ここでは、HPバー と ダメージアイコン を例に、ShakeUI の使い方を見ていきます。
-
① UIオブジェクトを用意する
- Canvas 配下に HPバー(Image)や キャラアイコン(Image)を作成します。
- 例えば
HPBarという名前の GameObject にImageコンポーネントが付いている状態を想定します。
-
② HPバーに「ShakeUI」コンポーネントを追加
- Unity の Hierarchy で
HPBarを選択。 - Inspector の「Add Component」から
ShakeUIを検索して追加します。 Target Rect Transformは空のままでOKです(自動で自分の RectTransform を使います)。DurationやPosition Strengthをお好みで調整しましょう。- 例: Duration = 0.25, Position Strength = 15, Rotation Strength = 8
- Unity の Hierarchy で
-
③ ダメージ処理から Shake() を呼ぶ
たとえば、プレイヤーの HP を管理しているコンポーネントがあるとします。
そこから「HPバーを揺らす」だけを分離して呼び出すと、責務がきれいに分かれます。using UnityEngine; /// <summary> /// 簡単なダメージテスト用コンポーネント。 /// スペースキーを押すとダメージを受けた & HPバーを揺らす。 /// (新InputSystemを使っていない、シンプルな例) /// </summary> public class PlayerDamageTester : MonoBehaviour { [SerializeField] private ShakeUI hpBarShake; // HPバーに付けた ShakeUI を参照 [SerializeField] private int maxHp = 100; [SerializeField] private int currentHp = 100; private void Update() { // デバッグ用:スペースキーでダメージ if (Input.GetKeyDown(KeyCode.Space)) { TakeDamage(10); } } private void TakeDamage(int amount) { currentHp = Mathf.Max(0, currentHp - amount); // HPが減ったタイミングで、HPバーを揺らす if (hpBarShake != null) { hpBarShake.Shake(); } // ここでは「HPの数値計算」と「UIの揺れ」が完全に分離されている点がポイント } }hpBarShakeには、HPバーにアタッチしたShakeUIをドラッグ&ドロップで設定します。- ダメージ処理はあくまで HP の計算だけ。揺れ演出は ShakeUI に丸投げ、という構成ですね。
-
④ 他のUIにも流用する(敵HPバー・警告アイコンなど)
- 敵キャラの頭上にある HPバー用の UI プレハブにも
ShakeUIをアタッチ。 - 敵のダメージ処理から
enemyHpBarShake.Shake()を呼ぶだけで、同じ揺れ演出を再利用できます。 - また、「画面中央の赤い警告アイコン」にも同じコンポーネントを付けて、
- HPが一定以下になったら
warningIconShake.Shake()を呼ぶ
といった使い方もできます。
- HPが一定以下になったら
- 敵キャラの頭上にある HPバー用の UI プレハブにも
メリットと応用
ShakeUI を導入することで、次のようなメリットがあります。
- 責務がはっきり分かれる
ダメージ処理は「数値の更新」に集中し、演出は ShakeUI に任せられます。
「HPが減ったのに揺れない」「揺れだけ変えたい」といった時に、どこを触ればいいかが明確になります。 - プレハブ化・レベルデザインが楽になる
HPバー付きの敵プレハブを作るとき、ShakeUI を組み込んだ UI プレハブをそのまま使い回せます。
レベルデザイナーは「ダメージを受けたら Shake() を呼ぶ」という約束事だけ守れば、複雑なアニメーションコードを触らずに演出を追加できます。 - 調整パラメータが1か所にまとまる
Duration / PositionStrength / RotationStrength / 減衰カーブなどを、コンポーネント単位でインスペクターから調整可能です。
「ボスのHPバーだけ揺れを強く」「UI全体の揺れ時間を短く」といった調整も、スクリプトを書き換えずに対応できます。 - Godクラス化を防げる
「UIManager」や「PlayerController」に演出コードを全部押し込むのではなく、
「揺れ」「点滅」「フェード」などをコンポーネントとして小さく分割していくことで、長期的にメンテしやすいプロジェクトになります。
応用として、ダメージ量に応じて揺れの強さを変えるような拡張も簡単です。
例えば、以下のような関数を ShakeUI に追加しておくと、外部から強さを指定して揺らせます。
/// <summary>
/// 強さを指定して揺らすバージョン。
/// 例:小ダメージ=0.5f、大ダメージ=1.5f など。
/// </summary>
public void ShakeWithIntensity(float intensity)
{
// 負の値は無効化
if (intensity <= 0f) return;
// 一時的に強さをスケールさせてから Shake を呼ぶ
float originalPosStrength = positionStrength;
float originalRotStrength = rotationStrength;
positionStrength *= intensity;
rotationStrength *= intensity;
Shake();
// 元の値に戻す
positionStrength = originalPosStrength;
rotationStrength = originalRotStrength;
}
このように、「揺れ」という機能をコンポーネントとして切り出しておくと、
・プレイヤーHPバー
・敵HPバー
・スキルアイコン
・警告UI
など、さまざまなUIで同じ仕組みを再利用しつつ、演出だけを柔軟に変えていけます。
Updateにすべてを書き殴るスタイルから卒業して、コンポーネント思考でUI演出を組み立てていきましょう。
