Unityを触り始めた頃は、つい「画面演出も敵の処理も全部ひとつの Update() に書く」みたいな実装をしがちですよね。
一度はそれでも動きますが、だんだんと
- 敵の登場処理に画面エフェクトがベタ書きされていて、他のシーンで使い回せない
- カメラ関連の処理とゲームロジックが混ざっていて、どこを直せばいいか分からない
- ちょっとした演出を追加するだけで巨大なスクリプトを開く羽目になる
といった「Godクラス問題」にぶつかります。
この記事では、電子系の敵が登場するときなどに使える「画面のブロックノイズ&色ズレ演出」を、ひとつの小さなコンポーネントに切り出して実装していきます。
シーン中のカメラにアタッチしておくだけで、他のスクリプトから Play() を呼び出せば使える、再利用性の高い仕組みにしていきましょう。
【Unity】電子ノイズで世界がバグる!「GlitchEffect」コンポーネント
ここでは、Unity6のURP(Universal Render Pipeline)を前提に、フルスクリーンのポストエフェクトとしてグリッチ(ノイズ)をかけるコンポーネントを作ります。
- ブロックノイズ(画面をランダムなブロックで歪ませる)
- 色ズレ(RGBチャンネルをずらしてサイバー感を出す)
- 短時間だけ再生して自動で元に戻る
といった基本機能を、ひとつの GlitchEffect コンポーネントにまとめます。
Shaderも含めてこの記事内に完結させるので、そのままコピペで動かせます。
フルコード:GlitchEffect.cs(コンポーネント本体)
まずは C# 側のコンポーネントです。
URPの「カスタムレンダーフィーチャー」を使わず、カメラの OnRenderImage を利用するシンプル構成にしています(ビルトイン互換の書き方ですが、URPでもカメラにアタッチすれば動きます)。
using UnityEngine;
/// <summary>
/// 画面全体にグリッチ(ノイズ&色ズレ)をかけるコンポーネント。
/// カメラにアタッチして使います。
/// 他のスクリプトから PlayGlitch() を呼ぶだけで一時的にノイズ演出を再生できます。
/// </summary>
[DisallowMultipleComponent]
[RequireComponent(typeof(Camera))]
public class GlitchEffect : MonoBehaviour
{
// シェーダーの参照(この記事内の GlitchEffect.shader を使います)
[SerializeField] private Shader glitchShader;
// 演出の基本長さ(秒)
[SerializeField] private float defaultDuration = 0.4f;
// ブロックノイズの強さ
[SerializeField, Range(0f, 1f)] private float noiseIntensity = 0.6f;
// 色ズレの強さ
[SerializeField, Range(0f, 1f)] private float colorShiftIntensity = 0.7f;
// 時間経過で強さを自動減衰させるか
[SerializeField] private bool autoFadeOut = true;
// フェードアウトカーブ(演出の減衰の仕方)
[SerializeField] private AnimationCurve fadeCurve =
AnimationCurve.EaseInOut(0f, 1f, 1f, 0f);
// 内部用マテリアル
private Material _material;
// 再生中フラグ
private bool _isPlaying = false;
// 再生開始時間
private float _startTime;
// 現在の演出長さ
private float _currentDuration;
// 外部から強制的に強度を上書きしたいときに使う(0〜1)
private float _externalIntensity = -1f;
// シェーダープロパティID(毎フレーム文字列検索しないようキャッシュ)
private static readonly int PropTime = Shader.PropertyToID("_TimeSeed");
private static readonly int PropNoiseIntensity = Shader.PropertyToID("_NoiseIntensity");
private static readonly int PropColorShiftIntensity = Shader.PropertyToID("_ColorShiftIntensity");
private static readonly int PropGlobalIntensity = Shader.PropertyToID("_GlobalIntensity");
private void Awake()
{
// glitchShader が未設定なら、名前から自動検索を試みる
if (glitchShader == null)
{
glitchShader = Shader.Find("Hidden/Custom/GlitchEffect");
}
if (glitchShader == null)
{
Debug.LogError("[GlitchEffect] GlitchEffect.shader が見つかりません。" +
"プロジェクトにシェーダーファイルを追加してから再度試してください。", this);
enabled = false;
return;
}
// マテリアルを生成(隠しオブジェクトとして扱う)
_material = new Material(glitchShader)
{
hideFlags = HideFlags.HideAndDontSave
};
_currentDuration = defaultDuration;
}
private void OnDestroy()
{
// メモリリーク防止のため、マテリアルを破棄
if (_material != null)
{
DestroyImmediate(_material);
}
}
/// <summary>
/// グリッチ演出を再生します。
/// 引数なし版:デフォルトの時間と強さで再生。
/// </summary>
public void PlayGlitch()
{
PlayGlitch(defaultDuration, 1f);
}
/// <summary>
/// グリッチ演出を指定時間・指定強度で再生します。
/// intensity は 0〜1 の範囲を想定。
/// </summary>
public void PlayGlitch(float duration, float intensity = 1f)
{
if (duration <= 0f)
{
duration = 0.01f;
}
_currentDuration = duration;
_startTime = Time.unscaledTime; // ポーズ中でも演出を進めたいので unscaledTime を使用
_isPlaying = true;
_externalIntensity = Mathf.Clamp01(intensity);
}
/// <summary>
/// 再生中かどうかを取得します。
/// </summary>
public bool IsPlaying => _isPlaying;
/// <summary>
/// カメラの描画結果に対してポストエフェクトを適用する。
/// Built-in 互換の OnRenderImage を利用しています。
/// URP でも Camera にアタッチすれば動作します。
/// </summary>
private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (_material == null || (!_isPlaying && _externalIntensity <= 0f))
{
// 演出が再生されていなければ、そのまま出力
Graphics.Blit(src, dest);
return;
}
float globalIntensity = 1f;
if (_isPlaying)
{
float elapsed = Time.unscaledTime - _startTime;
float t = Mathf.Clamp01(elapsed / _currentDuration);
if (autoFadeOut)
{
// カーブに沿って 1 → 0 に減衰させる
globalIntensity = fadeCurve.Evaluate(t);
}
if (elapsed >= _currentDuration)
{
// 演出終了
_isPlaying = false;
globalIntensity = 0f;
}
}
// 外部から intensity を指定していた場合は、それを乗算
if (_externalIntensity >= 0f)
{
globalIntensity *= _externalIntensity;
}
// もうほとんど見えないレベルになったら素通しにしてしまう
if (globalIntensity <= 0.001f)
{
Graphics.Blit(src, dest);
return;
}
// シェーダーにパラメータを渡す
_material.SetFloat(PropTime, Time.time); // ランダムシード用
_material.SetFloat(PropNoiseIntensity, noiseIntensity);
_material.SetFloat(PropColorShiftIntensity, colorShiftIntensity);
_material.SetFloat(PropGlobalIntensity, globalIntensity);
// ポストエフェクト適用
Graphics.Blit(src, dest, _material);
}
}
フルコード:GlitchEffect.shader(シェーダー)
続いて、実際にノイズ&色ズレを描画する Shader です。
プロジェクト内に GlitchEffect.shader という名前で保存してください。
Shader "Hidden/Custom/GlitchEffect"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_NoiseIntensity ("Noise Intensity", Range(0, 1)) = 0.6
_ColorShiftIntensity ("Color Shift Intensity", Range(0, 1)) = 0.7
_GlobalIntensity ("Global Intensity", Range(0, 1)) = 1.0
_TimeSeed ("Time Seed", Float) = 0
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Overlay" }
Cull Off ZWrite Off ZTest Always
Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_TexelSize;
float _NoiseIntensity;
float _ColorShiftIntensity;
float _GlobalIntensity;
float _TimeSeed;
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
// 疑似ランダム関数
float rand(float2 co)
{
return frac(sin(dot(co.xy, float2(12.9898, 78.233))) * 43758.5453);
}
fixed4 frag (v2f i) : SV_Target
{
float2 uv = i.uv;
// ランダムな横ブロックノイズ
// 画面を数十の帯に分割して、ランダムにUVをずらす
float blockCount = 40.0;
float row = floor(uv.y * blockCount);
float2 rowSeed = float2(row * 12.345 + _TimeSeed * 5.678, _TimeSeed);
float rowNoise = rand(rowSeed);
// ノイズの強さに応じて横方向にUVをずらす
float maxOffset = 0.04 * _NoiseIntensity * _GlobalIntensity;
float offsetX = (rowNoise - 0.5) * 2.0 * maxOffset;
// 一部の帯だけ強めにずらす
if (rowNoise > 0.7)
{
offsetX *= 2.0;
}
uv.x += offsetX;
// さらに細かいブロックノイズ
float2 blockSize = float2(0.05, 0.03); // ブロックの大きさ
float2 blockId = floor(uv / blockSize);
float blockNoise = rand(blockId + _TimeSeed);
float glitchChance = 0.2 * _NoiseIntensity * _GlobalIntensity;
if (blockNoise < glitchChance)
{
// ブロック単位でUVをランダムに飛ばす
float2 randomOffset = (rand(blockId + 1.23) - 0.5) * 0.2;
uv += randomOffset * _NoiseIntensity * _GlobalIntensity;
}
// 元の色を取得
fixed4 col = tex2D(_MainTex, uv);
// RGBチャンネルを少しずつ違うUVでサンプリングして色ズレを表現
float shift = 1.5 * _ColorShiftIntensity * _GlobalIntensity;
float2 uvR = uv + float2(_MainTex_TexelSize.x * shift, 0);
float2 uvG = uv;
float2 uvB = uv - float2(_MainTex_TexelSize.x * shift, 0);
fixed r = tex2D(_MainTex, uvR).r;
fixed g = tex2D(_MainTex, uvG).g;
fixed b = tex2D(_MainTex, uvB).b;
fixed4 glitchCol = fixed4(r, g, b, col.a);
// 元の色とグリッチ色をブレンド(GlobalIntensityでフェード)
fixed4 finalCol = lerp(col, glitchCol, _GlobalIntensity);
return finalCol;
}
ENDHLSL
}
}
FallBack Off
}
使い方の手順
ここからは、実際にシーンに組み込む手順を見ていきましょう。
例として「電子系の敵が登場するタイミングで、画面にグリッチを走らせる」ケースを想定します。
手順①:ファイルを配置する
GlitchEffect.csをAssets/Scripts/Effects/などのフォルダに保存。GlitchEffect.shaderをAssets/Shaders/などのフォルダに保存。- Unityに戻ると自動的にコンパイルされます。
手順②:メインカメラにコンポーネントを追加
- シーン内の Main Camera を選択。
Add Component→ 「GlitchEffect」と検索して追加。- インスペクターに以下の項目が出ていることを確認します。
Glitch Shader:空なら、右側の丸ボタンからGlitchEffectシェーダーを選択Default Duration:0.3〜0.5 くらいが扱いやすいですNoise Intensity:0.4〜0.8Color Shift Intensity:0.5〜0.9Auto Fade Out:オンのままでOKFade Curve:デフォルトのEaseInOutで問題なし
手順③:敵の登場スクリプトから再生する
次に、電子系の敵が登場する瞬間にグリッチを走らせてみましょう。
敵のプレハブに以下のようなスクリプトを追加します。
using UnityEngine;
/// <summary>
/// 電子系の敵が登場したときに、カメラのグリッチ演出を再生する例。
/// </summary>
public class ElectronicEnemySpawn : MonoBehaviour
{
[SerializeField] private float glitchDuration = 0.5f;
[SerializeField, Range(0f, 1f)] private float glitchIntensity = 1.0f;
private GlitchEffect _glitchEffect;
private void Start()
{
// シーン内の GlitchEffect を探す
_glitchEffect = FindObjectOfType<GlitchEffect>();
if (_glitchEffect == null)
{
Debug.LogWarning("[ElectronicEnemySpawn] シーン内に GlitchEffect が見つかりません。" +
"メインカメラに GlitchEffect コンポーネントを追加してください。");
}
// 敵の登場演出を開始
PlaySpawnSequence();
}
private void PlaySpawnSequence()
{
// ここに敵の出現アニメーションやSE再生などを書く
// 例: Animator で登場アニメ再生、パーティクル再生など
// カメラにグリッチ演出を再生
if (_glitchEffect != null)
{
_glitchEffect.PlayGlitch(glitchDuration, glitchIntensity);
}
}
}
このように、敵のロジック(登場処理)と画面演出(グリッチ)を分離しておくことで、
- 他の敵やイベント(ボス登場、ワープ演出など)からも簡単に使い回せる
- グリッチの強さや長さをカメラ側だけで調整できる
- 将来、別のグリッチエフェクトに差し替えたくなっても、敵側のコードをほぼ触らなくて良い
といったメリットが得られます。
手順④:テスト用の簡単トリガーを作る(任意)
プレイヤー側から試したい場合は、キーボード入力でグリッチを発動するだけの簡単コンポーネントを作っても良いですね。
using UnityEngine;
/// <summary>
/// テスト用:スペースキーでグリッチ演出を再生するコンポーネント。
/// デバッグ専用なので、本番ビルドでは外しておくとよいです。
/// </summary>
public class GlitchDebugTrigger : MonoBehaviour
{
[SerializeField] private GlitchEffect glitchEffect;
[SerializeField] private float duration = 0.4f;
[SerializeField, Range(0f, 1f)] private float intensity = 1f;
private void Reset()
{
// インスペクターで自動的にメインカメラの GlitchEffect を割り当てる
if (glitchEffect == null)
{
glitchEffect = FindObjectOfType<GlitchEffect>();
}
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space) && glitchEffect != null)
{
glitchEffect.PlayGlitch(duration, intensity);
}
}
}
これを適当な GameObject にアタッチしておけば、ゲーム中にスペースキーを押すだけでグリッチ演出が確認できます。
メリットと応用
GlitchEffect をコンポーネントとして切り出すことで、プレハブ管理やレベルデザインがかなり楽になります。
- カメラに付けるだけなので、新しいシーンを作るたびに同じ演出を簡単に再利用できる
- 敵プレハブ側は「登場したら
PlayGlitch()を呼ぶ」だけなので、敵のロジックと画面演出が分離される - レベルデザイナーは、カメラのインスペクターから
NoiseIntensityやColorShiftIntensityを調整するだけで雰囲気を変えられる - イベント用のタイムラインやシーケンサーからも、
PlayGlitch()を呼ぶだけで統一した演出を使える
「なにか電子的なイベントが起こったらカメラにノイズを走らせる」というルールを決めておくと、世界観の統一感も出しやすいですね。
改造案:敵のHPが減るほどノイズが激しくなる
応用として、「敵の体力が減るほど、画面のグリッチが激しくなる」という演出も簡単に作れます。
以下は、敵のHP割合に応じてグリッチ強度を変えながら定期的に発動する例です。
private void PlayDamageGlitch(float currentHp, float maxHp)
{
if (glitchEffect == null) return;
// HP が減るほど intensity が 0 → 1 に近づく
float hpRate = Mathf.Clamp01(currentHp / maxHp);
float intensity = 1f - hpRate;
// かなり弱めのノイズを短く何度も走らせるイメージ
float duration = Mathf.Lerp(0.15f, 0.35f, intensity);
glitchEffect.PlayGlitch(duration, intensity);
}
このように、GlitchEffect を「単なる見た目の飾り」ではなく、ゲームの状態をプレイヤーに伝えるUI的な演出として使うと、よりゲーム体験がリッチになります。
巨大な GameManager に直接ノイズ処理を書かず、小さなコンポーネントとして分離しておくことで、後からの改造・差し替えもずっと楽になりますね。
