Godotで「ダメージを受けたときに画面の端が赤く光る」演出を入れようとすると、ありがちな実装はこんな感じですよね。
- プレイヤーシーンのスクリプトに直接「ダメージ演出」のコードを書き足す
- UIシーン(CanvasLayer)のスクリプトに、プレイヤーからシグナルを飛ばして制御を書く
- さらにダメージ演出のアニメーションを
AnimationPlayerにゴリゴリ記述
これでも動きますが、だんだんこうなります。
- プレイヤーのスクリプトが「入力・移動・攻撃・UI制御…」と肥大化してカオス
- 別のシーン(敵用デバッグシーンなど)で同じダメージ演出を使いたいのに、コピペ地獄
- 「赤いフラッシュの強さや時間」を変えたいだけなのに、複数のシーンを探し回る羽目に
そこで、「ダメージ演出」だけを 1 つのコンポーネントとして切り出して、どの HUD / UI シーンにもポン付けできるようにしておくと、とても楽になります。継承や深いノード階層に縛られず、「合成(Composition)」で UI を組み立てる感じですね。
今回は、ダメージを受けた瞬間に、画面の端を赤く点滅させる TextureRect 制御コンポーネント、DamageVignette を作っていきましょう。
【Godot 4】被弾時の赤いフラッシュをコンポーネント化!「DamageVignette」コンポーネント
このコンポーネントは、ざっくり言うと:
- 任意の
TextureRectを指定して trigger()を呼ぶだけで- フェードイン → フェードアウトの赤いダメージビネット演出を再生
という役割だけに徹した、シンプルな UI コンポーネントです。プレイヤー・敵・トラップなど、誰がダメージを受けても、UI 側では同じコンポーネントを使い回せます。
フルコード:DamageVignette.gd
extends Node
class_name DamageVignette
## ダメージを受けた瞬間に画面端を赤く点滅させるコンポーネント。
## 任意の TextureRect を制御し、フェードイン・アウトのアニメーションを行う。
@export var target_texture_rect: TextureRect:
set(value):
target_texture_rect = value
# 自動初期化(エディタ上でプレビューしやすくするため)
if is_instance_valid(target_texture_rect):
_init_texture_rect()
## 最大まで点灯したときの不透明度(0.0〜1.0)
@export_range(0.0, 1.0, 0.01)
var max_alpha: float = 0.7
## 点灯するまでの時間(秒)
@export_range(0.01, 5.0, 0.01)
var fade_in_time: float = 0.1
## 消えるまでの時間(秒)
@export_range(0.01, 5.0, 0.01)
var fade_out_time: float = 0.35
## ダメージ演出のカーブ(0.0〜1.0 を時間に対する強さにマッピング)
## 例: 緩やかに立ち上がって、最後にスッと消える…など
@export var intensity_curve: Curve
## 連打をどう扱うか:
## true なら、再生中に trigger() されたらタイマーをリセットして再スタート
## false なら、再生中のときは新しい trigger() を無視
@export var restart_on_trigger: bool = true
## フラッシュ中かどうか
var _is_playing := false
## 0.0〜1.0 の正規化時間(0 が開始、1 が終了)
var _t := 0.0
## 内部用の Tween(Godot 4 の Tween はシーンツリーにぶら下がる必要がある)
var _tween: Tween
func _ready() -> void:
# target_texture_rect が未指定なら、同じ親の中からそれっぽいものを自動検出
if target_texture_rect == null:
_autodetect_texture_rect()
# カーブが未設定なら、線形の簡易カーブを自動生成
if intensity_curve == null:
intensity_curve = Curve.new()
intensity_curve.add_point(Vector2(0.0, 0.0))
intensity_curve.add_point(Vector2(0.3, 1.0))
intensity_curve.add_point(Vector2(1.0, 0.0))
if is_instance_valid(target_texture_rect):
_init_texture_rect()
func _init_texture_rect() -> void:
# TextureRect の初期設定:
# - 画面全体を覆うように拡大(必要に応じて UI 側で調整してください)
# - 初期アルファは 0(非表示)
# - マウス入力などをブロックしないようにする
target_texture_rect.modulate.a = 0.0
target_texture_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
# ビネット用テクスチャを貼るなら、ここでデフォルトを設定しても良い
# (本記事では「画像は自前で用意する」前提にしておきます)
func _autodetect_texture_rect() -> void:
# よくある構成:
# CanvasLayer
# └─ Control (HUD)
# └─ TextureRect (DamageVignette用)
#
# このコンポーネントと同じ親にある TextureRect を1つだけ見つけたら採用する。
if get_parent() == null:
return
var candidates: Array[TextureRect] = []
for child in get_parent().get_children():
if child is TextureRect:
candidates.append(child)
if candidates.size() == 1:
target_texture_rect = candidates[0]
elif candidates.size() > 1:
# 2つ以上ある場合は、名前で "Vignette" を含むものを優先
for c in candidates:
if "vignette" in c.name.to_lower():
target_texture_rect = c
return
## 外部から呼び出すメインAPI。
## ダメージを受けた瞬間に呼び出してください。
func trigger() -> void:
if not is_instance_valid(target_texture_rect):
push_warning("[DamageVignette] target_texture_rect が設定されていません。")
return
if _is_playing and not restart_on_trigger:
# 再生中は無視するモード
return
# 既存の Tween があれば止めて破棄
if _tween:
_tween.kill()
_tween = null
_is_playing = true
_t = 0.0
# アニメーション全体の長さ(秒)
var total_time := fade_in_time + fade_out_time
# Tween を作成して、_t を 0 → 1 に動かす
_tween = create_tween()
_tween.tween_property(self, "_t", 1.0, total_time).set_trans(Tween.TRANS_LINEAR).set_ease(Tween.EASE_IN_OUT)
_tween.finished.connect(_on_tween_finished)
# 即座に 1 フレーム分更新して、ラグを感じさせないようにする
_update_vignette(0.0)
func _process(delta: float) -> void:
if _is_playing:
_update_vignette(delta)
func _update_vignette(delta: float) -> void:
if not is_instance_valid(target_texture_rect):
return
# _t は Tween によって 0.0〜1.0 の範囲で変化する
var curve_t := clamp(_t, 0.0, 1.0)
var intensity := intensity_curve.sample(curve_t) # 0.0〜1.0
# カーブの値に応じてアルファを変化させる
var new_alpha := max_alpha * intensity
var color := target_texture_rect.modulate
color.a = new_alpha
target_texture_rect.modulate = color
func _on_tween_finished() -> void:
_is_playing = false
_tween = null
# 念のため完全に消灯しておく
if is_instance_valid(target_texture_rect):
var color := target_texture_rect.modulate
color.a = 0.0
target_texture_rect.modulate = color
## 外部から「強制的に消す」ためのAPI
func clear() -> void:
if _tween:
_tween.kill()
_tween = null
_is_playing = false
_t = 0.0
if is_instance_valid(target_texture_rect):
var color := target_texture_rect.modulate
color.a = 0.0
target_texture_rect.modulate = color
使い方の手順
ここでは、典型的な「プレイヤーがダメージを受けたら画面端が赤く光る」HUD 構成を例にします。
手順①:HUD シーンに TextureRect と DamageVignette を配置する
まずは UI / HUD 用のシーンを用意します。例として:
HUD (CanvasLayer)
└── Root (Control)
├── HealthBar (TextureProgressBar)
├── Crosshair (TextureRect)
├── DamageVignetteTexture (TextureRect) <-- 赤いビネット画像を貼る
└── DamageVignette (Node) <-- コンポーネントをアタッチ
DamageVignetteTextureに、画面端が暗く(赤く)なった PNG などを設定します。- アンカーをフルスクリーン にして、画面全体を覆うようにしておきましょう。
(インスペクタの「アンカーをフル矩形に」ボタンを押すと楽です) DamageVignetteノードを追加し、スクリプトに上記のDamageVignette.gdをアタッチします。
エディタ上で DamageVignette を選択し、インスペクタで以下を設定します。
target_texture_rectにDamageVignetteTextureをドラッグ&ドロップmax_alpha:0.6〜0.8 くらいがおすすめfade_in_time:0.1 秒前後fade_out_time:0.3〜0.5 秒くらい
target_texture_rect を指定し忘れても、同じ親に TextureRect が 1 つだけなら自動で拾ってくれますが、明示的に設定しておく方が安心です。
手順②:プレイヤーから DamageVignette を呼び出す
次に、プレイヤーがダメージを受けたときに trigger() を呼ぶようにします。プレイヤー側は「ダメージ演出」の中身を知らなくて OK です。
例:プレイヤーシーン構成
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── PlayerDamageHandler (Node) <-- ダメージ処理用コンポーネント(例)
HUD シーンは別に読み込んでいる想定です。例えば、メインシーンはこんな感じ:
Main (Node)
├── Player (CharacterBody2D)
└── HUD (CanvasLayer)
└── Root (Control)
├── HealthBar (TextureProgressBar)
├── Crosshair (TextureRect)
├── DamageVignetteTexture (TextureRect)
└── DamageVignette (Node)
プレイヤー側から HUD の DamageVignette を参照する方法はいくつかありますが、シンプルな例としては「オートロード(シングルトン)の GameUI 経由」が扱いやすいです。
GameUI.gd(オートロード)例:
extends Node
## HUD シーンをツリーにインスタンスしたあとでセットする想定
var damage_vignette: DamageVignette
func register_hud(hud_root: Node) -> void:
# どこかにある DamageVignette を探して登録
damage_vignette = hud_root.get_node_or_null("DamageVignette")
if damage_vignette == null:
push_warning("[GameUI] DamageVignette が HUD に見つかりませんでした。")
func play_damage_vignette() -> void:
if damage_vignette:
damage_vignette.trigger()
HUD のスクリプト側で登録:
extends CanvasLayer
func _ready() -> void:
# この CanvasLayer 直下の Root などを GameUI に登録
GameUI.register_hud(self)
プレイヤー側でダメージ時に呼ぶ:
extends CharacterBody2D
var hp := 100
func apply_damage(amount: int) -> void:
hp -= amount
if hp < 0:
hp = 0
# HPバー更新など…
# ダメージ演出を再生
GameUI.play_damage_vignette()
これで、プレイヤーがダメージを受けるたびに、HUD の DamageVignette コンポーネントが赤いフラッシュを再生してくれます。
手順③:敵用デバッグシーンや別キャラにも簡単に流用
ダメージ演出は「プレイヤーに紐づいていない」ので、敵用のデバッグシーンを作るときも、単に GameUI.play_damage_vignette() を呼ぶだけで同じ演出が使えます。
例えば、敵シーン内で「テスト用にスペースキーでダメージを受ける」コードを書いても:
extends CharacterBody2D
func _process(delta: float) -> void:
if Input.is_action_just_pressed("ui_accept"):
# ダメージテスト
GameUI.play_damage_vignette()
HUD 側のコンポーネントをまったく触らずに、どのシーンからでも同じ UI 演出を呼べます。
手順④:ビネット画像やカーブを変えて、好みの表現にチューニング
最後に、見た目の調整です。
- ビネット画像(TextureRect の Texture) を差し替えることで、
・画面端だけ赤く
・画面全体を薄く赤く
・血しぶき風のテクスチャ
など、いろいろな表現ができます。 intensity_curveを編集すると、
・最初は弱く、途中で一気に強く
・序盤だけ強くて、すぐにスッと引く
といった時間変化を細かくコントロールできます。
カーブはインスペクタから直接いじれるので、ゲームを再生しながら「いい感じの立ち上がり/引き具合」を探してみましょう。
メリットと応用
DamageVignette をコンポーネントとして分離しておくと、こんなメリットがあります。
- UI ロジックがプレイヤーから独立する
プレイヤーのスクリプトは「HPを減らす」「死んだらシグナルを出す」などゲームロジックだけに集中させて、
「どう見せるか」は UI コンポーネントに任せられます。 - シーン構造がスッキリする
HUD シーンの中で、「HPバー」「クロスヘア」「ダメージビネット」などをそれぞれ独立したコンポーネントとして管理できるので、
深い階層や巨大な 1 ファイル UI スクリプトから解放されます。 - 別プロジェクトへのコピペがしやすい
DamageVignette.gdとビネット用 TextureRect をセットで持っていくだけで、別ゲームでも即再利用できます。
継承ベースでガッチリ組んだ UI よりも、合成ベースのコンポーネントの方が「持ち運びやすい」のが大きな利点ですね。
応用としては:
- HP が低いときに、
trigger()を連打して「鼓動するような赤フラッシュ」にする - 毒ダメージ用に「緑のビネット」、スタミナ切れ用に「青のビネット」など、色違いコンポーネントを複数置く
- 3D ゲームでも、画面全体を覆うビネットとして同じ仕組みを流用
といった使い方もできます。
改造案:HP割合に応じて自動でビネットの強さを変える
例えば「HP が減るほどビネットが濃くなる」ようにしたい場合、こんなメソッドを追加するのもアリです。
## HP 割合(0.0〜1.0)に応じて max_alpha を変化させる例。
## プレイヤーの HP 更新時に呼び出してください。
func set_intensity_by_health_ratio(health_ratio: float) -> void:
# health_ratio が低いほど強く光らせたいので、1 - ratio を使う
var danger := clamp(1.0 - health_ratio, 0.0, 1.0)
# 0.2〜0.8 の範囲で線形に変化させる
max_alpha = lerp(0.2, 0.8, danger)
あとはプレイヤー側で:
func _update_health_ui() -> void:
var ratio := float(hp) / float(max_hp)
health_bar.value = ratio * 100.0
if GameUI.damage_vignette:
GameUI.damage_vignette.set_intensity_by_health_ratio(ratio)
のように呼び出せば、HP が減るほど「被弾枠」が濃くなり、常にピンチ感を演出できます。
こんな感じで、小さな UI 演出も 1 つのコンポーネントとして切り出しておくと、後からの拡張や調整がとても楽になるので、ぜひ「継承より合成」で組んでいきましょう。




