Skip to content

GiovanniZambiasi/Client-Side-Prediction

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

45 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Client-Side Prediction framework

Made with Mirror for Unity

This repository contains a garbage free client-side prediction framework, implemented in two different example scenarios:

My goal is to develop an easy-to-use, garbage free framework to streamline the implementation of client-side prediction across any scenario that needs it. Though the current solution is still a work-in-progress, it works, and will hopefully avoid the headache of adding all the boilerplate code to all predictive clients your project needs.

What is client-side prediction?

Client-side prediction is a way to hide the issues of a bad internet connection. It'll stop input lag, and minimize the jitteriness of packet loss. For more information, I recommend this article.

DEMO

CharacterController

The CharacterController example features a simple controller that moves and has gravity applied. It can be extended quite easily though. I recorded a demo where I simulated 150ms of lag using clumsy, and then applied a packet loss rate of 2%. Here's the Video:
CC_Example
Note that the demo doesn't include any kind of entity interpolation, so the server's client just flickers in the client's screen when packet loss is enabled. The client's cube however is fully predictive.

Rigidbody

The rigidbody example scenario is still a work in progress. The bodies are simulated via the PhysicsScene.Simulate method. When not simulating, the client is moved to an empty scene created in runtime called "Idle". This implementation is simple and easy to understand, but it stops the clients interacting with one another. I recorded a demo where I simulated 150ms of lag using clumsy, and then applied a packet loss rate of 2%. Here's the Video:
RB_EXAMPLE
Note that the demo doesn't include any kind of entity interpolation, so the server's cube just flickers in the client's screen when packet loss is enabled. The client's cube however is fully predictive.

How to use

The developer needs to fulfill 5 steps to implement prediction:

1) Implement state

Create your state block. This block should represent a snapshot of your client at any given time (position, rotation, speed, for example).

The state must implement the INetworkedClientState interface:

public interface INetworkedClientState : IEquatable<INetworkedClientState>
{
        /// <summary>
        /// The tick number of the last input packet processed by the server
        /// </summary>
        uint LastProcessedInputTick { get; }
}

The only requirement is to create a uint field to store the last processed input tick. This is important because the prediction will set the client at that state, and then reprocess all inputs from that tick till the most recent tick. It's recommended to create your state as a struct to avoid heap allocations.

Example:

public struct RigidbodyState : IEquatable<RigidbodyState>, INetworkedClientState
{
        public uint LastProcessedInputTick => lastProcessedInputTick;
        
        public Vector3 position;
        public Vector3 velocity;
        public Vector3 angularVelocity;
        public Quaternion rotation;
        public uint lastProcessedInputTick;

        public RigidbodyState(Vector3 position, Vector3 velocity, Vector3 angularVelocity, Quaternion rotation, uint lastProcessedInputTick)
        {
            this.position = position;
            this.velocity = velocity;
            this.angularVelocity = angularVelocity;
            this.rotation = rotation;
            this.lastProcessedInputTick = lastProcessedInputTick;
        }

        public bool Equals(RigidbodyState other)
        {
            return position.Equals(other.position) && velocity.Equals(other.velocity) && 
                   angularVelocity.Equals(other.angularVelocity) && rotation.Equals(other.rotation);
        }

        public bool Equals(INetworkedClientState other)
        {
            return other is RigidbodyState __other && Equals(__other);
        }
}

2) Implement input

Create your input block. This block should contain:

  • Information about your input in a given tick
  • The amount of time the input is meant for (delta time)

Similarly to the state block, it's recommended to use structs for your input to avoid heap allocations.

The input must implement INetworkedClientInput

public interface INetworkedClientInput
{
        /// <summary>
        /// The amount of time the input was recorded for 
        /// </summary>
        float DeltaTime { get; }
        
        /// <summary>
        /// The tick in which this input was sent on the client
        /// </summary>
        uint Tick { get; }
}

The two required fields are:

  • DeltaTime: the amount of time that has passed between ticks when this input state was recorded. This is necessary because the client's application can be running at a framerate lower than the target framerate.
  • Tick: what the current tick number is. This is used by the server while sending states to the clients. The state will be stamped with the Tick number of the last processed input block. The client will then use the stamp to determine which input ticks to predict.

Both fields will be supplied by the NetworkedClient when recording state.

Example:

public struct RigidbodyInput : INetworkedClientInput
{
       public float DeltaTime => deltaTime;
       public uint Tick => tick;

       public Vector2 movement;
       public float deltaTime;
       public uint tick;
       
       public RigidbodyInput(Vector2 movement, float deltaTime, uint tick)
       {
           this.movement = movement;
           this.deltaTime = deltaTime;
           this.tick = tick;
       }
}

3) Implement the Messenger

The Messenger is a component that's really a workaround. Because Mirror doesn't support generic arguments in a NetworkBehaviour, the messaging part of the NetworkedClient needs to be encapsulated in a separate component.

The Messenger is an interface:

public interface INetworkedClientMessenger<TClientInput, TClientState>
        where TClientInput : INetworkedClientInput
        where TClientState : INetworkedClientState
{
        event System.Action<TClientInput> OnInputReceived;

        TClientState LatestServerState { get; }
        
        void SendState(TClientState state);

        void SendInput(TClientInput input);
}



To implement it, the developer needs to create a NetworkBehaviour that implements the interface, using their state and input blocks as the TCLientInput and TClientState generic parameters, respectivelly:

public class NetworkedRigidbodyMessenger : NetworkBehaviour, INetworkedClientMessenger<RigidbodyInput, RigidbodyState>



The SendState method should call an Rpc and send the provided state to the clients:

public void SendState(RigidbodyState state)
{
            RpcSendState(state);
}

[ClientRpc(channel = Channels.DefaultUnreliable)]       // It's recommended to use the unreliable channel, since this message will be sent frequently
void RpcSendState(RigidbodyState state)
{
            _latestServerState = state;
}



The SendInput method should call a Cmd and send the provided input to the server:

public void SendInput(RigidbodyInput input)
{
            CmdSendInput(input);
}

[Command(channel = Channels.DefaultUnreliable)]       // It's recommended to use the unreliable channel, since this message will be sent frequently
void CmdSendInput(RigidbodyInput state)
{
            //...
}



Lastly, the Messenger must implement an Action of the input type, and invoke it whenever an input message is received:

public void SendInput(RigidbodyInput input)
{
            CmdSendInput(input);
}

[Command(channel = Channels.DefaultUnreliable)]
void CmdSendInput(RigidbodyInput state)
{
            OnInputReceived?.Invoke(state);
}

4) Implement the Client

The Client is an abstract class that the developer needs to inherit from. It'll be the main part of your prediction. The Client is responsible for controlling the ticks, recording the state and processing the inputs. It'll also reference the Messenger in order to send the networked messages.

public abstract class NetworkedClient<TClientInput, TClientState> : MonoBehaviour, INetworkedClient 
        where TClientInput : INetworkedClientInput
        where TClientState : INetworkedClientState 



To implement the Client, the developer needs to inherit from NetworkedClient, using their state and input blocks as the TCLientInput and TClientState generic parameters, respectivelly:

public class NetworkedRigidbody : NetworkedClient<RigidbodyInput, RigidbodyState>



The Client has 3 abstract methods that need to be implemented: a) void SetState(TClientState): Responsible for setting the object to a particular tick:

public override void SetState(RigidbodyState state)
{
           _rigidbody.position = state.position;
           _rigidbody.velocity = state.velocity;
           _rigidbody.rotation = state.rotation;
           _rigidbody.angularVelocity = state.angularVelocity;
}



b) void ProcessInput(TClientInput): Responsible for applying the input to the client for an amount of time (DeltaTime provided in the input block):

public override void ProcessInput(RigidbodyInput input)
{
           var __force = new Vector3(input.movement.x, 0f, input.movement.y);
           __force *= _speed * input.deltaTime;
           _rigidbody.AddForce(__force, ForceMode.Impulse);
           
           _physicsScene.Simulate(input.deltaTime);
}



c) TClientState RecordState: Responsible for creating the state block at the current time:

protected override RigidbodyState RecordState(uint lastProcessedInputTick)
{
           return new RigidbodyState(_rigidbody.position, _rigidbody.velocity, _rigidbody.angularVelocity, _rigidbody.rotation, lastProcessedInputTick);
}

5) Implement the Prediction

The prediction is an abstract class that the developer needs to inherit from. It'll be responsible for the local client's behaviour. It does two things:

  • Record the input block
  • Predict the outcome of said input
public abstract class ClientPrediction<TClientInput, TClientState> : MonoBehaviour 
       where TClientInput : INetworkedClientInput
       where TClientState : INetworkedClientState



To implement, the developer needs to inherit from ClientPrediction, using their state and input blocks as the TCLientInput and TClientState generic parameters, respectivelly:

public class RigidbodyPrediction : ClientPrediction<RigidbodyInput, RigidbodyState>



The only method that needs to be implemented is TCLientInput GetInput(float deltaTime, uint currentTick);. The developer will be responsible for storing the deltaTime and currentTick values in the input block:

protected override RigidbodyInput GetInput(float deltaTime, uint currentTick)
{
            var __movement = new Vector2
            {
                x = Input.GetAxis("Horizontal"), 
                y = Input.GetAxis("Vertical")
            };

            return new RigidbodyInput(__movement, deltaTime, currentTick);
}

At every tick, the component will check if any new states have been received by the server. If so, the client will be set to that new state, and all recorded input packets from that point onwards will be re-simulated. This fulfills the client-side prediction.

Finally...

Once the 5 previous steps have been completed, all that's left to do is assemble your object in unity. For that, the developer will need to create a prefab which contains, at least:

  • The NetworkedClient component created at step 4
  • The ClientPrediction component created at step 5
  • The Messenger component created at step 3
  • A NetworkIdentity component

Prefab_Example_1
Then, the dependencies of NetworkedClient and ClientPrediction need to be fulfilled: Prefab_Example_2

About

Experiments with Client-Side prediction using unity and Mirror

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages