Godot 4 で「ダメージを受けたときに画面をビリッと歪ませたい」と思ったとき、まず思いつくのは:
- プレイヤーシーンに直接
AnimationPlayerを追加して、カメラや CanvasLayer を揺らす - カメラを継承した
CustomCameraWithHitEffect.gdみたいなクラスを作って、そこに処理を全部書く
…みたいな「継承+ゴリゴリ実装」パターンだと思います。でもこのやり方だと:
- プレイヤー、敵、ボスなど、どのシーンにも同じような「被弾エフェクト」をコピペすることになる
- カメラを継承してしまうと、「別のカメラに差し替えたい」「マルチカメラにしたい」ときに柔軟性がなくなる
- UI とゲーム画面で別々にエフェクトをかけたいとき、さらにカオス
そこで今回は、「継承」ではなく「合成」で解決しましょう。画面全体に色収差(Chromatic Aberration)をかける処理を、どのシーンにもポン付けできるコンポーネントとして切り出します。
カメラやプレイヤーに依存しない、独立した ChromaticAbberation コンポーネントを用意しておけば:
- 「ダメージ受けたら呼ぶだけ」のシンプル API
- UI 専用の色収差、リプレイ用の色収差など、好きなだけ増やせる
- シーン構造が浅く・シンプルなまま保てる
というメリットがあります。
【Godot 4】被弾時に画面がビリッと歪む!「ChromaticAbberation」コンポーネント
今回の ChromaticAbberation コンポーネントは、以下のようなことをやってくれます:
- 画面全体に適用される 色収差シェーダ を自動でロード&適用
trigger()を呼ぶだけで、指定時間だけ RGB チャンネルをずらして歪ませる- 揺れ方(強さ・減衰カーブ・ランダム性)を
@exportでインスペクタから調整可能
ノード構成としては Camera2D / Camera3D にアタッチして使う想定ですが、実体は単なる Node なので、CanvasLayer などにぶら下げて使うこともできます。
フルコード:ChromaticAbberation.gd
extends Node
class_name ChromaticAbberation
"""
被弾時などに「画面全体の色チャンネルをずらして歪ませる」コンポーネント。
想定の使い方:
- Camera2D / Camera3D / CanvasLayer などに子ノードとしてアタッチ
- ダメージを受けたスクリプト側から `chromatic_abberation.trigger()` を呼ぶだけ
このコンポーネントは、親に自動で ColorRect + ShaderMaterial を仕込み、
シェーダのパラメータを時間経過で変化させることで色収差エフェクトを実現します。
"""
@export_category("Effect Settings")
## エフェクトが最大になる「初期の強さ」
## 値が大きいほど RGB のズレが大きくなります(0.0〜0.1 くらいが現実的)。
@export_range(0.0, 0.5, 0.005)
var max_offset: float = 0.05
## エフェクトが完全に消えるまでの時間(秒)
@export_range(0.05, 5.0, 0.05)
var duration: float = 0.4
## オフセットの減衰カーブ
## 0.0: 線形, 1.0: 先鋭に立ち上がってすぐ消える, -1.0: ゆっくり減衰
@export_range(-2.0, 2.0, 0.05)
var decay_curve: float = 0.0
## 各フレームでオフセットをランダムに揺らす強さ
## 0 ならランダムなし。0.0〜1.0 の範囲で少しずつ増やすと良いです。
@export_range(0.0, 1.0, 0.05)
var jitter_amount: float = 0.3
## エフェクト中に少しだけ画面を拡大して「ズーム感」を出すか
@export var enable_zoom_pulse: bool = true
## ズームの最大倍率(1.0 = 拡大なし、1.05 = 5% 拡大)
@export_range(1.0, 1.5, 0.01)
var zoom_scale: float = 1.03
@export_category("Shader / Target")
## 使用するシェーダファイルへのパス。
## デフォルトで res://shaders/chromatic_abberation.gdshader を探します。
@export_file("*.gdshader")
var shader_path: String = "res://shaders/chromatic_abberation.gdshader"
## このコンポーネントがエフェクトをかける対象。
## 通常は `get_viewport()` に対してフルスクリーンの ColorRect を重ねます。
@export var use_own_canvas_layer: bool = true
## 既存の CanvasLayer にぶら下げたい場合はここに指定
@export var target_canvas_layer: CanvasLayer
# 内部用
var _effect_rect: ColorRect
var _material: ShaderMaterial
var _time: float = 0.0
var _is_playing: bool = false
# ズーム用:親が Camera2D / Camera3D のときだけ使う
var _original_zoom_2d: Vector2
var _original_fov_3d: float
var _is_camera2d: bool = false
var _is_camera3d: bool = false
func _ready() -> void:
_setup_target()
_setup_shader()
_detect_camera_type()
func _process(delta: float) -> void:
if not _is_playing:
return
_time += delta
var t := clamp(_time / duration, 0.0, 1.0)
# 減衰カーブの適用
var strength := _apply_decay_curve(1.0 - t, decay_curve)
var base_offset := max_offset * strength
# ランダムな揺れを加算
var jitter := (randf() * 2.0 - 1.0) * jitter_amount * base_offset
var final_offset := base_offset + jitter
if _material and _material.shader:
_material.set_shader_parameter("offset_amount", final_offset)
# ズームエフェクト
if enable_zoom_pulse:
_apply_zoom_pulse(strength)
if t >= 1.0:
_finish_effect()
func trigger() -> void:
"""
外部から呼ぶ公開 API。
ダメージを受けたときなどに `chromatic_abberation.trigger()` を叩くだけ。
"""
_time = 0.0
_is_playing = true
if _material and _material.shader:
_material.set_shader_parameter("offset_amount", max_offset)
# ズームをリセットしてから開始
if enable_zoom_pulse:
_reset_zoom()
func _setup_target() -> void:
"""
色収差エフェクトを描画する ColorRect を用意する。
- use_own_canvas_layer = true の場合は、自前で CanvasLayer を作ってそこに ColorRect をぶら下げる
- false の場合は、export された target_canvas_layer の子に ColorRect を置く
"""
var parent_for_rect: Node
if use_own_canvas_layer:
var layer := CanvasLayer.new()
layer.name = "ChromaticAbberationLayer"
# 画面全体にかぶせたいので、比較的高いレイヤーにしておく
layer.layer = 100
add_child(layer)
parent_for_rect = layer
else:
if not target_canvas_layer:
push_warning("ChromaticAbberation: target_canvas_layer is not set. Fallback to own CanvasLayer.")
var fallback := CanvasLayer.new()
fallback.name = "ChromaticAbberationLayer"
fallback.layer = 100
add_child(fallback)
parent_for_rect = fallback
else:
parent_for_rect = target_canvas_layer
_effect_rect = ColorRect.new()
_effect_rect.name = "ChromaticAbberationRect"
_effect_rect.color = Color(1, 1, 1, 0) # 透明(シェーダで描画する)
_effect_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
_effect_rect.size_flags_horizontal = Control.SIZE_FLAGS_EXPAND_FILL
_effect_rect.size_flags_vertical = Control.SIZE_FLAGS_EXPAND_FILL
parent_for_rect.add_child(_effect_rect)
# ビューポートサイズに追従させる
_effect_rect.anchor_left = 0.0
_effect_rect.anchor_top = 0.0
_effect_rect.anchor_right = 1.0
_effect_rect.anchor_bottom = 1.0
_effect_rect.offset_left = 0.0
_effect_rect.offset_top = 0.0
_effect_rect.offset_right = 0.0
_effect_rect.offset_bottom = 0.0
func _setup_shader() -> void:
"""
シェーダファイルをロードし、ColorRect に ShaderMaterial として適用する。
"""
var shader: Shader
if ResourceLoader.exists(shader_path):
shader = load(shader_path)
else:
push_warning("ChromaticAbberation: Shader not found at %s. Using built-in fallback shader." % shader_path)
shader = _create_fallback_shader()
_material = ShaderMaterial.new()
_material.shader = shader
_effect_rect.material = _material
# 初期値
_material.set_shader_parameter("offset_amount", 0.0)
func _detect_camera_type() -> void:
"""
親が Camera2D / Camera3D なら、ズーム用の初期値を保存しておく。
"""
var p := get_parent()
if p is Camera2D:
_is_camera2d = true
_original_zoom_2d = p.zoom
elif p is Camera3D:
_is_camera3d = true
_original_fov_3d = p.fov
func _apply_decay_curve(v: float, curve: float) -> float:
"""
0〜1 の値 v に対して、簡易的な減衰カーブを適用する。
curve = 0 : 線形
curve > 0 : 先鋭に立ち上がってすぐ消える
curve < 0 : ゆっくり減衰
"""
v = clamp(v, 0.0, 1.0)
if curve == 0.0:
return v
elif curve > 0.0:
# 0 に近いほど小さく、1 に近いほど急激に落ちる
return pow(v, 1.0 + curve * 2.0)
else:
# 負のときはゆっくり減衰
return 1.0 - pow(1.0 - v, 1.0 + abs(curve) * 2.0)
func _apply_zoom_pulse(strength: float) -> void:
"""
エフェクト強度に応じてカメラのズーム / FOV を少しだけ変化させる。
strength: 0〜1
"""
if _is_camera2d:
var cam := get_parent() as Camera2D
var s := lerp(1.0, zoom_scale, strength)
cam.zoom = _original_zoom_2d * Vector2.ONE / s
elif _is_camera3d:
var cam3d := get_parent() as Camera3D
var s3 := lerp(1.0, zoom_scale, strength)
cam3d.fov = _original_fov_3d * s3
func _reset_zoom() -> void:
if _is_camera2d:
var cam := get_parent() as Camera2D
cam.zoom = _original_zoom_2d
elif _is_camera3d:
var cam3d := get_parent() as Camera3D
cam3d.fov = _original_fov_3d
func _finish_effect() -> void:
_is_playing = false
_time = 0.0
if _material and _material.shader:
_material.set_shader_parameter("offset_amount", 0.0)
if enable_zoom_pulse:
_reset_zoom()
func _create_fallback_shader() -> Shader:
"""
シェーダファイルが見つからなかったとき用のフォールバックシェーダ。
画面のテクスチャ(SCREEN_TEXTURE)を RGB チャンネルごとに少しずらして描画します。
"""
var code := """
shader_type canvas_item;
uniform float offset_amount : hint_range(0.0, 0.5) = 0.0;
void fragment() {
// 画面テクスチャ座標
vec2 uv = SCREEN_UV;
// 各チャンネルごとに少しずつ UV をずらす
vec2 offset_r = vec2(offset_amount, 0.0);
vec2 offset_g = vec2(0.0, offset_amount * -0.6);
vec2 offset_b = vec2(-offset_amount, 0.0);
float r = texture(SCREEN_TEXTURE, uv + offset_r).r;
float g = texture(SCREEN_TEXTURE, uv + offset_g).g;
float b = texture(SCREEN_TEXTURE, uv + offset_b).b;
COLOR = vec4(r, g, b, 1.0);
}
"""
var shader := Shader.new()
shader.code = code
return shader
使い方の手順
ここからは、実際に「プレイヤーがダメージを受けたときに画面がビリッと歪む」例で使い方を見ていきましょう。
手順①:シェーダファイルを用意する(任意)
フォールバックシェーダがあるので 必須ではありません が、よりリッチな色収差を使いたい場合は、例えば以下のようなシェーダを res://shaders/chromatic_abberation.gdshader として保存しておきます。
shader_type canvas_item;
uniform float offset_amount : hint_range(0.0, 0.5) = 0.0;
void fragment() {
vec2 uv = SCREEN_UV;
// 画面中心からの距離に応じてオフセットを強める(周辺ほど強い収差)
vec2 center = vec2(0.5, 0.5);
float dist = distance(uv, center);
float radial = smoothstep(0.0, 0.8, dist);
float amt = offset_amount * (0.3 + radial); // 中央も少しだけ揺らす
vec2 dir = normalize(uv - center + 0.0001);
vec2 offset_r = dir * amt;
vec2 offset_g = dir * -amt * 0.6;
vec2 offset_b = dir * amt * 0.4;
float r = texture(SCREEN_TEXTURE, uv + offset_r).r;
float g = texture(SCREEN_TEXTURE, uv + offset_g).g;
float b = texture(SCREEN_TEXTURE, uv + offset_b).b;
COLOR = vec4(r, g, b, 1.0);
}
パスを変えた場合は、コンポーネント側の shader_path をインスペクタで変更しておきましょう。
手順②:プレイヤーシーンにコンポーネントをアタッチ
2D アクションゲームの例として、以下のようなシーン構成を想定します。
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── Camera2D │ └── ChromaticAbberation (Node) └── Hurtbox (Area2D)
- プレイヤーシーンを開く
Camera2Dの子としてNodeを追加し、スクリプトにChromaticAbberation.gdをアタッチ- インスペクタで
max_offsetやdurationを好みに調整
これで、「プレイヤーのカメラにだけ色収差をかけるコンポーネント」が完成です。
手順③:ダメージ処理から trigger() を呼ぶ
プレイヤーのスクリプト側で、ダメージを受けたタイミングで ChromaticAbberation.trigger() を呼び出します。
# Player.gd (抜粋)
extends CharacterBody2D
@onready var chroma := $Camera2D/ChromaticAbberation
var hp: int = 5
func apply_damage(amount: int) -> void:
hp -= amount
if hp <= 0:
die()
return
# ここで色収差エフェクトを発火
if chroma:
chroma.trigger()
# ついでにノックバックや無敵時間などもここで処理
$AnimationPlayer.play("hurt")
これで、apply_damage() が呼ばれるたびに、画面全体が一瞬ビリッと歪みます。
手順④:UI や別カメラにも簡単に流用
コンポーネント化してあるので、例えば「UI にだけ色収差をかける」なんてことも簡単です。
HUD (CanvasLayer) ├── Control └── ChromaticAbberation (Node)
この場合は、ChromaticAbberation の use_own_canvas_layer を OFF にして、target_canvas_layer に親の HUD (CanvasLayer) を指定します。
ゲーム側からは、スコアが一定以上になったときなどに:
# GameManager.gd (抜粋)
@onready var hud_chroma := $HUD/ChromaticAbberation
func on_big_combo() -> void:
if hud_chroma:
hud_chroma.trigger()
とするだけで、UI だけがビリッと歪む演出を作れます。
メリットと応用
ChromaticAbberation をコンポーネントとして切り出すことで、次のようなメリットがあります。
- シーン構造がスッキリ
プレイヤーや敵のスクリプトに「色収差ロジック」を書かなくてよくなり、trigger()を呼ぶだけで済みます。 - カメラの種類に依存しない
Camera2D / Camera3D / CanvasLayer など、どこにでもアタッチできるので、後から 2D → 3D に変えても再利用しやすいです。 - レベルデザインが楽
ステージごとに「色収差の強さ」や「減衰カーブ」を変えて、怖いステージだけ強めにしたり、ボス戦だけド派手にしたり…といったチューニングが、インスペクタからノーコードでできます。 - 他エフェクトとの組み合わせが容易
画面シェイクコンポーネント、ホワイトアウトコンポーネントなどと組み合わせても、お互いを知らなくてよい「疎結合」な設計になります。
「継承でなんでも詰め込んだ巨大な Player.gd」を育てるのではなく、「ダメージ演出」「色収差」「カメラシェイク」などを小さなコンポーネントに分割して、必要なものだけアタッチしていくスタイルですね。
改造案:強さを動的に変えられる API を追加する
例えば「残り HP が少ないほど色収差を強くしたい」みたいな要望に応えるなら、trigger() にパラメータ付きのバージョンを追加しても良いですね。
func trigger_with_strength(strength: float) -> void:
"""
0.0〜1.0 の範囲でエフェクト強度を指定して発火する。
例: 残り HP に応じて強さを変えたいときなど。
"""
strength = clamp(strength, 0.0, 1.0)
_time = 0.0
_is_playing = true
var initial_offset := max_offset * strength
if _material and _material.shader:
_material.set_shader_parameter("offset_amount", initial_offset)
if enable_zoom_pulse:
_reset_zoom()
これを使えば、プレイヤーの HP に応じて:
var hp_ratio := float(hp) / float(max_hp)
var strength := 1.0 - hp_ratio # HP が減るほど強く
chroma.trigger_with_strength(strength)
といった「ゲームプレイと連動した視覚演出」も簡単に実現できます。
こんな感じで、小さなコンポーネントを積み上げていくと、Godot プロジェクトの見通しがどんどん良くなっていきます。ぜひ ChromaticAbberation をベースに、自分のゲーム専用の演出コンポーネントを増やしていってみてください。
