Predicting States

Due to the unpredictability of the Internet inputs may drop or arrive late. Predicting states is a simple way to compensate for these events.

If your game is fast-paced and reaction based, even if not using physics, predicting states can be useful to predict the next inputs you'll get from the server before you actually get them.

Both the server and client can predict inputs.

The server may predict inputs to accommodate for clients with unreliable connections.

Clients would predict inputs on objects they do not own, or as we often call them "spectated objects".

By predicting inputs you can place objects in the future of where you know them to be, and even make them entirely real-time with the client. Keep in mind however, when predicting the future, if you guess wrong there will be a de-synchronization which may be seen as jitter when it's corrected in a reconcile.

Below is an example of a simple replicate method.

//What moveData does is irrelevant in this example.
//We're only interested in how to predict a future state.
[Replicate]
private void Move(MoveData md, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
{ 
    float delta = (float)base.TimeManager.TickDelta;
    transform.position += new Vector3(md.Horizontal, 0f, md.Vertical) * _moveRate * delta;
}

Before we go any further you must first understand what each ReplicateState is. These change based on if input is known, replaying inputs or not, and more. You can check out the ReplicateState API which will explain thoroughly. You can also find this information right in the source of FishNet.

If you've read through ReplicateState and do not fully understand them please continue reading as they become more clear as this guide progresses. You can also visit us on Discord for questions!

Covered in the ReplicateStates API: CurrentCreated will only be seen on clients if they own the object. When inputs are received on spectated objects clients run them in the reconcile, which will have the state ReplayedCreated. Clients will also see ReplayedFuture and CurrentFuture on spectated objects.

A state ending in 'Future' essentially means input has not been received yet, and these are the states you could predict.

Let's assume your game has a likeliness that players will move in the same direction regularly enough. If a player was holding forward for three ticks the input would look like this...

(md.Vertical == 1)
(md.Vertical == 1)
(md.Vertical == 1)

But what if one of the inputs didn't arrive, or arrived late? The chances of inputs not arriving at all are pretty slim, but arriving late due to network variance is extremely common. If perhaps an input did arrive late the values may appear as something of this sort...

(md.Vertical == 1)
(md.Vertical == 1)
(md.Vertical == 0) //Didn't arrive here, but will arrive late next tick.
(md.Vertical == 1) //This was meant to arrive the tick before, but arrived late.

Because of this interruption the player may seem to move forward twice, pause, then forward again. Realistically to help cover this up you will have interpolation on your graphicalObject as shown under the prediction settings for NetworkObject. The PredictionManager also offers QueuedInputs which can give you even more of a buffer. For the sake of this guide though we're going to pretend both of those didn't get the job done, and you need to account for the late input.

Below is a simple way to track and use known inputs to create predicted ones.

private MoveData _lastCreatedInput = default;

[Replicate]
private void Move(MoveData md, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
{ 
    //If inputs are not known. You could predict
    //all the way into CurrentFuture, which would be
    //real-time with the client. Though the more you predict
    //in the future the more you are likely to mispredict.
    if (state == ReplicateState.ReplayedFuture)
    {
        uint lastCreatedTick = _lastCreatedInput.GetTick();
        //If it's only been 1 tick since the last created
        //input then run the logic below.
        //This essentially means if the last created tick
        //was 100, this logic would run if the future tick was 101
        //but not beyond that.
        uint thisTick = md.GetTick();
        if ((tickTick - lastCreatedTick) <= 1)
        {
            md = _lastCreatedInput;
            //Be sure to set the tick back to what it was even if in the future.
            md.SetTick(thisTick);
        }
    }
    //If created data then set as lastCreatedInput.
    else if (state == ReplicateState.ReplayedCreated)
    {
        //If MoveData contains fields which could generate garbage you
        //probably want to dispose of the lastCreatedInput
        //before replacing it. This step is optional.
        _lastCreatedInput.Dispose();
        //Assign newest value as last.
        _lastCreatedInput = md;
    }
    
    float delta = (float)base.TimeManager.TickDelta;
    transform.position += new Vector3(md.Horizontal, 0f, md.Vertical) * _moveRate * delta;
}

You can also use some of our built in extensions to simply the process.

EG: instead of checking for states ReplicateState.ReplayedFuture and ReplicateState.ReplayedCreated you can simply do:

state.IsFuture() or state.IsCreated().

This is example is pretty basic. In an actual project you're going to have fields other than movement in your MoveData, some which you probably do not want to future predict.

For example, if you had a Jump boolean that was true one replicate, it's probably not going to be true the next. If you were to set all the fields to exactly as they were in the last created input you're probably going to have some unwanted behavior. Overall though, the process is pretty simple.

You'll also probably want to do clean-up as needed, such as dispose of and perhaps clear references of _lastCreatedInput during the NetworkBehaviour stop callbacks.

Last updated