【Godot 4】VignetteDamage (瀕死視界) コンポーネントの作り方

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

Godot 4 でアクションゲームやローグライクを作っていると、「HP が減ったときの演出」って意外と面倒ですよね。
典型的なやり方は、

  • プレイヤーシーンの中に CanvasLayer を生やす
  • その下に ColorRectTextureRect を置いて、スクリプトでアルファ値をいじる
  • さらに HP 管理スクリプトから直接そのノードを参照して制御する

……と、だんだん「プレイヤーシーンが UI まみれ」になっていきます。
HP の管理クラスと、画面エフェクトのクラスがガッツリ結合してしまって、あとから UI だけ別シーンに分けたいときにツラくなるパターンですね。

そこで今回は、「HP が減ると画面の四隅が赤黒く暗くなるビネット効果」を、
どのシーンにもポン付けできるコンポーネントとして切り出してみましょう。
プレイヤーにも敵にもボス専用 HUD にも、同じコンポーネントをアタッチするだけで瀕死演出を共有できるようにします。


【Godot 4】HP連動ビネットで瀕死を「見せる」!「VignetteDamage」コンポーネント

VignetteDamage は、

  • HP の現在値と最大値を受け取る
  • HP が減るほど画面の四隅を赤黒く暗くする
  • 画面全体を覆うビネットを CanvasLayer + ColorRect で自動生成

という「瀕死視界」専用のコンポーネントです。
HP のロジックとはゆるく接続しつつ、UI 側はこのコンポーネントに丸投げしてしまいましょう。


フルコード:VignetteDamage.gd


extends CanvasLayer
class_name VignetteDamage
## HP が減ると画面の四隅を赤黒く暗くするビネットコンポーネント
##
## ・どのシーンにもポン付けできる CanvasLayer ベース
## ・HP の現在値 / 最大値をセットすると、自動でビネットの強さを更新
## ・ビネットの色、最大強度、開始しきい値などをエディタから調整可能
##
## 想定使用例:
##   - プレイヤーの「瀕死視界」
##   - ボス戦だけ強めのビネット
##   - ホラーゲームの「SAN値」的な視界演出

@export_category("HP Settings")
## 現在HP。ゲーム側から都度更新してもらう想定
@export var current_hp: float = 100.0:
	set(value):
		current_hp = max(value, 0.0)
		_update_vignette()

## 最大HP。0 だと計算できないので注意
@export var max_hp: float = 100.0:
	set(value):
		max_hp = max(value, 1.0)
		_update_vignette()

## どの HP 割合からビネットを出し始めるか (0.0〜1.0)
## 例: 0.5 なら HP 50% 以下でビネットが徐々に出てくる
@export_range(0.0, 1.0, 0.01)
@export var start_ratio: float = 0.5:
	set(value):
		start_ratio = clampf(value, 0.0, 1.0)
		_update_vignette()

@export_category("Visual Settings")
## ビネットの基本色。赤黒い感じにしたいなら (0.5, 0, 0, 1) など
@export var vignette_color: Color = Color(0.4, 0.0, 0.0, 1.0):
	set(value):
		vignette_color = value
		_update_vignette_color()

## HP が 0 のときの最大アルファ値 (0.0〜1.0)
@export_range(0.0, 1.0, 0.01)
@export var max_intensity: float = 0.8:
	set(value):
		max_intensity = clampf(value, 0.0, 1.0)
		_update_vignette()

## ビネットのフェード速度 (HP変化に追従するスムージング係数)
## 0.0 に近いほどゆっくり、1.0 で即時反映
@export_range(0.0, 1.0, 0.01)
@export var follow_speed: float = 0.25

@export_category("Shape Settings")
## 画面のどの範囲を「中央の安全地帯」とするか (0〜1)
## 例: 0.6 → 中央60%は比較的明るく、外側40%が暗くなる
@export_range(0.1, 1.5, 0.05)
@export var inner_radius: float = 0.7:
	set(value):
		inner_radius = max(value, 0.1)
		_update_shader_params()

## どのくらい外側までグラデーションを伸ばすか
@export_range(0.1, 3.0, 0.05)
@export var outer_radius: float = 1.4:
	set(value):
		outer_radius = max(value, 0.1)
		_update_shader_params()

## 画面のアスペクト比補正をするかどうか
@export var fix_aspect_ratio: bool = true:
	set(value):
		fix_aspect_ratio = value
		_update_shader_params()

## 画面全体を覆う ColorRect
var _rect: ColorRect
## 現在の目標強度 (0〜1)
var _target_strength: float = 0.0
## 実際に表示している強度 (スムージング用)
var _current_strength: float = 0.0

func _ready() -> void:
	# 画面全体を覆う ColorRect を自前で生成する
	_rect = ColorRect.new()
	_rect.name = "VignetteRect"
	_rect.color = Color.TRANSPARENT
	_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
	_rect.size_flags_horizontal = Control.SIZE_FLAGS_EXPAND_FILL
	_rect.size_flags_vertical = Control.SIZE_FLAGS_EXPAND_FILL
	add_child(_rect)

	# ビネット用のシェーダーをセットアップ
	_rect.material = _create_vignette_material()

	# 初期状態の反映
	_update_vignette_color()
	_update_shader_params()
	_update_vignette(force_immediate := true)

	# 画面サイズ変更に追従
	get_viewport().size_changed.connect(_on_viewport_resized)
	_on_viewport_resized()

func _process(delta: float) -> void:
	# HP から計算した _target_strength に向かって補間する
	if follow_speed <= 0.0:
		_current_strength = _target_strength
	else:
		var t := 1.0 - pow(1.0 - follow_speed, delta * 60.0)
		_current_strength = lerpf(_current_strength, _target_strength, t)

	# シェーダーパラメータに反映
	var mat := _get_material()
	if mat:
		mat.set_shader_parameter("strength", _current_strength * max_intensity)

## 外部から HP をまとめて更新したい場合のヘルパー
func set_hp(current: float, maximum: float) -> void:
	current_hp = current
	max_hp = maximum
	_update_vignette()

## シェーダーマテリアルの生成
func _create_vignette_material() -> ShaderMaterial:
	var shader_code := """
		shader_type canvas_item;

		// 中央からの距離に応じて四隅を暗くするシンプルなビネット
		uniform vec4 vignette_color : source_color;
		uniform float strength = 0.0;     // 0〜1, 外側の暗さ
		uniform float inner_radius = 0.7; // 中央の明るい範囲
		uniform float outer_radius = 1.4; // 暗くなる範囲の終点
		uniform bool fix_aspect_ratio = true;
		uniform vec2 screen_size = vec2(1920.0, 1080.0);

		void fragment() {
			vec2 uv = UV;

			// アスペクト比補正 (縦長/横長でもほどよく丸いビネットにする)
			if (fix_aspect_ratio) {
				float aspect = screen_size.x / max(screen_size.y, 1.0);
				uv.x = (uv.x - 0.5) * aspect + 0.5;
			}

			// 中央 (0.5, 0.5) からの距離
			float dist = distance(uv, vec2(0.5, 0.5));

			// inner_radius までは影響なし、outer_radius で最大
			float v = smoothstep(inner_radius, outer_radius, dist);

			// strength に応じてビネット色を乗算
			vec4 base = texture(SCREEN_TEXTURE, SCREEN_UV);
			vec4 overlay = vec4(vignette_color.rgb, vignette_color.a * v * strength);

			// 乗算系の暗くなるブレンド
			COLOR = mix(base, base * vignette_color, overlay.a);
		}
	"""

	var shader := Shader.new()
	shader.code = shader_code

	var mat := ShaderMaterial.new()
	mat.shader = shader
	return mat

func _get_material() -> ShaderMaterial:
	if _rect and _rect.material is ShaderMaterial:
		return _rect.material
	return null

## HP からビネット強度を計算して _target_strength を更新
func _update_vignette(force_immediate: bool = false) -> void:
	if max_hp <= 0.0:
		_target_strength = 0.0
		return

	var ratio := current_hp / max_hp  # 1.0 = フルHP, 0.0 = 瀕死
	ratio = clampf(ratio, 0.0, 1.0)

	# start_ratio 以上ならビネットなし
	if ratio >= 1.0:
		_target_strength = 0.0
	elif ratio >= start_ratio:
		# しきい値を超えるまでは 0
		_target_strength = 0.0
	else:
		# start_ratio 以下から 0→1 へ線形に強くしていく
		var t := (start_ratio - ratio) / max(start_ratio, 0.0001)
		_target_strength = clampf(t, 0.0, 1.0)

	if force_immediate:
		_current_strength = _target_strength

## 色の更新
func _update_vignette_color() -> void:
	var mat := _get_material()
	if mat:
		mat.set_shader_parameter("vignette_color", vignette_color)

## 形状パラメータの更新
func _update_shader_params() -> void:
	var mat := _get_material()
	if mat:
		mat.set_shader_parameter("inner_radius", inner_radius)
		mat.set_shader_parameter("outer_radius", outer_radius)
		mat.set_shader_parameter("fix_aspect_ratio", fix_aspect_ratio)
		mat.set_shader_parameter("screen_size", get_viewport().size)

## 画面サイズ変更時にシェーダーへ反映
func _on_viewport_resized() -> void:
	var mat := _get_material()
	if mat:
		mat.set_shader_parameter("screen_size", get_viewport().size)

使い方の手順

基本は「どこかのシーンに VignetteDamage を 1 個置いて、HP 変化時に current_hp を更新するだけ」です。
プレイヤーを例に、手順①〜④で見ていきましょう。

① スクリプトをプロジェクトに追加

  1. res://components/VignetteDamage.gd など、好きな場所に保存します。
  2. Godot を再起動 or スクリプトを保存すると、class_name VignetteDamage により
    「ノード追加」ダイアログから直接 VignetteDamage が選べるようになります。

② プレイヤーシーンにコンポーネントとしてアタッチ

典型的な 2D プレイヤーシーン構成の例:

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── Camera2D
 └── VignetteDamage (CanvasLayer)  ← 今回のコンポーネント
  • Player シーンを開く
  • ルートの Player (CharacterBody2D) を選択
  • 「子ノードを追加」→ 検索欄に「VignetteDamage」と入力 → 追加

これで、プレイヤーに「瀕死視界コンポーネント」が生えました。
HP ロジックは相変わらず Player 側に置いておいて構いません。

③ HP ロジックから VignetteDamage を更新する

例えば、プレイヤーがダメージを受けるたびに HP を減らしているとします:


# Player.gd (例)
extends CharacterBody2D

@export var max_hp: float = 100.0
var current_hp: float = 100.0

@onready var vignette: VignetteDamage = $VignetteDamage

func _ready() -> void:
	# 初期HPをコンポーネントに同期
	vignette.set_hp(current_hp, max_hp)

func apply_damage(amount: float) -> void:
	current_hp = max(current_hp - amount, 0.0)

	# ビネットコンポーネントへ通知
	vignette.current_hp = current_hp
	# もしくは:
	# vignette.set_hp(current_hp, max_hp)

	if current_hp <= 0.0:
		die()

func die() -> void:
	queue_free()

これだけで、HP が start_ratio(デフォルト 0.5)を下回ったあたりから
画面の四隅がじわ〜っと赤黒く暗くなっていきます。

④ ボス専用 HUD など、別シーンでも使い回す

コンポーネントなので、プレイヤー以外にも簡単に流用できます。

例:ボスの HP に応じて、画面全体の緊張感を上げるビネットを表示するシーン構成:

BossFightScene (Node2D)
 ├── Boss (CharacterBody2D)
 │    ├── Sprite2D
 │    └── CollisionShape2D
 ├── Camera2D
 ├── UI (CanvasLayer)
 │    ├── BossHpBar (TextureProgressBar)
 │    └── Label
 └── VignetteDamage (CanvasLayer)  ← ボス専用ビネット

この場合、BossFightScene のスクリプトからボスの HP を参照して、
シーン直下の VignetteDamage に反映するだけです:


# BossFightScene.gd (例)
extends Node2D

@onready var boss = $Boss
@onready var vignette: VignetteDamage = $VignetteDamage

func _ready() -> void:
	vignette.max_hp = boss.max_hp
	vignette.current_hp = boss.current_hp

func _process(_delta: float) -> void:
	# 毎フレーム同期してもOKだし、ダメージ時だけ呼んでもOK
	vignette.current_hp = boss.current_hp

プレイヤー用とボス用で VignetteDamage のパラメータ(色、強度、開始しきい値)を変えれば、
「プレイヤー瀕死は赤黒く」「ボス瀕死は紫がかったビネットで不穏に」みたいな演出も簡単ですね。


メリットと応用

このコンポーネント化のポイントは、「HP ロジックと画面エフェクトを疎結合にした」ことです。

  • シーン構造がスッキリ
    画面エフェクトのためだけの ColorRect やシェーダーを、各シーンごとに手作業で配置する必要がなくなります。
    「VignetteDamage を 1 個子ノードに追加する」だけで完結します。
  • 使い回しがしやすい
    プレイヤーだけでなく、ボス戦専用シーン、ホラー演出用のカメラシーンなど、
    どこにでも同じコンポーネントをペタッと貼れるので、演出の統一感も出しやすいです。
  • HP 実装の自由度を奪わない
    HP の計算は各キャラのスクリプトに任せておき、
    ただ「今の HP を教えてあげる」だけでビネットが動きます。
    将来 HP の仕様を変えても、VignetteDamage 側にはほぼ影響しません。
  • レベルデザインが楽になる
    ステージごとに「今日はちょっと暗めにしたい」「このステージはビネットなし」など、
    エディタ上の @export パラメータをいじるだけで調整できます。

「継承で PlayerUI を増やしていく」のではなく、
「必要な演出をコンポーネントとして後付けする」スタイルにすると、
大きくなったプロジェクトでも破綻しにくくなりますね。

改造案:HP ではなく「毒状態」で色を変える

例えば、毒状態のときだけ緑がかったビネットにしたい場合、
VignetteDamage にこんな関数を追加しても面白いです:


func set_poisoned(is_poisoned: bool) -> void:
	# 毒状態なら緑っぽい色と少し強めの強度に変更
	if is_poisoned:
		vignette_color = Color(0.0, 0.4, 0.0, 1.0)
		max_intensity = 0.9
		start_ratio = 1.0  # HP に関係なく常にビネットを出す
	else:
		# 通常の赤黒いビネットに戻す
		vignette_color = Color(0.4, 0.0, 0.0, 1.0)
		max_intensity = 0.8
		start_ratio = 0.5
	_update_vignette(true)

これをステータス異常管理コンポーネントから呼べば、
「毒 → 緑ビネット」「瀕死 → 赤ビネット」みたいな視覚的フィードバックも簡単に作れます。

こんな感じで、視覚効果をどんどんコンポーネント化していくと、
Godot プロジェクト全体がかなり見通し良くなっていきますよ。ぜひベースとして使い回してみてください。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!