【Godot】GodotのHPバー入門:減った分が遅れて追いつくゲージを作り、敵の頭上にも置く

作成: 2026-09-22最終更新: 2026-10-10

TextureProgressBarを2本重ねて、減った分が遅れて消えるHPゲージを作り、敵の頭上にも傾かずに置く方法を解説します。

HPバーは、いまの残りがすぐ読めて、食らった分も一瞬だけ見えると分かりやすくなります。Godot では、TextureProgressBar を2本重ね、手前はすぐに、奥だけ遅れて減らして作ります。

バーを1本だけ Tween で滑らかに減らすと、動いているあいだは今のHPが読めません。一瞬で減らすと、今度はどれだけ食らったのかが分かりません。

この記事では、減った分が遅れて消えるゲージを作り、同じゲージを敵の頭上にも傾かずに置く方法を解説します。

ゲージは、いまのHPまでの濃い塗りと、さっきまでのHPまで残るうすい塗りの2段。人形の頭上にも同じゲージが載る

この記事でわかること

  • バーが描いているのは割合で、塗りの絵は縮まずに切り取られる
  • 手前と奥の2本を重ねて、減った分だけを遅れて消す作り方
  • 連続で食らったときと回復したときに、奥のバーをどう扱うか
  • 実践:同じゲージをHUDと敵の頭上に置き、敵が傾いてもバーは水平に保つ

動作確認環境: Godot 4.7

Sponsored

バーが描く割合

ProgressBar も TextureProgressBar も、Range という同じクラスを継承しています。シーンツリーに Range というノードを置くわけではなく、どちらも次の3つの値を持っている、という意味です。

  • min_value: 空のときの値。既定は 0
  • max_value: 満タンのときの値。既定は 100
  • value: いまの値

バーが描くのは、この3つから出る割合です。min_value が 0 なら value / max_value で、70 / 100 なら7割の長さになります。

バーは min_value と max_value のあいだで value がどこにいるかを長さにし、数そのものは別の Label が出す

バーは長さしか持ちません。「残り70」という数そのものを出したいなら、Label を別に置きます。ProgressBar には割合を「70%」と出す機能もありますが、HPを 70 / 100 の形で見せたいなら Label のほうが自由です。

ProgressBar の色の変え方

ProgressBar のインスペクタを上から下まで見ても、塗りの色を指定する欄はありません。Modulate を触ると、下地も塗りも数字も一緒に染まります。

色を分けるには、インスペクタの Theme Overrides を開きます。見た目の設定をこのノードだけ上書きする欄で、その中の Styles を使います。

  • Fill に StyleBoxFlat(単色の四角を描くスタイル)を新規作成し、その BG Color を塗りの色にする
  • Background に別の StyleBoxFlat を新規作成し、その BG Color を下地の色にする
ProgressBarはFillとBackgroundにStyleBoxFlatを作り、それぞれのBG Colorで塗りと下地を塗り分ける

Show Percentage は既定でオンです。Value の既定が 0 なので、置いた直後の表示は「0%」になります。要らなければオフにします。

角を丸めたい、枠線を付けたい、といった程度なら ProgressBar で足ります。下地・塗り・枠を画像で分けて扱いたくなったら、次の TextureProgressBar を使います。

Sponsored

TextureProgressBar の3枚

TextureProgressBar は、テクスチャを3枚受け取ります。インスペクタの Textures グループを開くと、Under・Progress・Over の3つが並んでいます。

  • texture_under: 下地。値に関係なく全体が出る
  • texture_progress: 塗り。値の割合で切り取られる
  • texture_over: 上に載る枠。値に関係なく全体が出る
下地・塗り・枠の3枚。下地と枠は全体が出て、塗りだけが7割の位置で切り取られ、重ねると完成形のバーになる

大事なのは真ん中です。texture_progress は縮小されるのではなく、割合の位置で切り取られます。だから素材には、満タンの状態のバーを1枚描きます。半分の絵を入れても、それがさらに切られるだけです。枠を texture_progress に描いてしまうと、減った端から枠も一緒に欠けます。枠は中が透明な絵にして texture_over に分けます。

他のエンジンでいうと: TextureProgressBar は、Unity の uGUI では Image Type を Filled にした Image(Fill Amount で塗る割合を決める)、Unreal Engine の UMG では Progress Bar(Percent)にあたります。

色は Tint グループの Progress で変えられます。これは掛け算なので、白を基調にした絵を用意しておくと、指定した色がそのまま出ます。すでに色が付いた絵に別の色を掛けると、元の色に引きずられた結果になります。

2本重ねのゲージ

バーを1本だけ置いて、value を Tween(値を指定した秒数で少しずつ変える仕組み)で滑らかに減らすとします。動いているあいだの長さは、どこにも存在しないHPを指します。70まで減ったのに、画面は85を通過していて、今いくつなのかが読めません。

やりたいのは逆です。いまの割合は待たせずに手前へ出し、減る前の割合は奥に残して、その差が見えてから消します。残った差の長さが、まだ消えていない被弾の表示になります。

1本をTweenで減らすと途中の長さは今のHPではない。2本を重ねると、手前の端と奥の端の差が食らった分になる

作り方は、同じ大きさの TextureProgressBar を2本、同じ場所に重ねるだけです。Control はシーンツリーで下にある兄弟が手前に描かれるので、奥にしたいバーを上に置きます。

  • 奥(先に置く): texture_under に下地、texture_progress に残りを見せる色。枠は入れない
  • 手前(あとに置く): texture_progress にいまのHPの色、texture_over に枠。下地は入れない

手前に下地を入れると、奥が全部隠れます。下地は奥の1枚だけが持ち、枠は手前の1枚だけにまとめます。こうしておくと、半透明の部分が重なって濃くなることもありません。

遅らせるのは奥だけです。手前は代入したそのフレームで新しい割合になり、奥は Tween で0.2秒待ってから0.4秒かけて追いつきます。HPが増えたとき(回復)と最初の1回だけは、奥も待たずに同じ値にします(実践の set_hp())。

回復かどうかは、back.value ではなく、前回受け取った数(_shown_hp)と比べて決めます。奥のバーは、まだ追いついていない途中の値を持っているからです。100から70へ減ったあと、追いつく前に90まで回復したとします。back.value はまだ95あたりにいるので、90 >= back.value は成り立たず、回復なのに奥がさらに減りはじめます。前回渡された数と比べれば、この取り違えは起きません。

100から70へ減ったあと奥が追いつく途中で90が来る。95と比べると回復に見えず、前回の70と比べれば回復と分かる

連続で食らったときは、前の Tween を止めてから新しいものを作ります。待ち時間も数え直されるので、連打しているあいだ差は開いたままになり、止んでからまとめて縮みます。

奥のバーの Step は 0 にしておきます。既定の 1 のままだと、Tween が動かす途中の値が整数に丸められ、追いつく動きが階段になります。

敵の頭上のバー(Flip H と Top Level)

同じゲージを敵の頭上に置くと、新しい問題が出ます。

Control を Node2D の子に置くと、位置は親についてきます。ここまでは狙いどおりです。ところが、親の回転も拡大も、scale.x を -1 にした反転もそのまま受け取ります。敵が傾けばバーも傾き、数字は読めない角度になります。

そのままならバーは水平だが、親を回すとバーと数字が傾き、親の scale.x を -1 にするとバーの向きが逆になる

アンカー(親の矩形のどこに合わせるか)も当てになりません。親が Node2D だと、アンカーが基準にする矩形は幅も高さも 0 です。中央そろえも、いっぱいに広げるも働かないので、大きさは自分で決めます。

直し方は、何を動かしたいかで変わります。

  • 向きを変えるだけ: Sprite2D の Flip H を使います。絵の描画だけを左右反転するもので、変形として子や兄弟へは伝わりません。バーをスプライトの兄弟に置いておけば、絵だけが振り向いてバーは水平のままです
  • 親そのものを回す: バーに Top Level を付けます。親の変形をまとめて無視できますが、移動まで無視するので、位置は毎フレーム自分で入れ直します。この2つは必ずセットです
Flip H はスプライトの絵だけを反転してバーへ伝わらない。Top Level は親の変形を遮り、位置を毎フレーム入れる
Sponsored

実践:2本のバーを重ねて、敵の頭上にも置く

アクションRPGの被弾、格闘ゲームの体力ゲージ、シューティングのシールド——減った分を見せる形はどれも同じです。同じ部品をHUDと敵の頭上の両方に置いて、1つのスクリプトで動かします。

① 置くもの

まずゲージを1つのシーンとして作り、それを2か所に置きます。

  • 新しいシーンのルートに Control を置いて HpGauge。hp_gauge.gd を付けて、インスペクタの Mouse > Filter を Ignore にする
  • HpGauge の子に TextureProgressBar を置いて BackBar。先に置くほうが奥になる
  • その下に TextureProgressBar をもう1つ置いて FrontBar
  • 最後に Label を置いて HpLabel。位置 (8, 28)
  • BackBar と FrontBar も Mouse > Filter を Ignore にする
  • HpGauge.tscn として保存する

テクスチャは GradientTexture2D を新規作成して作ります。Width を 300、Height を 24 にし、Gradient を新規作成して左右を同じ色にすれば、単色の帯になります。2本とも Textures > Progress は白い帯、Max Value と Value は 100、Size は (300, 24) です。違うのは次の3行だけです。

BackBarFrontBar
Textures > Underうすいグレーの帯なし
Tint > Progressうすい青濃い青
Step01

シーンを保存したら、Main に置きます。

  • ルートに Node2D を置いて Main。main.gd を付ける
  • Main の子に CanvasLayer(カメラが動いても画面に固定される層)を置いて HUD。その子に HpGauge.tscn を置いて、位置 (24, 24)
  • Main の子に Node2D を置いて Enemy。位置 (576, 400)。enemy.gd を付ける
  • Enemy の子に Sprite2D を置いて Texture に icon.svg
  • Enemy の子に HpGauge.tscn を置いて、Scale を (0.4, 0.4)
  • プロジェクト設定のインプットマップに damage(J)、heal(K)、flip(L)、rotate(M)を追加する

Enemy の下では、Sprite2D とゲージを兄弟にします。子にしてしまうと、スプライト自身を回したり拡大したりしたときに、ゲージも一緒に変形します。

② 組み立て

# hp_gauge.gd
extends Control

const DELAY := 0.2
const CATCH_UP := 0.4

@onready var back: TextureProgressBar = $BackBar
@onready var front: TextureProgressBar = $FrontBar
@onready var label: Label = $HpLabel

var _tween: Tween
var _shown_hp := -1                          # まだ一度も受け取っていない印

func set_hp(current: int, maximum: int) -> void:
	if current == _shown_hp and maximum == front.max_value:
		return                               # 同じ数が来たら演出を消さない
	front.max_value = maximum
	back.max_value = maximum
	front.value = current
	label.text = "%d / %d" % [current, maximum]
	if _tween:
		_tween.kill()
	if _shown_hp < 0 or current >= _shown_hp:
		back.value = current                 # 最初の1回と、回復のとき
	else:
		_tween = create_tween()
		_tween.tween_interval(DELAY)
		_tween.tween_property(back, "value", float(current), CATCH_UP)
	_shown_hp = current
# enemy.gd
extends Node2D

const GAUGE_OFFSET := Vector2(-60, -90)

@onready var sprite: Sprite2D = $Sprite2D
@onready var gauge: Control = $HpGauge

func _ready() -> void:
	gauge.top_level = true                   # 親の回転を受け取らない
	$HpGauge/HpLabel.visible = false         # 頭上は小さいので数字を出さない

func _process(_delta: float) -> void:
	gauge.global_position = global_position + GAUGE_OFFSET   # 位置は自分で入れる

func set_hp(current: int, maximum: int) -> void:
	gauge.set_hp(current, maximum)           # 頭上のゲージへ渡すだけ

func toggle_flip() -> void:
	sprite.flip_h = not sprite.flip_h        # 絵だけが反転する

func toggle_rotate() -> void:
	rotation = 0.0 if rotation != 0.0 else 0.3
# main.gd
extends Node2D

const MAX_HP := 100

var _hp := MAX_HP

@onready var hud_gauge: Control = $HUD/HpGauge
@onready var enemy: Node2D = $Enemy

func _ready() -> void:
	_apply()

func _unhandled_input(event: InputEvent) -> void:   # UIが使わなかったキー入力がここへ届く
	if event.is_action_pressed("damage"):
		_hp = max(_hp - 30, 0)
		_apply()
	elif event.is_action_pressed("heal"):
		_hp = min(_hp + 20, MAX_HP)
		_apply()
	elif event.is_action_pressed("flip"):
		enemy.toggle_flip()
	elif event.is_action_pressed("rotate"):
		enemy.toggle_rotate()

func _apply() -> void:
	hud_gauge.set_hp(_hp, MAX_HP)
	enemy.set_hp(_hp, MAX_HP)

HPを持っているのは main.gd で、ゲージは受け取った数を塗るだけです。頭上のゲージは幅300の部品を0.4倍にしているので、幅は120になり、GAUGE_OFFSET の -60 で敵の真上の中央に来ます。戦闘側から health_changed(current, maximum) のようなシグナルで渡す形にすると、ゲージ側は相手を知らずに済みます。その繋ぎ方は シグナルの記事 にあります。

③ 動かして確かめる

Jを1回で手前だけ70になり差が残る。0.6秒後に差が消え、Jを3回で奥が残り、Kで回復すると差は出ない

F6 で再生して J を1回押すと、数字と手前のバーがすぐ 70 / 100 になり、奥は満タンのまま右端に差が残ります。DELAY の0.2秒待ってから CATCH_UP の0.4秒で追いつくので、押してから約0.6秒で差が消えます。手前も滑らかに動くなら1本を Tween で減らしていて、押した瞬間に差が出ないなら奥にも即座に代入しています。

いったん再生し直してHPを100に戻し、J を0.2秒より短い間隔で3回押すと、手前は70、40、10と落ち、奥は100の近くに残ります。最後に押してから約0.6秒で、奥が10までまとめて縮みます。そこで K を押すと、手前と数字がすぐ30へ伸び、奥もその場で同じ値にそろいます。待ってから奥が縮むようなら、回復の判定を back.value と比べています。

最後に L を押すと、敵の絵だけが左右反転し、頭上のバーは水平のままです。M を押して敵を傾けても、Top Level を付けたバーは水平のまま敵の上についてきます。バーが一緒に傾くなら top_level の行が、バーがその場に取り残されるなら _process() の行が抜けています。

ポイントは2つです。

  • 奥のバーは「見えている長さ」を持っている: 回復かどうかを back.value で判定すると、追いつく途中の値と比べることになります。前回受け取った数を覚えておいて、そちらと比べます
  • 下地と枠は片方ずつ持たせる: 手前に下地を入れると奥が全部隠れます。下地は奥、枠は手前にまとめておくと、重なりで濃くなる部分も出ません

Tween そのものの使い方は Tweenの記事、画面幅が変わってもHUDが崩れないようにする話は UIレイアウトの記事 にあります。

うまくいかないときは

症状原因直し方
ノードの Size を広げたのに、バーの長さが変わらないNine Patch Stretch がオフだと、絵はテクスチャの大きさのまま描かれるNine Patch Stretch(角を残したまま辺と中央を伸ばす)をオンにして Stretch Margin で角の幅を決めるか、テクスチャの幅を Size に合わせる
Top Level を付けた頭上のバーが、手前にいるキャラより上に描かれるtop_level は親の変形を切るだけでなく、描画の順番も上に出す重なりが大事な場面では Top Level を使わず、回すのは Sprite2D だけにする。バーは兄弟のまま水平に残る
カメラを寄せると、頭上のバーまで大きくなる敵の子のバーは、ワールドと一緒にカメラで拡大される大きさを固定したいなら、バーを CanvasLayer に置き、敵の get_global_transform_with_canvas().origin に頭上へのずらしを足して、毎フレーム位置を入れる

おまけ:先に知っておくと良いこと

  • 塗る向きを変える: TextureProgressBar の Fill Mode を変えると、右から左や下から上に塗れます。円形に塗る値もあり、クールダウンの表示に使えます。ProgressBar にも Fill Mode はありますが、円形はありません
  • ドラッグで値を変えたいとき: Slider の仲間を使います。ProgressBar の系統は見せるためのもので、マウスで動かす仕組みは持っていません
  • HPが少ないときに色を変える: コードで tint_progress に色を代入します。白を基調にした絵を用意していれば、指定した色がそのまま出ます
  • 表示だけのゲージはクリックを通す: Control の Mouse > Filter は既定が Stop、TextureProgressBar は Pass で、どちらもクリックを受け止めて、下に重なったボタンやゲーム画面へ届けません。親から子へは広がらないので、入れ物と2本のバーのそれぞれを Ignore にします。Label は最初から Ignore です

まとめ

  • バーが描くのは割合: min_value と max_value のあいだで value がどこにいるか。数は Label が出す
  • 塗りは切り取られる: texture_progress には満タンの絵を1枚。枠は texture_over に分ける
  • 減った分は2本で見せる: 手前はすぐ、奥だけ Tween で遅れて追いつかせる。下地は奥、枠は手前
  • 回復の判定は前回の数と比べる: 奥のバーが持っているのは、追いつく途中の見えている長さ
  • 頭上のバーは親の変形を受け取る: 向きだけなら Flip H、回すなら Top Level と毎フレームの位置

食らった量まで見せたいなら、2本目を重ねてみてください。あなたのゲームのゲージは、残量だけを伝えるものでしょうか。

次に読む学習パスの次の記事
UI

テーマシステムによる統一的なUI設計 - 一貫性のあるデザイン実装

この記事を読む
Godot このセクションのノート104