【Godot Engine】3Dシーン上に体力バーを表示する

3Dシーン上のキャラクターにいい感じに体力バーを表示する方法.

health bar styled

環境

Godot Engine v4.4.1

メインシーンを作る

まずはメインのシーンをざっと作る.後でキャラクターを置いて動かすための,最低限のカメラと光源および地面を用意すればよい.

main scene

キャラクターのシーン Actor を作る

新規シーンを作成し,CharacterBody3D 型を継承するシーン Actor をつくる.子ノードとして,地形との当たり判定を行うための CollisionShape3D と見た目がわかるように MeshInstance3D をつけておく.

actor scene

シーンができたらルートにスクリプト actor.gd をアタッチし,方向キーでキャラクターを動かすためのプログラムを書く.ここは CharacterBody3D 型のテンプレートのスクリプトを少し改変するだけでOK.

class_name Actor extends CharacterBody3D


const SPEED = 5.0


func _physics_process(delta: float) -> void:
	if not is_on_floor():
		velocity += get_gravity() * delta

	var input_dir := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
	var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
	if direction:
		velocity.x = direction.x * SPEED
		velocity.z = direction.z * SPEED
	else:
		velocity.x = move_toward(velocity.x, 0, SPEED)
		velocity.z = move_toward(velocity.z, 0, SPEED)

	move_and_slide()

ActorMain シーン内に配置して動作確認をする.

actor

Actor の体力を表す変数も用意しておく.これは後から体力バーのシーンから参照することになる.

 class_name Actor extends CharacterBody3D
 
 
 const SPEED = 5.0
+
+@export var max_health: int = 100
+
+var _current_health: int
+
+
+func _ready() -> void:
+	_current_health = max_health
 
 
 func _physics_process(delta: float) -> void:
 	...

体力バーのシーン HealthBar を作る

キャラクターにUIを追加

Actor の体力バーを作成する.ActorControl ノードを追加し,名前を HealthBar に変更する.HealthBar の直下に ProgressBar 型のノードを追加する.デフォルトだとバーの横幅が潰れてしまっているので,ProgressBarインスペクターから,Custom Minimum Sizex128 pxに設定し,ある程度横幅をもつようにする.

続いて,ProgressBarTransformPosition(-64, -28) pxに設定し,バーの下辺中央と原点が接するようにする.

体力バーの位置をキャラクターに追従させる

プログラムを実行してみるとわかるが,このままだと体力バーは画面上に表示されない.なぜなら,体力バーはゲーム画面上の座標 (0,0) の少し上,つまり画面外にあるためである.

outside

体力バーの位置が Actor の位置を追従するようにするためのプログラムを書く.スクリプト health_bar.gd を作成して HealthBar (ProgressBarではない!) にアタッチする.

health_bar.gd には自身の位置をキャラクターの位置に同期させるための処理を書く.具体的には,毎フレーム (_physics_process の呼び出し) ごとにシーンのカメラを取得し,unproject_position メソッドによってキャラクター位置の三次元座標 _actor.global_position を画面上の二次元座標に変換,得られた座標を体力バー位置の二次元座標 global_position に設定する.

class_name HealthBar extends Control


var _actor: Actor


func _ready() -> void:
	_actor = get_parent()


func _physics_process(delta: float) -> void:
	var camera: Camera3D = get_viewport().get_camera_3d()
	global_position = camera.unproject_position(_actor.global_position)

ゲームを実行すると,キャラクターの中心,つまり Actor シーンの原点の位置に体力バーが表示され,キャラクターの動きに追従するようになる.

healthbar

また,体力バーの位置を上下方向に調整するためのパラメタ y_offset を用意すると,後から見栄えを調整しやすくなる.下記プログラムでは,体力バーの表示位置を2メートルほど高い位置に設定している (Actor の上0.5メートルあたりに表示されるようにしている).

 class_name HealthBar extends Control
 
 
+@export var y_offset: float = 2.0
+
 var _actor: Actor
 
 
 func _ready() -> void:
 	_actor = get_parent()
 
 
 func _physics_process(delta: float) -> void:
 	var camera: Camera3D = get_viewport().get_camera_3d()
-	global_position = camera.unproject_position(_actor.global_position)
+	global_position = camera.unproject_position(_actor.global_position + Vector3.UP * y_offset)

キャラクターの原点の取り方について

この記事のゲームは割とやっつけで作っていたため,下記,上図のようにキャラクターの中心を原点にしているが,実際の開発でキャラクターのモデルをきちんと作ることを考えると下図のように足元を原点にしたほうが扱いやすいと思われる.

親クラスがおかしい場合に警告を表示する

HealthBarノードは親ノードがActorであることを前提に処理を行う.しかし,コードを読まないとその仕様に気づけないし,仕様書をきちんと作っても厳格に守るのは難しい.そのような場合は関数 _get_configuration_warnings を利用し,階層構造が不正な場合に警告を出すようにすると良いだろう. (@tools を有効にしておく必要がある)

https://docs.godotengine.org/ja/4.x/classes/class_node.html#class-node-private-method-get-configuration-warnings

func _get_configuration_warnings() -> PackedStringArray:
	if not owner is Actor:
		return ["HealthBarはActorの子ノードである必要があります"]
	else:
		return []

キャラクターの体力を体力バーに反映する

続いて,Actor の体力を表す変数 _current_healthHealthBar の値を同期させたい.これを実現するには,Actor がダメージを受けるなどして体力が変動したときに呼び出されるシグナルを用意するとよい.

以下のプログラムでは,Actorの体力を減らす関数 damage が呼び出されたとき, Actor の体力の最大値と現在の体力をシグナル health_changed で送信する.

また,シグナルの受信側で「体力の最大値に対する現在の体力の割合」を計算できると都合がいいので max_health の値も同時に送るようにする.

 class_name Actor extends CharacterBody3D
 
 
+signal health_changed(max_health: int, current_health: int)
+
 const SPEED = 5.0
 
 @export var max_health: int = 100
 
 var _current_health: int
 
 
 func _ready() -> void:
 	_current_health = max_health
+	health_changed.emit(max_health, _current_health)
 
 
 func _physics_process(delta: float) -> void:
 	...
 
 
+func damage(amount: int):
+	_current_health = min(max(0, _current_health - amount), max_health)
+	health_changed.emit(max_health, _current_health)

本記事では,実際に Actor に対して攻撃をして体力を減らす,といった処理の実装までは踏み込まないため,damage 関数の挙動を簡単に確認するために,マウスクリックで damage 関数を呼び出す処理を書いておく.

 class_name Actor extends CharacterBody3D
 
 ...
 
 
 func damage(amount: int):
 	_current_health = min(max(0, _current_health - amount), max_health)
 	health_changed.emit(max_health, _current_health)
 
 
+func _unhandled_input(event: InputEvent) -> void:
+	if event is InputEventMouseButton:
+		if event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
+			damage(5)

HealthBarActor の体力が変化したとき,その割合をプログレスバーに反映する処理を書く.

 class_name HealthBar extends Control
 
 
+@onready var bar: ProgressBar = $ProgressBar
+
 @export var y_offset: float = 2.0
 
 var _actor: Actor
 
 
 func _ready() -> void:
 	_actor = get_parent()
+	_actor.health_changed.connect(_on_actor_health_changed)
 
 
 func _physics_process(delta: float) -> void:
 	var camera: Camera3D = get_viewport().get_camera_3d()
 	global_position = camera.unproject_position(_actor.global_position + Vector3.UP * y_offset)
 
 
+func _on_actor_health_changed(max_health: int, current_health: int) -> void:
+   # プログレスバーの最大値に体力の割合をかけた値を設定する
+	bar.value = bar.max_value * (current_health / float(max_health))

ゲームを実行し,マウスクリックをするとプログレスバーの値が減少していくことがわかる.

health changed

動作確認ができたら,シーンツリー上のHealthBarを右クリックし,ブランチをシーンとして保存を選択し,別のシーンファイルに保存する.

体力バーの見た目を整える

スタイルを適当に整える.

ProgressBarShow Percentage をオフにし,Theme OverridesStylesBackgroundFill に適当な StyleBoxFlat を設定する.Backgroundは体力が減った後の背景部分,Fillは体力が減る前のゲージ部分に相当する. BackgroundExpand Margins の各値を 2 ぐらいにしておくと体力バーの枠線を表現できる.

また,スタイルの設定時は ProgressBarValue を一時的に0以外の適当な値にしておかないと Fill の見た目が確認できないので注意.

health bar styled

その他応用:ダメージ数値の表示

詳しい説明は省くが,今回と同様に Camera3D.unproject_position メソッドを利用することで,キャラクターが受けたダメージ表記を表示する,といったことも実現できる.お試しあれ.

おしまい