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
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.
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()
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.
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.
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'.
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.
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
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.