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
使い方の手順
では、実際にどう使うかを手順で見ていきましょう。
手順①:スクリプトを用意する
- プロジェクト内で、例えば
res://components/BlinkEffect.gdというパスに、上記のコードを保存します。 - 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_count…loop = falseのとき、何回点滅したら止めるかrespect_parent_visibility… 親階層の visible を尊重するかenabled… コンポーネントの有効/無効
例えば「ダメージを受けたときだけ 5 回点滅させたい」なら、
auto_start = falseloop = falseblink_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.1、blink_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 秒」などを直感的に調整できます。 - 継承ツリーが肥大化しない
「点滅する敵」のためだけに新しいクラスを増やさずに済むので、プロジェクト全体がスッキリします。
まさに「継承より合成」のお手本のようなコンポーネントですね。
改造案:フェードアウト付きの点滅にする
もう一歩踏み込んで、「点滅しながらだんだん透明になって消える」演出をしたい場合は、Sprite2D や CanvasItem の modulate.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 秒かけてフェードアウトしつつ点滅する」演出が簡単に作れます。
ここからさらに、色を変えたり、スケールを揺らしたりと、演出コンポーネントを積み重ねていくと、継承に頼らずリッチな表現がどんどん増やせますね。
