TO expand more, here is a sample code from my project
Here i have a multiplayer project where players can spawn boxes in the world.
The world scene controls the spawnage. Player nodes trigger the spawn_box request. So i have connected player spawn_box request signals to the world scene script so that the world would spawn boxes for every player multiplayer instance.

world.gd
extends Node3D
const PLAYER = preload("res://Assets/Player/player.tscn")
const BOX = preload("res://Assets/Items/box.tscn")
@onready var players: Node = $Players
const start_pos: Vector3 = Vector3(0,3,0)
@onready var items = $Items
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
var index =0
for i in GameManager.players:
spawn_player(GameManager.players[i])
index +=1
#Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
pass # Replace with function body.
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
pass
func spawn_item_world(player_transform:Transform3D, basis_z:Vector3):
var new_box = BOX.instantiate()
new_box.position = player_transform.origin - (basis_z * 3)
add_child(new_box)
pass
func spawn_item_world_spawner(player_transform:Transform3D, basis_z:Vector3):
var new_box = BOX.instantiate()
new_box.position = player_transform.origin - (basis_z * 3)
items.add_child(new_box)
pass
func spawn_player(player_data:PlayerData):
var new_pos:Vector3
var new_player = PLAYER.instantiate()
new_player.name = str(player_data.id)
new_player.player_data=player_data
players.add_child(new_player)
#new_player.spawn_item_signal.connect(spawn_item_world)
new_player.spawn_item_signal.connect(spawn_item_world_spawner)
var player_count = players.get_children().size()
if player_count > 1:
var last_player = players.get_children()[player_count-1]
new_pos = last_player.position + Vector3(3,3,3)
new_player.position = new_pos
else:
new_player.position = start_pos

player.gd
extends CharacterBody3D
class_name Player
var player_data:PlayerData
@export_category("SpringArm and Interact Ray properties")
@export var SPRING_ARM_LENGTH = 5.25
@export var SPRING_ARM_MARGIN = 0.1
@export var INTERACT_RAY_LENGTH = 2.5
@export_category("H node offset")
@export var OFFSET_X = 0
@export var OFFSET_Y = 2
@export var OFFSET_Z = 0
@export_category("Camera offset")
@export var OFFSET_H = 0.5
@export var OFFSET_V = 0
@export_category("Camera vertical angles")
@export var cam_v_min = -90.0 # degrees
@export var cam_v_max = 90.0 # degrees
@onready var mesh:Node3D = $Mesh
@onready var camera_3d: Camera3D = $h/v/SpringArm3D/Camera3D
const SPEED = 5.0
const JUMP_VELOCITY = 4.5
var PUSH_FORCE = 1.0
var THROW_FORCE = 20
var h_sensitivity = 0.20
var v_sensitivity = 0.20
var camrot_h = 0.0
var camrot_v = 0.0
var current_collider = null
var is_mouse_over = false
# Get the gravity from the project settings to be synced with RigidBody nodes.
var gravity = ProjectSettings.get_setting("physics/3d/default_gravity")
@onready var spring_arm_3d = $h/v/SpringArm3D
@onready var interact_ray = $h/v/SpringArm3D/Camera3D/RayCast3D
@onready var multiplayer_synchronizer: MultiplayerSynchronizer = $MultiplayerSynchronizer
@onready var label_3d: Label3D = $Mesh/Label3D
@onready var label_3d_2: Label3D = $Mesh/Label3D2
@onready var chat_ui: ChatUI = $CanvasLayer/ChatUI
var is_mouse_captured:bool = true
signal spawn_item_signal
func _enter_tree() -> void:
set_multiplayer_authority(str(name).to_int())
pass
func _ready():
if not is_multiplayer_authority(): return
camera_3d.h_offset=OFFSET_H
camera_3d.v_offset=OFFSET_V
$h.top_level = true
$h.position.x = OFFSET_X
$h.position.y = OFFSET_Y
$h.position.z = OFFSET_Z
interact_ray.position.x+=OFFSET_H
$h/v/SpringArm3D.add_excluded_object(self) # removes collision with player
$h/v/SpringArm3D.add_excluded_object($h/v/SpringArm3D)
spring_arm_3d.spring_length = SPRING_ARM_LENGTH
spring_arm_3d.margin = SPRING_ARM_MARGIN
label_3d.text = str("Player: ") + player_data.player_name
var ray_target_position = Vector3(0,0,(SPRING_ARM_LENGTH+INTERACT_RAY_LENGTH) * (-1))
interact_ray.target_position = ray_target_position
camera_3d.current = true
#chat_ui.show()
chat_ui.player_data = player_data
func _unhandled_input(_event):
if not is_multiplayer_authority(): return
if Input.is_action_just_pressed("action_interact"):
interact()
#print("cc2")
if Input.is_action_just_pressed("action_throw"):
spawn_item.rpc()
if Input.is_action_just_pressed("menu_inventory"):
print("menu press")
set_mouse_captured()
if Input.is_action_just_pressed("action_chat"):
chat_ui.show()
pass
func _input(event):
if not is_multiplayer_authority(): return
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
$h.rotate_y(deg_to_rad(-event.relative.x * h_sensitivity))
$h/v.rotate_x(deg_to_rad(-event.relative.y * h_sensitivity))
$h/v.rotation.x = clampf($h/v.rotation.x, deg_to_rad(cam_v_min), deg_to_rad(cam_v_max))
func _physics_process(delta):
if not is_multiplayer_authority(): return
# Add the gravity.
if not is_on_floor():
velocity.y -= gravity * delta
# Handle jump.
if Input.is_action_just_pressed("ui_accept") and is_on_floor():
velocity.y = JUMP_VELOCITY
# Get the input direction and handle the movement/deceleration.
# As good practice, you should replace UI actions with custom gameplay actions.
var input_dir = Input.get_vector("action_move_left", "action_move_right", "action_move_forward", "action_move_backward")
var direction = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
if direction:
direction=direction.rotated(Vector3.UP,$h.rotation.y)
#velocity.x = direction.x * SPEED
#velocity.z = direction.z * SPEED
velocity.x = lerp(velocity.x,direction.x * SPEED, 0.1)
velocity.z = lerp(velocity.z,direction.z * SPEED,0.1)
mesh.rotation.y=lerp_angle(mesh.rotation.y,atan2(-velocity.x,-velocity.z),0.15)
else:
velocity.x = move_toward(velocity.x, 0, SPEED)
velocity.z = move_toward(velocity.z, 0, SPEED)
push_objects()
check_ray_colliding_mouse()
move_and_slide()
camera_follow_player()
func camera_follow_player():
var camera_ideal_position: Vector3 = self.global_position + Vector3(OFFSET_X,OFFSET_Y,OFFSET_Z)
if self.global_position != $h.global_position - Vector3(OFFSET_X,OFFSET_Y,OFFSET_Z):
$h.global_position = lerp($h.global_position,camera_ideal_position,0.25)
func push_objects():
for i in get_slide_collision_count():
var c = get_slide_collision(i)
if c.get_collider() is RigidBody3D:
var body = c.get_collider()
var direction:Vector3 = body.position - global_position
var dir_normal = direction.normalized()
var imp:Vector3 = dir_normal * PUSH_FORCE
body.apply_central_impulse(imp)
func check_ray_colliding_mouse():
if interact_ray.is_colliding():
if is_mouse_over:
var collider_obj:Object = interact_ray.get_collider()
#current_collider = collider_obj
if current_collider != collider_obj:
mouse_exited(current_collider)
mouse_entered(collider_obj)
current_collider = collider_obj
else:
# Do nothing
pass
else:
if current_collider:
mouse_exited(current_collider)
current_collider = null
var collider_obj:Object = interact_ray.get_collider()
mouse_entered(collider_obj)
current_collider = collider_obj
is_mouse_over=true
else:
var collider_obj:Object = interact_ray.get_collider()
mouse_entered(collider_obj)
current_collider = collider_obj
is_mouse_over=true
else:
if is_mouse_over:
if current_collider:
mouse_exited(current_collider)
is_mouse_over=false
pass
func mouse_entered(collider):
if not collider == null:
print("entered player mouse")
collider.mouse_entered.emit()
pass
func mouse_exited(collider):
if not collider == null:
print("exited player mouse")
collider.mouse_exited.emit()
pass
func set_mouse_captured():
if is_mouse_captured:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
label_3d_2.show()
else:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
label_3d_2.hide()
is_mouse_captured = !is_mouse_captured
func interact():
print("player_press")
if interact_ray.is_colliding():
var collider_obj = interact_ray.get_collider()
print("collider class - ",collider_obj.name)
@rpc("any_peer","call_local")
func spawn_item():
var basis_z = camera_3d.global_transform.basis.z
var g_t = global_transform
spawn_item_signal.emit(g_t, basis_z)
pass
Sorry but too lazy to delete uneccessary things from script..
But pay attention to:
world.gd
extends Node3D
const PLAYER = preload("res://Assets/Player/player.tscn")
const BOX = preload("res://Assets/Items/box.tscn")
@onready var players: Node = $Players
const start_pos: Vector3 = Vector3(0,3,0)
@onready var items = $Items
func _ready() -> void:
var index =0
for i in GameManager.players:
spawn_player(GameManager.players[i]) # <-----------------------------------
index +=1
#Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
pass # Replace with function body.
func spawn_player(player_data:PlayerData):
var new_pos:Vector3
var new_player = PLAYER.instantiate()
new_player.name = str(player_data.id)
new_player.player_data=player_data
players.add_child(new_player)
#new_player.spawn_item_signal.connect(spawn_item_world)
new_player.spawn_item_signal.connect(spawn_item_world_spawner) # <----------------------------------
var player_count = players.get_children().size()
if player_count > 1:
var last_player = players.get_children()[player_count-1]
new_pos = last_player.position + Vector3(3,3,3)
new_player.position = new_pos
else:
new_player.position = start_pos
func spawn_item_world_spawner(player_transform:Transform3D, basis_z:Vector3):
var new_box = BOX.instantiate()
new_box.position = player_transform.origin - (basis_z * 3)
items.add_child(new_box)
pass
And in player.gd
extends CharacterBody3D
class_name Player
var player_data:PlayerData
signal spawn_item_signal
func _unhandled_input(_event):
if not is_multiplayer_authority(): return
if Input.is_action_just_pressed("action_interact"):
interact()
#print("cc2")
if Input.is_action_just_pressed("action_throw"):
spawn_item.rpc() #<---------------------------------------------------------------
if Input.is_action_just_pressed("menu_inventory"):
print("menu press")
set_mouse_captured()
if Input.is_action_just_pressed("action_chat"):
chat_ui.show()
pass
@rpc("any_peer","call_local")
func spawn_item():
var basis_z = camera_3d.global_transform.basis.z
var g_t = global_transform
spawn_item_signal.emit(g_t, basis_z)
pass