Getting quarternion errors when a player collides with a vehicle in mulitplayer

Godot Version

4.6.2

Question

Hello, I am making a multiplayer game in Godot where players can take control of different systems. I thought I’d start with a car. But I keep getting quartenion errors when a peer takes control of the car (becomes the authority for it) and a non-authorative peer collides with it. Thanks for any help!

I have a VehicleBody3D as the car and each player that joins controls a CharacterBody3D as their player, with the peer being the multiplayer authority for their own player. The car starts out with the authority being the host. A MultiplayerSynchroniser syncs the car’s position, rotation and steering.

(I also do a little trick to make the wheels appear in the correct place for non-authoritative peers since this doesn’t seem to work if physics are frozen - I update some wheel meshes to go to the correct wheel positions and hide the real wheels from view).

When the car first spawns, it checks if it is the multiplayer authority. If it is not, then the car will freeze and disable its physics process.

Initially it all works fine. The host and all the peers can jump on the car and collide with it, and it will move slightly in response to collisions. No errors.

When a player interacts with the car, it rpcs the authority for the car and requests to take control of it. After some checks, the authority then rpcs all other peers to update the multiplayer authority for the car. In response, peers adjust their physics process and freeze for the car. Looking at the variables in the remote tab, this seems to work as expected. I have turned off code to move the player to the car for now.

When the authoritative peer collides with the car, it moves about as expected. But when a non-authoritative peer collides with the car, Godot freezes, the peer seems to teleport very far away and I get a huge stream of hundreds errors that starts with:

instance_set_transform: Condition “!v.is_finite()” is true.
<C++ Source>  servers/rendering/renderer_scene_cull.cpp:991 @ instance_set_transform()
get_quaternion: Basis \[X: (nan, nan, nan), Y: (nan, nan, nan), Z: (nan, nan, nan)\] must be normalized in order to be casted to a Quaternion. Use get_rotation_quaternion() or call orthonormalized() if the Basis contains linearly independent vectors.
<C++ Error>   Condition “!is_rotation()” is true. Returning: Quaternion()
<C++ Source>  core/math/basis.cpp:716 @ get_quaternion()

and hundreds of:

 Quaternion: The axis Vector3 (nan, nan, nan) must be normalized.
<C++ Error>   Condition “!p_axis.is_normalized()” is true.
<C++ Source>  core/math/quaternion.cpp:285 @ Quaternion()

I just don’t understand why this works absolutely fine when the host is the authority for the car, but as soon as it changes to another peer it no longer works?

Here is my gdscript for the car (I split the script into different preformatted text sets as it didn’t render with colours for a couple of functions for some reason):

extends VehicleBody3D

var driver: Player
var level: Node
var debug := true

# driver's seat player position
@onready var player_target_origin: Node3D = $PlayerSeatDriver/PlayerTargetOrigin

# Wheels
@onready var vehicle_wheel_3dfl: VehicleWheel3D = $VehicleWheel3DFL
@onready var vehicle_wheel_3dfr: VehicleWheel3D = $VehicleWheel3DFR
@onready var vehicle_wheel_3drl: VehicleWheel3D = $VehicleWheel3DRL
@onready var vehicle_wheel_3drr: VehicleWheel3D = $VehicleWheel3DRR

# Wheel meshes for multiplayer
@onready var wheel_dark_fl: Node3D = $"wheel-darkFL"
@onready var wheel_dark_fr: Node3D = $"wheel-darkFR"
@onready var wheel_dark_rl: Node3D = $"wheel-darkRL"
@onready var wheel_dark_rr: Node3D = $"wheel-darkRR"

# Internal variables
var last_freeze := false

# Called when the node enters the scene tree for the first time.
func _ready() -> void:
	level = get_tree().get_first_node_in_group("level")
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(_delta: float) -> void:
	if multiplayer.multiplayer_peer == null:
		return
	
	if is_multiplayer_authority():
		freeze = false
		set_physics_process(is_multiplayer_authority())
	else:
		freeze = true	# Don't allow peers to simulate physics
		wheel_dark_fl.rotation.y = steering
		wheel_dark_fr.rotation.y = steering
	
	# decide what wheels to show
	if freeze != last_freeze:	# peer wheels hidden by default. 
		show_correct_wheels()
	last_freeze = freeze
# A player interacted with the driver's seat.
func interact(caller: Node) -> void:
	if caller is Player and driver == null:	# Check not already occupied
		# Request authority from the current Authority
		request_authority.rpc_id(get_multiplayer_authority(), 
				multiplayer.get_unique_id(), true)
# Current authority is requested to make this player the authority.
@rpc("any_peer", "call_local", "reliable")
func request_authority(id: int, boarding: bool) -> void:
	# Only allow another player to become authority if there is no driver.
	if is_multiplayer_authority() and driver == null:
		set_authority_id.rpc(id, boarding)
# Authority tells all peers to make player the driver.
@rpc("authority", "call_local", "reliable")
func set_authority_id(id: int, boarding: bool) -> void:
	set_multiplayer_authority(id)
	freeze = ! is_multiplayer_authority()
	set_physics_process(is_multiplayer_authority())
	
	# if a player is boarding the car, set this up
	if boarding:
		driver = level.get_player_by_id(id)
		if debug: print("drivable_car: driver: " + str(driver))
		
		if driver:
			# for just the new authority, move the player into position.
			if driver.is_multiplayer_authority() and boarding:
				if debug: print("drivable_car: driver is multiplayer authority and boarding")
				
				#driver.player_state_machine.external_transition("Player-Sitting")
				#driver.sync_position = false
	
	# if player is not boarding the car, then just assign authority to them.
	else:
		#driver.sync_position = false
		pass
# Decide what wheels to show depending on whether car is frozen.
func show_correct_wheels() -> void:
	if multiplayer.multiplayer_peer:
		if ! is_multiplayer_authority():
			wheel_dark_fl.show()
			wheel_dark_fr.show()
			wheel_dark_rl.show()
			wheel_dark_rr.show()
			vehicle_wheel_3dfl.hide()
			vehicle_wheel_3dfr.hide()
			vehicle_wheel_3drl.hide()
			vehicle_wheel_3drr.hide()
		else:
			wheel_dark_fl.hide()
			wheel_dark_fr.hide()
			wheel_dark_rl.hide()
			wheel_dark_rr.hide()
			vehicle_wheel_3dfl.show()
			vehicle_wheel_3dfr.show()
			vehicle_wheel_3drl.show()
			vehicle_wheel_3drr.show()

Many thanks for any help!

Update: I’ve discovered that when a peer becomes the authority for the car and a different peer then collides with the car, their transform becomes nan. I printed this in the output window for each player. First number is the multiplayer unique id:

1: player 1 position: (57.61417, 0.961967, -153.3502) rotation: (0.0, -3.021543, 0.0)
1: player 1 position: (nan, nan, nan) rotation: (0.0, -3.021543, 0.0)

So when a non-authoritative peer collides with the car, the collision somehow makes their transform invalid?