Godotでアクションゲームを作っていると、「当たり判定は取れてるけど、ヒット感が薄い…」という悩みが出てきますよね。
一番手軽なやり方は、プレイヤーや敵のスクリプトに「ダメージ時は色を変える処理」を直書きすることですが、
- プレイヤー、敵、壊れるオブジェクトなど、全部に同じような処理を書きがち
- 「白フラッシュの時間」「強さ」を変えたい時に、複数スクリプトを修正する羽目になる
- シェーダーを使った表現を試したいのに、各キャラごとにマテリアル設定をいじるのが面倒
といった「継承&コピペ地獄」に陥りやすいです。
そこで今回は、「ヒットしたら一瞬だけ真っ白になる」表現を、どのキャラにもポン付けできる
コンポーネント型として切り出した FlashWhite(ヒット閃光)コンポーネントを作ってみましょう。
攻撃ヒット時に flash() を呼ぶだけで、シェーダーでスプライトを白くフラッシュしてくれるようになります。
もちろん、プレイヤー・敵・動く床・ギミックなど、どんなスプライト系ノードにも再利用可能です。
【Godot 4】当たり判定に「バチッ」とした手応えを!「FlashWhite」コンポーネント
このコンポーネントは、
- 対象スプライトに専用シェーダーマテリアルを自動でセット
- 攻撃ヒット時などに
flash()を呼ぶだけで、指定時間だけ真っ白フラッシュ - フラッシュ時間・強さ・対象ノードを
@exportで柔軟に設定
という構成になっています。
「ダメージ処理」は各キャラのスクリプトに任せて、「見た目の演出」はこのコンポーネントに丸投げ、という分離ができるのがポイントですね。
フルコード(GDScript + シェーダー)
以下を res://components/flash_white.gd のように保存して使ってください。
extends Node
class_name FlashWhite
## FlashWhite(ヒット閃光)コンポーネント
##
## 攻撃ヒット時などに flash() を呼ぶと、
## 対象スプライトが一瞬だけ真っ白にフラッシュします。
##
## 「見た目の演出」をコンポーネントに閉じ込めることで、
## プレイヤー・敵・ギミックなど、どこからでも簡単に再利用できます。
@export_group("ターゲット設定")
## フラッシュ対象のノード。
## 通常は Sprite2D / AnimatedSprite2D / MeshInstance2D などを指定します。
## 未指定の場合、親ノードから自動で Sprite2D を探します。
@export var target_node: NodePath
@export_group("フラッシュ設定")
## フラッシュしている時間(秒)
@export_range(0.01, 1.0, 0.01)
var flash_duration: float = 0.1
## フラッシュの強さ(0.0〜1.0)。
## 1.0 で完全に真っ白、0.5 なら少しだけ白くなります。
@export_range(0.0, 1.0, 0.05)
var flash_intensity: float = 1.0
## すでにフラッシュ中に flash() を呼んだ時の挙動。
## true ならフラッシュ時間を延長、false なら無視。
@export var extend_if_flashing: bool = true
## デバッグ用に、シーン再生直後に一度だけフラッシュさせるか
@export var test_flash_on_ready: bool = false
# 内部用: 実際にマテリアルを持つ CanvasItem (Sprite2D 等)
var _canvas_item: CanvasItem
# 内部用: 生成した ShaderMaterial
var _shader_material: ShaderMaterial
# 内部用: フラッシュ残り時間
var _flash_timer: float = 0.0
# シェーダーコード(2D用)。Sprite2D, AnimatedSprite2D などで使用可能。
const FLASH_SHADER_CODE := """
shader_type canvas_item;
uniform float flash_strength : hint_range(0.0, 1.0) = 0.0;
void fragment() {
vec4 tex_color = texture(TEXTURE, UV);
// 通常の色と「真っ白」の線形補間
vec3 flashed = mix(tex_color.rgb, vec3(1.0, 1.0, 1.0), flash_strength);
COLOR = vec4(flashed, tex_color.a);
}
"""
func _ready() -> void:
_resolve_target()
_ensure_shader_material()
if test_flash_on_ready:
flash()
func _process(delta: float) -> void:
if _flash_timer > 0.0:
_flash_timer -= delta
if _flash_timer <= 0.0:
_set_flash_strength(0.0)
## パブリックAPI: 外部から呼ぶ用
## ダメージを受けた時などに呼び出してください。
func flash() -> void:
if not _canvas_item:
push_warning("FlashWhite: 有効な target_node が見つからないため、flash() を無視しました。")
return
if _flash_timer > 0.0 and not extend_if_flashing:
# すでにフラッシュ中で、延長しない設定なら何もしない
return
_flash_timer = flash_duration
_set_flash_strength(flash_intensity)
# --- 内部処理ここから ---
## ターゲットノード(CanvasItem)を解決する
func _resolve_target() -> void:
var node: Node = null
if target_node != NodePath():
node = get_node_or_null(target_node)
if node == null:
push_warning("FlashWhite: target_node '%s' が見つかりません。親から Sprite2D を自動検索します。" % target_node)
if node == null:
# 親ノードから Sprite2D / AnimatedSprite2D などを探す
var parent := get_parent()
if parent is CanvasItem:
node = parent
else:
# 親の子孫から探す(最初に見つかった CanvasItem を採用)
for child in parent.get_children():
if child is CanvasItem:
node = child
break
if node and node is CanvasItem:
_canvas_item = node
else:
push_warning("FlashWhite: CanvasItem ターゲットが見つかりませんでした。flash() は無視されます。")
## ターゲットにシェーダーマテリアルをセットする
func _ensure_shader_material() -> void:
if not _canvas_item:
return
# すでに ShaderMaterial が設定されている場合は、そのまま拡張して使う
var current_material := _canvas_item.material
if current_material is ShaderMaterial:
_shader_material = current_material
# 既存のシェーダーを上書きしたくない場合は、
# ここで「ラップ用シェーダー」を作るなどの工夫も可能です。
# 今回はシンプルに「ヒットフラッシュ専用」として扱います。
_shader_material.shader = Shader.new()
_shader_material.shader.code = FLASH_SHADER_CODE
else:
# 新しく ShaderMaterial を作成
_shader_material = ShaderMaterial.new()
_shader_material.shader = Shader.new()
_shader_material.shader.code = FLASH_SHADER_CODE
_canvas_item.material = _shader_material
# 初期状態ではフラッシュなし
_set_flash_strength(0.0)
## シェーダーの uniform を更新する
func _set_flash_strength(value: float) -> void:
if _shader_material:
_shader_material.set_shader_parameter("flash_strength", clamp(value, 0.0, 1.0))
使い方の手順
ここでは代表的な3パターンを例にします。
- ① プレイヤーキャラにヒット閃光をつける
- ② 敵キャラにヒット閃光をつける
- ③ 壊れる箱(ギミック)にヒット閃光をつける
手順①:コンポーネントをシーンに追加する
プレイヤーシーンの例:
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── FlashWhite (Node) ← このスクリプトをアタッチ
- 上記のように、Sprite2D と同じ階層に
Nodeを1つ追加します。 - その Node に、先ほどの
FlashWhite.gdをアタッチします。 target_nodeを空のままにしておくと、親ノード(Player)から最初に見つかった CanvasItem を自動で使います。
明示的に指定したい場合は、target_node = "../Sprite2D"のようにパスを設定しましょう。
敵シーンの例:
Enemy (CharacterBody2D) ├── AnimatedSprite2D ├── CollisionShape2D └── FlashWhite (Node)
壊れる箱シーンの例:
BreakableCrate (StaticBody2D) ├── Sprite2D ├── CollisionShape2D └── FlashWhite (Node)
どのシーンでも「Sprite系ノードの近くにコンポーネントをぶら下げる」だけでOKです。
この「ノード階層を深くせず、横にコンポーネントを並べる」スタイルが、合成(Composition)志向のポイントですね。
手順②:ダメージ処理から flash() を呼ぶ
プレイヤーのスクリプト側では、ダメージを受けたタイミングで flash() を呼びます。
# Player.gd (例)
extends CharacterBody2D
@onready var flash_white: FlashWhite = $FlashWhite
var hp: int = 10
func apply_damage(amount: int) -> void:
hp -= amount
if flash_white:
flash_white.flash()
if hp <= 0:
die()
func die() -> void:
queue_free()
敵キャラでも同じように:
# Enemy.gd (例)
extends CharacterBody2D
@onready var flash_white: FlashWhite = $FlashWhite
func hit_by_player(damage: int) -> void:
# HP処理など
if flash_white:
flash_white.flash()
壊れる箱でも同様です:
# BreakableCrate.gd (例)
extends StaticBody2D
@onready var flash_white: FlashWhite = $FlashWhite
func apply_hit() -> void:
if flash_white:
flash_white.flash()
# ちょっとだけ待ってから壊す、などの演出もOK
queue_free()
手順③:パラメータを調整する
FlashWhite ノードを選択すると、インスペクタに以下のパラメータが出てきます。
- flash_duration … 白く光っている時間(秒)
→ 素早いアクションなら 0.05〜0.1 秒くらいがオススメです。 - flash_intensity … 白さの強さ(0〜1)
→ 1.0 だと完全に真っ白、0.6 くらいにすると「ちょっとだけ白くなる」柔らかい表現になります。 - extend_if_flashing … 連続ヒット時にフラッシュ時間を延長するか
→ コンボゲームなど、ヒットが連続する場合は true にすると「光りっぱなし」になりやすいです。
→ false にすると「1発ごとにチカッと光る」印象になります。 - test_flash_on_ready … 再生開始時に一度だけテストフラッシュ
→ シェーダーがちゃんと効いているか確認したい時に使いましょう。
手順④:複数のキャラにコピペで適用
一度 FlashWhite コンポーネントを作ってしまえば、
- 敵Aシーンに FlashWhite を追加して、
flash_duration = 0.08 - 敵Bシーンにも追加して、
flash_intensity = 0.5(控えめな演出) - ボスシーンには、
flash_duration = 0.15(長めに光らせる)
というように、見た目だけをパラメータで調整できます。
ダメージのロジックは各スクリプトに任せつつ、視覚効果はコンポーネントに集約できるのが嬉しいところですね。
メリットと応用
この FlashWhite コンポーネントを導入すると、次のようなメリットがあります。
- シーン構造がスッキリ
各キャラのスクリプトに「色変更ロジック」を書かなくてよくなり、
ノード構造も「キャラ本体 + 見た目 + コンポーネント」というシンプルな形に保てます。 - 使い回しが超ラク
プレイヤー、敵、ギミックなど、「当たり判定があるもの全部」に同じ演出を一瞬で適用できます。
継承ツリーをいじらなくても、Node を1個足してスクリプトをアタッチするだけです。 - 演出の差し替えが容易
「白フラッシュじゃなくて、赤フラッシュにしたい」「アウトラインを光らせたい」などの要望が出た時も、
コンポーネント側のシェーダーを差し替えるだけで、全キャラに一括適用できます。 - レベルデザインがやりやすい
レベルデザイナーが「この敵だけ派手に光らせたい」と思ったら、
エディタからflash_intensityをいじるだけで完結します。スクリプト改修は不要です。
コンポーネント指向にしておくことで、「演出を変えたい」「別のゲームでも使いたい」といった時に、
FlashWhite.gd だけをコピペして持っていけば済むのがとても大きいです。
改造案:ダメージ量に応じてフラッシュ強度を変える
ちょっとした応用として、「ダメージが大きいほど強く光る」APIを追加してみましょう。
## 追加API: ダメージ量に応じてフラッシュ強度を変える
func flash_with_damage(damage: float, max_damage: float = 100.0) -> void:
if max_damage <= 0.0:
flash()
return
# ダメージ量を 0.0〜1.0 に正規化して、intensity に反映
var ratio := clamp(damage / max_damage, 0.0, 1.0)
var original_intensity := flash_intensity
flash_intensity = lerp(0.3, 1.0, ratio)
flash()
# 呼び出し後は元の設定に戻す
flash_intensity = original_intensity
敵側では、
func hit_by_player(damage: int) -> void:
if flash_white:
flash_white.flash_with_damage(damage, 50.0)
のように使えば、大ダメージの時だけ強く光る表現が簡単に作れます。
このように、「見た目の演出」をどんどんコンポーネント化していくと、
継承に縛られない、柔軟な Godot プロジェクトが組めるようになります。
ぜひ自分のプロジェクト用に、FlashWhite をベースにしたエフェクトコンポーネントを増やしていきましょう。
