Why my duplicated enemy not working as intended

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

I am very new to godot and making 2d platformer zombie game.
I have made an enemy that can do following things when collide with player.
(1) Disable player movement, disable player visibility, lock player’s position
(2) Play enemy grab attack animation, show button mashing progress bar
(3)-(1) If cleared, enable player visibility, unlock player’s position, enemy return to chase state
(3)-(2) After few sec, enable player movement
(4)-(1) If failed, play enemy execution animation
(4)-(2) when animation finished, enable player visibility, unlock player’s position
(4)-(3) After few sec, enable player movement

Everything works perfectly fine except, when I duplicated enemy, duplicated one is synchronized with original one. So when I collide with duplicated enemy, the button mashing progress bar will also be shown upon original one.
I would appreciate if you could kindly teach me how to fix this problem.

When it is not duplicated:

When it is duplicated (Left is duplicated one, and Right is original one):

Player Script: Sorry if my code is messy and annoyingly hard to see

class_name Player

extends KinematicBody2D

var speed = 200
var acceleration = 30
var friction = 15
var gravity = 1500

var air_jump = true
var jump_strength = 570
var air_jump_strength := 550
var air_friction = 1

var can_move = true
var player_in_hitbox: bool = false

var max_hp = 100
var current_hp

var _velocity := Vector2.ZERO

onready var position2D = $Position2D
onready var _animation_player: AnimationPlayer = $Position2D/PlayerSkinIK/AnimationPlayer
onready var hurtbox_collision = $HurtboxPlayer/CollisionShape2D and $HurtboxPlayer/CollisionShape2D2
onready var player_grab_finished = get_node("../Mushroom")
onready var label = get_node("Label")

signal player_in_hitbox_false() #signal for player movement disable
signal player_in_hitbox_true() #signal for player movement disable


func _ready():
	player_grab_finished.connect("grab_finished", self, "grab_finished_signal_received") #connect mushroom
	current_hp = max_hp


func get_input_direction() -> float:
	if player_in_hitbox: #disable player movement if true
		return 0.0
	var _horizontal_direction = ( #player movement direction
		Input.get_action_strength("move_right")
		- Input.get_action_strength("move_left")
	)
	return _horizontal_direction


func _process(delta: float) -> void:
	if get_global_mouse_position().x > $Position2D/PlayerSkinIK.global_position.x: #flip player sprite
		position2D.scale.x=1
	else:
		position2D.scale.x=-1
	
	if can_move == false: #lock player position when in Grab Attack
		self.position = get_node("../Mushroom/HitPosition").global_position


func grab_finished_signal_received(): #Grab Finished
	print("Grab Finished!")
	label.text="CAN'T MOVE"
	_animation_player.play("Player-Damaged IK")
	can_move = true
	_velocity.x = 1 * -250
	self.visible = true
	$player_in_hitboxTimer.start()

func _on_player_in_hitboxTimer_timeout(): #After Grab Finished
	print("FREE!")
	label.text="CAN MOVE"
	player_in_hitbox = false
	$player_in_hitboxTimer.is_stopped()
	emit_signal("player_in_hitbox_true")

func _on_HurtboxPlayer_area_entered(area: Area2D): #When Player Enter Enemy Hitbox
	if area.name == "HitboxMushroom":
		label.text="CAN'T MOVE"
		player_in_hitbox = true #Disable Movement
		can_move = false #Lock Position
		self.visible = true #Disable Visibility
		emit_signal("player_in_hitbox_false") #Emit Signal to Disable Shooting
		pass

Enemy Script:

extends KinematicBody2D

var max_hp = 100
var current_hp= max_hp
var defense = 0

var speed = 100
var chase_speed = 100
var gravity = 10
var direction = -1
var velocity = Vector2.ZERO

onready var _animation_player: AnimationPlayer = $Position2D/Node2D/AnimationPlayer
onready var sprite = get_node("Position2D/Node2D")
onready var flashTimer = $flashTimer
onready var player = get_node("../Player")
onready var enemy_collision = get_node("EnemyCollision")
onready var label = get_node("Label")

signal button_mashing_start()
signal grab_finished()


enum state {IDLE, CHASE, GRAB, FINISH, DEATH}
var enemy_state = state.IDLE


func _ready():
	enter_state(state.IDLE)
	$Position2D/Node2D/AnimationPlayer.connect("animation_finisehd", self, "on_animation_finished")
	current_hp = max_hp
	pass

func _physics_process(delta):
	process_state()
	pass

func enter_state(pass_state):
	if(enemy_state != pass_state):
		leave_state(enemy_state)
		enemy_state = pass_state
		
	if(pass_state == state.IDLE):
		_animation_player.play("Mushroom-Idle")
		label.text="IDLE"
	
	if(pass_state == state.CHASE):
		_animation_player.play("Mushroom-Idle")
		label.text="CHASE"
		
	if(pass_state == state.GRAB):
		_animation_player.play("Mushroom-Grab")
		label.text="GRAB01"
	
	if(pass_state == state.FINISH):
		_animation_player.play("Mushroom-Execution")
		label.text="GRAB02"
	
	if(pass_state == state.DEATH):
		_animation_player.play("Mushroom-Idle")
		label.text="DEATH"

func process_state():
	if(enemy_state == state.IDLE):
		process_idle()
	
	if(enemy_state == state.CHASE):
		process_chase()
	
	if(enemy_state == state.GRAB):
		process_grab()
	
	if(enemy_state == state.FINISH):
		process_finish()
	
	if(enemy_state == state.DEATH):
		process_death()

func leave_state(pass_state):
	pass

func process_idle():
	if is_on_wall() or not $Position2D/FloorRay.is_colliding() and is_on_floor():
		print("turn around")
		direction = direction * -1
		flip()
	velocity.y += gravity
	velocity.x = speed * direction 
	velocity = move_and_slide(velocity, Vector2.UP)
	if enemy_collision.is_colliding():
		velocity += enemy_collision.get_push_vector() * 2.5


func process_chase():
	if velocity.x>0:
		$Position2D.scale.x=-1
	elif velocity.x<0:
		$Position2D.scale.x=1
	
	velocity = (player.position - position).normalized()
	if not is_on_floor():
		velocity.y += gravity 
	
	if enemy_collision.is_colliding():
		velocity += enemy_collision.get_push_vector() * 2.5
	
	velocity = move_and_slide(velocity * chase_speed)


func process_grab():
	velocity.y += 0
	velocity.x = 0 * direction 
	velocity = move_and_slide(velocity, Vector2.UP)
	emit_signal("button_mashing_start")


func process_finish():
	velocity.y += 0
	velocity.x = 0 * direction 
	velocity = move_and_slide(velocity, Vector2.UP)


func process_death():
	velocity.x = 0 * direction 
	velocity.y += gravity
	velocity = move_and_slide(velocity, Vector2.UP)
	set_collision_mask_bit(0, false)



func _on_AnimationPlayer_animation_finished(anim_name):
	print("Grab02 End!")
	emit_signal("grab_finished")
	enter_state(state.CHASE)


func flip(): #Flip Sprite
	if direction>0:
		$Position2D.scale.x=-1
	elif direction<0:
		$Position2D.scale.x=1

func _on_HitboxMushroom_area_entered(area):
	if !area.name == "HurtboxPlayer":
		enter_state(state.CHASE)
	if area.name == "HurtboxPlayer":
		print("Grab01!")
		enter_state(state.GRAB)


func _on_PlayerDetector_area_entered(area: Area2D): #Player Search Area. Chase when Player entered
	if area.name == "HurtboxPlayer":
		print("found you !")
		enter_state(state.CHASE)
		pass

func _on_PlayerDetector_area_exited(area: Area2D): #Player Search Area. Stop Chase when Player exited
	if area.name == "HurtboxPlayer":
		print("lost you !")
		enter_state(state.IDLE)
		pass


func _on_ProgressBar_button_mashing_cleared():
	enter_state(state.CHASE)
	emit_signal("grab_finished")
	print("Return Chase!")
	pass 

func _on_ProgressBar_button_mashing_failed():
	enter_state(state.FINISH)
	print("Grab02!")
	pass 

Progress Bar Script:

extends ProgressBar

var start = false

signal button_mashing_cleared()
signal button_mashing_failed()

func _ready():
	set_visible(false)


func set_bar_value(value_to_set):
	value = value_to_set


func _physics_process(delta):
	if not start == false:
		set_visible(true)
		value -= 15 * delta 
	
		if Input.is_action_just_pressed("jump"):
			set_bar_value(value + 5)

		if(value <= 0):
			print("Bar Failed!")
			start = false
			set_visible(false)
			set_bar_value(50)
			emit_signal("button_mashing_failed")
		elif(value >= 100):
			print("Bar Cleared!")
			start = false
			set_visible(false)
			set_bar_value(50)
			emit_signal("button_mashing_cleared")


func _on_Mushroom_button_mashing_start():
	start = true

Try to expaind the method you use to duplicate the enemy.
I think this will solve the problem.

mrfatalo | 2022-08-21 17:41

I simply right clicked enemy node, and duplicated.

amaou310 | 2022-08-22 10:57

:bust_in_silhouette: Reply From: Inces

It is hard to dive in this wall of code. Can You shortly explain signals progression ? I mean, what signals trigger what in order from start to end of a whole action ?

If origin of the action is triggered by Zombies upon player entering their private Hitbox area, this is most likely the problem of duplicate itself. It must have connected second zombie to the area of first zombie. Don’t use duplicates. Save zombie as a scene and add another instance of it in your main scene. If You duplicate by code, You can choose to ignore duplicating signals, by inserting additional tag arguments.

Okay, I tried my best to explain which signals will trigger what, and hope it is clear enough to understand, but if its not please tell me.

**ENEMY SCRIPT**  
    (1) if HurtboxPlayer entered HitboxMushroom 
    will trigger
    ・enter_state(state.GRAB) 


(2) func process_grab(): for state GRAB 
will trigger
・emit_signal("button_mashing_start") 


(3) emit_signal("button_mashing_start") 
will trigger 
・func _on_Mushroom_button_mashing_start(): start = true
・if start = true: enable visible
・decrease the progress bar value by 15 every second
・increase the value by 5 every time the key is pressed


(4) emit_signal("button_mashing_cleared"), if the value is more than 100
will trigger
・emit_signal("grab_finished")


(5) emit_signal("button_mashing_failed"), if the value is less than 0
will trigger 
・func _on_Progressbar_button_mashing_failed(): enter_state(state.FINISH)


(6) func process_finish(): for state FINISH
will trigger
・play animation("execution")


(7) emit_signal("animation_finished"), when animation("execution") finished
will trigger 
・emit_signal("grab_finished")
・enter_state(state.CHASE)


(8) emit_signal("grab_finished")
will trigger 
・func grab_finished_signal_received():


**Player SCRIPT**
(1)if HitboxMushroom entered HurtboxPlayer
will trigger
・player_in_hitbox = true (disable player movement)
・can_move = false (lock player's position at position of Mushroom's position2D)
・self.visible = false (disable visibility)
・emit_signal("player_in_hitbox_false")

(2) emit_signal("player_in_hitbox_false")
will trigger
・shoot = false (disable shooting ability)


(9) func grab_finished_signal_received():
will trigger
・can_move = false (unlock player position)
・self.visible = true (enable visibility)
・_velocity.x = 1 * -250 (knockback)
・$player_in_hitboxTimer.start()


(10) $player_in_hitboxTimer.start()
will triger
・player_in_hitbox = false (enable player movement)
・$player_in_hitboxTimer.is_stopped() (stop the timer)
・emit_signal("player_in_hitbox_true")


(11) emit_signal("player_in_hitbox_true")
will trigger
・shoot = true (enable shooting ability)

amaou310 | 2022-08-22 04:34

Ok it is clear enough :slight_smile:
So it is true, when Zombie1 collides with the player, both Zombies enter grab state and continue their private signal progression. That means it is a bug coming from Duplicating functionality - it connected 1st zombie hitbox to 2nd zombie.
Sollution is as I stated above - just don’t use duplicate. Your zombie surely is saved as .tscn file. Add another one of these to your main scene, by dragging it from list of scenes. This is how You properly create multiple instances of the same type. Duplicates are only good for copying node with large amount of specified export values.

Inces | 2022-08-22 14:55

Instead of duplicating the node by right clicking, I added another instance of a zombie scene to my main scene. However, the problem still remains.

I think I have found the reason why this happened. In the player script, I set the player’s position at the position of Position2D(children of zombie node), in order to lock the player’s position when they collide.

self.position = get_node("../Mushroom/HitPosition").global_position

Because of this, when I collide with the duplicated zombie node, the player will instantly teleport to the position of Position2D of the original zombie node.

Also, the signal cannot connect with duplicated node.

onready var player_grab_finished = get_node("../Mushroom")
player_grab_finished.connect("grab_finished", self, "grab_finished_signal_received") 

get_node("…/Mushroom) seems to only work with the original zombie node, since the newly added zombie node will be named as “Mushroom2”

Do you have any idea how to cope with this problem. I would appreciate if you could kindly teach me.

amaou310 | 2022-08-23 13:58

It seems this is the limitation of basic signal functionality.
Advanced signalling pattern is done with autoloaded script, that holds all signals, and every node connects to it. It will look like this :

Autoload :

signal grab_finished

Player :

func ready():
       Autoload.connect("grab_finished",self, "grab_finished_signal_received")
func grab_finished_signal_received( who ):
        xxxxx

Zombie :

 Autoload.emit_signal("grab_finished", self)

so now You can have endless amount of zombies, and don’t need any hardcode reference to connect them. Every zombie now sends a signal with information about himself, so your player can recognize colliding zombie by “who” argument

Inces | 2022-08-23 16:16

That worked perfectly !! I have never knew I could do that. Thank you so much Inces.

I am now trying to disable player’s hurtbox collision once it collided with enemy, so I can prevent duplicated enemy to be collided while original zombie is in grab attack with player. However, $Collision2D.set_defferted("disabled", true) will make enemy return to Idle State since enemy cannot detect HurtboxPlayer as soo as it is disabled.

I also tried

var has_triggered: bool = false

func _on_HitboxMushroom_area_entered(area):
if area.name == "HurtboxPlayer" and has_triggered:
	has_triggered = true 

But this didnt workd. Duplicated zombie could still collide with player, while original zombie is in grab attack with player.

How would I keep my enemy to be in Grab attack state while disabling player’s collision.

amaou310 | 2022-08-24 14:24

SInce You already have developed signal system, let’s take advantage of that fully.
It is not just collision, but there will be multiple occasions in your game, when You will want things to behave differently, when player is grabbed.
Connect zombies to signal indicating grab and control their state from there. If player is grabbed by zombie which is not self, and state is chase - disable my detector. If grab is finished, enable detector.
You can also use autoload to design game state machine, like state.CUTSCENE. Whenever You will need zombie to stop when cutscene is played, You will just be able to do that in zombie process :

func process():
       if Autoload.gamestate == Autoload.state.CUTSCENE :
                return

Inces | 2022-08-24 15:28

Thank you again for kindly teaching me Inces.
Although you kindly taught me ways to achieve that, my knowledge and skills were not good enough to implement it with signal or autoload. Instead, I made new area2d for player called PlayerDetector. Player’s hurtbox will only be used for grab attack, and PlayerDetector will be used for Enemy’s player detect. So now I can disable Player’s hurtbox collisionshape2d and avoid another duplicated zombie to collide with player. I am pretty sure that using signal or autoload is much easier and flexible than my way, but for now this is the only way I can do.

I still have position2D teleport problem to solve. It would take couples of day to solve it but I would appreciate if you could help me again again later.

amaou310 | 2022-08-25 14:36

Sure :slight_smile:
If You used my signalling example, it would also fix the teleport to 1st zombie problem.
Because signal is now sent with information referencing the actual grabbing zombie , You could connect to this signal and teleport player to position of the zombie, that triggered grab.

Inces | 2022-08-25 15:50

Thank you Inces.

Wow. I could solve the teleport issue with signal as well !?
I could not understand your example of Autoload for grab state machine.
I did something like var HitPosition = get_node("../Zombie/HitPosition) for autoload, but didnt work.

amaou310 | 2022-08-26 14:47

You don’t know what autoload is ?
It is a script that is independent of SceneTree hierarchy, and can be referenced from anywhere in code. Read about it in documentation too.
You just need to create new script ( not a node, just a script, but extends from Node ), and go to Project/Project Settings/Autoload tab - add new script. You choose name for it ( in my example it was “Autoload” ) and now You can refer to it from anywhere in your code and compiler will autocomplete its name.

Why is it so good ? Because if You introduce every signal of your game inside it, You will never have to know scene tree hierarchy in order to reference a node for connecting. So no longer stuff like get_node/enemies/redenemies/zombie52 , but just Autoload. instead
You can think of it as radio transmitter, to communicate between far away lands. No need to know location to send a letter, just emit information into the ether, and a land who is interested will listen to it.

Inces | 2022-08-26 15:38

Oh my apologies. Did not meant to say I do not know what autoload is. I indeed do understand what autoload is however, I was not sure how to rearrange the code inside autoload in order to change enemy state. Simple autoload such as signal grab_finished was very much simple and easy to implement, as what I needed to do is simply write “signal grab_finished” inside autoload and connect it. Changing enemy’s state and solving position teleporting issue using autoload is tricky, since I am really not sure what to write inside autoload. For now, I set velocity.x, y and gravity to zero instead of locking the player’s position. Thank you very much for thorough explanation about autoload. It is indeed very useful since I would never need to write lines of code to reference a node.

amaou310 | 2022-08-28 07:03

Inside autoload introduce all the signals You have and that is it :slight_smile:
Right now your player keeps reference to zombie with hardcode :

onready var player_grab_finished = get_node("../Mushroom")

which only leads to first zombie.
Instead of using this hardcoded variable for setting teleport position, You should use reference sent by signal.
As I stated before, if You emit signal with another argument, it is sent along with signal :

Autoload.emit_signal("grab_finished", self)

so now zombie emits a signal with SELF
Thanks to this,Player, which is connected to Autoload in general, can now recognize which zombie sent a signal. And this is enough to teleport to it :

func grab_finished_signal_received( who ):
        global_position = who.get_node("Position2D").global_position

another example with gamestate machine is just a gimmick, it is optional, let’s leave that for now :wink:

Inces | 2022-08-28 17:34