Projectiles/Missiles Instantly Removed/Freed When Spawning from Specific Cannon Positions

I am developing a 2D game in Godot 4.4.1 where multiple cannons spawn missiles aimed at the player. The problem is that missiles often disappear immediately after spawning, especially from the top-left, top-right, and bottom-left cannons, while only the bottom-right cannon behaves normally. Debug logs show that the missiles are spawned at positions inside the camera’s view, but right after they appear, they trigger the “exiting” event and get removed from the scene. This suggests that something in the missile scene is incorrectly treating the missiles as if they were already offscreen or hit something when they spawn, causing them to be freed immediately. Here is the code:

extends Node2D

@onready var camera = $Camera2D
@onready var missiles: Node2D = $Missiles  
@onready var player: CharacterBody2D = $Player
@onready var game_time: Timer = $GameTime
@onready var countdown: Label = $Countdown

@export var missile_scene: PackedScene  
@export var spawn_interval: float = 2
@export var min_spawn_interval: float = 0.7 

@onready var canon: CharacterBody2D = $Canon
@onready var canon_2: CharacterBody2D = $Canon2
@onready var canon_3: CharacterBody2D = $Canon3
@onready var canon_4: CharacterBody2D = $Canon4

var has_spawn_started = false
var timer: Timer  
var stored_missiles = []  # Save last position when out of screen

func _ready() -> void:
	if not is_instance_valid(missiles):
		print("Missiles node is missing!")
		return
		
	# Timer 10s
	game_time.wait_time = 10
	game_time.start()
	#game_time.timeout.connect(_on_game_time_timeout)
	game_time.timeout.connect(player.call_deferred.bind("_on_game_time_timeout"))
	
	print("Initial Spawn Interval: ", spawn_interval)

func _process(delta: float) -> void:
	if game_time.is_stopped():
		countdown.global_position = player.global_position + Vector2(-32, -50)
		
	if game_time.time_left > 0:
		countdown.text = str(ceil(game_time.time_left))
		countdown.global_position = player.global_position + Vector2(-12, -50)
		
		# 10-5 seconds, spawn missiles from canons
		if game_time.time_left <= 10 and game_time.time_left > 5 and not has_spawn_started:				
			has_spawn_started = true
			
			timer = Timer.new()
			timer.wait_time = spawn_interval
			timer.autostart = true
			timer.timeout.connect(_spawn_missile)
			add_child(timer)
		
		# in 5 seconds, spawn the out missile to inside again / reverse
		if game_time.time_left <= 5 and has_spawn_started:
			player.set_run_speed(1000.0)
			_reverse_missiles()

	# Canon always point to player
	for c in [canon, canon_2, canon_3, canon_4]:
		c.rotation = (player.global_position - c.global_position).angle()

func _spawn_missile() -> void:
	if not missile_scene:
		print("ERROR: Missile scene is not assigned!")
		return
	
	# pick random canon as spawnpoint
	var canon_list = [canon, canon_2, canon_3, canon_4]
	var spawn_canon = canon_list[randi() % canon_list.size()]

	# Take position from Marker2D inside of canon scene
	var missile_spawn_marker = spawn_canon.get_node_or_null("MissileSpawn")

	if missile_spawn_marker == null:
		print("ERROR: Couldn't find MissileSpawn node in", spawn_canon.name)
		return

	# new missile spawn in Marker2D pos
	var new_missile = missile_scene.instantiate()
	new_missile.global_position = missile_spawn_marker.global_position

	print("Missile launched from:", spawn_canon.name, "at position", new_missile.global_position)

	# direction to player
	var target_position = player.global_position
	var direction_vector = (target_position - new_missile.global_position).normalized()

	new_missile.rotation = direction_vector.angle()
	new_missile.velocity = direction_vector * randf_range(340.0, 390.0)
	
	spawn_canon.shoot()

	# Save last position before out of screen
	new_missile.connect("tree_exiting", Callable(self, "_on_missile_exiting").bind(new_missile))
	
	# Add missile to main node
	missiles.add_child(new_missile)
	
	# Fasten spawn interval
	spawn_interval = max(spawn_interval - 0.3, min_spawn_interval)
	timer.wait_time = spawn_interval
	print("Updated Spawn Interval:", spawn_interval)

func _on_missile_exiting(missile) -> void:
	if is_instance_valid(missile):
		print("Missile exited at position:", missile.global_position)
		stored_missiles.append({
			"position": missile.global_position,
			"velocity": missile.velocity
		})
		missile.queue_free()

func _reverse_missiles() -> void:
	if stored_missiles.is_empty():
		return
	
	print("Reversing stored missiles:", len(stored_missiles))

	for missile_data in stored_missiles:
		var new_missile = missile_scene.instantiate()
		new_missile.global_position = missile_data["position"]
		
		# Pick canon as a last target
		var canon_list = [canon, canon_2, canon_3, canon_4]
		var target_canon = canon_list[randi() % canon_list.size()]
		
		# Calculate reverse trajectory to canon
		var direction_vector = (target_canon.global_position - new_missile.global_position).normalized()
		var direction_angle = direction_vector.angle()
		
		new_missile.rotation = direction_angle
		new_missile.velocity = direction_vector * missile_data["velocity"].length()
		
		missiles.add_child(new_missile)
	
	stored_missiles.clear()

func _on_player_hit() -> void:
	get_tree().call_deferred("reload_current_scene")

func _on_game_time_timeout() -> void:
	if is_instance_valid(player):   
		game_time.stop()
		countdown.text = "RESPECT"

Here is the log:

Godot Engine v4.4.1.stable.official.49a5bc7b6 - https://godotengine.org
Vulkan 1.3.278 - Forward+ - Using Device #0: NVIDIA - NVIDIA GeForce MX110

Posisi global MissileSpawn:(-169.8873, 231.1127)
Posisi global MissileSpawn:(175.8873, -176.1127)
Posisi global MissileSpawn:(238.1127, 172.8873)
Posisi global MissileSpawn:(-238.1127, -114.8873)
Initial Spawn Interval: 1.0
Missile launched from:Canon2at position(171.5988, -118.8705)
Updated Spawn Interval:0.7
Missile exited at position:(166.5971, -115.1788)
Missile launched from:Canon4at position(-172.0483, -119.2722)
Updated Spawn Interval:0.4
Missile exited at position:(-167.5382, -115.8233)
Missile launched from:Canonat position(-169.2526, 169.5353)
Updated Spawn Interval:0.1
Missile exited at position:(-164.9812, 165.4365)
Missile launched from:Canon4at position(-172.0483, -119.2722)
Updated Spawn Interval:0.1
Missile exited at position:(-167.4905, -115.7869)
Missile launched from:Canon4at position(-172.0483, -119.2722)
Updated Spawn Interval:0.1
Missile exited at position:(-166.9798, -115.3963)
Missile launched from:Canonat position(-169.2526, 169.5353)
Updated Spawn Interval:0.1
Missile exited at position:(-165.086, 165.5371)
Missile launched from:Canon3at position(174.6805, 174.1429)
Updated Spawn Interval:0.1
Missile launched from:Canonat position(-169.2526, 169.5353)
Updated Spawn Interval:0.1
Missile exited at position:(-165.0627, 165.5147)
Missile launched from:Canon3at position(174.6805, 174.1429)
Updated Spawn Interval:0.1
Missile launched from:Canon2at position(171.5988, -118.8705)
Updated Spawn Interval:0.1
Missile exited at position:(166.6624, -115.227)
Missile launched from:Canon3at position(174.6805, 174.1429)
Updated Spawn Interval:0.1
Missile launched from:Canon2at position(171.5988, -118.8705)
Updated Spawn Interval:0.1
Missile exited at position:(166.7753, -115.3103)
Missile exited at position:(118.9903, 122.6958)
Missile exited at position:(63.83698, 71.74464)
Missile exited at position:(20.38785, 31.60592)
Posisi global MissileSpawn:(-169.8873, 231.1127)
Posisi global MissileSpawn:(175.8873, -176.1127)
Posisi global MissileSpawn:(238.1127, 172.8873)
Posisi global MissileSpawn:(-238.1127, -114.8873)
Initial Spawn Interval: 1.0
--- Debugging process stopped ---



Show the logic of the missile. The bottom-right cannon is the only one inside the viewport so I’m guessing that the logic of freeing the missile is not correct. You should use a VisibleOnScreenNotifier2D instead using its signal VisibleOnScreenNotifier2D.screen_exited of checking the position to free them when they go out of the screen.

1 Like

oh damn, you’re right, it’s on my missile script all along…

func _process(delta: float) -> void:
	position += velocity * delta

	#var viewport_rect = get_viewport_rect()
	#print("viewport : ", viewport_rect)
	#if not viewport_rect.has_point(global_position):
		#queue_free()
		#

The viewport checking part was the problem, I never checked my missile scene, i dont know why :sweat_smile:, I thought the problem was on the Marker or on the main scene. Sorry to bother you @mrcdk , I see you replied to some of my problems earlier as well. Thanks, again.

1 Like

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