In my game, I am synchronizing player position using ClientNetworkTransform with Interpolation enabled. For regular movement this works perfectly fine and smoothens out everything.
However, when the character moves very quickly - for example dashing forward in a very short amount of time - the interpolation takes too long to keep up.
This makes the move look way too slow for all the non-authoritative clients.
I have tried to manually disable and enable this option which does help, but will lead to the transform essentially snapping in and out at those moments.
So I wanted to ask if there is a way to simply speed up the interpolation dynamically if for example the delta between the authority and non-authority exceeds a certain threshold?
Will get an answer with a script in the next couple of days… you can accomplish what you are trying to do but it requires overriding the NetworkTransform.Update method and controlling when the non-authority side updates.
The example I will provide you should provide you with a framework to expand upon.
Here is a dash example you can throw onto a box or the like and try out:
using System.Collections.Generic;
using Unity.Netcode;
using Unity.Netcode.Components;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
// This bypases the default custom editor for NetworkTransform
// and lets you modify your custom NetworkTransform's properties
// within the inspector view
[CustomEditor(typeof(PlayerMoveWithDash), true)]
public class PlayerMoveWithDashEditor : Editor
{
}
#endif
public class PlayerMoveWithDash : NetworkTransform
{
// Normal movement speed
public float Speed = 4.0f;
// Normal roation speed
public float RotSpeed = 1.0f;
// Dash speed
public float DashSpeed = 6.0f;
// Distance to dash
public float DashDistance = 4.0f;
// Delta threshold when player has reached final dash point
public float DashDelta = 0.25f;
public AuthorityModes AuthorityMode;
public enum AuthorityModes
{
Owner,
Server
}
public enum PlayerStates
{
Normal,
PendingDash,
Dash
}
protected override bool OnIsServerAuthoritative()
{
return AuthorityMode == AuthorityModes.Server;
}
public class PlayerStateUpate : INetworkSerializable
{
public PlayerStates PlayerState;
public Vector3 StartPos;
public Vector3 EndPos;
protected virtual void OnNetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
}
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref PlayerState);
if (PlayerState == PlayerStates.Dash)
{
serializer.SerializeValue(ref StartPos);
serializer.SerializeValue(ref EndPos);
}
OnNetworkSerialize(serializer);
}
}
private NetworkVariable<PlayerStateUpate> m_PlayerState = new NetworkVariable<PlayerStateUpate>(default, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
private List<PlayerStateUpate> m_PendingStates = new List<PlayerStateUpate>(new PlayerStateUpate[] { new PlayerStateUpate() { PlayerState = PlayerStates.Normal } });
public override void OnNetworkSpawn()
{
if (IsOwner)
{
var temp = transform.position;
temp.y = 0.5f;
transform.position = temp;
}
else
{
m_PlayerState.OnValueChanged += OnPlayerStateChanged;
}
base.OnNetworkSpawn();
}
public override void OnNetworkDespawn()
{
m_PlayerState.OnValueChanged -= OnPlayerStateChanged;
base.OnNetworkDespawn();
}
private void OnPlayerStateChanged(PlayerStateUpate previous, PlayerStateUpate current)
{
m_PendingStates.Add(current);
}
/// <summary>
/// Override the OnAuthorityPushTransformState to apply dash values before sending player state update.
/// This assures the local player's position is the most currently known position to other players.
/// </summary>
/// <param name="networkTransformState">The most current state sent to the client</param>
protected override void OnAuthorityPushTransformState(ref NetworkTransformState networkTransformState)
{
var pendingState = m_PendingStates[m_PendingStates.Count - 1];
// If we have a pending dash, then apply the dash values
if (pendingState.PlayerState == PlayerStates.PendingDash)
{
var targetPosition = transform.position + (transform.forward * DashDistance);
// Used by other players (nonauthority instances) to assure when they begin dashing
// they are starting from a synchronized position.
pendingState.StartPos = transform.position;
// Used by both authority and nonauthority instances to determine when they have reached
// a "close enough" distance, determined by DashDelta, to end the player's dash sequence.
pendingState.EndPos = targetPosition;
// Apply the updated state and mark it dirty
m_PlayerState.Value = pendingState;
m_PlayerState.SetDirty(true);
// Change the local state to Dash
pendingState.PlayerState = PlayerStates.Dash;
// Apply these changes back into our pending states list
m_PendingStates[m_PendingStates.Count - 1] = pendingState;
}
base.OnAuthorityPushTransformState(ref networkTransformState);
}
/// <summary>
/// Normal player movement
/// </summary>
private void PlayerMove()
{
transform.position = Vector3.Lerp(transform.position, transform.position + Input.GetAxis("Vertical") * Speed * transform.forward, Time.fixedDeltaTime);
var rotation = transform.rotation;
var euler = rotation.eulerAngles;
euler.y += Input.GetAxis("Horizontal") * 90 * RotSpeed * Time.fixedDeltaTime;
rotation.eulerAngles = euler;
transform.rotation = rotation;
}
/// <summary>
/// Dash state update invoked by both the authority and the nonauthority instances
/// </summary>
/// <param name="pendingState"></param>
private void PlayerDash(ref PlayerStateUpate pendingState)
{
transform.position = Vector3.Lerp(transform.position, pendingState.EndPos, Time.fixedDeltaTime * DashSpeed);
var distance = Vector3.Distance(pendingState.EndPos, transform.position);
if (distance <= DashDelta)
{
pendingState.PlayerState = PlayerStates.Normal;
}
}
/// <summary>
/// The Authority's primary update
/// </summary>
private void AuthorityStateUpdate()
{
var pendingState = m_PendingStates[m_PendingStates.Count - 1];
if (Input.GetKeyDown(KeyCode.Space) && pendingState.PlayerState == PlayerStates.Normal)
{
m_PendingStates.Add(new PlayerStateUpate() { PlayerState = PlayerStates.PendingDash });
Interpolate = false;
}
switch (pendingState.PlayerState)
{
case PlayerStates.PendingDash:
{
// Nudge the authority instance to kick off a state update
// See OnAuthorityPushTransformState
var position = transform.position;
position += transform.forward * (PositionThreshold * 2);
transform.position = position;
break;
}
case PlayerStates.Dash:
{
// Apply the dash
PlayerDash(ref pendingState);
// If the pending state was set back to normal, then
// remove that pending state (the normal state will always remain)
if (pendingState.PlayerState == PlayerStates.Normal)
{
m_PendingStates.Remove(pendingState);
Interpolate = true;
}
break;
}
case PlayerStates.Normal:
{
PlayerMove();
break;
}
}
}
/// <summary>
/// The nonauthority's primary update
/// </summary>
private void NonAuthorityStateUpdate()
{
var pendingState = m_PendingStates[m_PendingStates.Count - 1];
switch(pendingState.PlayerState)
{
case PlayerStates.PendingDash:
{
var distance = Vector3.Distance(pendingState.StartPos, transform.position);
// Nonauthority will wait until it has interpolated to the StartPos
if (pendingState.PlayerState == PlayerStates.PendingDash && distance <= PositionThreshold)
{
// Once reached, start the dash sequence
pendingState.PlayerState = PlayerStates.Dash;
// Apply the state's changes
m_PendingStates[m_PendingStates.Count - 1] = pendingState;
}
break;
}
case PlayerStates.Dash:
{
// Dash until we have reached the EndPos
PlayerDash(ref pendingState);
// If we reached the end position, the current pending state
// will be Normal and we remove it from the pendings states.
if (pendingState.PlayerState == PlayerStates.Normal)
{
m_PendingStates.Remove(pendingState);
}
break;
}
}
}
protected override void Update()
{
// If not spawned or the authority, exit early
if (!IsSpawned || CanCommitToTransform)
{
return;
}
// If non-authority's current state is Normal, then just interpolate to the
// authority's last sent state values.
var pendingState = m_PendingStates[m_PendingStates.Count - 1];
if (pendingState.PlayerState == PlayerStates.Normal)
{
base.Update();
}
}
private void FixedUpdate()
{
// If not spawned, exit early
if (!IsSpawned)
{
return;
}
// If we can commit to transform we are the authority
if (CanCommitToTransform)
{
// Run authority update
AuthorityStateUpdate();
}
else
{
// Otherwise, run the nonauthority update
NonAuthorityStateUpdate();
}
}
}
The other components I used with the above NetworkTransform derived component:
The script is not an “end all be all” but should give you an idea of how you can accomplish synchronized motion that requires you to “bend the rules of interpolation” (so to speak).
Thank you so much!!! This helps a lot and it looks exactly as smooth and fast as I wanted it to.
I was kind of hoping that it could be done in a more simple/straightforward manner but at least the result is great haha
So essentially I need to take steps to signal to all non-authorities when I am about to do a non-interpolated move. They will then ramp up to the most current position and replicate the same state-driven code locally with up-to-date parameters before going back to interpolating.
Yeah,
For motion that goes outside of the bounds of the network tick bound interpolation, you definitely need to incorporate some form of state management and “override and control” the NetworkTransform’s update on non-owner instances. Once you mess around with this kind of approach, things will start to fall into place…especially why I recommend handling the “motion model” side of things on a derived NetworkTransform.
Either case, glad you find that example useful!
any update for a simpler way ? what is the use of networkTransform.SetMaxInterpolationBound() ?
(I tried to set a small value like 0.1 but see no change)
This will set the maximum interpolation boundary for lerping where if you set something like 0.1 then when the statue update is being processed in the BufferedLinearInterpolators it will set the t (time) parameter in the Lerp method (typically a Vector3.Lerp but if using quaternions then it would be a Quaternion.Lerp).
We do have plans on updating the BufferedLinearInterpolator as there are some improvements and known edge case issues with it.
however, I can’t see any effects when tweaking interpolation settings in NetworkTransform inspector while in Multiplayer Play Mode. The interpolation looks always at the slowest speed settings, whatever mode.
Enabling On/Off interpolation with main toggle works though.
Do I miss something ?