Predictors

Summary

Shows how to customise entity movement prediction using predictors.

Predictor Assets

Predictors are used to predict or smooth entity motion. Predictors receive server transform and property updates, and control the transform and property values on the client. You can change the default predictors in the 'Default Predictors' section of the ksRoomType component. There are four predictor fields:

You can assign different predictors to an entity using the following fields in the ksEntityComponent inspector:

Some predictors, such as the ksConvergingInputPredictor, can only be used with player controllers and can only be assigned to the ksRoomType's 'Controller' field or the entity's 'Controller Predictor'.

Predictor types

ksLinearPredictor

ksLinearPredictor is the default predictor for entities without player controllers. It will linearly interpolate between each server position. If it reaches the last known server position before a new server position is received, it continues to linearly extrapolate. The linear predictor uses the ksTimeKeeper to determine which point in time to render the entity at. This ensures all entities using the linear predictor are rendered at the same point in time. The time keeper has some properties you can set to configure the behaviour of the linear predictor. Create a linear predictor asset whose properties you can edit by right-clicking in the project window and selecting 'Create->Reactor->Linear Predictor', or use the built-in 'DefaultLinearPredictor' asset.

Connect Script

    ...
    // Try to stay one sixtieth of a second behind the server when a new server frame arrives. Increasing this
    // value reduces the chance that we get ahead of the server and have to extrapolate which can lead to moving
    // entities momentarily penetrating other objects and popping out at the expense of increasing latency.
    // By default this is zero to have the minimum amount of latency.
    ksTimeKeeper timeKeeper = room.GetTimeAdjuster<ksTimeKeeper>();
    timeKeeper.TargetLatency = 1 / 60f;

    // Connect to the room
    room.Connect();
    ...

ksConvergingInputPredictor

ksConvergingInputPredictor is the default predictor for entities with player controllers that use input prediction. Accelerations are slower than on the server so that the client and server will converge. Create a converging input predictor asset whose properties you can edit by right-clicking in the project window and selecting 'Create->Reactor->Converging Input Predictor', or use the built-in 'DefaultConvergingInputPredictor' asset.

ksClientInputPredictor

ksClientInputPredictor Predicts movement and properties using only the player controller; server data is ignored. This is mostly useful for debugging the see how your player controlled entity behaves on the client without influence from the server.

Predicting Properties

Predictors can predict property values as well as transform data. To predict properties using a linear predictor or converging input predictor, you will need to create a linear predictor asset ('Create/Reactor/Linear Predictor') or converging input predictor asset ('Create->Reactor->Converging Input Predictor'), and configure the Predicted Properties in the inspector with the properties you want to predict. Each property has a 'Type' that determines how the property will be predicted. It can be set to one of the following:

Changing Predictors at Runtime

To change the predictor used by an entity at runtime, assign a predictor to ksEntity.InputPredictor if the entity is controlled by the local player, and ksEntity.NonInputPredictor if it is not. ksEntity.Predictor is the active preditor the entity is using. You can change the predictor used for room or player properties by assigning ksRoom.Predictor or ksPlayer.Predictor.

Set ksEntity.PredictionEnabled to enable or disable prediction for an entity at runtime. When prediction is disabled the entity will not use either the InputPredictor or the NonInputPredictor and ksEntity.Predictor will be null.

The following room script shows how to dynamically disable prediction for distant entities. For large worlds with thousands of moving entities, this can improve the performance of the client.

Client Room Script

using UnityEngine;
using KS.Reactor.Client.Unity;

public class crPredictorDistanceFilter : ksRoomScript
{
    // Disable prediction for entities that are further from the camera than this.
    public float Distance = 25f;

    private void Update()
    {
        float distanceSquared = Distance * Distance;
        foreach (ksEntity entity in Room.DynamicEntities)
        {
            entity.PredictionEnabled = (entity.ServerTransform.Position - Camera.main.transform.position).MagnitudeSquared() <= distanceSquared;
        }
    }
}

Custom Predictors

To create your own predictor script, right-click in the project browser. Select 'Create->Reactor->Predictor'. The following is a very basic sample predictor that update the entity's position using Vector3.SmoothDamp, linearly interpolates rotation, and updates a float property with id 0 using Mathf.SmoothDamp. It can be used with or without a player controller, or just to predict property 0 on a room or player. To create an asset from it that can be assigned to an entity or as a default predictor in the room type, right-click in the project browser and select 'Create->Reactor->SamplePredictor'.

Sample Predictor

using System;
using System.Collections.Generic;
using System.Collections;
using UnityEngine;
using KS.Reactor.Client.Unity;
using KS.Reactor.Client;
using KS.Reactor;

[CreateAssetMenu(menuName = ksMenuNames.REACTOR + "SamplePredictor", order = ksMenuGroups.SCRIPT_ASSETS)]
public class SamplePredictor : ksPredictor
{
    private ksReadOnlyTransformState m_serverState;
    private float m_serverProperty;
    private bool m_teleported = false;
    private Vector3 m_velocity;
    private float m_rotationSpeed;
    private float m_propertySpeed;

    // Initializes the predictor. Return false if initialization fails.
    public override bool Initialize()
    {
        return true;
    }

    // Called when the predictor is removed. Performs clean up.
    public override void Detached()
    {
        // The base method clears the Entity/Room reference.
        base.Detached();
    }

    // Return true if the predictor will predict the property value.
    public override bool IsPredictedProperty(uint propertyId)
    {
        // Predict property 0
        return propertyId == 0;
    }

    // Called once per server frame. Perform any relevant calculations with server frame data here, and/or store
    // the relevant server frame data to be used in calculating future client state in the ClientUpdate method.
    // If it returns false and idle is true, this function will not be called again until the server transform
    // or a server property changes.
    public override bool ServerUpdate(ksReadOnlyTransformState state, Dictionary<uint, ksMultiType> properties, bool teleport, bool idle)
    {
        m_serverState = state;
        m_teleported = teleport;

        // Properties is null if none of the predicted properties changed.
        if (properties != null)
        {
            // Get the server property value.
            ksMultiType property;
            if (properties.TryGetValue(0, out property))
            {
                m_serverProperty = property;
            }
        }
        // State is null if the predictor is predicting room or player properties.
        if (!teleport && state != null && Time.ServerUnscaledDelta > 0f)
        {
            // Calculate rotation interpolation speed.
            m_rotationSpeed = ksQuaternion.DeltaDegrees(state.Rotation, Entity.GameObject.transform.rotation) / Time.ServerUnscaledDelta;
        }
        if (Controller != null)
        {
            // Move the controller to the server position.
            Controller.Transform.Position = state.Position;
            Controller.Transform.Rotation = state.Rotation;
            Controller.Transform.Scale = state.Scale;
            if (Controller.Properties.Contains(0))
            {
                Controller.Properties[0] = m_serverProperty;
            }
            // If there's a controller we want to receive all server updates so we can reposition the controller every time a
            // server update arrives.
            return true;
        }
        // Only call this function when the server values change.
        return false;
    }

    // Called once per client render frame to update the client transform and smoothed properties.
    // If it returns false, this function will not be called again until the server transform or a
    // server property changes.
    public override bool ClientUpdate(ksTransformState state, Dictionary<uint, ksMultiType> properties)
    {
        // Properties is null if there are no predicted properties.
        if (properties != null)
        {
            if (m_teleported)
            {
                // Teleport to the server value.
                properties[0] = m_serverProperty;
                m_propertySpeed = 0f;
            }
            else if (Time.UnscaledDelta > 0f)
            {
                // Move towards the controller property value if there is a controller. Otherwise move towards the server value.
                float targetValue = Controller == null ? m_serverProperty : Controller.Properties[0];
                properties[0] = Mathf.SmoothDamp(properties[0], targetValue, ref m_propertySpeed, Time.UnscaledDelta);
            }
        }
        // State is null if the predictor is predicting room or player properties.
        if (state == null)
        {
            return false;
        }
        state.Scale = m_serverState.Scale;
        if (m_teleported)
        {
            // Teleport to the server position.
            m_teleported = false;
            state.Position = m_serverState.Position;
            state.Rotation = m_serverState.Rotation;
            m_velocity = ksVector3.Zero;
            return Controller != null;
        }
        if (Time.UnscaledDelta > 0f)
        {
            // Move towards the controller position if there is a controller. Otherwise move towards the server position.
            ksVector3 targetPosition = Controller == null ? m_serverState.Position : Controller.Transform.Position;
            ksQuaternion targetRotation = Controller == null ? m_serverState.Rotation : Controller.Transform.Rotation;

            // Smooth damp the position.
            state.Position = Vector3.SmoothDamp(state.Position, targetPosition, ref m_velocity, Time.UnscaledDelta);

            // Linearly interpolate rotation.
            state.Rotation = ksQuaternion.RotateTowards(state.Rotation, targetRotation, m_rotationSpeed * Time.UnscaledDelta);
        }
        // If there's a controller or we haven't reached the server position, we need more client updates.
        return Controller != null || state.Position != m_serverState.Position || state.Rotation != m_serverState.Rotation;
    }

    // Called when a new frame of input is generated. Only called for entities with player controllers.
    public override void InputUpdate(ksInput input)
    {
        // Update the player controller.
        ksPredictorUtils.UpdateController(Entity, Time.UnscaledDelta, Time.TimeScale, input);
        input.CleanUp();
        if (!m_teleported && Time.ServerDelta > 0f)
        {
            // Calculate rotation interpolation speed.
            m_rotationSpeed = Math.Max(m_rotationSpeed,
                ksQuaternion.DeltaDegrees(Controller.Transform.Rotation, Entity.GameObject.transform.rotation) / Time.ServerUnscaledDelta);
        }
    }
}