UIをちょっとリッチにしたいとき、つい「HPバー用のシーン」「スキルアイコン用のシーン」みたいに、それぞれにアニメーションを仕込んだくなりますよね。でも Godot 標準のやり方で真面目にやると…
- AnimationPlayer を毎回仕込む
- 各シーンごとに「ダメージ時に揺らす処理」をコピペ
- UIごとにノード階層がどんどん深くなる
といった具合に、あっという間に「UI専用の巨大シーン」が出来上がります。
さらに、後から「揺れの強さをちょっと弱くしたい」と思っても、各シーンを全部開いて直す羽目になりがちです。
そこで今回は、UIを揺らす処理をまるごとコンポーネント化してしまいましょう。
HPバーだろうが、スキルアイコンだろうが、ノードに 1 個アタッチするだけで「いい感じの揺れ」を共有できる ShakeUI コンポーネントを用意します。
【Godot 4】ダメージ演出を一瞬で盛る!「ShakeUI」コンポーネント
今回の ShakeUI は、以下のような機能を持つコンポーネントです。
- 任意の Control ノード(HPバー、アイコン、ボタンなど)を「一時的に揺らす」
- 揺れの強さ・時間・減衰カーブをパラメータで調整可能
- コードから
shake()を呼ぶだけで発動 - 元の位置を自動で保存・復元するので、他のレイアウトに干渉しない
「継承して専用 UI クラスを作る」のではなく、どんな UI にもポン付けできるコンポーネントとして設計してあります。
フルコード: ShakeUI.gd
extends Node
class_name ShakeUI
## 任意の Control ノードを「ガタガタ揺らす」ためのコンポーネント
##
## - HPバー、アイコン、ボタンなどの演出用
## - 親が Control であることを前提に position を揺らします
## - shake() を呼ぶだけで一時的に揺らし、終わったら自動で元の位置に戻します
@export_group("基本設定")
## 揺れの強さ(ピクセル)
## 例: 8 ~ 24 あたりが UI にはちょうどよい
@export var amplitude: float = 16.0
## 揺れの継続時間(秒)
@export_range(0.05, 2.0, 0.01, "or_greater")
@export var duration: float = 0.25
## 毎秒何回くらい位置を更新するか(見た目の「振動の速さ」)
## 値を上げるほど細かく揺れます(ただし更新負荷も少し増えます)
@export_range(10, 240, 1)
@export var frequency: int = 60
@export_group("揺れのカーブ")
## 時間に対する「揺れ強度の減衰カーブ」
## デフォルトでは線形に 1 → 0 へ減衰します
## インスペクタからカーブを編集して、最初だけ強く・最後ふわっと、なども可能
@export var intensity_curve: Curve
## ランダムシード(0 のままならランダム、固定すると毎回同じ揺れパターン)
@export var random_seed: int = 0
@export_group("自動トリガー")
## true の場合、シーンが ready になったタイミングで一度だけ自動で揺らす(デバッグ確認用)
@export var shake_on_ready: bool = false
## 内部状態
var _original_position: Vector2
var _is_shaking: bool = false
var _time: float = 0.0
var _rng := RandomNumberGenerator.new()
func _ready() -> void:
## 親が Control かチェックしておく(UI 専用コンポーネント)
if not owner or not (owner is Control):
push_warning("ShakeUI: 親ノードが Control ではありません。このコンポーネントは Control 用です。")
## 親ノードの現在位置を保存しておく
if owner and owner is Control:
_original_position = owner.position
## カーブ未設定なら、デフォルトで「線形減衰」のカーブを生成
if not intensity_curve:
intensity_curve = Curve.new()
intensity_curve.add_point(Vector2(0.0, 1.0)) # 開始時は強度 1
intensity_curve.add_point(Vector2(1.0, 0.0)) # 終了時は強度 0
## ランダムシードの初期化
if random_seed != 0:
_rng.seed = random_seed
else:
_rng.randomize()
if shake_on_ready:
shake()
func _process(delta: float) -> void:
if not _is_shaking:
return
_time += delta
if _time >= duration:
## 揺れ終了。位置を元に戻してフラグを下ろす
_reset_position()
_is_shaking = false
return
## 0.0 ~ 1.0 の正規化された経過時間
var t := _time / duration
## カーブから現在の強度(0.0 ~ 1.0)を取得
var strength_factor := intensity_curve.sample(clamp(t, 0.0, 1.0))
## 実際の現在の振幅(ピクセル)
var current_amplitude := amplitude * strength_factor
## ランダムなオフセットを生成
## -1.0 ~ 1.0 の乱数を x, y に掛ける
var offset_x := _rng.randf_range(-1.0, 1.0) * current_amplitude
var offset_y := _rng.randf_range(-1.0, 1.0) * current_amplitude
## 周波数を考慮して「更新タイミング」を制御
## 簡易的には、delta ごとに更新しても十分ですが、
## 高周波設定時に無駄に更新しないよう、一定間隔ごとに更新します。
var step := 1.0 / float(frequency)
if fmod(_time, step) < delta:
_apply_offset(Vector2(offset_x, offset_y))
func _exit_tree() -> void:
## シーンから抜けるときに位置を戻しておく(保険)
_reset_position()
## 公開 API: 揺れを開始する
##
## - duration, amplitude などはインスペクタの値を使う
## - すでに揺れている最中に呼ばれた場合、タイマーをリセットして「振り直し」
func shake() -> void:
if not owner or not (owner is Control):
push_warning("ShakeUI: 親ノードが Control ではないため、shake() を実行できません。")
return
## 最初の位置を必ず保存しておく(途中からアタッチした場合など)
_original_position = owner.position
_time = 0.0
_is_shaking = true
## 公開 API: 即座に揺れを止めて元の位置に戻す
func stop_shake() -> void:
if not _is_shaking:
return
_reset_position()
_is_shaking = false
## 内部: 親ノードにオフセットを適用
func _apply_offset(offset: Vector2) -> void:
if owner and owner is Control:
(owner as Control).position = _original_position + offset
## 内部: 親ノードの位置を保存された元の位置に戻す
func _reset_position() -> void:
if owner and owner is Control:
(owner as Control).position = _original_position
使い方の手順
例として、「ダメージを受けたときに HP バーとプレイヤーアイコンを揺らす UI」を作ってみます。
手順①: コンポーネントスクリプトを用意する
- 上記コードを
res://components/ui/ShakeUI.gdなどに保存します。 - Godot エディタで再読み込みすると、ノードにスクリプトをアタッチする際に
ShakeUIがクラスとして選べるようになります。
手順②: HPバーシーンにコンポーネントをアタッチ
例えば、HPバー用にこんなシーンを作っているとします:
HpBar (Control) ├── TextureProgressBar └── ShakeUI (Node)
HpBar(ルート Control)に 子ノードとしてNodeを追加し、名前をShakeUIに変更。- そのノードに
ShakeUI.gdをアタッチ。 - インスペクタで以下のように設定:
amplitude: 12.0duration: 0.2frequency: 60shake_on_ready: false(本番ではオフ)
これで、HpBar のスクリプトから $ShakeUI.shake() を呼べば、HPバー全体が一瞬ガタッと揺れます。
手順③: ダメージイベントから揺れを呼び出す
HpBar にスクリプトを付けて、HP の更新時に ShakeUI を呼び出してみましょう。
extends Control
@onready var hp_bar: TextureProgressBar = $TextureProgressBar
@onready var shaker: ShakeUI = $ShakeUI
var max_hp: int = 100
var current_hp: int = 100
func set_hp(value: int) -> void:
var old_hp := current_hp
current_hp = clamp(value, 0, max_hp)
hp_bar.value = current_hp
# HP が減ったときだけ揺らす
if current_hp < old_hp:
shaker.shake()
このようにしておけば、ゲーム本体側からは単に hp_bar_ui.set_hp(new_hp) を呼ぶだけで、
- HP 数値の更新
- 減ったときの揺れ演出
まで一括で処理できます。
手順④: プレイヤーアイコンやスキルアイコンにも再利用
同じコンポーネントを、別の UI にもそのまま使い回せます。
例: プレイヤーの顔アイコンをダメージ時に揺らす。
PlayerHUD (CanvasLayer)
├── HpBar (Control)
│ ├── TextureProgressBar
│ └── ShakeUI (Node)
├── PlayerIcon (TextureRect)
│ └── ShakeUI (Node)
└── SkillIcon (TextureButton)
└── ShakeUI (Node)
PlayerIcon や SkillIcon の子に ShakeUI ノードを生やして、同じようにスクリプトをアタッチするだけです。
例えば、プレイヤーがダメージを受けたときに HUD 全体で演出したければ:
func on_player_damaged() -> void:
$HpBar/ShakeUI.shake()
$PlayerIcon/ShakeUI.shake()
のように、どの UI を揺らすかを呼び出し側で自由に組み合わせられます。
ノード構造をいじらず、コンポーネントを生やしてメソッドを呼ぶだけ、というのがポイントですね。
メリットと応用
この ShakeUI コンポーネントを使う一番のメリットは、「揺れ演出」のロジックをすべて 1 箇所に閉じ込められることです。
- HPバー用のシーン、アイコン用のシーン…と UI ごとにアニメーションを作らなくてよい
- 揺れのチューニング(強さ・時間・減衰カーブ)を一括で行える
- 「とりあえず揺らしたい UI」にポン付けすれば即演出が乗る
- ノード階層が「Control + ShakeUI」程度で済み、AnimationPlayer だらけにならない
また、コンポーネント指向で作っておくと、
- 「UI Shake」だけ別プロジェクトにも簡単に持っていける
- 将来「3D の UI」や「World 空間の UI」にも応用しやすい
といった利点もあります。
「HPバーを継承した ShakingHpBar クラス」を増やすのではなく、「揺れる」という機能をコンポーネントとして合成する、という発想ですね。
改造案: 水平方向だけ揺らしたい場合
例えば、「HPバーは左右にだけ揺らしたい(縦には動かしたくない)」というケースもありますよね。
そんなときは shake_horizontal() のような補助メソッドを追加すると便利です。
## 横方向だけ揺らしたいとき用のヘルパー
func shake_horizontal() -> void:
# 通常の shake() をベースにしつつ、
# _process 内で y オフセットを 0 に固定するように改造してもよいですし、
# ここで一時的に amplitude を退避・変更してから shake() を呼ぶ方法もあります。
var original_amplitude := amplitude
amplitude = abs(amplitude) # 念のため正の値に
shake()
# 必要なら Timer で duration 後に amplitude を元に戻す、なども可能
実際には、_process() 内で「水平方向のみ」「垂直方向のみ」「回転も加える」などのモードを @export_enum で切り替えられるようにすると、より汎用的なコンポーネントになります。
まずは今回の ShakeUI をベースに、プロジェクトの UI に一括で「揺れ演出」を導入してみてください。
継承ベースで UI クラスを増やすより、こうしたコンポーネントを積み上げていく方が、後からのメンテもずっと楽になりますよ。
