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)
  1. プレイヤーシーンを開く
  2. Camera2D の子として Node を追加し、スクリプトに ChromaticAbberation.gd をアタッチ
  3. インスペクタで max_offsetduration を好みに調整

これで、「プレイヤーのカメラにだけ色収差をかけるコンポーネント」が完成です。

手順③:ダメージ処理から 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)

この場合は、ChromaticAbberationuse_own_canvas_layerOFF にして、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 をベースに、自分のゲーム専用の演出コンポーネントを増やしていってみてください。