3
\$\begingroup\$

Preface

I have been trying to find out "How to make Client-side Prediction and Server Reconciliation" from scratch with an easy-to-understand code in C#. So I decided to make my own implementation of Client-side Prediction and Server Reconciliation. The Client-side Prediction and Server Reconciliation system I want to make is one that everyone can use. I'd appreciate if you could discuss with me to find the a better way or a way that everyone would understand.

Original Files

Client Side: PlayerController.cs, ClientSend.cs and ClientHandler.cs Tom Weiland GitHub Link.
Server Side: Server.cs, ServerHandler.cs and ServerSend.cs: Tom Weiland GitHub Link.
The link that I read to try to make my own implementation: spectre1989 GitHub Link

Code

First off, the spectre1989 code has server and client in the same code and Tom Weiland doesn't, so I had to adapt the code. Secondly, spectre1989 code uses a type of tick, but since the server and client are in the same code, the tick in the client and server starts to count at the same time and because of that the tick works, but in the Tom Weiland code, the client and the server initiate in different time, because they are separated, so a tick system would not work (or could, but I don't now how to achieve this), so I made it use the current time instead.

PlayerController.cs

private void Update()
    {       
        float dt = Time.fixedDeltaTime;
        float client_timer = this.client_timer; 
        
        client_timer += Time.deltaTime;
        
        if (client_state_buffer.Any()) {client_state_buffer = client_state_buffer.Distinct().ToList();}
        if (client_state_msgs.Any()) {client_state_msgs = client_state_msgs.Distinct().ToList();}

        if (client_state_buffer.Any())
        {
            foreach (PlayerState state in client_state_buffer)
            {               
                DateTime newDateTime = new DateTime();
                newDateTime = newDateTime + (DateTime.Now - state.time);
                if (newDateTime > Convert.ToDateTime("00:00:30"))
                {
                    client_state_buffer.Remove(state);
                }
            }
        }
        
        if (client_state_msgs.Any())
        {
            foreach (PlayerState state in client_state_msgs)
            {
                DateTime newDateTime = new DateTime();
                newDateTime = newDateTime + (DateTime.Now - state.time);
                if (newDateTime > Convert.ToDateTime("00:00:30"))
                {
                    client_state_msgs.Remove(state);
                }
            }
        }   

        while(client_timer >= dt)
        {           
            client_timer -= dt;
            
            bool[] PlayerInput = CheckForInput();
            
            DateTime TimeNow = Convert.ToDateTime(DateTime.Now.ToString("HH:mm:ss"));
            
            InputState state = new InputState();
            state.time = TimeNow;
            state.inputs = PlayerInput;
            
            if (!client_input_buffer.Contains(state) && PlayerInput.Contains(true)) {client_input_buffer.Add(state);}
            
            PlayerState playerState = new PlayerState();
            playerState.position = controller.gameObject.transform.position;
            playerState.rotation = controller.gameObject.transform.rotation;
            playerState.time = TimeNow;

            Move(CalculateInputDirection(PlayerInput), controller, PlayerInput);        
            
            if (!client_state_buffer.Contains(playerState) && PlayerInput.Contains(true)) {client_state_buffer.Add(playerState);}       
            
            if (PlayerInput.Contains(true)) {ClientSend.PlayerMovement(state);}
        }       
        
        this.client_timer = client_timer;       
            
        while (ClientHasStateMessage(client_state_msgs))
        {
            PlayerState server_state = client_state_msgs.OrderByDescending(x => x.time).FirstOrDefault();   
            PlayerState prev_state = client_state_buffer.OrderByDescending(x => x.time).FirstOrDefault();
            
            if (server_state.time <= prev_state.time)
            {           
                Vector3 position_error = Vector3.zero;
                float rotation_error = 0f;
                
                position_error = server_state.position - prev_state.position;
                rotation_error = 1.0f - Quaternion.Dot(server_state.rotation, prev_state.rotation);
                
                if (position_error.sqrMagnitude > 0.0000001f || rotation_error > 0.00001f)
                {
                    gameObject.transform.position = server_state.position;
                    gameObject.transform.rotation = server_state.rotation;
                }
                
                client_state_msgs.Remove(server_state);
                client_state_buffer.Remove(prev_state);
            }
            else
            {               
                client_state_msgs.Remove(server_state);
            }
        }
    }

ClientSend.cs

public static void PlayerMovement(PlayerController.InputState state)
    {
        using (Packet _packet = new Packet((int)ClientPackets.playerMovement))
        {
            _packet.Write(state.time.ToString());
            
            _packet.Write(state.inputs.Length);
            foreach (bool input in state.inputs)
            {
                _packet.Write(input);
            }

            SendUDPData(_packet);
        }
    }

ClientHandle.cs

public static void PlayerPosition(Packet _packet)
    {
        int _id = _packet.ReadInt();
        
        DateTime time = Convert.ToDateTime(_packet.ReadString());
        Vector3 _position = _packet.ReadVector3();      
        
        Quaternion _rotation = _packet.ReadQuaternion();
        
        PlayerController.PlayerState state = new PlayerController.PlayerState();
        
        state.time = time;
        state.position = _position;
        state.rotation = _rotation;

        if (GameManager.players.TryGetValue(_id, out PlayerManager _player))
        {
            if (_player.gameObject.GetComponent<PlayerController>() != null)
            {
                _player.gameObject.GetComponent<PlayerController>().client_state_msgs.Add(state);
            } 
            else
            {               
                _player.transform.position = _position;
            }
        }
    }

ServerHandle.cs

public static void PlayerMovement(int _fromClient, Packet _packet)
    {
        string time = _packet.ReadString();
        bool[] inputs = new bool[_packet.ReadInt()];
        
        for (int i = 0; i < inputs.Length; i++)
        {
            inputs[i] = _packet.ReadBool();
        }           
        
        Server.InputState state = new Server.InputState();
        state.time = time;
        state.inputs = inputs;
        Server.SimulatePlayerInput(state, _fromClient);
    }

ServerSend.cs

public static void PlayerPosition(Player _player, Server.PlayerState state_msg)
    {
        using (Packet _packet = new Packet((int)ServerPackets.playerPosition))
        {
            _packet.Write(_player.id);          
            _packet.Write(state_msg.time);
            _packet.Write(state_msg.position);
            _packet.Write(state_msg.rotation);

            SendUDPDataToAll(_packet);
        }
    }

Server.cs

public static void SimulatePlayerInput(InputState state, int _fromClient)
    {       
        MovePlayer(CalculateInputDirection(state.inputs), clients[_fromClient].player, state.inputs);                       
                
        PlayerState state_msg = new PlayerState();
        state_msg.time = DateTime.Now.ToString("HH:mm:ss");
        state_msg.position = clients[_fromClient].player.gameObject.transform.position;
        state_msg.rotation = clients[_fromClient].player.gameObject.transform.rotation;
        
        ServerSend.PlayerPosition(clients[_fromClient].player, state_msg);      
    }
\$\endgroup\$
3
  • 3
    \$\begingroup\$ Welcome to Code Review ガブリエル Gabriel, I have edited your question to be more inline with the type of question Code Review would expect. Please be mindful that some of the language you used before can make your question sound off-topic so I'd recommend reading our help center. However if you think I've changed your question to something you don't want please feel free to overrule any edits I've made. \$\endgroup\$
    – Peilonrayz
    Sep 1 at 18:16
  • \$\begingroup\$ @Peilonrayz Its ok, Sorry for the inconvenience. \$\endgroup\$ Sep 1 at 18:33
  • \$\begingroup\$ @Peilonrayz Why nobody wants to help me? \$\endgroup\$ Sep 6 at 0:20
0
\$\begingroup\$

A better way, in which way ?

I'd appreciate if you could discuss with me to find the a better way [...]

That's a tricky one. Make it easier to read, optimize it, make the maintenance later on easier ? The better way is way too vague to know how you want it better. What is better to your eyes ?

[...] or a way that everyone would understand.

This one is magic, please understand that. Who is everyone ? Your boss, the next student that will have to fix a bug in your software, your teacher, your manager, your CEO, the guy that cleans the office when everyone is gone ?

But since I like to discuss, let's discuss. I will talk about the points that I think are important :

  • Your code can be undesrtood by yourself in a year or two, when you will have forgotten about how you made it in the first place. That's not everyone, just you is a really good start.
  • Your code respects some standards you have in your company, your department or just with yourself. That will help on understanding your code by others. That includes general C# coding standards.
  • Your code can be documented easily if needed (and it should be needed...)

So I won't talk about optimization here, since your problem is not in my domain.

1. Comment you code

In all your code, there is not a single comment. This already goes against your claim to be understood by everyone. You are already limiting this to "anyone who can read and understand C#" (that generally excludes the CEO...).

There are two ways, to insert valuable comments :

  1. Above a method or a class, that's XML Documentation
  2. Inside the method.

XML Documentation is used by Intellisense in Visual Studio to help you. It is also used by software that build documentation packages.

To create it, add three slashes before a method. There you can input the method description and anything there is to know about it.

Inside the method you should put comments that will help understand your algorythm.

2. No long methods

Your update method is very long, and doing lots of stuff. Wouldn't it be better, for the sake of readability, to split this into smaller methods, each of them doing one little thing ?

3. Naming conventions consistency

There are at least a million different naming conventions. And a million debates that justify each of them. Some are good, some seem better. Truth is, it doesn't really matter.

The only thing that matters is to stick to one naming convention. Make it the same for all your code, your department, your company (usually the company tells you the convention)...

Here you declare this :

public static void SimulatePlayerInput(InputState state, int _fromClient).

Why is one parameter starting with a lower case and the next one with an underscore ? Maybe you have a good reason, maybe not. But I do not understand your naming convention from that.

4. BUGS

I'm sorry to disappoint you, but your code as stated will generate errors...

client_state_buffer.Remove(state);

This is in an iterator loop, so the moment this instruction will be reached an exception will be thrown because you cannot modify a collection that you are iterating through. You need to use Linq RemoveAll method.

5. The code

Let's analyse your Update method (I will leave it to you to adapt what I'm saying here for the rest):

You start by initializing variables you will only use in a while loop. We will move these along in a new method, with the loop.

if (client_state_buffer.Any()) (and the following ifs)

So basically, first you remove duplicates, then you remove messages older than 30 seconds. For messages and buffer... We could make a cleaning method, then call it twice, couldn't we ? Why ? Because Don't Repeat Yourself, that's why. The method would look like this :

/// <summary>
/// Remove duplicates and entries older than 30 seconds in a given List<PlayerState>.
/// </summary>
/// <param name="states">The list to cleanup</param>
private void CleanupStateList(List<PlayerState> states)
{
    if (!state.Any())
        return; //We leave early if state is empty

    //Remove duplicates
    states = states.Distinct().ToList();

    //Remove all items older that 30 seconds
    //See Linq RemoveAll method
    states.RemoveAll(state => (DateTime.Now - state.time) > Convert.ToDateTime("00:00:30"));
}

Then the start of our upload method is just two calls to this method...

The first while loop should go in a separate method. Since the timer variable you declared earlier is only used in this loop, it will declared in the new method. I won't discuss what is done here but rather how it is presented :

private void HandlePlayerInput()
{
    //I removed the dt variable, only used twice in a row... Unless Unity updates this variable between two iterations.
    //I name clientTimer differently from the instance variable, because it is dangerous.
    //If you forget the this keyword, you can get nasty bugs, hard to debug...
    float clientTimer = this.client_timer + Time.deltaTime;
    while(clientTimer >= Time.fixedDeltaTime)
    {           
        clientTimer -= Time.fixedDeltaTime;
        
        //[...]
        //The rest of the loop is untouched
    }       
    
    this.client_timer = clientTimer;
}

That leaves us with the last while loop, which again we put in a new method :

private void CheckClientStateMessages()
{
    //Just a copy/Paste of the whole while loop, as I don't discuss the algorythm.
}

All in all, your big huge Update method looks like this now :

private void Update()
{
    CleanupStateList(client_state_buffer);
    CleanupStateList(client_state_msgs);

    HandlePlayerInput();

    CheckClientStateMessages();
}

OK, I think you may have catched the philosophy here... Hope this helps.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.