Confused about classes, inherited scenes, functions and variables

Godot Version

v4.6.1.stable.official [14d19694e]

Question

I have made a class called enemy and to this I have added a function:

func hit(damage):
     health -= damage
     print("enemy was hit, health is ",health)

Inside another scene, e.g. projectile, I have the following:

if ‘hit’ in collider:
     collider.hit(10)

and then inside each inherited scene for enemies I define

var health := 100

It sort of works, as the print happens, however, the enemy health only goes to 90 and no lower, as though the result of the function isn’t being passed back to the inherited scene or something. I tried this instead:

func hit(damage,health):
     health -= damage
     print("enemy was hit, health is ",health)

and

if ‘hit’ in collider:
     collider.hit(damage,collider.health)

but the same thing happens. I even tried adding “return health” at the end of the hit function but still the same behaviour. I feel like my issue is that I’m misunderstanding something fundamental, but I don’t even know what question to ask Google. Everytime I try to look up soluitions I am confronted by hundreds of tutorials about similar but not-quite-the-same things. Do I just need to have the “hit” function inside each inherited scene as opposed to the class? Or can someone point me in the right direction, please? Thanks :slight_smile:

I’m going to guess that you’re shadowing the health variable of your parent class. Do you have warnings about shadowing variables?

If that’s not the case then the best thing to do here is use the debugger. Set a breakpoint in your hit function and step through the code and check how health is getting updated. The debugger will show you the values of your variable.

Hey, please try and format your code properly so others are able to read it easier!

Apologies, I have now tried to format the code correctly but it still doesn’t have the colour-coding. I can’t seen an option in the post editor to do this, am I being stupid?

No warnings at all, can’t see anything about shadowing variables. The parent class doesn’t even have a health (I just use “var health” by itself). Yes, OK, will try the debugger, thank you.

There should be a little drop down menu that lets you select gdscript.

Please see the post I gave you, it explains to you how to format code properly. Even the default template, which you likely deleted, gave you an example on how to do that with syntax highlight.

If you defined a class using class_name you can and should determine the collider’s type with keyword is

if collider is Enemy:
    collider.hit(10)

This shouldn’t work, extended scripts already inherit variables from the previous script, and error if you try to re-define. If you defined health in the extended script then the base class Enemy should not be able to use health like in hit since it doesn’t have health. May be best to post your entire base class script and an extended one.


Since health is a basic type, int or float the value is copied into the function, any changes to the copied value do not affect the original.

var health: int = 100
var copy := health

print(health) # 100
print(copy)   # 100

copy -= 60

print(health) # 100
print(copy)   # 40

Complex types (anything with a .duplicate function) are referenced, that’s why passing nodes can change the original value. But you don’t need to pass in a node since the base type should have health defined already.


Differentiating between the scene and your scripts may help, scenes are not scripts so while they can be inherited from other scenes that has no effect on the scripts that may be used within the scene. Scripts can inherit from other scripts by keyword extends, and using static typing you can declare class_name Enemy to make extensions easier and deduce which type a node is by keyword is.


I don’t see any reason you want to extend the script so far, if you want different health values for each enemy you should use a @export variable for health

extends CharacterBody3D
class_name Enemy

@export var health: int = 100

func hit(damage: int) -> void:
    health -= damage

Then your projectile can use their function just as you’ve done before

func _on_body_entered(body: Node3D) -> void:
    if body is Enemy:
        body.hit(10)

On func hit damage and health are variables that are born on the start of the function and deleted on the end of it.
When you call hit(damage, collider.health), it implicitly does:

func hit(damage, health):
  var fake_health = health
  var fake_damage = damage
  fake_health -= fake damage
  print("enemy was hit, health is", health)

This is called “passing by value”. C#, lua, Go and many other languages have the same behavior.
If your colliders already have health, you can omit it from arguments of the hit() function. The health -= damage statement will then implicitly use self.health, where self is whatever object is calling that function.
However if you want your colliders to be able to decrement other colliders’ health, then rewrite to func hit(damage, other_collider) and do other_collider.health -= damage instead.

OK, I think I understand why what I am trying to do won’t work, thank you.

My next question then is, is it possible to do this so that different enemies can have different max health? You mentioned having this in the class script enemy.gd:

@export var health := 100

But then I’m not sure how to change this for different enemy types because I now get an error if trying to define health in turret.gd (also I take your point about scenes and scripts not being the same thing - my inherited scene is an enemy called turret and has only one script, turret.gd which extends enemy)

Here’s a more complete copy of my codes. enemy.gd:

class_name Enemy
extends CharacterBody3D

@onready var player = get_tree().get_first_node_in_group('Player')
@onready var skin = get_node('skin')
@onready var gravity := 10.0


func hit(damage,health):
	health -= damage

turret.gd (though literally the only thing in this script that’s relevant is var health := 200):

extends Enemy

@export var turnspeed := 2.0
@export var shot_timer := false
@onready var barrel := $skin/RayCast3D
var mortar := load("res://objects/mortar_enemy.tscn")
var instance
var detection_dist := 50.0

var health := 200

func _physics_process(delta: float) -> void:
	move_logic(delta)
	attack_logic()
	move_and_slide()
	
func move_logic(delta) -> void:
	var target_dir = (player.position - position)
	var target_angle = -Vector2(target_dir.x,target_dir.z).angle() - PI/2
	skin.rotation.y = rotate_toward(skin.rotation.y, target_angle, turnspeed * delta) 
	velocity.y -= gravity * delta

func attack_logic() -> void:
	if not shot_timer and (player.position - position).length() < detection_dist:
		$Timer.start()
		shot_timer = true
		#print("timer started")
		instance = mortar.instantiate()
		instance.position = barrel.global_position
		instance.transform.basis = barrel.global_transform.basis
		get_parent().add_child(instance)


func _on_timer_timeout() -> void:
	shot_timer = false

bullet.gd:

extends Area3D
const speed := 87.0

@onready var mesh = $CSGSphere3D
@onready var ray = $RayCast3D
@onready var particles = $GPUParticles3D
@onready var player = get_tree().get_first_node_in_group("Player")

@export var damage := 1


func _ready() -> void:
	pass 

func _process(delta: float) -> void:
	var travel_distance = speed * delta
	ray.target_position = Vector3(0,-1,0) * travel_distance
	position += transform.basis * Vector3(0,-speed,0) * delta
	if player.barrel.is_colliding():
		destroy()
	if ray.is_colliding():
		var collider = ray.get_collider()
		if 'hit' in collider:
			collider.hit(damage,collider.health)
		destroy()
		
func destroy() -> void:
	mesh.visible = false
	particles.emitting = true
	await get_tree().create_timer(1.0).timeout
	queue_free()

func _on_timer_timeout() -> void:
	queue_free()

Sorry I know this is long and my code is probably very shoddy, I’m just using this project to learn how Godot works. For this reason, I know it might seem pointless to be extending the enemy class at all, but I’m doing it to figure out how it all works as I feel like it will be useful to use in future. Thanks :slight_smile:

@export variables can be changed in the editor, so if you give the enemy script @export var health: int = 100 you will see a “Health” property appear for the node in the inspector. This exported value can change from instance to instance without editing the code.

Oh, I see, thank you!