Attaching a Progressbar (healthbar) to a custom node (HealthComponent)

Godot Version 4.6

Hi, I’m trying to connect a regular Health_Component to a Progress Bar ( The health bar, with an added child Progress Bar for the hurt bar) compatible with both enemies and the PC. I’m fairly new to both Godot and programming in general so I’m a bit lost.

I‘m mainly using tutorials on youtube for learning so I’ve used this one for the Health Bar: https://www.youtube.com/watch?v=f90ieBOoIYQ

While this is my Health_Component:

class_name HealthComponent extends Node

signal HealthChanged(current : float , max : float)signal Died

@export var max_health: float@export var body:       CharacterBody3D@export var model:      Node3Dvar current_health:     float

func _ready() → void:current_health = max_health_emit()

func Damage(amount : float):current_health = clamp(current_health - amount , 0.0 , max_health)_emit()if current_health == 0.0:Died.emit()

func Heal(amount : float):current_health = clamp(current_health + amount , 0.0 , max_health)_emit()

func _emit() → void:HealthChanged.emit(current_health,max_health)print(“HP %d / %d” % [current_health,max_health])

Any help or pointers to how to advance and learn how to do this is much appreciated.

Why is every function on a single line? That’s really hard to read.

  • Signal names should be snake_case.
  • Don’t name components “Component”. It makes the code longer to no purpose.
  • Make your variable names make sense in context of the component name. Health.current is a lot easier to read in code than HealthComponent.current_health. The same goes for signal names. Health.changed is clearer than Health.health_changed because it’s not redundant.
  • Function names should be snake_case.
  • You can use a setter to handle clamping, emitting health changed, and the died signal.
  • Changing current_health to an @onready var means you don’t have to set it in _ready().
  • Having the health setter emit the changed signal means you don’t need to do it in ready.
  • If you want something to happen when a signal is called, connect to the signal instead of creating a function to call it. It makes your code cleaner, and if you want to remove it later, you just delete the connection and the function.
  • The body and model variables aren’t needed.
class_name Health extends Node

signal changed(current: float , max: float)
signal died

@export var max: float

@onready var current: float = max:
	set(value):
	current = clampf(value, 0.0, max)
	changed.emit(current, max)
	if current == 0.0:
		died.emit()


func _ready() -> void:
	changed.connect(_on_health_changed)


func damage(amount : float):
	current -= amount


func heal(amount : float):
	current += amount


func _on_health_changed() -> void:
	print(“HP %d / %d” % [current, max])

So now with your progress bar:

extends ProgressBar

## The Health object we are tracking.
@export var health: Health


func _ready() -> void:
	health.changed.connect(_on_health_changed)
	max_value = health.max


func _on_health_changed(current: float, _max: float) -> void:
	value = current

Hi! I’ve done some modifications in my code in regards to your advice, also I’m no longer trying to connect them directly, because I’m just realizing that the point of a good composition is the ability to implement and remove parts with minimal errors and effort, and connecting the Health component to the HealthBar took away from that freedom, so I’ve connected them inside the Player script, trying to leave them modular.

In a way I have succeeded, the Progress Bar “Health” now responds to changes in health, be they positive (heal) or negative (hurt) but I’ve run into two problems.

1.-The Progress Bar “DamageBar” does not respond to the timer I’ve put, updating in a frame instead of seconds.

2.-If the value of “health.maxHP” is greater than the value of “health_bar.max_value” my ProgressBar “HealthBar” remains in its maximum value without updating, so I get a healthbar that does not move until the current health goes below the “health_bar.max_value”

Here is my code for Health component

class_name Health extends Node


signal changed(current : float , max : float)
signal died


@export var maxHP:      float
@export var body:       CharacterBody3D
@export var model:      Node3D

var current:            float 

func _ready():
	current = maxHP
	_emit()

func damage(amount : float):
	current = clamp(current - amount , 0.0 , maxHP)
	_emit()
	if current == 0.0:
		died.emit()

func heal(amount : float):
	current= clamp(current + amount , 0.0 , maxHP)
	_emit()

func _emit() -> void:
	changed.emit(current,maxHP)
	print("HP %d / %d" % [current,maxHP])

Here is my code for Healthbar

extends ProgressBar

@onready var timer = $Timer
@onready var damagebar = $DamageBar

var health = 0 : set = set_health


func set_health(new_health):
	var prev_health = health
	health = min(max_value,new_health)
	value = health
	
	if health < prev_health:
		timer.start()
	else:
		damagebar.value = health

func init_healt(_health):
	health = _health
	max_value = _health
	value = health
	damagebar.max_value = health
	damagebar.value = health
	
func new_max_health_range(new_max_health:float):
	max_value = new_max_health
	
	
func _on_timer_timeout():
	damagebar.value = health

and here is my code for the player.

class_name Player extends CharacterBody3D

@onready var innput = $input
@onready var movement = $Movement
@onready var jump = $Jump
@onready var health = $Health
@onready var health_bar = $CanvasLayer/HealthBar

func _ready() -> void:
	health.died.connect(_on_died)
	health_bar.health = health.maxHP
	
	
func _physics_process(delta: float) -> void:
	innput._update()
	
	movement.direction = innput.move_dir
	movement.tick(delta)
	
	jump.wants_to_jump = innput.jump_is_pressed
	jump.wants_to_stop_jump = innput.jump_is_released
	jump.tick(delta)
	
	health_bar.health = health.current
	
	
	if innput.hurt_is_pressed:
		health.damage(25)
		
	if innput.heal_is_pressed:
		health.heal(25)
		
	if position.y <= -10.0:
		health.current = 0
		_on_died()
		

func _on_died() -> void:
	print("Game Over!")
	get_tree().reload_current_scene()


You can do that. I actually just stopped doing that for the exact same reason you are doing it. I decided the Player didn’t need to know anything about the UI - or in fact the Health. It only needs to know if it’s dead or not. But I get where you’re coming from.

I can’t see your scene tree, so maybe you connected the signal outside of the code, but this should fix it:

func _ready() -> void:
	timer.timeout.connect(_on_timer_timeout)

What do you want to happen here?

The easiest solution is whenever health.maxHP changes update health_bar.max_value to equal it. You already have the signal.