Predictors
Summary
Shows how to customise entity movement prediction using predictors.
- Configure predictors using predictor assets.
- Give an overview of the built-in predictors.
- Configure which properties are predicted and their prediction behaviour.
- Create a basic custom predictor that uses
Vector3.SmoothDamp
on position and linearly interpolates rotation.
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:
- Entity The default predictor to use for entities that do not use input prediction. By default this is 'DefaultLinearPredictor'.
- Controller The default predictor to use for entities controlled by the local player. By default this is 'DefaultConvergingInputPredictor' If the player controller has
ksPlayerController.UseInputPrediction
set to false, the controlled entity will use the 'Entity' predictor instead. - Room The predictor for predicting room properties. By default this is none, which means room properties are not predicted.
- Player The predictor for predicting player properties. By default this is none, which means player properties are not predicted.
You can assign different predictors to an entity using the following fields in the ksEntityComponent inspector:
- Predictor The predictor to use when the entity is not controlled by the local player. If the checkbox is unchecked, it will use the default 'Entity' predictor from the ksRoomType.
- Controller Predictor The predictor to use when the entity is controlled by the local player. If the checkbox is unchecked, it will use the default 'Controller' predictor from the ksRoomType.
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:
- Linear Float - Use linear interpolation on a float.
- Linear Vector2 - Use linear interpolation on a ksVector2.
- Linear Vector3 - Use linear interpolation on a ksVector3.
- Linear Color - Use linear interpolation on a ksColor.
- Wrap Float - Use linear interpolation on a float and wrap onto a range defined by a min and max value.
- Spherical Vector2 - Use spherical interpolation on a ksVector2.
- Spherical Vector3 - Use spherical interpolation on a ksVector3.
- Quaternion - Use spherical interpolation on a ksQuaternion.
- Client - The client player controller has full control over the local property value. If the controller does not set the property value, the server value is used. The server is still authoritative; the client does not decide what other clients see.
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);
}
}
}