Getting Started

This guide assumes you understand prediction version 1 already. Many instances of this guide will reference the version 1 guide. A better guide targeted to absolutely new prediction developers will be available in the future.

Example Script

Much like Prediction V1 you will create a Replicate and Reconcile method. However, in version 2 you will find these methods are easier to implement and understand, as well more flexible.

First we're going to create our datas. There's a good chance you will not need to change these compared to any V1 data containers you've already written.

Below are containers for replication and reconciliation of a rigidbody.

public struct MoveData : IReplicateData
{
    public bool Jump;
    public float Horizontal;
    public float Vertical;
    public MoveData(bool jump, float horizontal, float vertical)
    {
        Jump = jump;
        Horizontal = horizontal;
        Vertical = vertical;
        _tick = 0;
    }

    private uint _tick;
    public void Dispose() { }
    public uint GetTick() => _tick;
    public void SetTick(uint value) => _tick = value;
}

public struct ReconcileData : IReconcileData
{
    //As of 4.1.3 you can use RigidbodyState to send
    //the transform and rigidbody information easily.
    public RigidbodyState RigidbodyState;
    //As of 4.1.3 PredictionRigidbody was introduced.
    //It primarily exists to create reliable simulations
    //when interacting with triggers and collider callbacks.
    public PredictionRigidbody PredictionRigidbody;
    
    public ReconcileData(PredictionRigidbody pr)
    {
        RigidbodyState = new RigidbodyState(pr.Rigidbody);
        PredictionRigidbody = pr;
        _tick = 0;
    }

    private uint _tick;
    public void Dispose() { }
    public uint GetTick() => _tick;
    public void SetTick(uint value) => _tick = value;
}

Learn more about using PredictionRigidbody.

Like version 1, we're going to subscribe to OnTick and OnPostTick using the OnStart and OnStopNetwork callbacks. Replicates will be run in OnTick and Reconciles OnPostTick. Only rigidbodies need to use OnPostTick to reconcile after the physics simulation; non-rigidbody controllers can replicate and reconcile using OnTick.

public override void OnStartNetwork()
{
    base.TimeManager.OnTick += TimeManager_OnTick;
    base.TimeManager.OnPostTick += TimeManager_OnPostTick;
}

public override void OnStopNetwork()
{
    base.TimeManager.OnTick -= TimeManager_OnTick;
    base.TimeManager.OnPostTick -= TimeManager_OnPostTick;
}

In this example the rigidbodies can 'jump'. One-time inputs still must be collected in update, though this is something we're looking to improve the work flow on. Here's fields to get the script started.

[SerializeField]
private float _jumpForce = 15f;
[SerializeField]
private float _moveRate = 15f;

private PredictionRigidbody PredictionRigidbody { get; private set; } = new();
private bool _jump;

Similar to version 1 the jump input is collected in update.

private void Awake()
{
    PredictionRigidbody.Initialize(GetComponent<Rigidbody>());
}

private void Update()
{
    if (base.IsOwner)
    {
        if (Input.GetKeyDown(KeyCode.Space))
            _jump = true;
    }
}

OnTick will now be used to build our replicate data. Since this is a player controlled object only the owner needs to build the data. If not owner default is returned.

private void TimeManager_OnTick()
{
    Move(BuildMoveData());
}

private MoveData BuildMoveData()
{
    if (!base.IsOwner)
        return default;

    float horizontal = Input.GetAxisRaw("Horizontal");
    float vertical = Input.GetAxisRaw("Vertical");
    MoveData md = new MoveData(_jump, horizontal, vertical);
    _jump = false;

    return md;
}

Notice prediction V2 uses a different attribute and method signature for replication.

[Replicate]
private void Move(MoveData md, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
{
    /* ReplicateState is set based on if the data is new, being replayed, ect.
    * Visit the ReplicationState enum for more information on what each value
    * indicates. At the end of this guide a more advanced use of state will
    * be demonstrated. */
    Vector3 forces = new Vector3(md.Horizontal, 0f, md.Vertical) * _moveRate;
    PredictionRigidbody.AddForce(forces);

    if (md.Jump)
    {
        Vector3 jmpFrc = new Vector3(0f, _jumpForce, 0f);
        PredictionRigidbody.AddForce(jmpFrc, ForceMode.Impulse);
    }
    //Add gravity to make the object fall faster.
    PredictionRigidbody.AddForce(Physics.gravity * 3f);
    //Simulate the added forces.
    PredictionRigidbody.Simulate();
}

On non-owned objects a number of replicates will arrive as ReplicateState Created, but will contain default values. This is our TimeManager.RedundancyCount feature working.

This is normal and indicates that the client or server had gracefully stopped sending states as there is no new data to send. This can be useful if you are Predicting States.

As expected OnPostTick will be used to build a reconcile data and send it to clients.

private void TimeManager_OnPostTick()
{
    /* The base.IsServer check is not required but does save a little
    * performance by not building the reconcileData if not server. */
    if (IsServer)
    {
        ReconcileData rd = new ReconcileData(PredictionRigidbody);
        Reconciliation(rd);
    }
}

The Reconcile method also has a different signature and uses a different attribute.

[Reconcile]
private void Reconciliation(ReconcileData rd, Channel channel = Channel.Unreliable)
{
    //Sets state of transform and rigidbody.
    Rigidbody rb = PredictionRB.Rigidbody;
    rb.SetState(rd.RigidbodyState);
    //Applies reconcile information from predictionrigidbody.
    PredictionRB.Reconcile(rd.PredictionRB);
}

NetworkObject Inspector Changes

PredictedObject is not used in version 2. Previously, in version 1, PredictedObject would try to guess an objects future state based on position and velocities. Now in version 2 input and world states are forwarded to clients, having no need for PredictedObject. The example just above of more advanced replicates is a demonstration of how to custom implement future or predicted states.

To activate prediction you must enable Use Prediction. You will find some options disabled as the alternative to them are not yet complete.

As before you must set the Graphical Object.

Owner interpolation is over how many ticks to interpolate the graphics when you own the object. Typically a value of 1 is fine; this will begin moving the object as soon as the input is given by the owner.

Enable Teleport will allow you to set distances which an object's movement must exceed in order for the smoothing to teleport rather than occur over time.

Adaptive Interpolation and Interpolation are both disabled at this time. Adaptive Interpolation will be primarily used for non-owned objects which are affected heavily by physics. The adaptive setting will use runtime values to regularly calculate the best way to smooth objects to reduce desynchronization artifacts. Once available additional adaptive options will be revealed when enabled.

With Adaptive Interpolation disabled a set interpolation interval will be used, just like Owner Interpolation. A set interpolation is ideal for objects which are not at all, or lightly influenced by physics. Examples are character controllers, or rigidbody controllers that set velocity rather than carry force.

Networked Non-Controlled Physics

Many games will require physics bodies to be networked, even if not controlled by players or the server. These objects can also work along-side the new state system using the same information above.

The code will be very similar to our example above, only removing input fields on the MoveData.

//This structure is the same but with input fields removed.
public struct MoveData : IReplicateData
{
    private uint _tick;
    public void Dispose() { }
    public uint GetTick() => _tick;
    public void SetTick(uint value) => _tick = value;
}

private void TimeManager_OnTick()
{
    //Rather than collect input simply pass in default
    //when calling Move.
    Move(default);
}

Last updated