Godot Version
Version 4.5
Question
Hello, this is my first forum post and I’m still pretty new to the engine and GDScript, but I’ve tried pretty much everything I can think of in order to do what the title hopefully suggests: making a ProgressBar that changes its %/value based off of a player node’s health variable. I’ve looked at many tutorials, and have so far tried using classes to reference nodes, and referencing nodes directly; but whatever I’ve tried, it hasn’t worked, so I’m here now. Also P.S, I’m using a 3D scene and 3D objects to test the ProgressBar’s value depletion.
My player character contains a script with its attributes, including the HP I mentioned, and a reference to the ProgressBar node (renamed HealthBar):
extends CharacterBody3D
class_name Player
@onready var healthbar: HealthBar
@export var player_hp : float = 100
#my most recent attempt at calling the HealthBar
func _ready() -> void:
healthbar._hp_setup(player_hp)
In my CharacterBody3D node (my Player), it has a child of the HealthBar. This is the Healthbar’s script:
extends ProgressBar
class_name HealthBar
var change_value_tween: Tween
var opacity_tween: Tween
func _hp_setup(max_val: float):
value = max_val
max_value = max_val
$ProgressBar.value = max_val
$ProgressBar.max_value = max_val
func change_value(new_value: float):
value = new_value
In order to test that the HealthBar was interacting with the Player’s health, I set up an Area3D and a signal in the Player’s script to deplete the health variable by 1 each time it enters the Area3D. This worked, but did not adequately display on the ProgressBar, and I was also having trouble setting it up to happen each frame the Player was in the Area3D:
func _on_entity_area_body_entered(_body: Node3D) -> void:
player_hp -= 1
healthbar.change_value(player_hp)
print(player_hp)
print("entered")
I can try and provide more information. If you have an alternative way of doing this that’s completely different I’m open to any ideas really, but most of all I’d like to know what exactly is going wrong with the methods I tried. Sorry for the long post/
First off, your code looks fine, so you should check the collision layers of the player an area3D and verify that they detect each other. You can set a breakpoint inside that body_entered function. If it’s hit during runtime, it’s working as expected.
I would make a “player.receive_damage(damage_amount: float)“ for clarity, and emit a “player damaged” signal. Also you can clamp health values between 0 and max in there.
To damage the player every frame they are inside an area3D:
Create a variable for the player inside the area3D script.
Connect the “on_body_entered” function, check if the body is the player class. If it is, then assign it to the global player variable in the script. Also connect “on_body_exited” and delete the player reference (make null)
Use the _process function to check if the player variable is valid. If it is, call the player.receive_damage(damage_amount) function
Sorry for the late reply. I’m not sure if I wrote your suggestions out correctly, but I did my best:
extends Area3D
class_name EntityArea
@onready var player = $"../Player"
func _process(_delta: float) -> void:
on_body_entered(player)
func on_body_entered(body: Node3D) -> void:
if body == player:
print("entered")
print (player.player_hp)
player.player_hp -= 1
else:
return
I wrote this in a script attached to the Area3D, but I ran into the same problem as before. Referencing the player, I first tried referencing it by its class, but I was getting some errors regarding having access to the player’s variables, so I tried referencing the node next (the code written above.) Perhaps part of it is that I’m just failing to visualize the changes you suggested. One thing I do know though is that the Area3D collision is being detected as I printed a debug message to the terminal every time I entered it. However, this only was proven when the function was not called in the _process function of the Area3D. I also attempted to rewrite the ProgressBar script’s function like (I think?) you also suggested for clarity, I tried to change some things in there too:
extends ProgressBar
class_name HealthBar
@onready var player = Player
func receive_damage(damage_amount: float):
damage_amount = 1
$ProgressBar.value = player.player_hp - damage_amount
$ProgressBar.max_value = player.player_hp
If I’m missing something really obvious or simple I’m really sorry, just thought I’d ask for some clarity anyways so I can hopefully get a better idea of what you’re suggesting to me.
Why do you have two ProgressBars?
Your HealthBar extends ProgressBar, so it is a ProgressBar, just with some additional code. But in that code you reference $ProgressBar, a child node of your HealthBar that is also a ProgressBar?
Oops, my bad. I thought I had to reference the base class in order to edit attributes of the HealthBar. I have since changed it, I have another ProgressBar under it to act as a background to the Healthbar, but did not intend to reference that. Unfortunately I still have not gotten the HealthBar value to change.
If the HealthBar is a direct child of the player, you can use $HealthBar to access it. (This is using the node’s name, I’m assuming the node has the same name as the class.)
In your previous code, you didn’t seem to assign anything to the var healthbar.
: HealthBar is just a type hint, meaning this var can only store references to HealthBar objects, but it never got the actual reference. It should be something like this:
@onready var healthbar: HealthBar = $HealthBar
Ah, I see. I changed it now. There must be a disconnect between the “value” property of the healthbar and the player’s health variable still, although I fixed what you mentioned. a few more attempts later and I’m still unable to override the value property with player’s health variable. (If you don’t know how and/or don’t want to answer this its fine, just thought I’d ask while this post is fairly active.)
What do you mean by that? What exactly did you try, what did you expect to happen, and how did it fail?
Usually you would set the healthbar’s value property to the current health value when the latter gets changed. But then you have to make sure to always update the healthbar’s value, no matter from where you change the player’s hp. In the EntityArea script you posted, player.player_hp got changed and it didn’t look like the healthbar got notified about this.
The easiest way to do this is probably a setter function. Setter functions will always be called when the related property receives a new value. This allows you to change player_hp from anywhere without worrying about the healthbar every time.
@onready var healthbar: HealthBar = $HealthBar
@export var player_hp : float = 100:
set(value):
player_hp = value
if is_instance_valid(healthbar):
healthbar.value = value
Now you can just change the player_hp from any EntityArea like
func on_body_entered(body: Node3D) -> void:
if body is Player:
body.player_hp -= 1
or
func _physics_process(delta: float) -> void:
for body in get_overlapping_bodies():
if body is Player:
body.player_hp -= delta # 1 damage per second while inside the area
and the healthbar should get updated automatically.
Ah, I had no idea about setter functions before this. Thank you for the valuable insight, I’ll definitely keep that in mind for the future.
But to explain what didn’t work before, I referenced the player in the HealthBar script, and within that script I wrote this:
extends ProgressBar
class_name HealthBar
@onready var player: Player
func receive_damage(_damage_amount: float):
value = player.player_hp
$HealthBar.max_value = player.player_hp
After using the setter function, this code works, but before, the health bar did not update according to the value of player_hp, which I had printed to the terminal to make sure the value of player_hpwas being changed. Basically, I was expecting that, because I was assigning the HealthBar’s value attribute to be the player_hp, that it would update on the healthbar accordingly. (Apparently not, as my hypothesis was not correct on this)