Unityを触り始めたころは、つい何でもかんでも Update() に書いてしまいがちですよね。プレイヤーの入力、移動、UI更新、エフェクト、カメラ制御……すべて一つのスクリプトに押し込むと、最初は動いていても、あとから「ちょっと画面を揺らしたい」「演出を足したい」と思ったときに地獄が始まります。
とくに「画面揺れ(Screen Shake)」は、ダメージや爆発など、さまざまな場所から呼び出したくなる代表的な演出です。これをプレイヤーのスクリプトや敵のスクリプトに直書きしてしまうと、あちこちに似たようなコードが散らばり、調整も再利用も大変になります。
そこでこの記事では、カメラの揺れだけを担当する小さなコンポーネントとして、ScreenShake を用意しておき、TakeDamage や Explode などの処理からは「揺れて!」と呼び出すだけにする構成を紹介します。これで、演出の強さや揺れ方を一箇所で管理でき、プレハブの再利用やレベルデザインもぐっと楽になります。
【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);
}
}
}
使い方の手順
-
コンポーネントを用意する
上のScreenShake.csをプロジェクトのScriptsフォルダなどに保存します。 -
カメラにアタッチする
シーン内のカメラ(例:Main Camera)を選択し、ScreenShakeコンポーネントを追加します。Target Transformは未設定なら自動でカメラ自身が使われます。- 2Dゲームなら
Axis Scale Zを 0 に、3Dなら必要に応じて Z も揺らしましょう。 - 揺れの好みは
Default Intensity,Default Duration,Noise Frequency,Attenuation Curveで調整できます。
-
ダメージや爆発から呼び出す
例として、プレイヤーがダメージを受けたときに画面を揺らすコードはこんな感じです。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()を呼び出すだけです。 -
レベル全体で再利用する
- カメラ+
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);
}
}
このように、小さなコンポーネント+呼び出し側のシンプルな関数という構成にしておくと、演出をどんどん足してもコードが破綻しにくくなります。ぜひ自分のプロジェクト用にパラメータやカーブを調整して、気持ちいい画面揺れを育ててみてください。
