UIボタンや拾えるアイテムを「目立たせたい」とき、ついAnimationPlayer をベタ貼りして、シーンごとにアニメーションを作っていませんか?
あるいは、ButtonやSprite2Dを継承したカスタムシーンを量産して、「あのボタンだけアニメーションが違う…どれが本物のプレハブだっけ?」というカオスに陥りがちですよね。
Godot 4 では、依然として「ノード継承+深いシーン階層」で何でも解決しがちですが、
脱・初心者を目指すなら、ここは一歩進んでコンポーネント指向(継承より合成)に寄せていきたいところです。
そこで今回は、「親ノードの scale を定期的に拡大縮小させて注目させる」だけに責務を絞ったコンポーネントScalePulse を作っていきます。
このコンポーネントをポン付けするだけで、UIボタンでもアイテムでも、
「ちょっとだけふわふわ膨らむ」注目アニメーションを、アニメーションリソースなしで実現できます。
【Godot 4】ふわっと目立たせるパルスアニメ!「ScalePulse」コンポーネント
まずはフルコードから。res://components/scale_pulse.gd のような場所に置いて、
どのシーンからも使い回せるようにしておきましょう。
extends Node
class_name ScalePulse
"""
ScalePulse コンポーネント
親ノードの scale を一定のリズムで拡大縮小させて「注目させる」ためのコンポーネント。
想定用途:
- UIボタンをふわっと脈打つように目立たせる
- 拾えるアイテム(コインやポーションなど)を見つけやすくする
- インタラクト可能なオブジェクトをプレイヤーにアピールする
親ノードのタイプ:
- Control / Node2D / Sprite2D / TextureRect など、scale プロパティを持つもの
"""
@export_category("Pulse Settings")
@export var enabled: bool = true:
set(value):
enabled = value
_set_process_state()
@export var pulse_scale: Vector2 = Vector2(1.2, 1.2)
## 拡大時のスケール倍率。
## 例: (1.2, 1.2) なら、元の 120% までふわっと拡大します。
@export_range(0.05, 5.0, 0.01, "or_greater") var pulse_duration: float = 0.6
## 1 回の「拡大→縮小」にかかる時間(秒)。
## 小さいほど速くパルスし、大きいほどゆったりとした動きになります。
@export_range(0.0, 5.0, 0.01, "or_greater") var delay_before_start: float = 0.0
## アクティブ化してからパルス開始までの遅延(秒)。
## 画面に出てすぐ動くと煩い場合に、少し遅らせるのに便利です。
@export_range(0.0, 1.0, 0.01) var easing: float = 0.3
## 補間の「なめらかさ」を決めるイージング係数。
## 0 に近いと直線的、1 に近いと加減速が強くなります。
@export_category("Behavior")
@export var randomize_phase: bool = true
## シーン内に同じ ScalePulse が複数あるとき、
## それぞれ開始タイミングをランダムにずらして「同時に脈打たない」ようにするフラグ。
@export var affect_children: bool = false
## true にすると、親ノードだけでなく子孫ノード全体をまとめてスケーリングします。
## 通常は false 推奨(親ノードの scale だけで十分なことが多い)。
@export_category("Debug")
@export var preview_in_editor: bool = false
## エディタ上でもプレビューしたい場合に ON。
## ※あまり多用するとエディタがうるさくなるので注意。
# 内部状態
var _base_scale: Vector2
var _time: float = 0.0
var _started: bool = false
var _random_offset: float = 0.0
func _ready() -> void:
# 親ノードが必要(scale を持つノードを想定)
if get_parent() == null:
push_warning("ScalePulse: 親ノードがありません。scale を持つノードの子として配置してください。")
set_process(false)
set_physics_process(false)
return
# 親の初期スケールを保存
_base_scale = get_parent().scale
# ランダムフェーズずらし
if randomize_phase:
_random_offset = randf() * pulse_duration
else:
_random_offset = 0.0
_time = 0.0
_started = false
_set_process_state()
func _notification(what: int) -> void:
# エディタ上での挙動制御
if what == NOTIFICATION_EDITOR_ENTER:
_set_process_state()
elif what == NOTIFICATION_EDITOR_EXIT:
# エディタから抜けるときは元のスケールに戻しておく
_reset_scale()
set_process(false)
func _set_process_state() -> void:
# 実行中 or エディタプレビュー時のみ処理を回す
var should_process := enabled and (Engine.is_editor_hint() == false or preview_in_editor)
set_process(should_process)
# 物理フレームは不要なので process のみ
func _process(delta: float) -> void:
if not enabled:
return
_time += delta
# 開始ディレイを考慮
if not _started:
if _time >= delay_before_start:
_started = true
# 遅延を引いて、ランダム位相を加える
_time = (_time - delay_before_start) + _random_offset
else:
# ディレイ中はスケールを初期値に保つ
_apply_scale(_base_scale)
return
# パルスの位相を 0..1 に正規化
var phase := fmod(_time, pulse_duration) / max(pulse_duration, 0.0001)
# 0..1..0 の三角波を生成(拡大→縮小)
var tri := 1.0 - abs(phase * 2.0 - 1.0) # 0 → 1 → 0
# イージング(少し丸みを持たせる)
var eased := _ease_in_out(tri, easing)
# 現在のスケールを計算
var current_scale := _base_scale.lerp(pulse_scale * _base_scale, eased)
_apply_scale(current_scale)
func _apply_scale(new_scale: Vector2) -> void:
var parent := get_parent()
if parent == null:
return
if affect_children:
# 子孫ごとまとめてスケーリングしたい場合
parent.scale = new_scale
else:
# 通常は親ノードの scale だけ調整すれば OK
parent.scale = new_scale
func _ease_in_out(t: float, k: float) -> float:
"""
0..1 の t を、簡易的なイージングで丸める。
k は 0..1 の係数で、0 に近いほど線形、1 に近いほど強いカーブ。
"""
if k <= 0.0:
return t
if k >= 1.0:
# ほぼ S 字カーブ
return t * t * (3.0 - 2.0 * t)
# k をブレンド係数として線形補間
var smooth := t * t * (3.0 - 2.0 * t)
return lerp(t, smooth, k)
func _reset_scale() -> void:
var parent := get_parent()
if parent:
parent.scale = _base_scale
func reset_to_base_scale() -> void:
"""
外部から呼び出して、スケールを初期値に戻したいときに使います。
例: ボタンが押されたらアニメーションを止めて元のサイズに戻す、など。
"""
_time = 0.0
_started = false
_reset_scale()
func stop_pulse() -> void:
"""
パルスアニメーションを停止し、現在のスケールを維持します。
"""
enabled = false
set_process(false)
func start_pulse() -> void:
"""
パルスアニメーションを再開します。
"""
_time = 0.0
_started = false
enabled = true
_set_process_state()
使い方の手順
ここからは、実際のシーンに ScalePulse を組み込む手順を見ていきましょう。
UIボタンとフィールドアイテムの 2 パターンで例を出します。
手順①: スクリプトをプロジェクトに追加
res://components/フォルダを作成(なければ)- その中に
scale_pulse.gdを作成し、上記コードをコピペ - 保存すると
ScalePulseがクラスとして認識され、ノード追加ダイアログからも検索できるようになります
手順②: UIボタンにパルスを付ける例
例えば、「決定ボタン」をふわっと目立たせたいケース。
MainMenu (Control) ├── Panel │ └── StartButton (Button) │ └── ScalePulse (Node) └── その他UI...
StartButtonを選択し、その子として + ボタン → ノード追加 を開くScalePulseを検索して追加ScalePulseノードを選択し、インスペクタで以下のように設定pulse_scale = Vector2(1.1, 1.1)(UIなので控えめに)pulse_duration = 0.8(ゆっくりとした脈打ち)delay_before_start = 0.2(フェードイン後に動き始めるイメージ)randomize_phase = false(ボタンは単体なので不要)
これだけで、StartButton がふわっと拡大縮小するようになります。
アニメーションリソースも、AnimationPlayer も不要です。
手順③: フィールド上のアイテムにパルスを付ける例
次に、2D アクションゲームの「コイン」アイテムを目立たせる例です。
Coin (Area2D) ├── Sprite2D ├── CollisionShape2D └── ScalePulse (Node)
Coinシーンを開き、ルートArea2Dの子としてScalePulseを追加ScalePulseを選択し、インスペクタで:pulse_scale = Vector2(1.3, 1.3)(ちょっと大きめに)pulse_duration = 0.5(テンポよく)randomize_phase = true(複数コインが並んでも同じタイミングにならない)affect_children = false(親の scale だけで十分)
この Coin シーンをステージにばらまくだけで、
全てのコインが「それぞれ違うタイミング」でふわふわ光っているように見えます。
手順④: 実行時に ON/OFF する例
「特定のタイミングでだけ目立たせたい」ケースもあります。
例えば、インタラクト可能なオブジェクトを、プレイヤーが近づいたときだけパルスさせるイメージです。
InteractableObject (Node2D) ├── Sprite2D ├── CollisionShape2D └── ScalePulse (Node)
InteractableObject.gd 側で、以下のように制御できます。
extends Node2D
@onready var pulse: ScalePulse = $ScalePulse
func _ready() -> void:
# 最初は非アクティブ
pulse.stop_pulse()
pulse.reset_to_base_scale()
func on_player_enter_range() -> void:
# プレイヤーが近づいたらパルス開始
pulse.start_pulse()
func on_player_exit_range() -> void:
# 離れたらパルス終了&元に戻す
pulse.stop_pulse()
pulse.reset_to_base_scale()
このように、見た目のアテンション制御をコンポーネントに丸投げしておくと、
ゲームロジック側は「いつ ON/OFF するか」だけ考えれば良くなります。
メリットと応用
ScalePulse をコンポーネントとして切り出すメリットを、いくつか挙げてみます。
- シーン構造がシンプルになる
UIボタンやアイテムごとにAnimationPlayerを生やす必要がなく、
「動きのロジック」はすべてScalePulseに集約されます。 - 継承地獄からの脱出
AnimatedButtonやPulsingCoinみたいな「動き込みのカスタムシーン」を増やす代わりに、ButtonやCoinはそのまま、ScalePulseを後付けするだけで機能追加できます。 - パラメータで挙動を統一・調整しやすい
「全 UI のパルス速度を 0.6 → 0.8 に変更したい」といった場合、
コンポーネントのデフォルト値を変えるだけで新規シーンに自動で反映できます。 - テストがしやすい
単体のScalePulseシーンで挙動を確認できるので、
大きな UI シーンを開かなくても調整・デバッグが可能です。
応用として、例えば「プレイヤーが HP 少ないときだけ UI の HP バーをパルスさせる」など、
ゲームの状態に応じて start_pulse() / stop_pulse() を切り替えると、
かなりリッチなフィードバックが簡単に実装できます。
改造案: マウスホバー中だけパルスさせる UI ボタン
最後に、Button 側から ScalePulse を制御して、
「マウスが乗っている間だけ」ふわっとさせる簡単な改造例を置いておきます。
# HoverPulseButton.gd
extends Button
@onready var pulse: ScalePulse = $ScalePulse
func _ready() -> void:
# 最初は止めておく
pulse.stop_pulse()
pulse.reset_to_base_scale()
mouse_entered.connect(_on_mouse_entered)
mouse_exited.connect(_on_mouse_exited)
func _on_mouse_entered() -> void:
pulse.start_pulse()
func _on_mouse_exited() -> void:
pulse.stop_pulse()
pulse.reset_to_base_scale()
このように、「アニメーションの実装」は ScalePulse に閉じ込めておき、
ボタンやアイテムのスクリプトは「いつアニメーションさせるか」だけを知っていれば十分、という構造ですね。
継承より合成で、小さくて再利用しやすいコンポーネントをどんどん増やしていきましょう。
