C# Multiplayer Help Syncing "Bullets"

Godot Version

Godot 4, C#

Question

For some reason I’m having tons of trouble understanding really what to do properly to use godot multiplayer in C#. I followed a few tutorials but almost damn near everything is in GDScript.

The problem right now is I can’t seem to easily sync my bombs and my text labels. My bombs are made with RPC calls and my text label is setup as the player is. When I add the text label to the multiplayer sync it doesn’t fix the issue and the tutorial I followed doesn’t do that. One thing it did do was setup the scene and all players all at the same time while Im trying to sync players who can join and leave freely.

Anyone know what might be wrong with my scenes or my code? Here are some visual examples and code to show you:
Godot_v4.1.1-stable_mono_win64_F7yL7ZJJkK
Code:
Player:

using Godot;

// useful: https://stackoverflow.com/questions/73161622/how-to-make-godot-character-jump
// also useful: https://github.com/finepointcgi/Basics-of-Multiplayer-Lan-Tutorial-CSharp/blob/master/Player.cs
public partial class Player : CharacterBody3D
{
    #region Fields

    [Export]
    public float ArcHeight { get; set; } = 1;

    [Export]
    public PackedScene Bomb { get; set; }

    [Export]
    public Sprite3D CursorSprite { get; set; }

    [Export]
    public float KickCooldown { get; set; } = 1.0f;

    [Export]
    public float ThrowPower { get; set; } = 5;

    private Camera3D _camera;
    private RigidBody3D _grabbedObject;
    private float _kickTimer = 0f; // Set the cooldown time in seconds
    private AnimatedSprite3D _sprite;
    private MultiplayerSynchronizer _synchronizer;
    private Vector3 _velocity = Vector3.Zero;
    private GravityComponent Gravity;
    private JumpComponent JumpFactor;
    private SpeedComponent Speed;

    #endregion Fields

    #region Methods

    private Vector3 lastMovedDirection = Vector3.Forward;

    public override void _EnterTree()
    {
        //base._EnterTree();
        GD.Print("Player ID: ", int.Parse(Name));
        SetMultiplayerAuthority(int.Parse(Name));
    }

    public override void _PhysicsProcess(double delta)
    {
        if (_synchronizer.GetMultiplayerAuthority() != Multiplayer.GetUniqueId()) return; // todo: custom properties?
        if (InputShortcuts.Jump) { Jump(); };
        if (InputShortcuts.Bomb) { Rpc("PlaceBomb", _velocity.Normalized()); };
        if (InputShortcuts.Throw) { Rpc("Throw"); };
        Gravity.Apply(this, ref _velocity, delta);
        Move();
    }

    public override void _Process(double delta)
    {
        if (_synchronizer.GetMultiplayerAuthority() != Multiplayer.GetUniqueId()) return; // todo: custom properties?
        HandleInput(delta);

        // Handle kick cooldown
        if (_kickTimer > 0)
            _kickTimer -= (float)delta;

        var mouse = GetMouse3DPosition();
        CursorSprite.GlobalPosition = mouse + (Vector3.Up / 2);
    }

    public override void _Ready()
    {
        _synchronizer = GetNode<MultiplayerSynchronizer>("MultiplayerSynchronizer");
        if (_synchronizer.GetMultiplayerAuthority() != Multiplayer.GetUniqueId()) return;
        _sprite = GetNode("Pivot").GetNode<AnimatedSprite3D>("Animation");
        _camera = GetNode("CameraPivot").GetNode<Camera3D>("Camera3D");
        Gravity = GetNode<GravityComponent>("Gravity");
        JumpFactor = GetNode<JumpComponent>("Jump");
        Speed = GetNode<SpeedComponent>("Speed");
        _camera.Current = true;
    }

    public void Kick()
    {
    }

    [Rpc(MultiplayerApi.RpcMode.AnyPeer, CallLocal = true)]
    public void PlaceBomb(Vector3 direction)
    {
        // todo: placing bomb should let playerr inside bomb but moving out should cock blockl them
        if (_kickTimer <= 0)
        {
            Node3D ins = Bomb.Instantiate() as Node3D;
            Vector3 pos = GetMouse3DPosition();
            Vector3 bombPosition = GlobalPosition + (pos - GlobalPosition).Normalized() * 3; // 3 is sudo radius
            // Set the bomb position and apply velocity
            GetTree().Root.GetNode("Main").AddChild(ins);
            Vector3 throwDirection = (pos - GlobalPosition).Normalized();
            ins.GlobalPosition = GlobalPosition + throwDirection + Vector3.Up;
            var bom = ins as Bomb;
            _kickTimer = KickCooldown;
        }
    }

    public void SetUpPlayer(string name)
    {
        GetNode<Label3D>("Tag").Text = name;
    }

    [Rpc(MultiplayerApi.RpcMode.AnyPeer, CallLocal = true)]
    public void Throw()
    {
        if (_kickTimer <= 0)
        {
            Node3D ins = Bomb.Instantiate() as Node3D;
            var bomb = ins as Bomb;
            Vector3 pos = GetMouse3DPosition();
            Vector3 throwDirection = (pos - GlobalPosition).Normalized();
            // Set the bomb position and apply velocity
            GetTree().Root.GetNode("Main").AddChild(ins);
            ins.GlobalPosition = GlobalPosition + throwDirection + Vector3.Up;
            bomb.Move(throwDirection, pos, ThrowPower, ArcHeight);

            //bomb.Terminus();
            _kickTimer = KickCooldown;
        }
    }

    private Vector3 GetMouse3DPosition()
    {
        var camera = GetNode("CameraPivot").GetNode<Camera3D>("Camera3D");
        var spaceState = GetWorld3D().DirectSpaceState;
        var cursorPosition = GetViewport().GetMousePosition();
        var from = camera.ProjectRayOrigin(cursorPosition);
        var to = camera.ProjectRayNormal(cursorPosition) * 2000; // Scale ray length for extended reach
        var rayParams = PhysicsRayQueryParameters3D.Create(from, from + to);
        var intersection = spaceState.IntersectRay(rayParams);
        if (intersection.Count > 0)
        {
            var position = (Vector3)intersection["position"];
            return position;
        }

        return Vector3.Zero;
    }

    private void Grab(RigidBody3D body)
    {
        _grabbedObject = body;
        _grabbedObject.GlobalTransform = GlobalTransform;
        _grabbedObject.LinearVelocity = Vector3.Zero;
        _grabbedObject.AngularVelocity = Vector3.Zero;
        _grabbedObject.CollisionLayer = 0;
        _grabbedObject.CollisionMask = 0;
        //_grabbedObject.Mode = RigidBody.ModeEnum.Kinematic;
        _grabbedObject.LinearDamp = 0;
        _grabbedObject.AngularDamp = 0;
        // _grabbedObject.LinearFactor = Vector3.Zero;
        //_grabbedObject.AngularFactor = Vector3.Zero;
        _grabbedObject.GravityScale = 0;
    }

    private void HandleInput(double delta)
    {
        Camera3D camera = GetNode<Camera3D>("CameraPivot/Camera3D");
        Vector3 camForward = -camera.GlobalTransform.Basis.Z;
        Vector3 camRight = camera.GlobalTransform.Basis.X;
        Vector3 direction = Vector3.Zero;
        if (InputShortcuts.Up) direction += camForward;
        if (InputShortcuts.Down) direction -= camForward;
        if (InputShortcuts.Left) direction -= camRight;
        if (InputShortcuts.Right) direction += camRight;
        if (direction != Vector3.Zero)
        {
            direction = direction.Normalized();
            GetNode<Node3D>("Pivot").LookAt(Position - direction, Vector3.Up);
            lastMovedDirection = new Vector3(direction.X, direction.Y, direction.Z);

            if (Mathf.Abs(direction.X) > Mathf.Abs(direction.Z))
            {
                _sprite.Play(direction.X > 0 ? "walk_right" : "walk_left");
            }
            else
            {
                _sprite.Play(direction.Z > 0 ? "walk_down" : "walk_up");
            }
        }
        else
        {
            if (Mathf.Abs(lastMovedDirection.X) > Mathf.Abs(lastMovedDirection.Z))
            {
                _sprite.Play(lastMovedDirection.X > 0 ? "idle_right" : "idle_left");
            }
            else
            {
                _sprite.Play(lastMovedDirection.Z > 0 ? "idle_down" : "idle_up");
            }
        }

        JumpFactor.ProcessCoyoteTime(this);
        UpdateVelocity(direction, (float)delta);
    }

    private void Jump()
    {
        if (JumpFactor.CanJump)
        {
            JumpFactor.Apply(ref _velocity, Gravity);

            JumpFactor.ResetCantJump();
        }
    }

    private void Move()
    {
        Velocity = _velocity;
        //GD.Print("Velocity: " + Velocity);
        MoveAndSlide();
    }

    private void Release(RigidBody3D body)
    {
        if (_grabbedObject == null)
            return;

        _grabbedObject.CollisionLayer = 1;
        _grabbedObject.CollisionMask = 1;
        //_grabbedObject.Mode = RigidBody.ModeEnum.Rigid;
        _grabbedObject.LinearDamp = 0.1f;
        _grabbedObject.AngularDamp = 0.1f;
        //_grabbedObject.LinearFactor = Vector3.One;
        //_grabbedObject.AngularFactor = Vector3.One;
        _grabbedObject.GravityScale = 1;

        var playerVelocity = _velocity;
        var throwForce = playerVelocity * 2;
        _grabbedObject.ApplyCentralImpulse(throwForce);

        _grabbedObject = null;
    }

    private void UpdateVelocity(Vector3 direction, float delta)
    {
        float targetSpeed = direction.Length() > 0 ? Speed.Speed : 0f;
        _velocity.X = Mathf.Lerp(_velocity.X, direction.X * targetSpeed * delta, 0.1f);
        _velocity.Z = Mathf.Lerp(_velocity.Z, direction.Z * targetSpeed * delta, 0.1f);
    }

    #endregion Methods
}

Multiplayer Manager:

using Godot;
using System;
using System.Collections.Generic;
using System.Linq;

// Reference: https://godotengine.org/article/multiplayer-in-godot-4-0-scene-replication/
public partial class MultiplayerManager : Node
{
    public static List<PlayerInfo> Players = new List<PlayerInfo>();

    [Export]
    public PackedScene playerScene;

    private static int index = 0;

    [Export]
    private string host = "localhost";

    private bool isClient = false;

    private bool isServer = false;

    private ENetMultiplayerPeer peer = new ENetMultiplayerPeer();

    [Export]
    private int port = 135;

    public override void _Ready()
    {
        Multiplayer.PeerConnected += Multiplayer_PeerConnected;
        Multiplayer.PeerDisconnected += Multiplayer_PeerDisconnected;
        Multiplayer.ConnectedToServer += Multiplayer_ConnectedToServer;
        Multiplayer.ConnectionFailed += Multiplayer_ConnectionFailed;
    }

    [Rpc(MultiplayerApi.RpcMode.AnyPeer, CallLocal = true, TransferMode = MultiplayerPeer.TransferModeEnum.Reliable)]
    public void AddPlayers()
    {
        GD.Print("Adding players");
        Players.ForEach(item =>
        {
            if (GetNodeOrNull<Player>(item.Id.ToString()) != null)
            {
                return;
            }
            GD.Print("Adding player: ", item.Name);
            Player currentPlayer = playerScene.Instantiate<Player>();
            currentPlayer.Name = item.Id.ToString();
            currentPlayer.SetUpPlayer(item.Name);
            AddChild(currentPlayer);
            foreach (Node3D spawnPoint in GetTree().GetNodesInGroup("PlayerSpawner"))
            {
                if (int.Parse(spawnPoint.Name) == index)
                {
                    GD.Print("Spawning player at: ", spawnPoint.GlobalPosition);
                    currentPlayer.GlobalPosition = spawnPoint.GlobalPosition;
                }
            }
            index++;
        });
    }

    public void CreateClient()
    {
        if (!isClient)
        {
            // if is server kill server
            if (isServer) { return; }
            var result = peer.CreateClient(host, port);
            if (result == Error.Ok)
            {
                isClient = true;
                peer.Host.Compress(ENetConnection.CompressionMode.RangeCoder);
                Multiplayer.MultiplayerPeer = peer;
            }
            else
            {
                GD.Print("Client failed to connect");
                return;
            }
        }
    }

    public void CreateServer()
    {
        if (!isServer)
        {
            // if is client kill client
            if (isClient) { return; }
            var result = peer.CreateServer(port, 4);
            if (result == Error.Ok)
            {
                isServer = true;
                peer.Host.Compress(ENetConnection.CompressionMode.RangeCoder);
                Multiplayer.MultiplayerPeer = peer;
            }
            else
            {
                GD.Print("Server failed to start");
                return;
            }
        }
    }

    [Rpc(MultiplayerApi.RpcMode.AnyPeer)]
    public void SendPlayerInfo(string name, int id)
    {
        PlayerInfo playerInfo = new PlayerInfo()
        {
            Name = name,
            Id = id
        };

        if (!Players.Contains(playerInfo))
        {
            Players.Add(playerInfo);
        }

        if (Multiplayer.IsServer())
        {
            foreach (var item in Players)
            {
                Rpc("SendPlayerInfo", item.Name, item.Id);
            }
        }
    }

    private void Multiplayer_ConnectedToServer()
    {
        GD.Print("Connected to server");
        var root = GetTree().Root.GetNode("Main");
        GD.Print("Root: ", root);
        var hud = root.GetNode("HUD");
        GD.Print("HUD: ", hud);
        var name = hud.GetNode<LineEdit>("EnterName");
        GD.Print("Name: ", name);
        RpcId(1, "SendPlayerInfo", name.Text, Multiplayer.GetUniqueId());
        Rpc("AddPlayers");
    }

    private void Multiplayer_ConnectionFailed()
    {
        GD.Print("Connection failed");
    }

    private void Multiplayer_PeerConnected(long id = 1)
    {
        GD.Print("Peer connected: ", id);
    }

    private void Multiplayer_PeerDisconnected(long id)
    {
        GD.Print("Peer disconnected: ", id);
        Players.RemoveAll(p => p.Id == id);
        var players = GetTree().GetNodesInGroup("Players");
        players.ToList().ForEach(p => { if (p.Name == id.ToString()) p.QueueFree(); });
    }
}

HUD:

using Godot;
using System;

public partial class HUD : Control
{
    private Button _host;
    private Button _join;

    // Called when the node enters the scene tree for the first time.
    public override void _Ready()
    {
        _host = GetNode<Button>("Host");
        _join = GetNode<Button>("Join");

        _host.Pressed += _host_Pressed;
        _join.Pressed += _join_Pressed;
    }

    private void _host_Pressed()
    {
        var multiplayer = GetTree().Root.GetNode("Main") as MultiplayerManager;
        GD.Print("Hosting server");
        multiplayer.CreateServer();
        multiplayer.SendPlayerInfo(GetNode<LineEdit>("EnterName").Text, 1);
        multiplayer.Rpc("AddPlayers");
        this.Hide();
    }

    private void _join_Pressed()
    {
        var multiplayer = GetTree().Root.GetNode("Main") as MultiplayerManager;
        GD.Print("Joining server");
        multiplayer.CreateClient();
        this.Hide();
    }
}

PlayerInfo:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

public class PlayerInfo
{
    public int Id;
    public string Name;
    public int Score;
}

Here is some of the scenes setup:

I don’t have any experience with this system but is look like client don’t know about new instances of new objects.

This is actually the video I followed lol. My issue is I do it differently where I let the client join after the server starts.