Almost every game needs health. The player takes damage, an enemy dies, a bar on screen ticks down. It sounds trivial, and the naive version is, but the naive version also tangles your player script into your UI in a way you'll regret. Here's how to build it so it stays clean.
Put health in its own component
The instinct is to dump health into your player script. Don't. Make a small reusable component instead, and now your player, your enemies, and that breakable barrel all share the exact same code.
Create a Node called HealthComponent with this script:
extends Nodeclass_name HealthComponentsignal health_changed(current: int, max: int)signal died@export var max_health: int = 100var current_health: intfunc _ready() -> void: current_health = max_healthfunc take_damage(amount: int) -> void: current_health = clampi(current_health - amount, 0, max_health) health_changed.emit(current_health, max_health) if current_health <= 0: died.emit()func heal(amount: int) -> void: current_health = clampi(current_health + amount, 0, max_health) health_changed.emit(current_health, max_health)Two things make this work. clampi() keeps health between 0 and max, so you never overheal or show negative numbers. And the signals mean this component announces what happened without knowing or caring who's listening. That decoupling is the whole point.
Wire it into a character
Add a HealthComponent as a child of your player or enemy, then react to its signals:
extends CharacterBody2D@onready var health: HealthComponent = $HealthComponentfunc _ready() -> void: health.died.connect(_on_died)func _on_died() -> void: queue_free() # or play a death animation, drop loot, respawn, etc.When something hits this character, it calls health.take_damage(25). The character doesn't manage its own health number at all. It just owns a component and listens for the one event it cares about.
Connect a health bar
This is where the component pays off. Drop a TextureProgressBar (or a plain ProgressBar) into your UI and connect it to the same health_changed signal:
extends TextureProgressBar@onready var health: HealthComponent = $"../Player/HealthComponent"func _ready() -> void: health.health_changed.connect(_on_health_changed) # Set the starting state _on_health_changed(health.current_health, health.max_health)func _on_health_changed(current: int, maximum: int) -> void: max_value = maximum value = currentThe bar updates itself whenever health changes, and the player script has no idea the bar exists. Want a damage number popup or a screen flash too? Connect another listener to the same signal. Zero changes to the player or the component.
A smooth bar instead of a snapping one
A bar that jumps instantly reads as cheap. Tween it instead so it slides down:
func _on_health_changed(current: int, maximum: int) -> void: max_value = maximum var tween := create_tween() tween.tween_property(self, "value", current, 0.2)Two hundred milliseconds of slide and suddenly it feels like a real game.
Take it further
Once health is a clean component, everything else slots in: armor that reduces incoming damage, invincibility frames after a hit, regeneration over time, damage types. That's exactly the territory our Stats and Leveling System quest covers, building a full component-based stat system with modifiers, equipment bonuses, and damage formulas on top of this same idea.
If you want the deeper version of the architecture behind this, the RPG stat system guide goes further on modifiers and damage math.
FAQ
Where should I store health in Godot?
In a dedicated HealthComponent node, not directly in your player script. A component is reusable across the player, enemies, and destructible objects, and it lets your UI and other systems react through signals without coupling to the character.
How do I make a health bar in Godot 4?
Use a ProgressBar or TextureProgressBar, set its max_value and value, and update them from a health_changed signal. Connecting the bar to a signal means it refreshes automatically whenever health changes, with no per-frame polling.
How do I keep health from going below zero or above the max?
Use clampi() when you change it: current_health = clampi(current_health - amount, 0, max_health). That caps the value at both ends in one call, so you never display negative health or heal past the maximum.