Unityを触り始めたころは、つい何でもかんでも Update() に書いてしまいがちですよね。プレイヤーの入力、移動、UI更新、エフェクト、カメラ制御……すべて一つのスクリプトに押し込むと、最初は動いていても、あとから「ちょっと画面を揺らしたい」「演出を足したい」と思ったときに地獄が始まります。

とくに「画面揺れ(Screen Shake)」は、ダメージや爆発など、さまざまな場所から呼び出したくなる代表的な演出です。これをプレイヤーのスクリプトや敵のスクリプトに直書きしてしまうと、あちこちに似たようなコードが散らばり、調整も再利用も大変になります。

そこでこの記事では、カメラの揺れだけを担当する小さなコンポーネントとして、ScreenShake を用意しておき、TakeDamageExplode などの処理からは「揺れて!」と呼び出すだけにする構成を紹介します。これで、演出の強さや揺れ方を一箇所で管理でき、プレハブの再利用やレベルデザインもぐっと楽になります。

【Unity】ノイズで気持ちいい画面揺れ!「ScreenShake」コンポーネント

ここでは、カメラの位置にノイズベースのオフセットを加えて揺らす ScreenShake コンポーネントを作ります。

  • ダメージ・爆発などから強さと時間を指定して揺らせる
  • 揺れの減衰カーブ(時間経過でだんだん弱くなる)を設定可能
  • ノイズの周波数(どれくらい細かく揺れるか)を調整可能
  • カメラの元の位置を自動で復元するので、他の移動処理と共存しやすい

フルコード:ScreenShake.cs


using UnityEngine;

/// <summary>カメラの画面揺れを制御するコンポーネント</summary>
[DisallowMultipleComponent]
public class ScreenShake : MonoBehaviour
{
    // --- 基本設定 ---

    [Header("ターゲット設定")]
    [Tooltip("揺らしたいTransform。未設定ならこのコンポーネントが付いているTransformを使用します。")]
    [SerializeField] private Transform targetTransform;

    [Header("揺れの基本パラメータ")]
    [Tooltip("揺れのデフォルト強さ(半径)。PlayShake() の intensity が 0 のときに使われます。")]
    [SerializeField] private float defaultIntensity = 0.5f;

    [Tooltip("揺れのデフォルト継続時間(秒)。PlayShake() の duration が 0 のときに使われます。")]
    [SerializeField] private float defaultDuration = 0.3f;

    [Tooltip("ノイズの周波数(値が大きいほど細かく震える)")]
    [SerializeField] private float noiseFrequency = 20f;

    [Header("減衰カーブ")]
    [Tooltip("時間経過に対する揺れの減衰カーブ(横軸:0〜1の経過割合 / 縦軸:強さ倍率)")]
    [SerializeField] private AnimationCurve attenuationCurve =
        AnimationCurve.EaseInOut(0f, 1f, 1f, 0f);

    [Header("軸ごとの揺れ具合")]
    [Tooltip("X軸方向(横揺れ)のスケール")]
    [SerializeField] private float axisScaleX = 1f;

    [Tooltip("Y軸方向(縦揺れ)のスケール")]
    [SerializeField] private float axisScaleY = 1f;

    [Tooltip("Z軸方向(奥行き揺れ)のスケール(2Dなら 0 推奨)")]
    [SerializeField] private float axisScaleZ = 0f;

    // --- 内部状態 ---

    /// <summary>元のローカル位置(揺れのオフセットを足す基準)</summary>
    private Vector3 _originalLocalPosition;

    /// <summary>現在の揺れの残り時間(秒)</summary>
    private float _shakeTimeRemaining = 0f;

    /// <summary>現在の揺れの合計時間(減衰カーブ用の正規化に使う)</summary>
    private float _shakeTotalDuration = 0f;

    /// <summary>現在の揺れの強さ(半径)</summary>
    private float _currentIntensity = 0f;

    /// <summary>ノイズ用の時間カウンタ</summary>
    private float _noiseTime = 0f;

    /// <summary>現在適用しているオフセット(元の位置に足している分)</summary>
    private Vector3 _currentOffset = Vector3.zero;

    private void Awake()
    {
        // ターゲット未設定なら自分自身のTransformを使う
        if (targetTransform == null)
        {
            targetTransform = transform;
        }

        // 起動時のローカル位置を保存
        _originalLocalPosition = targetTransform.localPosition;
    }

    private void OnEnable()
    {
        // 有効化されたときに位置をリセット
        ResetPosition();
    }

    private void OnDisable()
    {
        // 無効化されたときにも位置をリセット
        ResetPosition();
    }

    private void Update()
    {
        // 揺れが残っていなければ、オフセットを徐々にゼロに戻す
        if (_shakeTimeRemaining <= 0f)
        {
            // オフセットをスムーズに0へ戻す(他の移動と干渉しにくくするため)
            _currentOffset = Vector3.Lerp(_currentOffset, Vector3.zero, Time.deltaTime * 10f);
            ApplyOffset();
            return;
        }

        // 経過時間を進める
        _shakeTimeRemaining -= Time.deltaTime;
        _noiseTime += Time.deltaTime * noiseFrequency;

        // 正規化された経過割合(0〜1)
        float normalizedTime = 1f - Mathf.Clamp01(_shakeTimeRemaining / _shakeTotalDuration);

        // 減衰カーブに基づく強さ倍率
        float attenuation = attenuationCurve.Evaluate(normalizedTime);

        // 実際の強さ
        float intensity = _currentIntensity * attenuation;

        // Perlinノイズを使って-1〜1の揺れ値を生成
        float offsetX = (Mathf.PerlinNoise(_noiseTime, 0.0f) * 2f - 1f) * axisScaleX;
        float offsetY = (Mathf.PerlinNoise(0.0f, _noiseTime) * 2f - 1f) * axisScaleY;
        float offsetZ = (Mathf.PerlinNoise(_noiseTime, _noiseTime) * 2f - 1f) * axisScaleZ;

        // 正規化されたノイズベクトルに強さを掛ける
        Vector3 noiseVector = new Vector3(offsetX, offsetY, offsetZ);
        _currentOffset = noiseVector * intensity;

        // 実際に位置へ反映
        ApplyOffset();
    }

    /// <summary>
    /// 画面揺れを開始する(外部から呼び出すメインAPI)。
    /// intensity または duration に 0 以下を渡すと、それぞれデフォルト値が使われます。
    /// </summary>
    /// <param name="intensity">揺れの強さ(半径)</param>
    /// <param name="duration">揺れの継続時間(秒)</param>
    public void PlayShake(float intensity = 0f, float duration = 0f)
    {
        // 0 以下ならデフォルト値を使用
        if (intensity <= 0f)
        {
            intensity = defaultIntensity;
        }
        if (duration <= 0f)
        {
            duration = defaultDuration;
        }

        // すでに揺れている場合、「より強い」「より長い」揺れを優先する
        _currentIntensity = Mathf.Max(_currentIntensity, intensity);
        _shakeTotalDuration = Mathf.Max(_shakeTotalDuration, duration);
        _shakeTimeRemaining = Mathf.Max(_shakeTimeRemaining, duration);

        // 新しい揺れを始めるたびにノイズ時間をランダム化してパターンを変える
        _noiseTime = Random.Range(0f, 1000f);
    }

    /// <summary>
    /// 現在の揺れを強制的に停止し、位置を元に戻す。
    /// </summary>
    public void StopShake()
    {
        _shakeTimeRemaining = 0f;
        _shakeTotalDuration = 0f;
        _currentIntensity = 0f;
        _currentOffset = Vector3.zero;
        ApplyOffset();
    }

    /// <summary>
    /// 元のローカル位置に戻す(Awake/OnEnable/OnDisable から呼ばれる)</summary>
    private void ResetPosition()
    {
        // オフセットをクリアし、元のローカル位置に戻す
        _currentOffset = Vector3.zero;
        if (targetTransform != null)
        {
            targetTransform.localPosition = _originalLocalPosition;
        }
    }

    /// <summary>
    /// 現在のオフセットを targetTransform に適用する。
    /// </summary>
    private void ApplyOffset()
    {
        if (targetTransform == null) return;

        // 元のローカル位置 + オフセット で位置を決定
        targetTransform.localPosition = _originalLocalPosition + _currentOffset;
    }

    // --- デバッグ用:エディタ上でテストしやすくするオプション ---

    [Header("デバッグ設定")]
    [Tooltip("Play中にこのキーを押すとテスト用の揺れを再生します。")]
    [SerializeField] private KeyCode debugShakeKey = KeyCode.None;

    [Tooltip("デバッグ揺れの強さ")]
    [SerializeField] private float debugIntensity = 0.7f;

    [Tooltip("デバッグ揺れの長さ")]
    [SerializeField] private float debugDuration = 0.4f;

    private void LateUpdate()
    {
        // Update で揺れを計算し、LateUpdate ではデバッグ入力だけを見る例
        // (カメラ制御が LateUpdate で動くプロジェクトでも扱いやすくするため)
        if (debugShakeKey != KeyCode.None && Input.GetKeyDown(debugShakeKey))
        {
            PlayShake(debugIntensity, debugDuration);
        }
    }
}

使い方の手順

  1. コンポーネントを用意する
    上の ScreenShake.cs をプロジェクトの Scripts フォルダなどに保存します。
  2. カメラにアタッチする
    シーン内のカメラ(例:Main Camera)を選択し、ScreenShake コンポーネントを追加します。
    • Target Transform は未設定なら自動でカメラ自身が使われます。
    • 2Dゲームなら Axis Scale Z を 0 に、3Dなら必要に応じて Z も揺らしましょう。
    • 揺れの好みは Default Intensity, Default Duration, Noise Frequency, Attenuation Curve で調整できます。
  3. ダメージや爆発から呼び出す
    例として、プレイヤーがダメージを受けたときに画面を揺らすコードはこんな感じです。
    
    using UnityEngine;
    
    public class PlayerHealth : MonoBehaviour
    {
        [SerializeField] private int maxHp = 100;
        [SerializeField] private ScreenShake screenShake; // インスペクタで割り当て
    
        private int _currentHp;
    
        private void Awake()
        {
            _currentHp = maxHp;
        }
    
        public void TakeDamage(int amount)
        {
            _currentHp -= amount;
            _currentHp = Mathf.Max(_currentHp, 0);
    
            // ダメージ量に応じて揺れの強さを変える例
            float normalized = Mathf.Clamp01((float)amount / maxHp);
            float intensity = Mathf.Lerp(0.2f, 1.0f, normalized);
            float duration = 0.2f + 0.3f * normalized;
    
            if (screenShake != null)
            {
                screenShake.PlayShake(intensity, duration);
            }
    
            if (_currentHp <= 0)
            {
                // 死亡処理など
            }
        }
    }
    

    敵の爆発時に揺らしたい場合も同様に、ScreenShake への参照を持っておき、PlayShake() を呼び出すだけです。

  4. レベル全体で再利用する
    • カメラ+ScreenShake をプレハブ化しておけば、どのシーンでも同じ揺れ演出をすぐ使えます。
    • シーンごとに揺れ方を変えたい場合は、プレハブを複製してパラメータだけ変えたバリエーションを作ると便利です(例:ホラー用のゆっくり大きな揺れ、アクション用の短く鋭い揺れ)。

メリットと応用

ScreenShake を独立したコンポーネントとして切り出すことで、次のようなメリットがあります。

  • 責務が明確:カメラの揺れだけを担当するので、プレイヤーや敵のスクリプトが肥大化しません。
  • プレハブ管理が楽:カメラプレハブに ScreenShake を仕込んでおけば、どのシーンでも同じ挙動で使い回せます。
  • レベルデザインがしやすい:デザイナーやレベル担当が、シーン内のカメラを選んで Default Intensity などをいじるだけで演出を調整できます。コードを書き換える必要がありません。
  • 演出の一元管理:ダメージ・爆発・スキル発動など、さまざまなイベントから PlayShake() を呼ぶだけで、揺れ方の調整はすべて ScreenShake 内で完結します。

応用としては、

  • 特定のボス戦だけ揺れを強める・長くする
  • プレイヤーがダメージを受けたときは縦揺れ、爆発は横揺れを強める
  • スロー演出中は揺れを弱める(タイムスケールと連動させる)

といった演出バリエーションも簡単に実現できます。

最後に、「爆発の距離に応じて揺れの強さを変える」改造案の一例を載せておきます。ScreenShake に直接書くのではなく、爆発側のスクリプトに追加するイメージです。


private void ShakeByDistance(Transform cameraTransform, ScreenShake screenShake,
                             Vector3 explosionPosition, float maxRadius,
                             float maxIntensity, float maxDuration)
{
    // カメラと爆発地点の距離
    float distance = Vector3.Distance(cameraTransform.position, explosionPosition);

    // 距離を 0〜maxRadius に正規化し、0 に近いほど強く揺らす
    float t = Mathf.Clamp01(distance / maxRadius);
    float intensity = Mathf.Lerp(maxIntensity, 0f, t);
    float duration  = Mathf.Lerp(maxDuration, 0.1f, t);

    if (intensity > 0.01f && screenShake != null)
    {
        screenShake.PlayShake(intensity, duration);
    }
}

このように、小さなコンポーネント+呼び出し側のシンプルな関数という構成にしておくと、演出をどんどん足してもコードが破綻しにくくなります。ぜひ自分のプロジェクト用にパラメータやカーブを調整して、気持ちいい画面揺れを育ててみてください。