Area2D - Triggered one more time when player node come back to previous scene

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By Seanfy
:warning: Old Version Published before Godot 3 was released.

Hi guys, I need help on a problem when switching scenes through Area2D, I got two scenes setup:

Scene A has just a big ‘Start’ button that will switch to scene B when pressed.

Scene B has a big image as background, a player node(KinematicBody2D) added from a singleton script, an Area2D, let’s say ‘Portal’, and a UI button ‘Back’ will switch back to scene A.

When using ‘Start’ or ‘Back’ button to switch between scene A and B, it works all good, it also works when I control the player node step onto ‘Portal’ area and switch back to scene A, but when I click ‘Start’ button at this time, it will take me to scene B and then switch back to scene A immediately, then it work properly again if I click ‘Start’ button, this only happens one time, after I switch scene from B to A through Area2D

here is the output window with my comments:

-move player to the ‘Portal’ area in scene B-
body enter
player current pos: (1541.038696, 857.218872)
-after this I move player position out of area2D to prevent it from triggering again-
change scene to res://scenes/sceneA.tscn

-click ‘Start’ button in scene A-
change scene to res://scenes/sceneB.tscn

-HERE IS THE PROBLEM: body enter was triggered again immediately -
*I printed the player position in body enter event and it’s not in the ‘Portal’ positon, see the last triggered position above (1541.038696, 857.218872)
body enter
player current pos: (874, 490)
change scene to res://scenes/sceneA.tscn

I’ve looked into this problem for some time but still no clue at the moment, thanks in advance!

I assume you use change_scene, right? How do you make the player appear in the scene from your singleton code?

Zylann | 2016-11-08 13:32

I checked my script and seems there is no change_scene were used, I followed the exact steps from official doc here: Singletons (AutoLoad) to change between scenes. Just add a variable to indicate if the target scene need to display the player:
func goto_scene(path, showplayer=false)

For my player, it’s in a player scene, it’s instanced in the singleton script as a variable and kept there, it will be removed by it’s parent before freeing the current scene(if player node exists in the scene) and will be re-positioned and added to the new scene’s tileset map node as it’s child, after the scene was loaded.

Seanfy | 2016-11-08 13:59

Well the code from the doc uses something similar to change_scene (manually) but that’s not really relevant in fact.

I suspect that you are setting the position of the player after adding it to the tree, which could trigger an event if there is an Area2D at the spawn point before you move it away.

And even in that case, it would be normal for the Area2D to trigger if the player spawns directly on it. I would solve this problem like Minecraft does with portals, by adding a flag that is true until you leave the area, preventing you from loop-teleporting.

Now even if after checking these you still have a problem when the player spawns back in the map, then you have a problem I don’t see so far so I would need to see the code.

Zylann | 2016-11-08 14:10

I actually move the player away from the area2d position to (0,0) once it enters the area2d, just to see if it’s because of loop-teleporting but it seems not working, I will try the flag way you described:

here is the code:

from sceneA to sceneB, by click the ‘start’ button:

func _on_Button_pressed():
get_node("/root/global").goto_scene("res://scenes/sceneB.tscn")

from sceneB to sceneA, by entering the area2d:

func _on_Area2D_body_enter( body ):
print("body enter")
print(body.get_pos())
body.set_pos(Vector2(0,0))
get_node("/root/global").goto_scene("res://scenes/sceneA.tscn")

switch scene, the whole singleton code:

    extends Node
var current_scene = null
var playerscene = preload("res://player/test1/player.tscn")
var player = null

func _ready():
	var root = get_tree().get_root()
	current_scene = root.get_child(root.get_child_count()-1)
	player = playerscene.instance()

func goto_scene(path, showplayer=false):
	# remove player node from current_scene
	var playernode = current_scene.find_node("player",true,false)
	if playernode != null:
		playernode.set_pos(Vector2(0,0))
		playernode.get_parent().remove_child(playernode)
	
	call_deferred("_deferred_goto_scene",path,showplayer)

func _deferred_goto_scene(path,showplayer):
	print("change scene start")
	current_scene.free()
	
	# Load new scene
	var s = ResourceLoader.load(path)
	print(path)
	# Instance the new scene
	current_scene = s.instance()
	
	# Add it to the active scene, as child of root
	get_tree().get_root().add_child(current_scene)
	
	# optional, to make it compatible with the SceneTree.change_scene() API
	get_tree().set_current_scene( current_scene )
	
	# put player on map
	if showplayer == true:
		var pos = current_scene.get_node("startpos").get_pos()
		player.set_pos(pos)
		current_scene.find_node("obstacles",true,false).add_child(player)

Seanfy | 2016-11-08 14:40

Try setting your player’s position before switching the scene. As far as I can see you are doing it after you have switched the scene.

timoschwarzer | 2016-11-08 15:00

I set the player position to (0,0) twice, first when it enter the area2d and triggered body_enter event, second is when change scene is about to happen and before player is removed from the current scene, I think they are all happened before switch the scene…

The last part to set the player’s new position needs to be at this point because I need to get the position from the new scene(a Position2D node), but the player should be at position (0,0) already.

Seanfy | 2016-11-08 15:09

I think the problem is that physics are running one frame behind the renderer, so you have to set the player’s position, wait one frame and then load the scene.

timoschwarzer | 2016-11-08 15:12

I see you store player in a variable, but why are you trying to find playernode in goto_scene while you don’t use it in _deferred_goto_scene?

Zylann | 2016-11-08 15:19

this playernode is a temp variable, used to check if the current scene has a player node in it.

Seanfy | 2016-11-08 15:29

Be careful then, because you use the playernode temporary variable to move the player, remove it from the tree, and forget it after… so what you do with it is useless in the end, and causes a potential node leak. But I think by plain luck that player is actually the same node, because only your singleton is instancing it, am I right? If it is, then you don’t need to find it, you have it already.

I’ll try to do the same things at home to see if I get the same problem, because it’s a bit confusing.

Zylann | 2016-11-08 18:39

Thanks @Zylann, Yes i’m aware of the node leak case you pointed out, I’m actually don’t understand the parenting structure quite well, by the time I wrote the code I was not sure if the variable ‘player’ has the correct parenting relationship in the scene, that’s why I used a temp var. But yes you are right it’s confusing, I should use ‘player’ at the actual set_pos code, even it’s the same as the temp ‘playernode’ :slight_smile:

Seanfy | 2016-11-08 18:57

:bust_in_silhouette: Reply From: Zylann

So I reproduced the same problem with a test project. This is indeed caused by what timoschwarzer said, the physics engine is updated after scripts, so if you change the position of a KinematicBody2D, its actual position within the physics engine will change only at the next synchronization, which will have happened one frame later, not at the time you set the position.

I can think of these options:

  1. When the player is added to the tree, ignore all teleports until he is not in a portal. You can use a boolean variable to do this.

  2. Don’t keep a reference on the player, and create it again when the scene change, at the right position. But that would imply the player node is a visual representation (an avatar) and won’t contain everything about the player itself (the progression of the person who plays).

  3. Wait one frame before triggering teleports, but that’s similar to 1).

I personally like option 1) because it also works in case you want to make the player spawn on the portal itself like in Minecraft.

When changing scenes, removing problematic objects from all the collision layers could help too.
Then call groups and re-set the values.

eons | 2016-11-09 02:13

Sorry for the late reply. I didn’t get it until I add my player in sceneA after scene change as well, then it works properly. Thank you guys for help me finding out the real problem:)

I assume a node needs to be parented in a scene tree in order to have it’s fixed process running and update it’s physical data? or is there a way to achieve ‘wait one frame’ manually, or force the node to update it’s physics?

Seanfy | 2016-11-10 15:22