script works with one node but not multiple instances... instantiated node scripts seem to conflict with each other

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By mikefrom1974

UPDATE: removing github ling as it’s no longer needed and the solution is apparent in the pasted code.

Newish godot user here (some experience with 3.5 but waited for 4.0 to deep dive).
I have a simple 2D scene set up from a tutorial. I like to extend tutorials so that I break stuff and have to fix it because let’s be honest that’s where the real learning happens.

I have a player node and a mob node that both work fine with moving, jumping, and killing each other… I decided to create a spawner and try to get multiple mobs involved. However, when I spawn multiple mobs the die() function seems to stop working properly. If I instance only one mob all works as expected, but if I set a timer to instance multiple mobs it stops working.

When working (single spawned mob):
Player jumps on mob, mob explodes and goes away.

When not working (multiple spawning mobs):
Player jumps on mob, mob stops moving, resumes idle, doesn’t go away.

Main question: Is there something I am missing when spawning multiple instances of nodes that would make the scripting conflict?

Goal: I know I could wipe and start over, or code around it… but I want to understand WHY this is happening, so I can learn from it and prevent mistakes in the future.

Player script:

extends CharacterBody2D

const SPEED = 300.0
const JUMP_VELOCITY = -400.0

@onready var anim = $AnimationPlayer
@onready var sprite = $AnimatedSprite2D

var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")
var alive = true

func _physics_process(delta):
	if alive:
		var direction = Input.get_axis("ui_left", "ui_right")
		var crouch = Input.get_axis("ui_down", "ui_up")
		if direction:
			velocity.x = direction * SPEED
			sprite.flip_h = velocity.x < 0
		else:
			velocity.x = move_toward(velocity.x, 0, SPEED)
		
		if not is_on_floor():
			velocity.y += gravity * delta
			if velocity.y < 0:
				anim.play("jump")
			else:
				anim.play("fall")
		else:
			if Input.is_action_just_pressed("ui_accept") and is_on_floor():
				velocity.y = JUMP_VELOCITY
			if direction:
				anim.play("run")
			elif crouch < 0:
				anim.play("crouch")
			else:
				anim.play("idle")
		
		move_and_slide()

func die():
	alive = false
	anim.play("death")
	await anim.animation_finished
	get_tree().change_scene_to_file("res://scenes/main.tscn")

mob script:

extends CharacterBody2D

const JUMP_SPEED = 110
const JUMP_DELAY = 0.6

@export var player: CharacterBody2D

@onready var sprite = $Sprite
@onready var my_collision = $Collision

var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")
var last_jump = JUMP_DELAY
var alive = true
var loop_timer = 0

func initialize(spawn_position):
	alive = true
	self.global_position = spawn_position

func _ready():
	player = get_tree().get_first_node_in_group("players")
	sprite.play("idle")

func _physics_process(delta):
	loop_timer += 1 * delta
	if loop_timer > 3:
		alive = true
	if alive:
		if not is_on_floor():
			velocity.y += gravity * delta
			if velocity.y < 0:
				sprite.play("jump")
			else:
				sprite.play("fall")
		else:
			velocity.x = 0
			sprite.play("idle")
		
		if player:
			var h_dir = 1
			var direction = (self.global_position - player.global_position).normalized()
			if direction.x < 0:
				sprite.flip_h = true
			else:
				sprite.flip_h = false
				h_dir = -1
			if is_on_floor() && last_jump >= JUMP_DELAY:
				last_jump = 0
				velocity.y = -JUMP_SPEED
				velocity.x = JUMP_SPEED * h_dir
			else:
				last_jump += 1 * delta
				
		
		move_and_slide()

func die():
	alive = false
	player = null
	sprite.play("death")
	await sprite.animation_finished
	self.queue_free()


func _on_head_area_body_entered(body):
	if body.is_in_group("players"):
		body.velocity.y = body.JUMP_VELOCITY * .8
		self.die()


func _on_body_area_body_entered(body):
	if alive:
		if body.is_in_group("players"):
			player = null
			body.die()

world script:

extends Node2D

const MAX_FROG_SPAWN_DELAY = 5
const MIN_FROG_SPAWN_DELAY = 1

@export var frog_scene: PackedScene

@onready var timer = $Timer

var timer_length = MAX_FROG_SPAWN_DELAY
var frog_num = 0

func _ready():
	timer.wait_time = randi_range(MIN_FROG_SPAWN_DELAY, MAX_FROG_SPAWN_DELAY)
	timer.start()

func _on_timer_timeout():
	spawn_frog()
	var yaynay = randi_range(1, 10)
	if yaynay > 5:
		timer_length -= 1
		if timer_length < MIN_FROG_SPAWN_DELAY:
			timer_length = MIN_FROG_SPAWN_DELAY
	timer.wait_time = timer_length
	timer.start()

func spawn_frog():
	frog_num += 1
	var spawn_position = get_node("FrogSpawn").global_position
	var frog = frog_scene.duplicate().instantiate()
	frog.initialize(spawn_position)
	add_child(frog)

Layout:

World
|- Player
|- FrogSpawn
|- Timer

The only thing that looks a bit weird is in the spawn_frog() method where the frog_scene is duplicated before instantiated. You should be able to simply call instantiate.

However, I doubt that’s the issue if spawning a single frog once with the same method works. (I’m assuming you don’t just add the mob child scene to the game level scene when you say “single spawned mob”.)

Are there any errors in the console? If so, what are they?

You may need to share your project on github or something and let us know. It’s WAY easier to debug as I doubt I could duplicate everything from GDScript alone.

tuon | 2023-06-26 18:13

@tuon you are correct, the duplicate doesn’t make a difference. I just added it during troubleshooting.

The single spawned frog is spawned once with the script, not manually added.

No errors in console.

I am editing the original Q with a github link.

mikefrom1974 | 2023-06-26 20:31

:bust_in_silhouette: Reply From: tuon

The issue is in the frog/mob script.

In the die() method the alive flag is set to false and then the death animation is played.

But in the _physics_process() method, the alive flag is flipped back to true if the loop_timer is greater than 3 (seconds).

If the frog is killed before that 3 seconds accumulates in the timer, the death animation completes and the frog is queue_freed. This is probably why it seemed to work when there was just the one frog. I saw this too, but also saw the bug happen with just the one frog.

When the frog is killed after 3 seconds accumulates in the timer, the idle animation will be played interrupting the death animation. The death animation is the only animation that is not set to loop - which means that the animation finished signal is never emitted in this case.

An easy “fix” is the call set_physics_process(false) at the beginning of the die() method. However, a clean up of the internal logic may be in order as well.

That was exactly it.
I had inserted that as a previous fix to make sure the frog wouldn’t move improperly in a different spawn situation. In a typical case of “can’t see the forest for the trees” I never noticed those few lines of code again even after the alive mechanics were moved to the ready and die functions.
It’s good to know WHY it messed up (as if I’d just deleted everything and started over it would WORK but I wouldn’t know why it didn’t before).

Thanks!

mikefrom1974 | 2023-06-26 23:22