Physics "Deteriorate" Overtime Working with RigidBody3D

Just to ensure that’s not a precision problem. How far away are you from the world origin when the issue appears?

Interesting.

Alright. Let us know if you figure something out.

Ok, I found what is causing the problem.

My game places the 3D scene inside a 2D scene with SubViewports, which handle the minimap and the GUI and pixelate the visuals.

After a little debugging, the problem comes from the script in this 2D scene, specifically the function minimap_handler().

If anyone can help me find the problem within this function, that would be great!

extends Control

@onready var game_node: Node3D = $Game/SubViewport/Node3D

@onready var minimap_camera: Camera3D = $Minimap/SubViewport/Camera3D
@onready var minimap: SubViewportContainer = $Minimap

@onready var money_fill: Label = $GameGui/VBoxContainer/MoneyFill
@onready var money_outline: Label = $GameGui/VBoxContainer/MoneyFill/MoneyOutline
@onready var quota_fill: Label = $GameGui/VBoxContainer/QuotaFill
@onready var quota_outline: Label = $GameGui/VBoxContainer/QuotaFill/QuotaOutline
@onready var time_fill: Label = $GameGui/TimeFill
@onready var time_outline: Label = $GameGui/TimeFill/TimeOutline


func _process(_delta: float) -> void:
	minimap_handler()
	gui_text_handler()


func gui_text_handler():
	# Update the money label
	if Global.money:
		money_fill.text = "Money: $" + str("%0.2f" % Global.money)
		money_outline.text = "Money: $" + str("%0.2f" % Global.money)
	
	# Update quota label
	if Global.quota and Global.round_quota:
		quota_fill.text = "Quota: $" + str(int(snappedf(Global.round_quota, 1.0))) + "/" + str(int(snappedf(Global.quota, 1.0)))
		quota_outline.text = "Quota: $" + str(int(snappedf(Global.round_quota, 1.0))) + "/" + str(int(snappedf(Global.quota, 1.0)))
	elif Global.quota:
		quota_fill.text = "Quota: $0/" + str(int(snappedf(Global.quota, 1.0)))
		quota_outline.text = "Quota: $0/" + str(int(snappedf(Global.quota, 1.0)))
	
	# Changes the time label
	if Global.time:
		var minutes: int = int(Global.time / 60.0)
		var seconds: float = fmod(Global.time, 60.0)
		time_fill.text = "%02d:%02d" % [minutes, seconds]
		time_outline.text = "%02d:%02d" % [minutes, seconds]


func minimap_handler():
	# Change the minimap position
	if Global.car:
		minimap_camera.position.x = Global.car.position.x
		minimap_camera.position.z = Global.car.position.z
		
		# Rotate map based on car
		minimap_camera.rotation.y = Global.car.rotation.y
		
		# Rotates civilian icons and destination icons
		for child in game_node.get_children():
			
			# Civilian waypoint
			if child.name == "Road":
				for civilian in child.get_children():
					if civilian.name.contains("Civilian"):
						# Get references
						var waypoint = null
						for civilian_child in civilian.get_children():
							if civilian_child.name == "Waypoint":
								waypoint = civilian_child
						var minimap_icon = waypoint.get_child(4)
						var minimap_move_icon = waypoint.get_child(7)
						var civilian_minimap_visibility_notifier = waypoint.get_child(5)
						var icon_visibility_notifier = waypoint.get_child(6)
						
						# Rotate the icon
						minimap_icon.rotation.y = minimap_camera.rotation.y - civilian.rotation.y
						
						# If the civilian is on the minimap screen
						civilian_minimap_visibility_notifier.connect("screen_entered", Callable(func():
							
							if minimap_icon.get_meta("icon_on_screen") == true: return
							minimap_icon.set_meta("icon_on_screen", true)
							
							minimap_icon.visible = true
							minimap_move_icon.visible = false
							minimap_icon.position = Vector3(0, 30, 0)
							
						))
						
						# If the civilian is off the minimap screen
						civilian_minimap_visibility_notifier.connect("screen_exited", Callable(func():
							
							if minimap_icon.get_meta("icon_on_screen") == false: return
							minimap_icon.set_meta("icon_on_screen", false)
							
							minimap_move_icon.visible = true
							minimap_icon.visible = false
							
							move_minimap_icon(minimap_icon, minimap_move_icon, icon_visibility_notifier)
							
						))
						
						# See if the icon has moved off the screen when adusting when not on the civilian
						icon_visibility_notifier.connect("screen_entered", Callable(func():
							if minimap_icon.get_meta("should_move") == false: return
							minimap_icon.set_meta("should_move", false)
						))
						icon_visibility_notifier.connect("screen_exited", Callable(func():
							if minimap_icon.get_meta("should_move") == true: return
							minimap_icon.set_meta("should_move", true)
							move_minimap_icon(minimap_icon, minimap_move_icon, icon_visibility_notifier)
						))
						
						# When civilian is ready to be picked up, make sure that we can see the markers
						Global.new_destination.connect(Callable(func(type):
							
							if minimap_move_icon.visible == true or type != "civilian": return
							
							minimap_icon.set_meta("should_move", true)
							minimap_icon.set_meta("icon_on_screen", false)
					
							minimap_move_icon.visible = true
							minimap_icon.visible = false
							
							move_minimap_icon(minimap_icon, minimap_move_icon, icon_visibility_notifier)
							
						))
			
			# Destination waypoint
			if child.name == "Waypoint":
				# References
				var minimap_icon = child.get_child(4)
				var minimap_move_icon = child.get_child(7)
				var destination_minimap_visibility_notifier = child.get_child(5)
				var icon_visibility_notifier = child.get_child(6)
				
				# Set texutre and rotate icon 
				minimap_icon.texture = preload("res://assets/Textures/dropoff.png")
				minimap_move_icon.get_child(0).texture = preload("res://assets/Textures/dropoff.png")
				minimap_icon.rotation.y = minimap_camera.rotation.y
				
				# If the destination is on the minimap screen
				destination_minimap_visibility_notifier.connect("screen_entered", Callable(func():
					
					if minimap_icon.get_meta("icon_on_screen") == true: return
					minimap_icon.set_meta("icon_on_screen", true)
					
					minimap_move_icon.visible = false
					minimap_icon.visible = true
					minimap_icon.position = Vector3(0, 30, 0)
					
				))
				
				# If the destination is off the minimap screen
				destination_minimap_visibility_notifier.connect("screen_exited", Callable(func():
				
					if minimap_icon.get_meta("icon_on_screen") == false: return
					minimap_icon.set_meta("icon_on_screen", false)
					
					minimap_move_icon.visible = true
					minimap_icon.visible = false
					move_minimap_icon(minimap_icon, minimap_move_icon, icon_visibility_notifier)
					
				))
						
				# See if the minimap has moved off the screen when adusting when not on the destination
				icon_visibility_notifier.connect("screen_entered", Callable(func():
					if minimap_icon.get_meta("should_move") == false: return
					minimap_icon.set_meta("should_move", false)
				))
				icon_visibility_notifier.connect("screen_exited", Callable(func():
					if minimap_icon.get_meta("should_move") == true: return
					minimap_icon.set_meta("should_move", true)
					move_minimap_icon(minimap_icon, minimap_move_icon, icon_visibility_notifier)
				))
				
				# When destination is set, make sure that we can see the markers
				Global.new_destination.connect(Callable(func(type):
					
					if minimap_move_icon.visible == true or type != "destination": return
					
					minimap_icon.set_meta("should_move", true)
					minimap_icon.set_meta("icon_on_screen", false)
					
					minimap_move_icon.visible = true
					minimap_icon.visible = false
					print("here destination")
					move_minimap_icon(minimap_icon, minimap_move_icon, icon_visibility_notifier)
					
				))


func move_minimap_icon(icon, minimap_move_icon, icon_visibility_notifier):
	
	if icon.get_meta("is_moving") == true: return
	
	if icon.get_meta("icon_on_screen") == false:
		
		icon.set_meta("is_moving", true)
		
		# Reset the position and move linear towards the center of the minimap camera
		icon_visibility_notifier.position = Vector3(0, 30, 0)
		
		var tween = create_tween()
		tween.tween_property(icon_visibility_notifier, "global_position", Vector3(minimap_camera.global_position.x, 30, minimap_camera.global_position.z), 0.05)
		tween.play()
		
		# Wait until the visibility checker comes back on screen
		while true:
			
			await get_tree().create_timer(0.00001).timeout
			
			# Checks if the destination is on screen
			if icon.get_meta("should_move") == false or icon.get_meta("icon_on_screen") == true:
				
				icon.set_meta("is_moving", false)
				tween.stop()
				
				# Move the icon
				tween = create_tween()
				tween.tween_property(minimap_move_icon, "global_position", icon_visibility_notifier.global_position, 0.5)
				minimap_move_icon.get_child(0).rotation.y = icon.rotation.y - minimap_move_icon.rotation.y
				if not minimap_move_icon.position.is_equal_approx(icon.position):
					minimap_move_icon.look_at(icon.global_position)
				tween.play()
				
				# Recall to check if we need to move agian
				move_minimap_icon(icon, minimap_move_icon, icon_visibility_notifier)
				break

As for @conz3d idea, after a little bit of testing, that seemed not to affect the issue.

How did you figure out that minimap_handler() is to blame for the issue? Did you just test it by one-by-one removing a function to see if it fixed the issue? I’m asking because there is not a single line in that function that does any operation on the car, so it shouldn’t affect the car at all.

Could you post the node tree structure for your scene? It’s possible that an ancestral node, whose position is repeatedly modified, will affect the car’s physics.
As referenced by @athousandships:

Yes, I removed the line, and the problem is fixed.

Here is the 2D scene hierarchy:

Here is the 3D scene hierarchy:

I tried to switch the car to a CharacterBody3D, but that did not solve the issue.

It is something in the function that is stated above

Alright, I isolated the problem to this while loop.

		
while true:
        await get_tree().create_timer(0.00001).timeout
		
		# Checks if the destination is on screen
		if icon.get_meta("should_move") == false or icon.get_meta("icon_on_screen") == true:
			
			icon.set_meta("is_moving", false)
			tween.stop()
			
			# Move the icon
			tween = create_tween()
			tween.tween_property(minimap_move_icon, "global_position", icon_visibility_notifier.global_position, 0.5)
			minimap_move_icon.get_child(0).rotation.y = icon.rotation.y - minimap_move_icon.rotation.y
			if not minimap_move_icon.position.is_equal_approx(icon.position):
				minimap_move_icon.look_at(icon.global_position)
			tween.play()
			
			# Recall to check if we need to move agian
			move_minimap_icon(icon, minimap_move_icon, icon_visibility_notifier)
			break

If anyone sees anything wrong with this, let me know!

await get_tree().create_timer(0.00001).timeout

I’d wager its the 100 microsecond timer that’s called every physics frame that’s causing issues.

I’m not seeing anything that’s weird about that snippet of code. Within that snippet, have you tried to find the specific line that causes the issue?

Similar to @puffcrushin, I would also say that the 0.00001-timer is a little weird. I’m not sure if it’s causing your problem, but it’s definitely weird. An alternative would be to wait for one process-frame:

await get_tree().process_frame

The outcome is probably the same, but it’s worth changing regardless.


If you narrow it down further, let us know.

IMHO the code contains 3 elements that cause problems:

  1. the minimap_handler() (which is called from the _process() function which in turn is called from the engine several times per seccond) connects callables to signals (like civilian_minimap_visibility_notifier.connect("screen_entered", Callable(...)) which means that after some time there are hundreds of callables connected to those signals.
    This can cause performance problems later on.
  2. The move_minimap_icon() function that contains a while true loop that (with the timer set at 10 microseconds could run up to 100000 times per second, even if the states it checks update at far lower rates
  3. The move_minimap_icon() function calls itself recursively which can lead to a stack overflow.

@tomyy 's first point ended up being the solution.

I changed all of the connect functions to use CONNECT_ONE_SHOT, looking something like this.

civilian_minimap_visibility_notifier.connect(“screen_entered”, func():

	if minimap_icon.get_meta("icon_on_screen") == true: return
	minimap_icon.set_meta("icon_on_screen", true)
						
	minimap_icon.visible = true
	minimap_move_icon.visible = false
	minimap_icon.position = Vector3(0, 30, 0)
	
	,CONNECT_ONE_SHOT
						
)

If I encounter any more problems, I will let you guys know.

Thank you, everyone, for your insights!