Godotでアクションゲームを書いていると、プレイヤーや敵に「ダメージを受けた直後は無敵時間(いわゆる i-frames)」を付けたくなりますよね。
でも、そのたびに…

  • プレイヤーシーンのスクリプトに無敵フラグやタイマー処理を書き足す
  • 敵シーンにも同じようなロジックをコピペする
  • Sprite の点滅処理を各キャラごとに微妙に書き換える

…みたいな「コピペ地獄」や、「親クラスに無敵処理をまとめたけど継承ツリーがどんどん太る」問題にハマりがちです。

そこでこの記事では、どのキャラにもポン付けできるコンポーネントとして、「Invincibility(無敵時間)」コンポーネントを作っていきます。
「ダメージを受けたら Hurtbox の監視を一時停止し、親ノードを点滅させる」という処理を、1コンポーネントに合成するだけで実現してみましょう。

【Godot 4】被弾後は自動で点滅&ノーダメ!「Invincibility」コンポーネント

このコンポーネントは次のようなことをやってくれます。

  • 指定時間だけ Hurtbox の監視を停止(無敵時間)
  • その間、親の見た目を点滅させる(Sprite2D or CanvasItem の modulate を利用)
  • ダメージ側のスクリプトは、invincibility.start() を呼ぶだけ

つまり、プレイヤーでも敵でも動く床でも、「ダメージを受ける可能性があるノード」にこのコンポーネントをアタッチして、Hurtbox から呼び出すだけでOKです。


フルコード:Invincibility.gd


extends Node
class_name Invincibility
## 無敵時間コンポーネント
## - Hurtbox など「当たり判定ノード」の監視を一時的に OFF にする
## - 親ノード(または指定ノード)を点滅させる

## 無敵時間の長さ(秒)
@export_range(0.0, 10.0, 0.1)
var duration: float = 1.0

## 点滅の間隔(秒)
@export_range(0.01, 1.0, 0.01)
var blink_interval: float = 0.1

## 点滅させる対象ノード(未指定なら親ノードを使う)
## Sprite2D, AnimatedSprite2D, Node2D, Control など CanvasItem を想定
@export var target_visual: CanvasItem

## 無敵中に OFF にする Hurtbox ノード
## Area2D や Hurtbox コンポーネントなど、監視を一時的に止めたいノード
@export var hurtbox_node: Node

## 無敵中に Hurtbox を無効化する方法
## - "monitoring" : Area2D の monitoring プロパティを OFF
## - "disabled"   : CollisionShape2D の disabled プロパティを ON
## - "visible"    : Hurtbox の visible を OFF(UI的な判定可視化向け)
@export_enum("monitoring", "disabled", "visible")
var disable_mode: String = "monitoring"

## 無敵中かどうか
var _is_invincible: bool = false

## 内部タイマー
var _invincible_timer: Timer
var _blink_timer: Timer

## オリジナルの状態を保存しておく
var _original_modulate: Color
var _original_visible: bool = true

func _ready() -> void:
	# ターゲットが指定されていなければ親ノードを対象にする
	if target_visual == null:
		if owner is CanvasItem:
			target_visual = owner
		elif get_parent() is CanvasItem:
			target_visual = get_parent()
	
	if target_visual:
		_original_modulate = target_visual.modulate
		_original_visible = target_visual.visible
	
	# タイマーを2つ用意(無敵時間管理用 & 点滅用)
	_invincible_timer = Timer.new()
	_invincible_timer.one_shot = true
	_invincible_timer.autostart = false
	add_child(_invincible_timer)
	_invincible_timer.timeout.connect(_on_invincible_timeout)
	
	_blink_timer = Timer.new()
	_blink_timer.one_shot = false
	_blink_timer.autostart = false
	_blink_timer.wait_time = blink_interval
	add_child(_blink_timer)
	_blink_timer.timeout.connect(_on_blink_timeout)


## 公開API
## ダメージを受けた側から呼び出す:
##   $Invincibility.start()
## または、Hurtbox から:
##   invincibility.start()
func start(new_duration: float = -1.0) -> void:
	if new_duration > 0.0:
		duration = new_duration
	
	if duration <= 0.0:
		return
	
	if _is_invincible:
		# すでに無敵中なら、タイマーだけ延長する
		_invincible_timer.start(duration)
		return
	
	_is_invincible = true
	_apply_hurtbox_disabled(true)
	
	# 無敵時間タイマー開始
	_invincible_timer.start(duration)
	
	# 点滅タイマー開始
	if target_visual:
		_blink_timer.wait_time = blink_interval
		_blink_timer.start()


## 無敵状態かどうかを問い合わせる
func is_invincible() -> bool:
	return _is_invincible


## 無敵を強制終了する(例: シーン切り替え時など)
func stop() -> void:
	if not _is_invincible:
		return
	
	_is_invincible = false
	_apply_hurtbox_disabled(false)
	
	_invincible_timer.stop()
	_blink_timer.stop()
	_reset_visual()


## Hurtbox の有効/無効を切り替える
func _apply_hurtbox_disabled(disabled: bool) -> void:
	if hurtbox_node == null:
		return
	
	match disable_mode:
		"monitoring":
			# Area2D の監視ON/OFF
			if hurtbox_node.has_method("set_monitoring"):
				hurtbox_node.set("monitoring", not disabled)
		"disabled":
			# CollisionShape2D の disabled プロパティ
			if hurtbox_node.has_method("set_disabled"):
				hurtbox_node.set("disabled", disabled)
			elif hurtbox_node.has_method("set_deferred"):
				hurtbox_node.set_deferred("disabled", disabled)
		"visible":
			if "visible" in hurtbox_node:
				hurtbox_node.visible = not disabled


## 無敵時間が終わったとき
func _on_invincible_timeout() -> void:
	_is_invincible = false
	_apply_hurtbox_disabled(false)
	
	# 点滅終了
	_blink_timer.stop()
	_reset_visual()


## 点滅タイマーのコールバック
func _on_blink_timeout() -> void:
	if not target_visual:
		return
	
	# 2パターン:modulate を薄くする or visible を ON/OFF
	# ここでは modulate のアルファを切り替える簡易版
	var c := target_visual.modulate
	if c.a < 1.0:
		c.a = 1.0
	else:
		c.a = 0.2
	target_visual.modulate = c


## 見た目を元に戻す
func _reset_visual() -> void:
	if not target_visual:
		return
	target_visual.modulate = _original_modulate
	target_visual.visible = _original_visible

使い方の手順

ここでは 2D アクションゲームのプレイヤーを例にして説明します。敵やトゲ床などにも同じ手順で付けられます。

シーンにコンポーネントをアタッチ
まず、上のコードを res://components/invincibility/Invincibility.gd などに保存します。
その後、プレイヤーシーンに Invincibility ノードを追加します。

Player (CharacterBody2D)
├── Sprite2D
├── CollisionShape2D
├── Hurtbox (Area2D)
│ └── CollisionShape2D
└── Invincibility (Node) <-- このコンポーネント

Inspector でパラメータを設定

duration … 無敵時間(例: 1.0 秒)

blink_interval … 点滅の速さ(例: 0.1 秒)

target_visual … 点滅させたい Sprite2D(未指定なら親の CanvasItem)

hurtbox_node … Hurtbox(Area2D や CollisionShape2D)をドラッグ&ドロップ

disable_mode … Hurtbox の無効化方法(Area2D なら "monitoring" 推奨)

Hurtbox から Invincibility を呼び出す
Hurtbox(Area2D)側でダメージ判定が発生したら、親の Invincibility を呼び出します。


# 例: Hurtbox.gd(Area2D にアタッチ)
extends Area2D

@onready var invincibility: Invincibility = get_parent().get_node(“Invincibility”)

func _on_area_entered(area: Area2D) -> void:
# すでに無敵中ならダメージを無視
if invincibility and invincibility.is_invincible():
return

# ここでHPを減らすなどの処理
get_parent().call(“apply_damage”, 1)

# 無敵時間を開始
if invincibility:
invincibility.start()


プレイヤー(親)側は「ダメージ処理」だけに集中
プレイヤー本体のスクリプトには、HP 管理やノックバックなどだけを書きます。
無敵時間や点滅はコンポーネントに丸投げできます。


# Player.gd
extends CharacterBody2D

var hp: int = 5

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

func die() -> void:
queue_free()


同じノリで、敵キャラやトゲ床にも Invincibility をアタッチしておけば、「ダメージを受けるオブジェクト」全員が同じ無敵ロジックを共有できます。


別の使用例:敵キャラにもそのまま合成

敵キャラシーンでもやることは同じです。

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── Hurtbox (Area2D)
 └── Invincibility (Node)

Hurtbox 側のスクリプトはプレイヤーとほぼ同じで、get_parent().apply_damage() を呼ぶ相手が変わるだけです。
「無敵+点滅」のロジックは完全に共通化されているので、敵の種類が増えてもコンポーネントをペタペタ貼るだけで済みます。


メリットと応用

この「Invincibility」コンポーネントを使うことで、次のようなメリットがあります。

  • シーン構造がスッキリ
    プレイヤーや敵のスクリプトから「無敵時間」「点滅」「Hurtbox 無効化」のロジックを追い出せるので、
    本体スクリプトは「移動」「攻撃」「HP管理」に集中できます。
  • 再利用性が高い
    プレイヤー・敵・トラップなど、「ダメージを受けるなら何にでも」そのまま使えます。
    継承ツリーをいじらずに、コンポーネントを合成するだけで機能追加できるのが気持ちいいところですね。
  • パラメータ駆動でレベルデザインが楽
    敵Aは 0.5秒、敵Bは 2秒の無敵時間、といった調整を Inspector 上でポチポチ変えるだけでテストできます。
  • Hurtbox の実装に依存しにくい
    disable_mode を変えるだけで、Area2D / CollisionShape2D / 可視Hurtbox などに対応できます。

応用として、例えば「無敵時間中は色を赤くする」「シェーダーで点滅する」なども簡単に拡張できます。
下のように _on_blink_timeout() を差し替えれば、色変更ベースの点滅にできます。


func _on_blink_timeout() -> void:
	if not target_visual:
		return
	
	# 赤っぽく点滅させる例
	var c := target_visual.modulate
	if c == _original_modulate:
		target_visual.modulate = Color(1.0, 0.4, 0.4, 1.0)
	else:
		target_visual.modulate = _original_modulate

こんな感じで、「無敵」というよくあるロジックを 1 コンポーネントに閉じ込めておくと、
あとから「全キャラの無敵演出を一括で変えたい」となったときも、このスクリプトだけをいじれば全員に反映されます。
継承ツリーをいじるより、コンポーネントを合成していくスタイルのほうが、長期的にかなり楽になりますよ。