Godot 엔진에서 HP바 만들기

지난 글에서 캐릭터에 중력과 가속도 등 물리를 적용해 점프 등을 구현했다. 이번 글에서는 몹을 만들어보기에 앞서 플레이어의 HP, 기력 등의 속성을 부여하고 체력바 등의 GUI로 구현하려고 한다.

Godot 4.4에서 체력과 기력 바는 ProgressBar 노드로 쉽게 만들 수 있다. 플레이어의 상태를 관리하는 변수를 UI와 연동해서 실시간으로 반영되도록 설정해보겠다.


UI 노드 추가

CanvasLayer 추가
CanvasLayer 추가

이전에 Main으로 이름 붙였던 루트 노드의 하위 위치에 'CanvasLayer' 노드를 추가한다. 추가한 뒤 다른 노드와 구분을 위해 노드의 이름을 'UI'로 변경해주었다. 이제 화면 위에 표시되는 그래픽 인터페이스 요소는 이 UI 노드 안에 생성해주면 된다.


ProgressBar 노드 추가
ProgressBar 노드 추가

이제 HP와 기력을 나타내는 바를 만들어야 하므로 UI 노드 안에 'ProgressBar'노드를 추가한다.


생성된 ProgressBar
생성된 ProgressBar

'ProgressBar' 노드를 추가하고 뷰포트를 확대해보면 바 요소가 추가된 모습을 확인할 수 있다. 인스펙터에서 크기를 변경할 수 있고 상단 탭에서 프리셋을 선택해 만들 수도 있다.


노드 이름 변경
노드 이름 변경

다른 노드와 구분하기 위해 'CanvasLayer' 노드의 이름은 UI로 변경하고 'ProgressBar' 노드의 이름은 HPBar라고 변경했다.(이미지 상에서는 띄어쓰기를 했으나 나중에 없앴다. 이후에 스크립트에서 표기할 때 똑같이만 써주면 된다.)


ProgressBar 인스펙터
ProgressBar 인스펙터

'ProgressBar' 인스펙터를 보면 'Show Percentage' 옵션이 있는데, 체크를 해제하면 뷰포트에 표시되어있던 퍼센트가 사라진다.


ProgressBar 스타일 변경
ProgressBar 스타일 변경

'Theme Overrides'에서 'Styles'를 보면 'Background'와 'Fill' 옵션이 있다. 각각 배경색과 바 색상을 지정할 수 있다. 여기서는 '새 StyleBoxFlat'을 추가해 색상을 넣었다.


ProgressBar 스타일 분리
ProgressBar 스타일 분리

바의 색상을 지정한 뒤 '유일하게 만들기'를 클릭하면 해당 노드를 복제했을 때 링크를 끊는다.  '유일하게 만들기'를 하지 않으면 복제했을 때 하나의 바 색상을 변경했을 때 다른 바까지 같이 변경된다.


생성된 바 UI
생성된 바 UI

크기를 조정하고 인스펙터에서 값을 조정했다. 'Value'를 낮추면 게이지가 줄어들었을 때 어떻게 보이는지 확인할 수 있다.


ProgressBar 위치 조정
ProgressBar 위치 조정

'ProgressBar' 노드의 위치는 뷰포트에서 직접 컨트롤 하기보다 인스펙터에서 값을 조정하는 편이 좋다. 'Anchors Preset'에서 원하는 위치를 선택하면 뷰포트에서 맞춰지는데, 초기 설정이 왼쪽 위로 되어있다. 뷰포트에서 조작을 해버렸다면 다시 '왼쪽 위'를 선택해도 변경되지 않을 수 있으니 다른 위치로 선택했다가 다시 왼쪽 위를 선택하면 맞춰진다.

이제 'Transform' 항목에서 사이즈와 위치를 조정하면 된다.


배치된 UI
배치된 UI

저장한 뒤 'F5'를 눌러 상태바가 제대로 자리 잡았는지 보자.


플레이어 상태 변수 작성 및 GUI 연결

HP 바와 기력 바를 배치했다면 이제 플레이어에게 HP와 기력을 할당하고 GUI 노드와 연결해야 한다.

이전 글에서 플레이어에게 적용했던 물리 스크립트에 이어 다음과 같이 작성했다.

extends CharacterBody2D
# 현실 물리 단위 (1m = 100px로 가정)
const METERS_TO_PIXELS = 100.0  # 1미터 = 100픽셀
# 물리 상수
@export var max_speed: float = 5.0 * METERS_TO_PIXELS
@export var ground_acceleration: float = 10.0 * METERS_TO_PIXELS
@export var ground_friction: float = 8.0 * METERS_TO_PIXELS
@export var air_drag: float = 0.02
@export var gravity: float = 9.8 * METERS_TO_PIXELS
@export var jump_strength: float = -5.0 * METERS_TO_PIXELS
@export var max_fall_speed: float = 20.0 * METERS_TO_PIXELS
# 체력과 기력
@export var max_hp: float = 100.0
@export var max_stamina: float = 100.0
var hp: float = max_hp
var stamina: float = max_stamina
@export var stamina_drain_rate: float = 10.0
@export var stamina_recovery_rate: float = 5.0
# UI 참조
@onready var hp_bar = get_node("/root/Main/UI/HPBar")
@onready var stamina_bar = get_node("/root/Main/UI/StaminaBar")
var last_hp: float = hp
var last_stamina: float = stamina
var input_direction: float = 0.0
func _ready() -> void:
hp_bar.max_value = max_hp
hp_bar.value = hp
stamina_bar.max_value = max_stamina
stamina_bar.value = stamina
func _physics_process(delta: float) -> void:
# 입력 방향 계산
input_direction = 0.0
if Input.is_action_pressed("ui_right"):
input_direction += 1.0
if Input.is_action_pressed("ui_left"):
input_direction -= 1.0
# 수평 이동
if is_on_floor():
if input_direction != 0:
velocity.x += input_direction * ground_acceleration * delta
else:
var friction_force: float = -sign(velocity.x) * ground_friction * delta
if abs(velocity.x) > abs(friction_force):
velocity.x += friction_force
else:
velocity.x = 0.0
else:
if input_direction != 0:
velocity.x += input_direction * ground_acceleration * 0.5 * delta
velocity.x -= velocity.x * air_drag
# 최대 속도 제한
velocity.x = clamp(velocity.x, -max_speed, max_speed)
# 점프
if Input.is_action_just_pressed("ui_select") and is_on_floor():
velocity.y = jump_strength
# 중력 적용
if not is_on_floor():
velocity.y += gravity * delta
# 최대 낙하 속도 제한
if velocity.y > max_fall_speed:
velocity.y = max_fall_speed
# 플레이어 상태 관리
update_player_state(delta)
# 디버깅 출력
print("Velocity X: ", velocity.x, " Velocity Y: ", velocity.y, " HP: ", hp, " Stamina: ", stamina)
# 이동 적용
move_and_slide()
func update_player_state(delta: float) -> void:
# 기력 회복
if is_on_floor():
stamina += stamina_recovery_rate * delta
stamina = clamp(stamina, 0, max_stamina)
# 테스트용 데미지 (Tab 키 누르면 체력 감소)
if Input.is_action_just_pressed("ui_focus_next"):
take_damage(10.0)
# 테스트용 기력 소모 (Shift+Tab 키 누르면 기력 감소)
if Input.is_action_just_pressed("ui_focus_prev"):
drain_stamina(10.0)
# UI 업데이트
update_ui()
# UI 업데이트 함수
func update_ui() -> void:
if last_hp != hp or last_stamina != stamina:
hp_bar.value = hp
stamina_bar.value = stamina
last_hp = hp
last_stamina = stamina
# 데미지 받기 함수
func take_damage(amount: float) -> void:
hp -= amount
hp = clamp(hp, 0, max_hp)
if hp <= 0:
print("Player died!")
# 기력 소모 함수
func drain_stamina(amount: float) -> void:
stamina -= amount
stamina = clamp(stamina, 0, max_stamina)
if stamina <= 0:
print("Stamina depleted!")


HP 바와 기력 바를 적용한 플레이 화면
HP 바와 기력 바를 적용한 플레이 화면

Tab 키를 누르면 체력이 감소하고 Shift+Tab을 누르면 체력과 기력이 함께 감소하도록 설정하고 테스트해보았다. 위 이미지와 같이 잘 적용된 모습을 볼 수 있다.


댓글

이 블로그의 인기 게시물

전체 화면으로 현재 시간 보여주는 웹 시계 사이트 Bonfire Clock

블렌더 3D 카툰 렌더링으로 웹툰 배경 만들기

블렌더 3D에서 침대 모델링하는 방법