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 など)なら基本同じです。
手順①:スクリプトを用意してコンポーネント化
- 上記のコードを
DamageFlash.gdという名前で保存します。 - Godot エディタの「スクリプト」タブから開き、
class_name DamageFlashがあることを確認します。
これでインスペクタから コンポーネントとして追加できる状態になります。
手順②:プレイヤーにアタッチしてみる
典型的な 2D プレイヤーのシーン構成を例にします。
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── DamageFlash (Node)
Playerシーンを開く。- + ボタンから
Nodeを追加し、名前をDamageFlashに変更。 - そのノードに
DamageFlash.gdをアタッチ(またはインスペクタのスクリプト欄からDamageFlashを選択)。 - インスペクタで必要なら
target_nodeをSprite2Dにドラッグ&ドロップflash_colorをColor.WHITE(白フラッシュ)やColor(1, 0.3, 0.3)(薄赤)に変更flash_durationやflash_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_color や flash_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 をポン付けして、「継承より合成」の気持ちよさを体感してみてください。
