Godot 4 でアクションゲームやシューティングを作っていると、ダメージを受けたときに「キャラを一瞬白く光らせたい」「赤く点滅させたい」という欲求、出てきますよね。
多くの人は最初、こんな感じで実装しがちです。

  • プレイヤー用のスクリプトに take_damage() を書く
  • その中で modulate を直接いじる
  • 敵にも同じような take_damage() + 点滅コードを書く

結果として…

  • プレイヤー、敵、ギミックなど、あらゆるスクリプトに「点滅ロジック」がコピペされる
  • 点滅の色や時間を変えたいとき、全部のスクリプトを探して修正するハメになる
  • ノード階層に「点滅専用の子スプライト」を追加したりして、シーン構造がどんどん深くなる

これはまさに「継承ベタベタ問題」と同じで、「ダメージを受ける」というゲームロジックと「見た目の点滅エフェクト」がベッタリ結合してしまっている状態です。
そこで今回は、どのキャラにも「ポン付け」できる DamageFlash コンポーネント を用意して、合成(Composition)で被弾演出を付けるスタイルにしてみましょう。

【Godot 4】被弾演出をポン付け!「DamageFlash」コンポーネント

このコンポーネントは、親ノードの Sprite 系ノードの modulate を一時的に変更して点滅させるだけに特化した、小さな Node スクリプトです。

  • プレイヤー
  • 敵キャラ
  • 動く床や破壊可能オブジェクト

など、どんなシーンにも ノードを 1 個足して、メソッドを 1 回呼ぶだけで被弾点滅を付けられます。


フルコード:DamageFlash.gd


extends Node
class_name DamageFlash
## 親の Sprite / Sprite2D / AnimatedSprite2D などを
## 一時的に白や赤に光らせる「被弾点滅」コンポーネント。
##
## 親ノードから:
##   - get_node("DamageFlash").flash()
## と呼ぶだけでOK。
##
## 「どのノードの modulate を変えるか」は @export で指定できます。

@export_group("ターゲット設定")
## 点滅させたいノード(Sprite2D, AnimatedSprite2D, CanvasItem など)
## 未設定の場合は、親ノードから最初に見つかった Sprite2D / AnimatedSprite2D を自動検出します。
@export var target_node: CanvasItem

@export_group("エフェクト設定")
## 点滅の色。白で「フラッシュ」、赤で「被弾感」など。
@export var flash_color: Color = Color.WHITE

## 1 回のフラッシュの長さ(秒)
@export_range(0.01, 1.0, 0.01, "or_greater")
var flash_duration: float = 0.12

## 連続して何回点滅させるか
@export_range(1, 10, 1)
var flash_count: int = 1

## 点滅中は元の色に戻さず「点灯しっぱなし」にするか
## true: 最後のフラッシュ色のまま終了
## false: 最後に元の色に戻す(通常はこちら推奨)
@export var keep_flash_color_at_end: bool = false

@export_group("挙動設定")
## すでに点滅中に flash() が呼ばれたときの挙動
## true: 既存の点滅をキャンセルして、最初からやり直す
## false: 新しいリクエストを無視する
@export var restart_when_called_again: bool = true

## 親ノードの modulate を使うかどうか。
## false の場合、target_node の modulate を直接変更します。
@export var use_parent_modulate: bool = false

## 点滅中かどうかのフラグ(読み取り専用想定)
var is_flashing: bool = false:
    get:
        return is_flashing

## 内部用: 元の色を保存しておく
var _original_color: Color
## 内部用: 実際に色をいじる対象
var _actual_target: CanvasItem
## 内部用: 進行中のコルーチンをキャンセルするための参照
var _flash_coroutine: GDScriptFunctionState

func _ready() -> void:
    # ターゲットが指定されていなければ自動検出
    if target_node == null:
        _actual_target = _auto_find_target()
    else:
        _actual_target = target_node

    if _actual_target == null:
        push_warning("DamageFlash: ターゲットとなる CanvasItem が見つかりませんでした。flash() を呼んでも何も起きません。")
        return

    # 初期色を保存しておく
    _original_color = _get_modulate()

## 自動で Sprite2D / AnimatedSprite2D / Node2D(=CanvasItem) を探す
func _auto_find_target() -> CanvasItem:
    # まず親ノードをチェック
    var parent := get_parent()
    if parent is CanvasItem:
        return parent

    # 親の子供から Sprite2D / AnimatedSprite2D を探す
    if parent:
        for child in parent.get_children():
            if child is CanvasItem:
                return child

    return null

## 現在の modulate を取得(親を使うかどうかを考慮)
func _get_modulate() -> Color:
    if use_parent_modulate and get_parent() is CanvasItem:
        return (get_parent() as CanvasItem).modulate
    elif _actual_target:
        return _actual_target.modulate
    return Color.WHITE

## modulate を設定(親を使うかどうかを考慮)
func _set_modulate(color: Color) -> void:
    if use_parent_modulate and get_parent() is CanvasItem:
        (get_parent() as CanvasItem).modulate = color
    elif _actual_target:
        _actual_target.modulate = color

## 外部から呼ぶメインAPI
## 例: get_node("DamageFlash").flash()
func flash(
    color: Color = flash_color,
    duration: float = flash_duration,
    count: int = flash_count
) -> void:
    if _actual_target == null:
        push_warning("DamageFlash: ターゲットが設定されていないため flash() は無視されました。")
        return

    if is_flashing:
        if not restart_when_called_again:
            # すでに点滅中で、再スタートしない設定の場合は無視
            return
        # 既存のコルーチンをキャンセル(実際にはフラグだけ変える)
        is_flashing = false

    # パラメータを正規化
    duration = max(duration, 0.01)
    count = max(count, 1)

    # 新しい点滅処理を開始
    _flash_coroutine = _do_flash(color, duration, count)

## 実際の点滅処理(コルーチン)
func _do_flash(color: Color, duration: float, count: int) -> GDScriptFunctionState:
    is_flashing = true
    _original_color = _get_modulate()

    # 1回分のON/OFFを duration/2 ずつ使う
    var half := duration * 0.5

    # コルーチン開始
    return _run_flash_coroutine(color, half, count)

func _run_flash_coroutine(color: Color, half: float, count: int) -> GDScriptFunctionState:
    return _flash_step(color, half, count)

func _flash_step(color: Color, half: float, remaining: int) -> GDScriptFunctionState:
    # Godot 4 では async/await 的に書くと読みやすいので、あえて分けています
    return _flash_step_async(color, half, remaining)

@warning_ignore("unused_parameter")
async func _flash_step_async(color: Color, half: float, remaining: int) -> void:
    # ループ開始時に is_flashing が false なら中断
    if not is_flashing:
        return

    for i in remaining:
        # 点灯
        _set_modulate(color)
        await get_tree().create_timer(half).timeout
        if not is_flashing:
            break

        # 消灯(元の色に戻す)
        if i < remaining - 1 or not keep_flash_color_at_end:
            _set_modulate(_original_color)
        await get_tree().create_timer(half).timeout
        if not is_flashing:
            break

    # 最後に元の色へ戻す(設定による)
    if not keep_flash_color_at_end:
        _set_modulate(_original_color)

    is_flashing = false

## 外部から「強制的に点滅を止めて元の色に戻す」ためのAPI
func stop_and_reset() -> void:
    is_flashing = false
    if _actual_target:
        _set_modulate(_original_color)

使い方の手順

ここでは 2D を例にしますが、CanvasItem を継承しているノード(Control, Sprite2D など)なら基本同じです。

手順①:スクリプトを用意してコンポーネント化

  1. 上記のコードを DamageFlash.gd という名前で保存します。
  2. Godot エディタの「スクリプト」タブから開き、class_name DamageFlash があることを確認します。
    これでインスペクタから コンポーネントとして追加できる状態になります。

手順②:プレイヤーにアタッチしてみる

典型的な 2D プレイヤーのシーン構成を例にします。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── DamageFlash (Node)
  1. Player シーンを開く。
  2. + ボタンから Node を追加し、名前を DamageFlash に変更。
  3. そのノードに DamageFlash.gd をアタッチ(またはインスペクタのスクリプト欄から DamageFlash を選択)。
  4. インスペクタで必要なら
    • target_nodeSprite2D にドラッグ&ドロップ
    • flash_colorColor.WHITE(白フラッシュ)や Color(1, 0.3, 0.3)(薄赤)に変更
    • flash_durationflash_count を調整

target_node を空のままにしておくと、自動的に親や子の CanvasItem を探してくれるので、シンプルに使いたい場合は未設定でもOKです。

手順③:ダメージ処理から呼び出す

プレイヤー側のスクリプトは「ダメージを受けた」という事実だけを知り、見た目の演出は DamageFlash に丸投げします。


# Player.gd (例)
extends CharacterBody2D

var hp: int = 10

func take_damage(amount: int) -> void:
    hp -= amount
    if hp <= 0:
        die()
        return

    # 被弾時の点滅を再生
    var flash: DamageFlash = get_node("DamageFlash")
    flash.flash() # パラメータ省略でインスペクタ設定を使用

func die() -> void:
    queue_free()

これだけで、Player は「ダメージを受ける」ロジックと「見た目の点滅」ロジックが完全に分離されます。

手順④:敵やギミックにもコピペ無しで再利用

敵キャラのシーン例:

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── DamageFlash (Node)

# Enemy.gd (例)
extends CharacterBody2D

var hp: int = 3

func take_damage(amount: int) -> void:
    hp -= amount

    # 赤く2回点滅させる
    var flash: DamageFlash = get_node("DamageFlash")
    flash.flash(Color(1, 0.2, 0.2), 0.1, 2)

    if hp <= 0:
        die()

func die() -> void:
    queue_free()

動く床などのギミックにも同じようにアタッチすれば、「攻撃を受けたときに一瞬白くなる床」なども簡単に作れます。

MovingPlatform (Node2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── DamageFlash (Node)

メリットと応用

1. シーン構造がシンプルなまま
Godot でありがちな「Sprite の下にさらに演出用の Sprite をぶら下げる」「ダメージ用ノードを何階層も下に置く」みたいなことをせずに、演出は 1 個の Node コンポーネントに閉じ込められます。

2. ロジックと見た目の分離
プレイヤーや敵のスクリプトは「HP を減らす」「死亡判定する」といったゲームロジックだけを書き、
「何色で何回点滅するか」は DamageFlash に丸投げできます。
継承クラスごとに点滅コードをコピペする必要がなくなり、コンポーネント指向のメリットが素直に効いてきます。

3. パラメータをインスペクタから調整できる
flash_colorflash_duration などはエディタ上で簡単に調整できるので、
レベルデザイナーやアーティストが「この敵は長めに赤く光らせたい」といった要望を、スクリプトを触らずに実現できます。

4. 再利用性が高い
どのシーンにも同じコンポーネントをポン付けできるので、「被弾演出の仕様変更」が入っても DamageFlash だけ直せばOK。
プロジェクト全体のメンテナンスコストがかなり下がります。

改造案:無敵時間(i-frames)との連携

例えば「点滅中は無敵にしたい」場合、DamageFlash に「点滅完了を通知するシグナル」を追加して、プレイヤー側で無敵フラグを管理する、という拡張が考えられます。


# DamageFlash.gd に追加
signal flash_finished

# _flash_step_async の最後あたりに追加
if not keep_flash_color_at_end:
    _set_modulate(_original_color)

is_flashing = false
emit_signal("flash_finished")

そしてプレイヤー側:


# Player.gd (一部)
var invincible: bool = false

func _ready() -> void:
    var flash: DamageFlash = get_node("DamageFlash")
    flash.flash_finished.connect(_on_flash_finished)

func take_damage(amount: int) -> void:
    if invincible:
        return

    hp -= amount
    if hp <= 0:
        die()
        return

    invincible = true
    get_node("DamageFlash").flash()

func _on_flash_finished() -> void:
    invincible = false

このように、コンポーネント側は「演出」と「状態通知」だけを担当し、
ゲームロジック側は「その通知をどう解釈するか」に集中できると、よりきれいなコンポーネント指向設計になりますね。

ぜひプロジェクトのあちこちに DamageFlash をポン付けして、「継承より合成」の気持ちよさを体感してみてください。