Upgrade menu for a Tower Defence game

Godot Version 4.2.2

I am in the process of making my first Tower Defence game based on a tutorial I was doing. I thought to take it a bit further and see where I could get with this.

Current issue is that I don’t know how to reference and change a variable associated with an instance of a child that has been added to the scene. Well really my issue is that I’m hot garbage with coding but that’s a bit broad.

Below is the code in my main.gd to create the tower:

@onready var cannon : PackedScene = preload("res://Scenes/cannon.tscn")

var in_build_menu : bool = false
var in_upgrade_menu : bool = false

func buy_tower(cost: int, scene : PackedScene) -> void:
	if Global.money >= cost:
		in_build_menu = false
		Global.money -= cost
		var temp_tower : StaticBody3D = scene.instantiate()
		$Towers.add_child(temp_tower)
		temp_tower.global_position = indicator.global_position

func _on_cannon_button_pressed():
	buy_tower(250, cannon)

I have a global.gd that has a bunch of variables and has been popped into the Autoload tab for the project settings:

# Player variables
var health : int = 100
var money : int = 1500 #default 250

# World variables
var wave : int = 0
var enemies_alive : int = 0

# Tower variables
var cannon_damage : int = 5
var shredder_damage : int = 2

# Which variables to reset upon death/new game
func reset() -> void:
	health = 100
	money = 250
	wave = 0
	enemies_alive = 0
	cannon_damage = 5
	shredder_damage = 2

Then the code for the UI upgrade menu:

func _on_upgrade_boost_pressed():
	pass # ¯\_(ツ)_/¯

And then I have the code in my tower in the towercannon.gd, and the code I am specifically trying to edit using the upgrade button which is:

var bullet_damage = Global.cannon_damage

This is a screengrab of the nodes and the scripts I am using. I apologise for the amateur setup. Still getting the hang of game dev.

The UI panel with an upgrade button is where I want to push through the damage upgrade. What I want to do is click on a tower that has been added, and then press the upgrade button and increase the damage or any other variable specifically for that tower.

Any help would be massively appreciated.

Does your tower have a button?

I would do something like this:

In the tower scene, when its button is pressed, the _on_button_pressed is called. It emits the tower_selected signal. This signal is connected to the GUI, which receives the tower instance and upgrades its cannon.

Something like this

towercannon.gd:

func upgrade_cannon():
    #value = (Do some calculations)
    bullet_damage += value

func _on_button_pressed():
    tower_selected.emit(self)

gui.gd:

var selected_tower = null

func _on_tower_selected(tower_instance):
    selected_tower = tower_instance
    #Show upgrade menu

func _on_upgrade_boost_pressed():
	selected_tower.upgrade_cannon()
1 Like

I don’t have a button on the tower. However, I do have the tower in a group called “CannonTower.”

I think I may have figured it out. I created a function within the TowerCannon.gd with the following code:

func upgrade_damage():
	bullet_damage += Global.cannon_up_damage
	# pull upgrade damage value from global.gd

Then in my Main.gd I connected the UI button for upgrades and used the temporary code below to get some sort of output from hitting the upgrade button:

func _on_upgrade_boost_pressed():
	get_tree().call_group("CannonTower", "upgrade_damage")
	# Just add damage, damnit!

And it works.

I don’t have to foresight to see how this will break, so I guess I will just have to see when it falls apart. I just need to adjust the code to allow for the variations in tower group names, and to add a cost per upgrade with UI visibility disabling after the button is pressed, and it should be golden.

Thanks for the reply.

That will upgrade all your cannon towers at the same time, yeah? If you want to be able to upgrade a single specific tower, then you do need some way to select them.

I think overall @iakl’s method is good, but you don’t need a button, you can do it with the input_event signal, which is available on any node that has collision, and which should fire when someone clicks on the tower. See the documentation.

Interestingly enough, it’s upgrading them individually.

I wholly expected it to upgrade all towers at the least and I would work from there, but it only works on the one.

Huh. How on earth does it know which one to upgrade, then? Are they not all in the relevant group, or something?

I have no idea, haha!

I am adding them to the Towers node using:

$Towers.add_child(temp_tower)

But besides that, I have no idea what I did to make it work.

I have tried @iakl 's solution using if Input.is_action_pressed("Interact"): in the _process() function. "Interact" is predefined in the project settings as left mouse button, and it spits out an error.
Identifier "tower_selected" not declared in the current scope.

I tried declaring it as a variable with no value and it spits out another error.
Invaild call. Nonexistent function 'emit' in base 'Nil'.

I’m certain I stuffed up something there.

Is the tutorial you’re following free somewhere? If so, could you share it with me?

It is not. It was part of a course that I got on discount from Zenva. Dirt cheap too, compared to it’s original price.

1 Like

Okay, no worries, thank you.

As for your issue, my best guess is that because func upgrade_damage() and bullet_damage are a function and variable local to each tower, when you call them from get_tree().call_group(“CannonTower”, “upgrade_damage”), they run and update locally from where the signal came from.
But it’s hard to say for certain, as I don’t think your tower code is pasted and no individual towers are child to Cannon in the image above. As written above, I feel like the button should only work for a child named specifically “CannonTower”, since I don’t see any mention of a group by that name.

Here comes the code dump.

TowerCannon.gd

extends StaticBody3D

var bullet : PackedScene = preload("res://Scenes/bullet.tscn") # preload bullet scene for this tower
var target_list : Array = [] # array with enemies that are in range
var current_target : CharacterBody3D # current target that tower is to shoot at
var can_shoot : bool = true # can tower shoot?
var bullet_damage = Global.cannon_damage

func _process(delta): # auto shoot function based on tick
	if is_instance_valid(current_target): # is the enemy valid?
		look_at(current_target.global_position) # face tower to targeted enemy
		if can_shoot: # can it shoot?
			shoot() # shooty-tooty
			can_shoot = false # set can_shoot to false to run timer
			$ShootingCooldown.start() # start the cooldown timer so that it can rerun all functions before shooting
	else: # if target is no longer valid
		for i in get_node("BulletContainter").get_child_count():
			get_node("BulletContainter").get_child(i).queue_free() # remove the bullet from the container
		
func shoot() -> void:
	var temp_bullet : CharacterBody3D = bullet.instantiate()
	temp_bullet.target = current_target
	temp_bullet.bullet_damage = bullet_damage
	get_node("BulletContainter").add_child(temp_bullet)
	temp_bullet.global_position = $MeshInstance3D/Aim.global_position

func choose_target(_target_list: Array) -> void:
	var temp_array : Array = _target_list # create temp array using target_list variable
	var current_enemy : CharacterBody3D = null # create variable to fill list with enemies of type CharacterBody3D
	for i in temp_array:
		if current_enemy == null:
			current_enemy = i
		else:
			if i.get_parent().get_progress() > current_enemy.get_parent().get_progress(): # swap targets if outside range
				current_enemy = i
	
	current_target = current_enemy

func _on_mob_detector_body_entered(body): # is enemy in range? Then do this.
	if body.is_in_group("Enemy"):
		target_list.append(body) # add target to array
		choose_target(target_list) # choose the closest target

func _on_mob_detector_body_exited(body): # has enemy left range? Then do this.
	if body.is_in_group("Enemy"):
		target_list.erase(body) # remove target from array
		choose_target(target_list) # change to new target on array

func _on_shooting_cooldown_timeout():
	can_shoot = true # yes, I can do the pew pew

func upgrade_damage():
	bullet_damage += Global.cannon_up_damage # add global upgrade damage to current bullet damage

The root node for the cannon.tscn has been added to the group named CannonTower

Apologies for the excessive comments.

1 Like

I think I’ve figured it out.

You’re adding your towers to the Tree $Towers. This makes every tower a child of $Towers as its own sub-tree.

Then, when you get_tree().call_group(“CannonTower”, “upgrade_damage”), you are calling every member of group “CannonTower” in the tree the signal came from. Since every tower is in its “own” tree, it only calls “upgrade_damage” for itself.

I believe if the signal were to come from $Towers instead, it would upgrade every tower “below” it (every child that is of group CannonTower) in the tree structure.

1 Like

Well that makes sense. Looks like my luck let me stumble upon a viable solution then.

Thanks for looking into it. And thanks everyone for giving me a hand with this.

I’ll mark this bad boy as solved.

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.