HPバーは、いまの残りがすぐ読めて、食らった分も一瞬だけ見えると分かりやすくなります。Godot では、TextureProgressBar を2本重ね、手前はすぐに、奥だけ遅れて減らして作ります。
バーを1本だけ Tween で滑らかに減らすと、動いているあいだは今のHPが読めません。一瞬で減らすと、今度はどれだけ食らったのかが分かりません。
この記事では、減った分が遅れて消えるゲージを作り、同じゲージを敵の頭上にも傾かずに置く方法を解説します。
この記事でわかること
- バーが描いているのは割合で、塗りの絵は縮まずに切り取られる
- 手前と奥の2本を重ねて、減った分だけを遅れて消す作り方
- 連続で食らったときと回復したときに、奥のバーをどう扱うか
- 実践:同じゲージをHUDと敵の頭上に置き、敵が傾いてもバーは水平に保つ
動作確認環境: Godot 4.7
バーが描く割合
ProgressBar も TextureProgressBar も、Range という同じクラスを継承しています。シーンツリーに Range というノードを置くわけではなく、どちらも次の3つの値を持っている、という意味です。
min_value: 空のときの値。既定は0max_value: 満タンのときの値。既定は100value: いまの値
バーが描くのは、この3つから出る割合です。min_value が 0 なら value / max_value で、70 / 100 なら7割の長さになります。

バーは長さしか持ちません。「残り70」という数そのものを出したいなら、Label を別に置きます。ProgressBar には 割合を「70%」と出す機能もありますが、HPを 70 / 100 の形で見せたいなら Label のほうが自由です。
ProgressBar の色の変え方
ProgressBar のインスペクタを上から下まで見ても、塗りの色を指定する欄はありません。Modulate を触ると、下地も塗りも数字も一緒に染まります。
色を分けるには、インスペクタの Theme Overrides を開きます。見た目の設定をこのノードだけ上書きする欄で、その中の Styles を使います。
- Fill に StyleBoxFlat(単色の四角を描くスタイル)を新規作成し、その BG Color を塗りの色にする
- Background に別の StyleBoxFlat を新規作成し、その BG Color を下地の色にする

Show Percentage は既定でオンです。Value の既定が 0 なので、置いた直後の表示は「0%」になります。要らなければオフにします。
角を丸めたい、枠線を付けたい、といった程度なら ProgressBar で足ります。下地・塗り・枠を画像で分けて扱いたくなったら、次の TextureProgressBar を使います。
TextureProgressBar の3枚
TextureProgressBar は、テクスチャを3枚受け取ります。インスペクタの Textures グループを開くと、Under・Progress・Over の3つが並んでいます。
texture_under: 下地。値に関係なく全体が出るtexture_progress: 塗り。値の割合で切り取られるtexture_over: 上に載る枠。値に関係なく全体が出る

大事なのは真ん中です。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を通過していて、今いくつなのかが読めません。
やりたいのは逆です。いまの割合は待たせずに手前へ出し、減る前の割合は奥に残して、その差が見えてから消します。残った差の長さが、まだ消えていない被弾の表示になります。

作り方は、同じ大きさの 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 は成り立たず、回復なのに奥がさらに減りはじめます。前回渡された数と比べれば、この取り違えは起きません。

連続で食らったときは、前の Tween を止めてから新しいものを作ります。待ち時間も数え直されるので、連打しているあいだ差は開いたままになり、止んでからまとめて縮みます。
奥のバーの Step は 0 にしておきます。既定の 1 のままだと、Tween が動かす途中の値が整数に丸められ、追いつく動きが階段になります。
敵の頭上のバー(Flip H と Top Level)
同じゲージを敵の頭上に置くと、新しい問題が出ます。
Control を Node2D の子に置くと、位置は親についてきます。ここまでは狙いどおりです。ところが、親の回転も拡大も、scale.x を -1 にした反転もそのまま受け取ります。敵が傾けばバーも傾き、数字は読めない角度になります。

アンカー(親の矩形のどこに合わせるか)も当てになりません。親が Node2D だと、アンカーが基準にする矩形は幅も高さも 0 です。中央そろえも、いっぱいに広げるも働かないので、大きさは自分で決めます。
直し方は、何を動かしたいかで変わります。
- 向きを変えるだけ: Sprite2D の Flip H を使います。絵の描画だけを左右反転するもので、変形として子や兄弟へは伝わりません。バーをスプライトの兄弟に置いておけば、絵だけが振り向いてバーは水平のままです
- 親そのものを回す: バーに Top Level を付けます。親の変形をまとめて無視できますが、移動まで無視するので、位置は毎フレーム自分で入れ直します。この2つは必ずセットです

実践: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行だけです。
| BackBar | FrontBar | |
|---|---|---|
| Textures > Under | うすいグレーの帯 | なし |
| Tint > Progress | うすい青 | 濃い青 |
| Step | 0 | 1 |
シーンを保存したら、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) のようなシグナルで渡す形にすると、ゲージ側は相手を知らずに済み ます。その繋ぎ方は シグナルの記事 にあります。
③ 動かして確かめる

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本目を重ねてみてください。あなたのゲームのゲージは、残量だけを伝えるものでしょうか。