Godot でイベント演出を作っていると、キャラやオブジェクトを「一瞬だけ点滅させたい」「会話中だけチカチカさせたい」といった場面、けっこうありますよね。
多くの人は最初、以下のような実装をしがちです。

  • プレイヤーや敵のスクリプトの中に visible を直接いじる処理を書く
  • Timer ノードを子として生やして、そこにロジックを書き込む
  • 「点滅する敵」「点滅する床」など、点滅専用の派生シーンを作って継承で増やしていく

このやり方でも動きはしますが、次第にこんな問題が出てきます。

  • 「点滅ロジック」があちこちのスクリプトにコピペされてメンテがつらい
  • 「このシーンは Timer が 3 つ、あのシーンは 2 つ…」とノード構造がカオス
  • 「この敵だけ点滅周期変えたい」と思ったら継承ツリーがどんどん深くなる

そこで今回は、「点滅したいノードにポン付けするだけ」で使えるコンポーネント
BlinkEffect を用意しました。
親ノードの visible を一定間隔で切り替えるだけの、シンプルだけど使い回し抜群のコンポーネントです。

【Godot 4】演出用の点滅はこれ一個でOK!「BlinkEffect」コンポーネント

このコンポーネントは「継承」ではなく「合成(Composition)」で点滅機能を付与します。
プレイヤーでも敵でも、UI でも動く床でも、「点滅させたいノードの子」に BlinkEffect を 1 個付けるだけ。
あとはエディタ上の @export パラメータで、点滅速度や開始タイミングを調整しましょう。

フルコード:BlinkEffect.gd


## BlinkEffect.gd
## 親ノードの visible を一定間隔で ON/OFF するコンポーネント
## いろんなノードに「点滅機能」を後付けするためのスクリプトです。

class_name BlinkEffect
extends Node

## --- 設定パラメータ(インスペクタから調整できます) ---

@export var auto_start: bool = true:
	## シーンが ready になったときに自動で点滅を開始するかどうか。
	## false にすると、スクリプトから start() を呼んだときだけ点滅します。
	get:
		return auto_start
	set(value):
		auto_start = value

@export_range(0.01, 10.0, 0.01, "suffix:s") var interval: float = 0.2:
	## 点滅の間隔(秒)。
	## 0.2 なら、0.2 秒ごとに visible を切り替えます。
	get:
		return interval
	set(value):
		interval = max(0.01, value) # ゼロや負数を防ぐ
		_update_timer_settings()

@export var initial_visible: bool = true:
	## 点滅開始時の「最初の visible 状態」。
	## true にすると最初は見えている状態から、false にすると見えない状態から始まります。
	get:
		return initial_visible
	set(value):
		initial_visible = value

@export var loop: bool = true:
	## ずっと点滅し続けるかどうか。
	## false にすると、blink_count 回だけ点滅して止まります。
	get:
		return loop
	set(value):
		loop = value

@export_range(1, 9999, 1) var blink_count: int = 10:
	## loop = false のとき、何回点滅したら止めるか。
	## 「1 回点滅」は、ON→OFF で 1 回と数えます。
	get:
		return blink_count
	set(value):
		blink_count = max(1, value)

@export var respect_parent_visibility: bool = true:
	## 親が非表示のときは強制的に非表示にするかどうか。
	## true にすると、親.visible = false のときは強制的に見えません。
	get:
		return respect_parent_visibility
	set(value):
		respect_parent_visibility = value
		_apply_visibility()

@export var enabled: bool = true:
	## コンポーネント自体の有効/無効。
	## false にするとタイマーを止め、親の visible を元の状態に戻します。
	get:
		return enabled
	set(value):
		if enabled == value:
			return
		enabled = value
		if not is_inside_tree():
			return
		if enabled:
			start()
		else:
			stop()

## --- 内部変数 ---

var _timer: Timer
var _parent_node: Node
var _is_running: bool = false
var _toggled_times: int = 0
var _base_visible: bool = true

func _ready() -> void:
	# 親ノードを取得。通常は Sprite2D, Node2D, Control などを想定。
	_parent_node = get_parent()
	if _parent_node == null:
		push_warning("BlinkEffect: 親ノードが存在しません。このコンポーネントは何かの子ノードとして使ってください。")
		return

	# 親が visible プロパティを持っているか軽くチェック
	if not _parent_node.has_method("is_visible") and not _parent_node.has_method("get_visible"):
		push_warning("BlinkEffect: 親ノードが visible を持たない可能性があります。Node2D や Control などで使うことを推奨します。")

	# 親の初期 visible を記録
	if "visible" in _parent_node:
		_base_visible = _parent_node.visible
	else:
		_base_visible = true

	# 内部用 Timer を生成(シーンツリーには自動で追加)
	_timer = Timer.new()
	_timer.one_shot = false
	_update_timer_settings()
	add_child(_timer)
	_timer.timeout.connect(_on_timer_timeout)

	if enabled and auto_start:
		start()


func _exit_tree() -> void:
	# 親から抜けるときは、visible を元に戻しておくと安全
	_restore_base_visibility()


## --- 公開 API ---

func start() -> void:
	## 点滅を開始します。
	if _parent_node == null:
		return
	_is_running = true
	_toggled_times = 0

	# 開始時の visible を決める
	if "visible" in _parent_node:
		_parent_node.visible = initial_visible
		_base_visible = initial_visible

	_apply_visibility()

	if _timer != null:
		_timer.start()


func stop() -> void:
	## 点滅を停止し、元の visible に戻します。
	_is_running = false
	if _timer != null:
		_timer.stop()
	_restore_base_visibility()


func is_running() -> bool:
	## 現在点滅中かどうかを返します。
	return _is_running


func reset_base_visible(current_visible: bool) -> void:
	## 「元の visible 状態」を外部から上書きしたいときに使います。
	## 例: 無敵時間が終わったら true に固定したい、など。
	_base_visible = current_visible
	_apply_visibility()


## --- 内部処理 ---

func _update_timer_settings() -> void:
	if _timer == null:
		return
	# interval 秒ごとに ON/OFF を切り替えるので、Timer の wait_time は interval
	_timer.wait_time = interval


func _on_timer_timeout() -> void:
	if not enabled or not _is_running:
		return
	if _parent_node == null:
		return

	# visible をトグル
	if "visible" in _parent_node:
		_parent_node.visible = not _parent_node.visible
		_toggled_times += 1
		_apply_visibility()

		# loop しない場合は、所定回数で止める
		if not loop and _toggled_times >= blink_count:
			stop()


func _apply_visibility() -> void:
	## respect_parent_visibility の設定に応じて最終的な visible を決める
	if _parent_node == null:
		return
	if not "visible" in _parent_node:
		return

	if respect_parent_visibility:
		# 親の階層の visibility を尊重する(親が非表示なら強制的に非表示)
		var parent_visible := true
		var node := _parent_node
		while node != null:
			if "visible" in node and not node.visible:
				parent_visible = false
				break
			node = node.get_parent()
		_parent_node.visible = _parent_node.visible and parent_visible
	# respect_parent_visibility = false の場合は、単純に _parent_node.visible をそのまま使う


func _restore_base_visibility() -> void:
	if _parent_node == null:
		return
	if "visible" in _parent_node:
		_parent_node.visible = _base_visible

使い方の手順

では、実際にどう使うかを手順で見ていきましょう。

手順①:スクリプトを用意する

  1. プロジェクト内で、例えば res://components/BlinkEffect.gd というパスに、上記のコードを保存します。
  2. Godot エディタを再読み込みすると、ノード追加ダイアログスクリプトのクラスとして BlinkEffect が使えるようになります。

手順②:点滅させたいノードの「子」として追加する

例として、プレイヤーキャラを点滅させるケースを考えます。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── BlinkEffect (Node)
  • Player シーンを開く
  • ルートの Player (CharacterBody2D) を選択
  • 右クリック → 「子ノードを追加」 → 「Node」を追加
  • その Node に BlinkEffect.gd をアタッチ(もしくは、クラス名 BlinkEffect を直接選択)

これで、Player の visible を点滅させるコンポーネントがくっつきました。

手順③:インスペクタでパラメータ調整

BlinkEffect ノードを選択すると、インスペクタに以下のような項目が出てきます。

  • auto_start … シーン読み込み時に自動で点滅開始するか
  • interval … 点滅の間隔(秒)
  • initial_visible … 点滅開始時の最初の状態(見えている/見えていない)
  • loop … 無限ループするかどうか
  • blink_countloop = false のとき、何回点滅したら止めるか
  • respect_parent_visibility … 親階層の visible を尊重するか
  • enabled … コンポーネントの有効/無効

例えば「ダメージを受けたときだけ 5 回点滅させたい」なら、

  • auto_start = false
  • loop = false
  • blink_count = 5

と設定しておき、ダメージ処理の中で start() を呼び出すだけです。


# Player.gd (例)
extends CharacterBody2D

@onready var blink: BlinkEffect = $BlinkEffect

func take_damage(amount: int) -> void:
	# HP 減少などの処理...
	blink.start() # ここで点滅開始

手順④:他のシーンでも再利用する

このコンポーネントは「親の visible をいじるだけ」なので、いろんな用途に流用できます。

例1:敵キャラの出現演出
Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── BlinkEffect (Node)
  • 最初は enabled = false にしておき、スポーン時に enabled = true; blink.start() を呼ぶ
  • interval = 0.1blink_count = 8 などで「シュインシュイン」と高速点滅させる
例2:動く床の警告演出
MovingPlatform (Node2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── BlinkEffect (Node)

床が落ちる直前に点滅させたい場合:


# MovingPlatform.gd (例)
extends Node2D

@onready var blink: BlinkEffect = $BlinkEffect

func warn_and_drop() -> void:
	blink.interval = 0.15
	blink.loop = false
	blink.blink_count = 6
	blink.start()
	await get_tree().create_timer(0.15 * 6).timeout
	# ここで床を落とす処理
	queue_free()
例3:UI ボタンの強調表示
StartButton (Button)
 └── BlinkEffect (Node)

タイトル画面で「スタートボタン」を点滅させてプレイヤーの視線を誘導する、なんてことも簡単です。

メリットと応用

BlinkEffect をコンポーネントとして切り出すことで、次のようなメリットがあります。

  • シーン構造がシンプル
    「点滅するプレイヤー」「点滅する敵」などの派生シーンを作らず、元のシーンに 1 ノード足すだけで済みます。
  • ロジックの再利用性が高い
    点滅の実装は BlinkEffect の中だけ。バグ修正や仕様変更も 1 箇所で完結します。
  • 演出調整が楽
    @export でパラメータが見えるので、レベルデザイナーや非プログラマでも「この敵は 0.1 秒間隔」「この床は 0.3 秒」などを直感的に調整できます。
  • 継承ツリーが肥大化しない
    「点滅する敵」のためだけに新しいクラスを増やさずに済むので、プロジェクト全体がスッキリします。

まさに「継承より合成」のお手本のようなコンポーネントですね。

改造案:フェードアウト付きの点滅にする

もう一歩踏み込んで、「点滅しながらだんだん透明になって消える」演出をしたい場合は、Sprite2DCanvasItemmodulate.a をいじる処理を追加するだけです。

例えば、以下のようなメソッドを BlinkEffect に追加してみましょう。


func fade_out_during_blink(total_duration: float = 1.0) -> void:
	## 点滅しながら徐々に透明にしていく簡易フェードアウト。
	## 親が CanvasItem (Sprite2D, Control など) のときに有効です。
	if _parent_node == null:
		return
	if not _parent_node is CanvasItem:
		push_warning("BlinkEffect.fade_out_during_blink: 親が CanvasItem ではないため、フェードアウトできません。")
		return

	var canvas_item := _parent_node as CanvasItem
	var tween := create_tween()
	tween.tween_property(canvas_item, "modulate:a", 0.0, total_duration)

この関数を start() の直後に呼べば、


blink.start()
blink.fade_out_during_blink(1.0)

のように、「1 秒かけてフェードアウトしつつ点滅する」演出が簡単に作れます。
ここからさらに、色を変えたり、スケールを揺らしたりと、演出コンポーネントを積み重ねていくと、継承に頼らずリッチな表現がどんどん増やせますね。