Syncing car between multiplayer players

Godot Version

Godot Version 4.3

Question

GitHub page GitHub - NicholasRucinski/mGTA

I am trying to learn how Godot multiplayer works by creating an open-world game. I think I have the players syncing and working correctly. However, now that I am trying to add cars, I can’t get it to work. I’ve been able to sync the car’s position and rotation, but only the host can drive them. From what I’ve seen I think I need everything to be done by the host but I can’t figure out how to have the host be the authority of the car but still get input from the player. I’m not sure if I’m thinking about this wrong. Any help would be appreciated

Here is the code for my car

extends VehicleBody3D

@export var max_steering = 0.3
@export var engine_power = 100
@export var is_active = false
@export var driver_node : Node3D = null
@onready var state = $State

var max_rpm = 500
var max_torque = 100

func _process(delta: float) -> void:
	if driver_node != null:
		state.sync_driver_node = driver_node
		state.sync_position = global_position
		state.sync_rotation = global_rotation
	if Input.is_action_just_pressed("debug"):
		print("Is Active: " + str(is_active))
		print("Driver: " + str(driver_node))
	
func _physics_process(delta: float) -> void:
	if get_multiplayer_authority() != multiplayer.get_unique_id():
		global_position = state.sync_position
		global_rotation = state.sync_rotation
		driver_node = state.sync_driver_node
		return
	if driver_node == null: return
	handle_driving(delta)
	
func handle_driving(delta: float) -> void:
	if driver_node == null:
		steering = 0
		$"wheel-bl".engine_force = 0
		$"wheel-br".engine_force = 0
		return
	steering = move_toward(steering, Input.get_axis("move_right", "move_left") * max_steering, delta * 5)
	var acceleration = Input.get_axis("move_back", "move_forward")
	print(str(is_active) + " Steering = " + str(steering) + " Accl = " + str(acceleration))
	var rpm = $"wheel-bl".get_rpm()
	$"wheel-bl".engine_force = acceleration * max_torque * (1 - rpm / max_rpm)
	rpm = $"wheel-br".get_rpm()
	$"wheel-br".engine_force = acceleration * max_torque * (1 - rpm / max_rpm)

func set_driver(driver: Node3D):
	print("Set driver on " + str(multiplayer.get_unique_id()))
	driver_node = driver

Car scene
image
State replication

MultiplayerSynchronizers only communicate one way. From the authority.

The authority defaults to the host, and can be changed with Node.set_multiplayer_authority(id).

But you typically want to broadcast player input to the server via a MultiplayerSynchronizer and have the trusted authority move the player characters on thier behalf.

Hi, thanks for taking the time to respond. I think I get what you are saying. I kinda need to handle the car the same way as the player right? If I set it up that way where in each car function I return if it is not the host then I still don’t get movement when the join players try to control the car.

Here is what I changed it to. To make things simpler, I removed the requirement that players be in a car to control it. Right now I am syncing position and rotation.

extends VehicleBody3D

@export var max_steering = 0.3
@export var engine_power = 100
@export var is_active = false
@export var driver_node : Node3D = null

var max_rpm = 500
var max_torque = 100

#func _enter_tree() -> void:
	#set_multiplayer_authority(str(get_parent_node_3d().name).to_int())

func _process(delta: float) -> void:
	if not is_multiplayer_authority(): return
	if Input.is_action_just_pressed("debug"):
		print("Is Active: " + str(is_active))
		print("Driver: " + str(driver_node))
	
func _physics_process(delta: float) -> void:
	if not is_multiplayer_authority(): return
	handle_driving(delta)
	
func handle_driving(delta: float) -> void:
	if not is_multiplayer_authority(): return
	#if driver_node == null:
		#steering = 0
		#engine_force = 0
		#$"wheel-bl".engine_force = 0
		#$"wheel-br".engine_force = 0
		#return
	steering = move_toward(steering, Input.get_axis("move_right", "move_left") * max_steering, delta * 5)
	var acceleration = Input.get_axis("move_back", "move_forward")
	var rpm = $"wheel-bl".get_rpm()
	$"wheel-bl".engine_force = acceleration * max_torque * (1 - rpm / max_rpm)
	rpm = $"wheel-br".get_rpm()
	$"wheel-br".engine_force = acceleration * max_torque * (1 - rpm / max_rpm)

func set_driver(driver: Node3D):
	if not is_multiplayer_authority(): return
	print("Set driver on " + str(multiplayer.get_unique_id()))
	driver_node = driver

No not exactly, i wouldnt put any is_multiplayer_authority() unless you are about to remove, or add, the car scene with queue_free and the like. Because spawners work on authority too and only the spawner’s authority should add/remove scenes.

I would uncouple your input handling from happening in processing, and instead use the dedicated _unhandled_input callback.

Example:

@export var steering_input:float = 0.0 # send this to server
func _unhandled_input(event):
  If event.is_action("move_right") or event.is_action("move_left")
    steering_input = Input.get_axis("move_right", "move_left")

func handle_driving(delta: float) -> void:
  # move the car locally for better client feel
  steering = move_toward(steering, stearing_input * max_steering, delta * 5)
  ...

You can setup the second Input synchronizer to send the steering_input property to the server. This is also the beginnings of client-side-prediction if you allow the local input to be used and have the server validate input, steer the server car instance, and send back the true car position and state.

If you adhere to this design pattern your synced classes should have a minimal amount of network code within them.

Your set_driver code makes is a little more complex.
But if you took the input decoupling a step further by making its own node. And you added is_multiplayer_authority() to the top of the input callback. Then the server and clients all need to set the authority on the input node.

Input node

Extends Node

Signal new_input(steer_input:float)

@export var steering_input:float = 0.0 # send this to server
func _unhandled_input(event):
  if not is_multiplayer_authority():
  If event.is_action("move_right") or event.is_action("move_left")
    steering_input = Input.get_axis("move_right", "move_left")
  new_input.emit(steering_input)
@export var driver_authority: int =1
func set_driver(driver: Node3D):
  $InputNode.set_multiplayer_authority(driver_authority)
  if not is_multiplayer_authority(): return
	print("Set driver on " + str(multiplayer.get_unique_id()))
  driver_node = driver

Hopefully last thing and thanks again for helping. I am getting this error when the authority changes for the input node.


I added the input node and a synchronizer for it like you said and then synced steering and acceleration. But then I was getting a lot of “ignoring sync from non authority node” errors and when I checked the remote nodes for the host and the joined player, only the joined player was updating the authority. So I added a rpc to change the authority on all players I think but now I only get one sync error. The steering is correctly moving the tires for the most recent player to set itself as the driver but the car still only moves when the host is the driver.

extends Node

signal new_input(steer_input: float, accel: float)

@export var steering_input: float = 0.0
@export var acceleration: float = 0.0

func _unhandled_input(event: InputEvent) -> void:
	if not is_multiplayer_authority(): return
	if event.is_action("move_right") or event.is_action("move_left"):
		steering_input = Input.get_axis("move_right", "move_left")
	if event.is_action("move_back") or event.is_action("move_forward"):
		acceleration = Input.get_axis("move_back", "move_forward")
	new_input.emit(steering_input, acceleration)
extends VehicleBody3D

@export var max_steering = 0.3
@export var engine_power = 100
@export var driver_node : Node3D = null

var steering_input: float = 0.0
var acceleration: float = 0.0

var max_rpm = 500
var max_torque = 100

func _process(delta: float) -> void:
	if Input.is_action_just_pressed("debug"):
		print("Driver: " + str(driver_node))
	
func _physics_process(delta: float) -> void:
	handle_driving(delta)
	
func handle_driving(delta: float) -> void:
	#if driver_node == null:
		#steering = 0
		#engine_force = 0
		#$"wheel-bl".engine_force = 0
		#$"wheel-br".engine_force = 0
		#return
	steering = move_toward(steering, steering_input * max_steering, delta * 5)
	var rpm = $"wheel-bl".get_rpm()
	$"wheel-bl".engine_force = acceleration * max_torque * (1 - rpm / max_rpm)
	rpm = $"wheel-br".get_rpm()
	$"wheel-br".engine_force = acceleration * max_torque * (1 - rpm / max_rpm)

func set_driver(driver: Node3D) -> void:
	if driver == null: return
	driver_node = driver
	$Input.set_multiplayer_authority(driver.multiplayer.get_unique_id(), true)
	print("Driver set: " + str(driver.multiplayer.get_unique_id()))

	rpc("remote_set_driver", driver.multiplayer.get_unique_id())

@rpc("any_peer", "call_remote")
func remote_set_driver(driver_id: int) -> void:
	$Input.set_multiplayer_authority(driver_id, true)
	print("Driver updated on peer: " + str(multiplayer.get_unique_id()))

func _on_input_new_input(steer_input: float, accel: float) -> void:
	steering_input = steer_input
	acceleration = accel

RPCs require some care, I would try to make the authority a property that a MultiplayerSynchronizer replicates as it can handle new players joining.

you can setup a special set function

@export var driver_authority = 1 : set = remote_set_driver

func remote_set_driver(driver_id: int) -> void:
    driver_authority = driver_id
	$Input.set_multiplayer_authority(driver_id, true)
	print("Driver updated on peer: " + str(multiplayer.get_unique_id()))