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」を作ってみます。

手順①: コンポーネントスクリプトを用意する

  1. 上記コードを res://components/ui/ShakeUI.gd などに保存します。
  2. Godot エディタで再読み込みすると、ノードにスクリプトをアタッチする際に ShakeUI がクラスとして選べるようになります。

手順②: HPバーシーンにコンポーネントをアタッチ

例えば、HPバー用にこんなシーンを作っているとします:

HpBar (Control)
 ├── TextureProgressBar
 └── ShakeUI (Node)
  • HpBar(ルート Control)に 子ノードとして Node を追加し、名前を ShakeUI に変更。
  • そのノードに ShakeUI.gd をアタッチ。
  • インスペクタで以下のように設定:
    • amplitude: 12.0
    • duration: 0.2
    • frequency: 60
    • shake_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)

PlayerIconSkillIcon の子に 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 クラスを増やすより、こうしたコンポーネントを積み上げていく方が、後からのメンテもずっと楽になりますよ。