I need some help with multiplayer netcoding

i am working on a multiplayer game and i guess i am just not fully grasping it. i have some code but just cant get things to work. can anyone please look at it and help me identify whats wrong and how it works. i am using netcode for game objects. my thought process for what im trying to do right now is to get input from the client then send it to the server the server does the thing and tells the client to the thing also. this code isnt exactly what i would want but is the result of me just trying lots of things to try and get it.
sorry if its hard to read

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Netcode;

public class playermove : NetworkBehaviour
{
    public Transform playerBody;
    public Rigidbody rb;
    public float jumpForce = 1f;
    private Vector3 moveDirection;
    public float moveForce = 4f;
    public float lookSpeedX = 2.0f;  // Horizontal mouse sensitivity
    public float lookSpeedY = 2.0f;  // Vertical mouse sensitivity
    public float upperLimit = 80f;   // Upper limit for vertical look
    public float lowerLimit = -80f;
    public Camera playerCamera;
    private float xRotation = 0f;
    private Vector3 direction = Vector3.zero;

    void Start()
    {
        if (rb == null)
        {
            rb = GetComponent<Rigidbody>();
        }

        if (IsLocalPlayer)
        {
            Cursor.lockState = CursorLockMode.Locked;
            Cursor.visible = false;
        }
        else
        {
            playerCamera.gameObject.SetActive(false); // Disable the camera for other players
        }
    }

    void Update()
    {
        if (Input.GetKey(KeyCode.Escape))
        {
            playerCamera.gameObject.SetActive(true); // Enable the camera for the local player
            Cursor.visible = true;
            Cursor.lockState = CursorLockMode.None;
        }

        playerl();

    }

    void playerm()
    {
        if (IsLocalPlayer)
        {
            direction = Vector3.zero;

            // Handle jumping
            if (Input.GetKey(KeyCode.Space) && IsGrounded())
            {
                // Call the server-side function to apply the jump
                RequestJumpServerRpc();
            }

            // Handle movement (WASD keys)
            if (Input.GetKey(KeyCode.W)) direction += transform.forward * moveForce;  // Move forward
            if (Input.GetKey(KeyCode.A)) direction += transform.right * -moveForce;   // Move left
            if (Input.GetKey(KeyCode.S)) direction += transform.forward * -moveForce; // Move backward
            if (Input.GetKey(KeyCode.D)) direction += transform.right * moveForce;   // Move right

            direction = direction.normalized;

            if (direction != Vector3.zero)
            {


                playermoveServerRpc(direction);
            }
            else
            {
                // No movement input, stop the player's movement locally
                playerstopServerRpc(); // Keep the y velocity (for jumping/falling)
            }
        }
    }

    private bool IsGrounded()
    {
        return Physics.Raycast(transform.position, Vector3.down, 1.1f);
    }

    // Jumping logic (ServerRPC and ClientRPC for synchronized jumping)
    [ServerRpc]
    void RequestJumpServerRpc()
    {
        JumpClientRpc();
    }

    [ClientRpc]
    void JumpClientRpc()
    {
        if (IsLocalPlayer)
        {
            rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
        }
    }

    [ServerRpc]
    void playermoveServerRpc(Vector3 direction)
    {
        if (IsLocalPlayer)
        {

            playermoveClientRpc(direction);
        }

    }
        [ClientRpc]
        void playermoveClientRpc(Vector3 direction)
        {


            rb.AddForce(direction * moveForce, ForceMode.Force);


        }

        [ServerRpc]
        void playerstopServerRpc()
        {
            if (IsLocalPlayer)
            {
                playerstopClientRpc();
            }
        }

        [ClientRpc]
        void playerstopClientRpc()
        {


            rb.velocity = new Vector3(0, rb.velocity.y, 0);

        }







        // Handle player camera and mouse input for looking around
    void playerl()
    {
       if (IsLocalPlayer)
       {
                float mouseX = Input.GetAxis("Mouse X");
                float mouseY = Input.GetAxis("Mouse Y");

                // Rotate the camera horizontally based on the mouseX input
                transform.Rotate(Vector3.up * mouseX * lookSpeedX);

                // Rotate the camera vertically based on the mouseY input, but clamp the angle to prevent flipping
                xRotation -= mouseY * lookSpeedY;
                xRotation = Mathf.Clamp(xRotation, lowerLimit, upperLimit);

                // Apply the vertical rotation to the camera
                Camera.main.transform.localRotation = Quaternion.Euler(xRotation, 0f, 0f);
            
       }
    }
}

You didn’t say what’s wrong. :wink:

But the general setup is off. For one, you want to handle input on the client-side. Nobody likes pressing the jump button and it takes 100 ms for it to execute, but sometimes it even takes 300 ms because of latency jitter. Input needs to be instant, at most you could verify if the input action is allowed on the server side and if not, the server informs the client to fix its position. Use NetworkTransform to sync position/rotation.

Secondly, we don’t do character controllers with dynamic physics. This is an advanced topic even for singleplayer games but it gets absurdly challenging to implement over the network to the point where it’s out of reach for 99% of developers (I wouldn’t even dare to go there).

Be sure to implement the character controller using kinematic physics, effectively calculating transform.position and .rotation to the values they should have on every Update. Unity has a free and highly customizable Kinematics Controller although it’s not necessarily beginner’s material, it’s worth checking out to see whether you like it or not (the default player is quite good already).

Lastly, the code looks like it’s coming from some archaic, simplistic beginner’s tutorial from many many years ago (they still repeat it to this day I’m afraid). A few things worth mentioning:

rb is always going to be null in Start, no need to check that.

This is not guaranteed to be set in Start. You need to implement OnNetworkSpawn and perform the check here as best practice. See this table here.

Generally speaking, I’d advise to split scripts based on their role. Effectively create a LocalPlayerMove and RemotePlayerMove component. Then, in OnNetworkSpawn, activate the corresponding script like so:

// LocalPlayerMove script:
void OnNetworkSpawn()
{
    base.OnNetworkSpawn();
    enabled = IsLocalPlayer;
}

This guarantees that anything in this particular script will only execute for the local player, with the exception of Awake/Start methods. Because sooner rather than later you will get gray hairs or lose some when the code is intertwined with if (IsLocalPlayer) and if (!IsLocalPlayer || IsOwner && IsServer) and so on. These checks will quickly become major brainf*cks so you want to make that distinction only once as early as possible and keep the server/owner/local code in separate scripts for best clarity of mind.

If you do want the server to handle input, you should send the input (eg W and D pressed) and have the server figure out the direction. This spares you sending 3x floats (12 bytes plus overhead) where otherwise you could send 4 bits in a single byte, but more straightforward just a bool[]. It also disallows the player from cheating an absurd movement speed if that’s important to you.

We now just use the [Rpc] attribute which provides greater flexibility.

That check is likely nonsensical. Keep in mind that every ServerRpc method only runs on the server (host). On the server side, the IsLocalPlayer will only be true for the host player, or not at all if it’s a dedicated server. So if a client player sends this ServerRpc it will simply be ignored.

Yea this code is a bit sloppy in areas as i have been making changes to it for 4 hours trying to see if i can get it working so there are lots of “idk man this might work” iva had the issue change many times but i e had it come down to idk how this stuff works. This is my first multiplayer project and my second actual unity project. Ive been working on unity for like half a year but i have experience from godot so i got the main stuff pretty quickly. I just feel like theres something with the communication between the server and client im not getting or something with the differentiation of the players i dont understand as common issues i had were things doing nothing or one the host and clients inputs effecing each other. I well be going through and trying things again on Tuesday.

I would say that is too early to jump into multiplayer. I did the same early in my development career and just ended up frustrated. Now 5 years later I have come back to multiplayer and it makes way more sense. You have to be very solid on foundational concepts before tackling it, either that or have a very limited scope.

If your just trying to gain knowledge then grab some ready built examples and pick them apart or follow some tutorials. If your dead set on making a multiplayer game, I would suggest doing something turn based to build your knowledge and confidence, with less frustration. Its not the kind of thing you can go in blind and figure it out.

Yea i agree it is very early but i wanted a break from my main project and wanted to challenge myself. All i want to do is make a flat plate with 2 first person players that can walk. I don’t want more than that. I dont plan on adding any major gameplay or anything. I mean i like the idea of adding a multiplayer to my main project but that would be long in the future.

Its always good to challenge yourself :+1:

I would grab a sample from whatever networking solution you have chosen and try to tweak it to meet your goal. Then you can dig into the code and see how it works. Just remember, samples are not always using best practices, they are meant to be quick to setup and easier to understand. There are some examples out there that are using best practices (Photon Fusion BR200 comes to mind) and it’s really challenging to even understand how things work.

Thanks for the suggestion i will try that out next week.

I recommend doing some artificial, isolated, technical tests just to get a grip on things. One thing at a time. As soon as you do two or three things at once (eg input + physics + rpcs) you can quickly end up in a spot where one thing affects the other and it becomes difficult to make out which is doing what.

In that case, focus on the RPC sends and receives and verify client and host run them as expected. Then add some input processing to directly manipulate an object. Then add physics or whatever, and so on. Create the low-level things first (eg connection, spawn, RPCs, net vars) and make sure they work flawlessly before moving to higher level things (eg input, physics) and then the yet even higher level things (animation, vfx, gui).

Because normally we tend to do the opposite - get the player moving and animated with sound, then try to network that. That’s much more gruesome to work with. Network games need a solid foundation first and foremost.

Do you have experience in net coding?

Nop. First time.

1 Like

oh okey. Got it. I’m also for the first time.

ok so i took a step back and simplified things. i have 2 beans that can both jump and cant move or look. with one camera looking on the plane.

here is my code

public class playermove : NetworkBehaviour
{
    public Transform playerBody;
    public Rigidbody rb;
    public float jumpForce = 1f;
    private Vector3 moveDirection;
    public float moveForce = 4f;
    public float lookSpeedX = 2.0f;  // Horizontal mouse sensitivity
    public float lookSpeedY = 2.0f;  // Vertical mouse sensitivity
    public float upperLimit = 80f;   // Upper limit for vertical look
    public float lowerLimit = -80f;
    public Camera playerCamera;
    private float xRotation = 0f;
    private Vector3 direction = Vector3.zero;

    void Start()
    {
        if (IsLocalPlayer)
        {
            Cursor.lockState = CursorLockMode.Locked;
            Cursor.visible = false;
        }
    }

    void Update()
    {
        if (Input.GetKey(KeyCode.Escape))
        {
            playerCamera.gameObject.SetActive(true); // Enable the camera for the local player
            Cursor.visible = true;
            Cursor.lockState = CursorLockMode.None;
        }
     playerm();
    }

    void playerm()
    {
        if (IsLocalPlayer)
        {
            // Handle jumping
            if (Input.GetKey(KeyCode.Space) && IsGrounded())
            {
                // Call the server-side function to apply the jump
                RequestJumpServerRpc();
            }
        }
    }

    private bool IsGrounded()
    {
        return Physics.Raycast(transform.position, Vector3.down, 1.1f);
    }


    [ServerRpc]
    void RequestJumpServerRpc()
    {
        rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
        JumpClientRpc();
    }

    [ClientRpc]
    void JumpClientRpc()
    {
        if (IsLocalPlayer)
        {
            rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
        }
    }
}

the one issue i am having an issue with the syncing as i thought. i have the input detected by the client which tells the server to jump which then tells the client to jump, right. it almost works, i client works and jumps on the hosts screen and clients screen, however the host jump doesn’t happen on the clients screen. also smaller thing, the hosts jump seems to be like half as strong. i would mess around more but work is going to be busy so might not get much time to. do you know why this stuff is happoning.

You actually make the object jump both on the server (host) and the client by applying the force to both.

Are you using NetworkTransform and NetworkRigidbody to sync these objects? If you don’t then on the host the force will be added twice since it will add the force acting as the server and then add the force again acting as the client. Whereas for the client it will work fine for both host (adds server-side force) and client (adds client-side force).

I recommend using NetworkTransform and scrap the use of a dynamic rigidbody as I said earlier because networked (non-kinematic) physics is challenging even for experts.