Global CanvasLayer for Ui

Godot Version 4

Question
Hello, I am currently follwong the 2D action adventure rpg tutorial series from Micheal Games. For my healtbars i tried going a dofferent direction and im having problems implementing them right.

I created a seperate scene with a:

CanvasLayer (general ui script)
I—Control
I----ProgressBar(Healtbar)(healtbar script)
I------ProgressBar(Damagebar)

My Canvas layer has a script that is set to a global Script.

The healtbar has a script that controls both Progressbars and the code itself works.
I have tested it by just adding ist directly to my Play scene.

But now that i have my setup as its own scene with a global autoload, im getting either errors, or the bars are not updating.

My Player has an hp and max_hp value that im tring to pass as a signal (with emit) in his “hp_changed func”.

I seem to be referenzing thoses variables wrong.

My question in general ist just,

  1. How to i get variables from a different scene (in this example “Player.hp and Player.max_hp”) to my autoload global “PlayerHud”
  2. Is it right that i autoload the script which is on my CanvasLayer to initialize the healthbar? This is returning null on many variables because the player didnt load in yes.

Sorry if im not provising rhe code or if i forgot anything. Im writing this on my phone rn.
Also i am very new to godot and game making in general.

Thank you all!

Without code is hard to tell where the problems are.

Did you add the script (.gd file) or the scene (.tscn file) as the autoload? If you did the former then try removing it and adding the .tscn file as the autoload. This may fix the errors.

To access your player you could pass it to the autoload like:

# player.gd
# ...
func _ready() -> void:
    PlayerHud.player = self
    # ...

But that will cause problems if your player node gets freed.

It would be better if you had a function in your autoload like change_health(current, max) and then make the player call it like:

# player.gd
# ...
func take_damage(amount:int) -> void:
    hp -= amout
    PlayerHud.change_health(hp, max_hp)
1 Like

I tried it both ways. Currently my game only runs if i autoload the script that is attached to my HUD scene. if i add my scene to autoload i geht this errorcform my Player script:

Invalid access to property or key ‘health_bar’ on a base object of type ‘Nil’.

Player script:

class_name Player extends CharacterBody2D


var cardinal_direction : Vector2 = Vector2.DOWN
var direction : Vector2 = Vector2.ZERO
const DIR_4 = [ Vector2.RIGHT, Vector2.DOWN, Vector2.LEFT, Vector2.UP ]

@onready var animation_player: AnimationPlayer = $AnimationPlayer
@onready var effect_animation_player: AnimationPlayer = $EffectAnimationPlayer
@onready var hit_box: HitBox = $HitBox
@onready var sprite: Sprite2D = $Sprite2D
@onready var state_machine: PlayerStateMachine = $StateMachine
@onready var damage_bar: ProgressBar = $HealthBar/DamageBar
#@onready var health_bar: ProgressBar = $HealthBar


var health_bar : HealthBar = null


signal DirectionChanged( new_direction: Vector2 )
signal player_damaged( hurt_box : HurtBox)
signal healthchanged

var invulnerable : bool = false
@export var hp : int = 3
@export var max_hp : int = 20



# Called when the node enters the scene tree for the first time.
func _ready():
	PlayerManager.player = self
	PlayerManager.player_ready.emit(self)
	state_machine.initialize(self)
	hit_box.damaged.connect(_take_damage)

	await get_tree().create_timer(0.05).timeout  # Kurze Wartezeit für HUD

	var path = "/root/PlayerHud/Control/HealthBar"
	while PlayerHud.health_bar == null:
		await get_tree().process_frame  # eine Frame warten

	health_bar = PlayerHud.health_bar
	health_bar.init_health(hp)
	update_hp(0)

	pass


# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
	
	#direction.x = Input.get_action_strength("right") - Input.get_action_strength("left")
	#direction.y = Input.get_action_strength("down") - Input.get_action_strength("up")
	direction = Vector2(
		Input.get_axis("left", "right"),
		Input.get_axis("up", "down")
	).normalized()
	
	pass
	
	
func _physics_process(delta: float) -> void:
	move_and_slide()



func set_direction() -> bool:
	if direction == Vector2.ZERO:
		return false
		
	var direction_id : int = int( round( ( direction + cardinal_direction * 0.1 ).angle() / TAU * DIR_4.size() ) )
	var new_dir = DIR_4[ direction_id ]
	
	if new_dir == cardinal_direction:
		return false
		
	cardinal_direction = new_dir
	DirectionChanged.emit( new_dir)
	sprite.scale.x = -1 if cardinal_direction == Vector2.LEFT else 1
	
	return true
	
	
func update_animation( state : String ) -> void:
	animation_player.play( state + "_" + AnimDirection() )
	pass
	
	
func AnimDirection() -> String:
	if cardinal_direction == Vector2.DOWN:
		return "down"
	elif cardinal_direction == Vector2.UP:
		return "up"
	else:
		return "side"
	
		
func _take_damage( hurt_box : HurtBox) -> void:
	if invulnerable == true:
		return
	update_hp( -hurt_box.damage )
	if hp > 0:
		player_damaged.emit( hurt_box )
	else:
		player_damaged.emit( hurt_box )
		update_hp(99)
	pass
	
func update_hp ( delta : int ) -> void:
	hp = clampi( hp + delta, 0, max_hp)
	if health_bar:
		health_bar.hp = hp
	pass

func make_invulnerable ( _duration : float = 1.0 ) -> void:
	invulnerable = true
	hit_box.monitoring = false
	
	await get_tree().create_timer( _duration).timeout
	invulnerable = false
	hit_box.monitoring = true
	pass

in my Autoload i have my PlayerManager script which should give acces to my player data.

Playermanager

extends Node

const PLAYER_CHARACTEER = preload("res://scenes/player/PlayerCharacteer.tscn")
var player : Player
signal player_ready(player: Player)
var player_spawned : bool = false


func _ready () -> void:
	add_player_instance()
	await get_tree().create_timer(0.2).timeout
	player_spawned = true

func add_player_instance() -> void:
	player = PLAYER_CHARACTEER.instantiate()
	add_child( player )
	
func set_player_position( _new_pos : Vector2) -> void:
	player.global_position = _new_pos
	
func set_as_parent( _p : Node2D ) -> void:
	if player.get_parent():
		player.get_parent().remove_child( player )
	_p.add_child( player )
	
func unparent_player( _p : Node2D ) -> void:
	_p.remove_child( player )
	
	

PlayerHud

extends CanvasLayer

@onready var health_bar : HealthBar = $Control/HealthBar

HealthBar

class_name HealthBar
extends ProgressBar

@onready var timer: Timer = $Timer
@onready var damage_bar: ProgressBar = $DamageBar

signal health_depleted

var hp = 0 : set = _set_health

func _set_health(new_health: int) -> void:
	var prev_health = hp
	hp = min(max_value, new_health)
	value = hp

	if hp <= 0:
		emit_signal("health_depleted")

	if hp < prev_health:
		timer.start()
	else:
		damage_bar.value = hp

func init_health(_hp: int) -> void:
	hp = _hp
	max_value = _hp
	value = _hp
	damage_bar.max_value = _hp
	damage_bar.value = _hp

func _on_timer_timeout() -> void:
	damage_bar.value = hp

Most of this code is identical to the youtube tutorial i mentioned. He used a heart system like zelda games and i tried adding this healthbar in a similar fashion.

If i can supply any more info just ask me. In the mean time i will try out your suggestions.
Thank you :slight_smile:

It “works” when adding the script because PlayerHud.health_bar is null and the player _ready() is stuck in this loop:

The reason PlayerHud.health_bar is null is because the script doesn’t know anything about the scene. That’s why you need to add the scene itself instead of only the script.

Are you sure you are adding the tscn with the same name as when you added the gd file as the autoload? That error means that PlayerHud is null

Im a bit confused with the question.

I added the Player_HUD.tsc to my globals. I have changed all parts in my code that said Player_Hud to Player_HUD because of casing. My script is called player_hud and is not in my globals. The script should load because it is attached to my Player_HUD scene’s CanvasLayer.

I have found a way to make it work. my player script has a function on change health and i created 2 signals for hp and max hp in that function. my player hud manager connects to those signal with a funktion for health changed and max health changed and sends those values to its progressbar child.

Sorry to join in on this so late, but this does not seem right to me. An autoloaded script is managed by Godot at the root level so it is globally available. You should not then attach that to a node in the scene tree. You may have ‘got away with it’ but I am pretty sure this is going to come back and bite you.

Attach a normal script to your canvas layer, and then you can access the variables in your global script any time from your canvas layer script (or anywhere else). Attaching a global script to a node seems like madness to me. I could be wrong, but this does not seem right in any way to me. (If I have misunderstood then please ignore this.)

Thats something i copied from a youtube tutorial series. So currently my Player HUD scene is a global autoload. Im not adding the playerHUD scene to anything in my project. Its just something that is loaded at all time. It will problably be anoying in the fututre when i make a hub zone where i dont need tio show the player hp but other then that i dont see a big problem.

I mean in an ideal world i would be able to script everything clean an perfect but i just startet a week ago and have no game making and programming experience. without youtube and chatgpt i wouldnt be able to do anything right now :laughing:

1 Like