Godotで「電子系の敵が出てくるときに画面がビリビリッとノイズる」みたいな演出、やりたくなりますよね。ただ、素直にやろうとすると:
- 敵シーンの中に専用のCanvasLayerやAnimationPlayerを仕込む
- 演出用のシェーダーを各シーンごとにコピペ
- 敵ごとに微妙に違うノイズ演出を作り始めて、だんだんカオスに…
みたいな「継承&複雑なノード階層地獄」になりがちです。
敵シーンを継承して「ノイズ付き敵」「ノイズなし敵」を作り始めると、後から仕様変更が来たときに全部直す羽目になります。
そこで今回は、「画面全体のグリッチ演出」をひとつのコンポーネントとして切り出して、どのシーンからでも簡単に呼び出せる GlitchEffect コンポーネントを作っていきましょう。
敵の登場時だけでなく、被ダメージ時、フェーズ遷移時などにも再利用できるようにしておくと、レベルデザインがかなり楽になります。
【Godot 4】一発で画面をビリビリさせる!「GlitchEffect」コンポーネント
このコンポーネントは:
- シーンのどこかに 1 個置いておくだけ
- 敵やイベント側から
trigger_glitch()を呼ぶだけ - ブロックノイズ・色ズレ・画面揺れをまとめて制御
という「合成(Composition)」志向の作りにします。
敵やプレイヤーは「グリッチ演出のことを知らない」まま、ただコンポーネントに対してシグナルを送るだけ、という構造にしましょう。
フルコード:GlitchEffect.gd
extends CanvasLayer
class_name GlitchEffect
## 画面全体に「ブロックノイズ+色ズレ+軽いシェイク」をかけるコンポーネント
## 任意のシーンに1つ置いておき、敵やイベントから trigger_glitch() を呼び出して使う。
@export_category("基本設定")
@export var auto_disable: bool = true:
## true のとき、エフェクト終了後に自動で非表示にする
## false の場合は常に表示され、強度0で「待機」させる運用も可能
set(value):
auto_disable = value
if not auto_disable:
visible = true
@export var default_duration: float = 0.4:
## デフォルトのグリッチ持続時間(秒)
## trigger_glitch() の duration を省略したときに使われる
set(value):
default_duration = max(0.01, value)
@export_category("ノイズ・色ズレ")
@export_range(0.0, 1.0, 0.01) var max_noise_strength: float = 0.8
## ブロックノイズの最大強度(0~1)
@export_range(0.0, 1.0, 0.01) var max_color_shift: float = 0.6
## RGBチャンネルのズレ量の最大値(0~1)
@export_category("画面揺れ")
@export_range(0.0, 32.0, 0.1) var max_shake_amplitude: float = 12.0
## グリッチ中の画面揺れの最大ピクセル量
@export_range(0.0, 50.0, 0.1) var shake_frequency: float = 18.0
## 画面揺れの周波数(大きいほど激しく揺れる)
@export_category("ランダム性")
@export var randomize_seed_on_start: bool = true
## true のとき、_ready でランダムシードを初期化して
## 毎回少し違うノイズパターンにする
@export_category("ターゲット")
@export var target_viewport: Viewport:
## どの Viewport をキャプチャしてグリッチさせるか
## 空の場合は自動で get_viewport() を使う
@export_category("デバッグ")
@export var preview_on_start: bool = false
## true にすると、シーン再生時に一度だけ自動でグリッチを再生して確認できる
## 内部状態
var _time: float = 0.0
var _duration: float = 0.0
var _is_playing: bool = false
var _base_offset: Vector2 = Vector2.ZERO
## ノード参照
var _noise_texture: NoiseTexture2D
var _shader_material: ShaderMaterial
var _texture_rect: TextureRect
func _ready() -> void:
if randomize_seed_on_start:
randomize()
# Viewport の自動設定
if target_viewport == null:
target_viewport = get_viewport()
# 子ノードをコード側で生成する方式にして、
# シーンツリーを汚さない「コンポーネント感」を出す
_create_render_target()
_create_glitch_quad()
visible = false
_base_offset = position
if preview_on_start:
await get_tree().process_frame
trigger_glitch()
func _process(delta: float) -> void:
if not _is_playing:
return
_time += delta
var t := clamp(_time / _duration, 0.0, 1.0)
# 後半でスッと収束するようにイージング
var ease := pow(1.0 - t, 2.0)
# ノイズ・色ズレの強度を時間とともに減衰させる
var noise_strength := max_noise_strength * ease
var color_shift := max_color_shift * ease
# シェーダーパラメータに反映
_shader_material.set_shader_parameter("u_noise_strength", noise_strength)
_shader_material.set_shader_parameter("u_color_shift", color_shift)
_shader_material.set_shader_parameter("u_time", Time.get_ticks_msec() / 1000.0)
# 画面揺れ
var shake_amp := max_shake_amplitude * ease
var shake_offset := Vector2(
sin(_time * shake_frequency) * shake_amp,
cos(_time * shake_frequency * 0.8) * shake_amp
)
position = _base_offset + shake_offset
if _time >= _duration:
_stop_effect()
func trigger_glitch(duration: float = -1.0) -> void:
## 外部から呼ぶメインAPI
## duration を指定しなければ default_duration が使われる
if duration <= 0.0:
duration = default_duration
_duration = duration
_time = 0.0
_is_playing = true
visible = true
# 開始時は強めに
_shader_material.set_shader_parameter("u_noise_strength", max_noise_strength)
_shader_material.set_shader_parameter("u_color_shift", max_color_shift)
func _stop_effect() -> void:
_is_playing = false
position = _base_offset
# シェーダーパラメータをリセット
_shader_material.set_shader_parameter("u_noise_strength", 0.0)
_shader_material.set_shader_parameter("u_color_shift", 0.0)
if auto_disable:
visible = false
func _create_render_target() -> void:
## ViewportTexture を張るための TextureRect を作成する
_texture_rect = TextureRect.new()
_texture_rect.name = "GlitchQuad"
_texture_rect.anchor_left = 0.0
_texture_rect.anchor_top = 0.0
_texture_rect.anchor_right = 1.0
_texture_rect.anchor_bottom = 1.0
_texture_rect.offset_left = 0.0
_texture_rect.offset_top = 0.0
_texture_rect.offset_right = 0.0
_texture_rect.offset_bottom = 0.0
_texture_rect.stretch_mode = TextureRect.STRETCH_SCALE
_texture_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
# メインViewportの内容を貼る
var vp_tex := target_viewport.get_texture()
_texture_rect.texture = vp_tex
add_child(_texture_rect)
func _create_glitch_quad() -> void:
## ノイズ用のNoiseTexture2Dとシェーダーを作成して TextureRect に適用
_noise_texture = NoiseTexture2D.new()
var fast_noise := FastNoiseLite.new()
fast_noise.noise_type = FastNoiseLite.TYPE_SIMPLEX
fast_noise.frequency = 4.0
_noise_texture.noise = fast_noise
_noise_texture.width = 256
_noise_texture.height = 256
_noise_texture.seamless = true
var shader_code := _get_glitch_shader_code()
var shader := Shader.new()
shader.code = shader_code
_shader_material = ShaderMaterial.new()
_shader_material.shader = shader
_shader_material.set_shader_parameter("u_noise_tex", _noise_texture)
_shader_material.set_shader_parameter("u_noise_strength", 0.0)
_shader_material.set_shader_parameter("u_color_shift", 0.0)
_shader_material.set_shader_parameter("u_time", 0.0)
_texture_rect.material = _shader_material
func _get_glitch_shader_code() -> String:
## 画面全体に対してブロックノイズと色ズレをかけるシェーダー
return """
shader_type canvas_item;
uniform sampler2D u_noise_tex;
uniform float u_noise_strength : hint_range(0.0, 1.0) = 0.0;
uniform float u_color_shift : hint_range(0.0, 1.0) = 0.0;
uniform float u_time = 0.0;
void fragment() {
vec2 uv = UV;
// ブロックノイズ用にUVを粗くサンプリング
vec2 block_uv = floor(uv * 40.0) / 40.0;
float noise = texture(u_noise_tex, block_uv + vec2(u_time * 0.5, 0.0)).r;
// 0~1 → -1~1 に変換
float n = noise * 2.0 - 1.0;
// 横方向にガタつく感じのオフセット
float glitch_offset = n * 0.03 * u_noise_strength;
uv.x += glitch_offset;
// ライン状のチラつきを追加
float line = sin(uv.y * 400.0 + u_time * 40.0);
float line_mask = step(0.8, line); // 0 or 1
uv.x += line_mask * 0.02 * u_noise_strength;
// 元の画面色をサンプリング
vec4 base_col = texture(TEXTURE, uv);
// 色ズレ(RGBチャンネルを少しずつずらす)
vec2 shift = vec2(0.004, 0.0) * u_color_shift;
float r = texture(TEXTURE, uv + shift).r;
float g = base_col.g;
float b = texture(TEXTURE, uv - shift).b;
vec3 col = vec3(r, g, b);
// ノイズで明滅させる
col += n * 0.15 * u_noise_strength;
COLOR = vec4(col, base_col.a);
}
"""
使い方の手順
-
コンポーネントシーンを作る
上のコードをGlitchEffect.gdという名前で保存し、
新規シーンでCanvasLayerをルートにして、このスクリプトをアタッチしておきます。
そのシーンをGlitchEffect.tscnなどの名前で保存しておきましょう。 -
メインシーンに配置する
ゲームのメインシーン(例:Main.tscn)にGlitchEffect.tscnを1つだけインスタンスします。
これで「画面全体にかけるグリッチコンポーネント」が常駐します。 -
敵やイベントから呼び出す
敵の登場アニメーションが終わったタイミングや、スポーン直後に
GlitchEffect.trigger_glitch()を呼び出します。
ここでは、敵スクリプトからget_tree().get_first_node_in_group()で探すパターンを例にします。 -
パラメータを調整して好みのノイズにする
max_noise_strength,max_color_shift,max_shake_amplitudeなどをいじって、
「ちょっとしたノイズ」から「画面が崩壊しそうなノイズ」まで調整しましょう。
シーン構成例1:プレイヤー+電子系の敵
Main (Node) ├── Player (CharacterBody2D) │ ├── Sprite2D │ └── CollisionShape2D ├── EnemySpawner (Node) ├── GlitchEffect (CanvasLayer) ← このコンポーネント └── UI (CanvasLayer)
敵のスクリプト例:
extends Node2D
@export var spawn_glitch_duration: float = 0.3
func _ready() -> void:
# スポーン時にグリッチを走らせる
_play_spawn_glitch()
func _play_spawn_glitch() -> void:
var glitch := get_tree().get_first_node_in_group("glitch_effect")
if glitch == null:
# グループ登録していない場合は、直接パスで取るなど別の方法でもOK
glitch = get_tree().root.get_node_or_null("Main/GlitchEffect")
if glitch and glitch.has_method("trigger_glitch"):
glitch.trigger_glitch(spawn_glitch_duration)
上のコードを活かすには、GlitchEffect ノードを「glitch_effect」グループに登録しておきます:
# GlitchEffect.gd の _ready 内のどこかに追加してもOK
func _ready() -> void:
add_to_group("glitch_effect")
# 既存の処理...
シーン構成例2:ボス登場シーン専用のノイズ
BossIntro (Node2D) ├── Camera2D ├── Boss (Node2D) ├── GlitchEffect (CanvasLayer) ← このシーン専用に置いてもよい └── AnimationPlayer
ボス登場アニメーションの途中で、AnimationPlayer の「Call Method Track」を使って GlitchEffect.trigger_glitch(0.6) を呼ぶようにすれば、
ボスが画面ににじみ出るような演出を簡単に仕込めます。
メリットと応用
- シーン構造がシンプル:敵やプレイヤーにノイズ用ノードを増やさず、演出は
GlitchEffectに集約できます。 - 継承ではなく合成で管理:
EnemyBaseに「グリッチ機能」を持たせる必要はなく、ただ「コンポーネントに通知するだけ」で済みます。 - パラメータ調整が一箇所:ゲーム全体の「ノイズ感」を1つのノードで統一できるので、後からトーンを変えるのが楽です。
- 再利用性が高い:タイトル画面、ゲームオーバー演出、テレポート演出など、どのシーンでも同じコンポーネントを再利用できます。
応用として、敵の種類によってノイズの強さを変えるのも簡単です。たとえば「ボスのときだけ強めにする」メソッドを追加する改造案:
func trigger_boss_glitch() -> void:
## ボス専用の強めグリッチ(色ズレと揺れを一時的にブースト)
var prev_noise := max_noise_strength
var prev_color := max_color_shift
var prev_shake := max_shake_amplitude
max_noise_strength = 1.0
max_color_shift = 0.9
max_shake_amplitude = 20.0
await trigger_glitch(0.8)
# 終了後に元の値に戻す
max_noise_strength = prev_noise
max_color_shift = prev_color
max_shake_amplitude = prev_shake
こんな感じで、「画面演出」はどんどんコンポーネント化していくと、
各キャラやギミックのスクリプトは「ゲームロジックだけ」に集中できて、かなり開発体験が良くなりますね。
