Multiplayer In a FPS help

Godot 4.4

I’m trying to make my boomer shooter multiplayer and i feel like i have everything set up properly but it’s not behaving right.

Here are some screenshots of how i have my scene set up and some of the scripts




Additional if it’s the problem, here is my player code:

extends CharacterBody3D

====== CONFIG ======

const SPEED = 15.0
const JUMP_VELOCITY = 13

@export var SENSITIVITY = 0.004
@export var WALL_JUMP_FORCE := 14.0
@export var WALL_JUMP_COOLDOWN := 1
@export var WALL_DETECT_DISTANCE := 1.0

@export var weapon = “None”
@export var last_received_network_pos: Vector3
@export var last_received_network_rot: Vector3
@export var network_update_received := false

headbob / tilt / fov

var bob_frequency = 1.1
var tilt_amount := 0.0
var tilt_speed := 7.0
var max_tilt := deg_to_rad(2)
var bob_amplitude = 0.1
var t_bob = 0.0
var default_fov := 90
var max_fov := 105
var fov_speed := 4.0
var prev_bob_y = 0.0
var prev_bob_dir = 0

wall jump state

var can_wall_jump = true
var wall_jump_timer = 0.0

====== NODES ======

@onready var aimray = $Head/Cameras/FirstPerson/AimRay
@onready var first_person_cam := $Head/Cameras/FirstPerson as Camera3D
@onready var third_person_cam := $Head/Cameras/ThirdPerson as Camera3D
@onready var cameras_container := $Head/Cameras
@onready var head = $Head
@onready var base_head_pos = head.transform.origin
@onready var body = $“Chest Model/Node/Body”
@onready var footstep = $Footstep
@onready var weaponsmanager = $Head/Cameras/FirstPerson/WeaponsManager
@onready var playermodelweaponmanager = $“Chest Model/Node/Body/ThirdPersonWeapon”
@onready var barrel = $Head/Cameras/FirstPerson/Barrel
@onready var camera = $Head/Cameras
var bullet_trail = load(“res://Player/pellet.tscn”)
var bodyrotation
var cam_mode = “First”

knockback

var is_local_player = false
var knockback_velocity := Vector3.ZERO
var knockback_damping := 2.0
var cam_rot_x := 0.0 # vertical rotation (pitch)

====== NETWORKED ======

@export var network_position: Vector3
@export var network_rotation: Vector3
@export var network_velocity: Vector3

====== READY / AUTHORITY ======

func _ready() → void:
refresh_weapons()
if is_multiplayer_authority() and get_tree().get_multiplayer().get_unique_id() == get_multiplayer_authority():
set_local_player()
_send_network_state()

else:
	set_remote_player()

func set_local_player():
is_local_player = true
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
if cameras_container:
first_person_cam.current = true
third_person_cam.current = false
set_process_input(true)
set_physics_process(true)
print(“Local player ready”)

func set_remote_player():
is_local_player = false
if cameras_container:
cameras_container.queue_free()
set_process_input(false)
set_physics_process(true)
print(“Remote player ready”)

====== INPUT / LOOK ======

func _unhandled_input(event: InputEvent) → void:
if not is_local_player:
return

if event is InputEventMouseMotion:
	# yaw
	rotate_y(-event.relative.x * SENSITIVITY)
	# pitch
	body.rotate_x(-event.relative.y * SENSITIVITY)
	body.rotation.x = clamp(body.rotation.x, deg_to_rad(-90), deg_to_rad(90))
	first_person_cam.rotation.x = body.rotation.x
	third_person_cam.rotation.x = body.rotation.x

func _input(event: InputEvent) → void:
if not is_local_player:
return

if Input.is_action_just_pressed("toggle_camera_mode"):
	match cam_mode:
		"First":
			cam_mode = "Third"
			third_person_cam.make_current()
		"Third":
			cam_mode = "First"
			first_person_cam.make_current()

if Input.is_action_just_pressed("None") and weapon != "None":
	weapon = "None"
	refresh_weapons()

if Input.is_action_just_pressed("Bomb") and weapon != "Bomb":
	weapon = "Bomb"
	refresh_weapons()

====== PHYSICS / MOVEMENT ======

func _physics_process(delta: float) → void:
if is_local_player:
# === LOCAL MOVEMENT ===
if wall_jump_timer > 0.0:
wall_jump_timer -= delta
else:
can_wall_jump = true

	# gravity
	if not is_on_floor():
		velocity += get_gravity() * delta

	# jump / wall jump
	if Input.is_action_just_pressed("jump"):
		if is_on_floor():
			velocity.y = JUMP_VELOCITY
			$Jump.play()
			$Jump.pitch_scale = 1.0
		elif can_wall_jump:
			var wall_normal = _check_wall()
			if wall_normal != Vector3.ZERO:
				_perform_wall_jump(wall_normal)
				$Jump.play()
				$Jump.pitch_scale = 1.5

	# input
	var input_dir := Input.get_vector("move_left", "move_right", "move_forwards", "move_back")
	var direction = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()

	# tilt
	tilt_amount = lerp(tilt_amount, -input_dir.x * max_tilt, delta * tilt_speed)
	head.rotation.z = tilt_amount

	# smooth movement
	if is_on_floor():
		if direction != Vector3.ZERO:
			$"Legs Model/AnimationPlayer".play("Walk")
			velocity.x = lerp(velocity.x, direction.x * SPEED, delta * 14.0)
			velocity.z = lerp(velocity.z, direction.z * SPEED, delta * 14.0)
		else:
			$"Legs Model/AnimationPlayer".play("Normal")
			velocity.x = 0
			velocity.z = 0
	else:
		$"Legs Model/AnimationPlayer".play("Normal")
		velocity.x = lerp(velocity.x, direction.x * SPEED, delta * 10.0)
		velocity.z = lerp(velocity.z, direction.z * SPEED, delta * 10.0)

	# headbob
	t_bob += delta * velocity.length() * float(is_on_floor())
	head.transform.origin = base_head_pos + _headbob(t_bob)

	# knockback
	if knockback_velocity.length() > 0.01:
		velocity.x += knockback_velocity.x * delta
		velocity.z += knockback_velocity.z * delta
		knockback_velocity = knockback_velocity.lerp(Vector3.ZERO, delta * knockback_damping)

	# FOV
	var horizontal_speed := Vector3(velocity.x, 0, velocity.z).length()
	var target_fov = default_fov + (max_fov - default_fov) * clamp(horizontal_speed / SPEED, 0, 1)
	first_person_cam.fov = lerp(float(first_person_cam.fov), float(target_fov), delta * fov_speed)
	third_person_cam.fov = lerp(float(third_person_cam.fov), float(target_fov), delta * fov_speed)


	# move & broadcast
	move_and_slide()
	_send_network_state()
else:
	if not is_local_player:
# Apply gravity
		if not is_on_floor():
			network_velocity += get_gravity() * delta
		else:
			network_velocity.y = 0
		
		# Move character based on network_velocity
		velocity = network_velocity
		move_and_slide()
		
		# Interpolate position & rotation toward networked state
		global_position = global_position.lerp(network_position, clamp(delta * 10.0, 0.0, 1.0))
		rotation = rotation.lerp(network_rotation, clamp(delta * 10.0, 0.0, 1.0))
if not is_local_player and network_update_received:
	print("Remote player network update received at ", network_position)

====== NETWORK RPC ======

@rpc(“any_peer”)
func update_network_state(peer_id: int, pos: Vector3, rot: Vector3):
if not is_local_player:
network_position = pos
network_rotation = rot
print(“Received network update from peer:”, peer_id, “pos:”, pos, “rot:”, rot)

func _send_network_state():
if is_local_player:
var peer = multiplayer.get_multiplayer_peer()
# Only send if this is a real network peer (ENet)
if peer and peer.get_class() != “OfflineMultiplayerPeer”:
rpc(“update_network_state”, global_position, rotation)

====== WEAPONS / UTIL ======

func refresh_weapons() → void:
if weaponsmanager and playermodelweaponmanager:
weaponsmanager._weapon_check()
playermodelweaponmanager._weapon_check()

func _weapon_idle():
match weapon:
“Shotgun”:
$“Chest Model/AnimationPlayer”.play(“Shotgun Idle”)
$Head/Cameras/FirstPerson/ViewModel/AnimationPlayer.play(“Shotgun Idle”)
“None”:
$“Chest Model/AnimationPlayer”.play(“Normal”)
$Head/Cameras/FirstPerson/ViewModel/AnimationPlayer.play(“Normal”)

---- WALL JUMP HELPERS ----

func _check_wall() → Vector3:
var space_state = get_world_3d().direct_space_state
var left_query = PhysicsRayQueryParameters3D.create(global_position, global_position + -head.global_transform.basis.x * WALL_DETECT_DISTANCE)
left_query.exclude = [self]
var left_result = space_state.intersect_ray(left_query)

var right_query = PhysicsRayQueryParameters3D.create(global_position, global_position + head.global_transform.basis.x * WALL_DETECT_DISTANCE)
right_query.exclude = [self]
var right_result = space_state.intersect_ray(right_query)

if left_result and left_result.has("normal"):
	return left_result.normal
elif right_result and right_result.has("normal"):
	return right_result.normal
return Vector3.ZERO

func _perform_wall_jump(normal: Vector3) → void:
can_wall_jump = false
wall_jump_timer = WALL_JUMP_COOLDOWN
velocity = normal * WALL_JUMP_FORCE
velocity.y = JUMP_VELOCITY * 0.9

---- HEADBOB ----

func _headbob(time) → Vector3:
return Vector3(cos(time * bob_frequency / 2) * bob_amplitude, sin(time * bob_frequency) * bob_amplitude, 0)

---- FOOTSTEP ----

func _on_footstep() → void:
footstep.pitch_scale = randf_range(0.9, 1.1)
footstep.volume_db = randf_range(0, 6.0)
if is_on_floor():
footstep.play()

func _on_animation_player_animation_finished(anim_name: StringName) → void:
_weapon_idle()

=== KNOCKBACK / EXPLOSION ===

func apply_explosion_force(origin: Vector3, force: float) → void:
var dir := (global_position - origin)
if dir.length() < 0.5:
dir = -transform.basis.z
dir.y = 0
dir = dir.normalized()

var horizontal_force = dir * max(force * 2.5, force * 2.0)
knockback_velocity += horizontal_force

var vertical_diff := global_position.y - origin.y
if vertical_diff > -2:
	velocity.y = max(velocity.y, force * 0.35)
elif vertical_diff < -0.1:
	velocity.y = min(velocity.y, -force * 0.25)

No sane person will help you with your problem when your code is formatted the way it is.

Please edit your post by formatting the pasted code snippet with ``` on both sides of each code snippet. You can click the </> button to make the text editor do it for you.

It should look like this:

func _ready():
	# Formatted code snippet example

Look at @gertkeno’s post: Forum code formatting guide

Ah thanks! I’m new to the forum so i didn’t know

You can edit your previous message to format it correctly.

Also you only “say its not behaving right”. Please elaborate on your issue. Whats supposed to happen? What do you believe is the problem?

I’ll make a new post soon since i fixed what i was posting about before but new issues have come up today.