mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 02:50:32 +00:00
inherit old NT from custom
This commit is contained in:
parent
3f0dbadf34
commit
ab03d2d72a
@ -18,7 +18,7 @@ enum ForecastState
|
||||
}
|
||||
|
||||
[RequireComponent(typeof(Rigidbody))]
|
||||
public class ForecastRigidbody : NetworkBehaviour
|
||||
public class ForecastRigidbody : NetworkTransformOld // reuse NT for smooth sync for now. simplify into a lite-version later.
|
||||
{
|
||||
Transform tf; // this component is performance critical. cache .transform getter!
|
||||
Renderer rend;
|
||||
@ -35,7 +35,7 @@ public class ForecastRigidbody : NetworkBehaviour
|
||||
|
||||
[Header("Blending")]
|
||||
[Tooltip("Blend to remote state over a N * rtt time.\n For a 200ms ping, we blend over N * 200ms.\n For 20ms ping, we blend over N * 20 ms.")]
|
||||
public float blendingRttMultiplier = 2;
|
||||
public float blendingRttMultiplier = 3;
|
||||
public float blendingTime => (float)NetworkTime.rtt * blendingRttMultiplier;
|
||||
[Tooltip("Blending speed over time from 0 to 1. Exponential is recommended.")]
|
||||
public AnimationCurve blendingCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||||
@ -46,9 +46,6 @@ public class ForecastRigidbody : NetworkBehaviour
|
||||
[Tooltip("Locally applied force is slowed down a bit compared to the server force, to make catch up more smooth.")]
|
||||
[Range(0.05f, 1)] public float localForceDampening = 0.2f; // 50% is too obvious
|
||||
|
||||
[Header("Smoothing")]
|
||||
public readonly SortedList<double, TransformSnapshot> clientSnapshots = new SortedList<double, TransformSnapshot>(16);
|
||||
|
||||
[Header("Collision Chaining")]
|
||||
[Tooltip("Enable to have actively predicted Rigidbodies activate other Rigidbodies they collide with.")]
|
||||
public bool collisionChaining = true;
|
||||
@ -67,23 +64,16 @@ public class ForecastRigidbody : NetworkBehaviour
|
||||
public float angularVelocitySensitivity = 5.0f; // Billiards demo: 0.1 is way too small, takes forever for IsMoving()==false
|
||||
float angularVelocitySensitivitySqr; // ² cached in Awake
|
||||
|
||||
[Tooltip("Applying server corrections one frame ahead gives much better results. We don't know why yet, so this is an option for now.")]
|
||||
public bool oneFrameAhead = true;
|
||||
|
||||
[Header("Bandwidth")]
|
||||
[Tooltip("Reduce sends while velocity==0. Client's objects may slightly move due to gravity/physics, so we still want to send corrections occasionally even if an object is idle on the server the whole time.")]
|
||||
public bool reduceSendsWhileIdle = true;
|
||||
|
||||
#if UNITY_EDITOR // PERF: only access .material in Editor, as it may instantiate!
|
||||
[Header("Debugging")]
|
||||
public bool debugColors = false;
|
||||
Color originalColor = Color.white;
|
||||
public Color predictingColor = Color.green;
|
||||
public Color blendingColor = Color.yellow; // when actually interpolating towards a blend in front of us
|
||||
#endif
|
||||
|
||||
protected virtual void Awake()
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
|
||||
tf = transform;
|
||||
rend = GetComponentInChildren<Renderer>();
|
||||
predictedRigidbody = GetComponent<Rigidbody>();
|
||||
@ -93,10 +83,8 @@ protected virtual void Awake()
|
||||
|
||||
initialSyncInterval = syncInterval;
|
||||
|
||||
// in fast mode, we need to force enable Rigidbody.interpolation.
|
||||
// otherwise there's not going to be any smoothing whatsoever.
|
||||
// PERF: disable this for now!
|
||||
// predictedRigidbody.interpolation = RigidbodyInterpolation.Interpolate;
|
||||
// make sure predicted physics look smooth
|
||||
predictedRigidbody.interpolation = RigidbodyInterpolation.Interpolate;
|
||||
|
||||
// cache computations
|
||||
velocitySensitivitySqr = velocitySensitivity * velocitySensitivity;
|
||||
@ -106,9 +94,7 @@ protected virtual void Awake()
|
||||
// note if objects share a material, accessing ".material" will
|
||||
// instantiate one which can be a massive performance overhead.
|
||||
// only use debug colors when debugging!
|
||||
#if UNITY_EDITOR // PERF: only access .material in Editor, as it may instantiate!
|
||||
if (debugColors) originalColor = rend.material.color;
|
||||
#endif
|
||||
}
|
||||
|
||||
public override void OnStartClient()
|
||||
@ -172,19 +158,21 @@ protected void BeginPredicting()
|
||||
{
|
||||
predictedRigidbody.isKinematic = false; // full physics sync
|
||||
|
||||
// collision checking is disabled while following.
|
||||
// enable while predicting again.
|
||||
predictedRigidbody.detectCollisions = true;
|
||||
|
||||
state = ForecastState.PREDICTING;
|
||||
#if UNITY_EDITOR // PERF: only access .material in Editor, as it may instantiate!
|
||||
if (debugColors) rend.material.color = predictingColor;
|
||||
#endif
|
||||
// we want to predict until the first server state for our [Command] AddForce came in.
|
||||
// we know the time when our [Command] arrives on server: NetworkTime.predictedTime.
|
||||
predictionStartTime = NetworkTime.predictedTime; // !!! not .time !!!
|
||||
OnBeginPrediction();
|
||||
// Debug.Log($"{name} BEGIN PREDICTING @ {predictionStartTime:F2}");
|
||||
|
||||
// CUSTOM CHANGE: NT is disabled until physics changes to dynamic.
|
||||
// by default it gets enabled based on a syncvar.
|
||||
// for prediction, we want to enable it immediately when we begin predicting.
|
||||
// IMPORTANT: the syncvar also disables it at the right time,
|
||||
// BUT Unity Inspector sometimes doesn't refresh unless clicking A->B->A again. this works fine.
|
||||
enabled = true;
|
||||
// END CUSTOM CHANGE
|
||||
}
|
||||
|
||||
double blendingStartTime;
|
||||
@ -197,7 +185,7 @@ protected void BeginBlending()
|
||||
|
||||
// clear any previous snapshots from teleports, old states, while prediction etc.
|
||||
// start building a buffer while blending, to later follow while FOLLOWING.
|
||||
clientSnapshots.Clear();
|
||||
//clientSnapshots.Clear();
|
||||
// Debug.Log($"{name} BEGIN BLENDING");
|
||||
}
|
||||
|
||||
@ -205,123 +193,48 @@ protected void BeginFollowing()
|
||||
{
|
||||
predictedRigidbody.isKinematic = true; // full transform sync
|
||||
state = ForecastState.FOLLOWING;
|
||||
#if UNITY_EDITOR // PERF: only access .material in Editor, as it may instantiate!
|
||||
if (debugColors) rend.material.color = originalColor;
|
||||
#endif
|
||||
// reset the collision chain depth so it starts at 0 again next time
|
||||
remainingCollisionChainDepth = 0;
|
||||
|
||||
// perf: setting kinematic rigidbody's positions still causes
|
||||
// significant physics overhead on low end devices.
|
||||
// try disable collisions while purely following.
|
||||
predictedRigidbody.detectCollisions = false;
|
||||
// don't disable collisions while following!
|
||||
// otherwise player can't interact with it again!
|
||||
// predictedRigidbody.detectCollisions = false;
|
||||
|
||||
OnBeginFollow();
|
||||
// Debug.Log($"{name} BEGIN FOLLOW");
|
||||
}
|
||||
|
||||
void UpdateServer()
|
||||
{
|
||||
// bandwidth optimization while idle.
|
||||
if (reduceSendsWhileIdle)
|
||||
{
|
||||
// while moving, always sync every syncInterval..
|
||||
// while idle, only sync once per second.
|
||||
//
|
||||
// we still need to sync occasionally because objects on client
|
||||
// may still slide or move slightly due to gravity, physics etc.
|
||||
// and those still need to get corrected if not moving on server.
|
||||
//
|
||||
// TODO
|
||||
// next round of optimizations: if client received nothing for 1s,
|
||||
// force correct to last received state. then server doesn't need
|
||||
// to send once per second anymore.
|
||||
syncInterval = IsMoving() ? initialSyncInterval : 1;
|
||||
}
|
||||
|
||||
// always set dirty to always serialize in next sync interval.
|
||||
SetDirty();
|
||||
}
|
||||
|
||||
// movement detection is virtual, in case projects want to use other methods.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected virtual bool IsMoving() =>
|
||||
// straight forward implementation
|
||||
// predictedRigidbody.velocity.magnitude >= motionSmoothingVelocityThreshold ||
|
||||
// predictedRigidbody.angularVelocity.magnitude >= motionSmoothingAngularVelocityThreshold;
|
||||
// faster implementation with cached ²
|
||||
predictedRigidbody.velocity.sqrMagnitude >= velocitySensitivitySqr ||
|
||||
predictedRigidbody.angularVelocity.sqrMagnitude >= angularVelocitySensitivitySqr;
|
||||
|
||||
void Update()
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
if (isClientOnly) UpdateClient();
|
||||
}
|
||||
|
||||
// NetworkTransform improvement: server broadcast needs to run in LateUpdate
|
||||
// after other scripts may changed positions in Update().
|
||||
// otherwise this may run before user update, delaying detection until next frame.
|
||||
// this could cause visible jitter.
|
||||
void LateUpdate()
|
||||
Vector3 lastSetPosition = Vector3.zero;
|
||||
protected override void ApplySnapshot(NTSnapshot interpolated)
|
||||
{
|
||||
if (isServer) UpdateServer();
|
||||
}
|
||||
|
||||
// Prediction uses a Rigidbody, which would suggest position changes to happen in FixedUpdate().
|
||||
// However, snapshot interpolation needs to run in Update() in order to look perfectly smooth.
|
||||
void UpdateClient()
|
||||
{
|
||||
// PREDICTING checks state, which happens in update()
|
||||
// ignore server snapshots while simulating physics
|
||||
if (state == ForecastState.PREDICTING)
|
||||
{
|
||||
// we want to predict until the first server state came in.
|
||||
// -> our [Command] AddForce is sent locally.
|
||||
// -> predictionStartTime was set to NetworkTime.predictedTime,
|
||||
// which is the time on server when the [Command] will arrive.
|
||||
// -> we want to wait until the last received is at least >= start time.
|
||||
// TODO add a safety buffer on top to make sure it's the state after [Command]?
|
||||
// but technically doesn't make a difference if it just barely moved anyway.
|
||||
if (lastReceivedState.timestamp > predictionStartTime)
|
||||
{
|
||||
// Debug.Log($"{name} END PREDICTING because received state = {lastReceivedState.timestamp:F2} > prediction start = {predictionStartTime:F2}");
|
||||
BeginBlending();
|
||||
}
|
||||
}
|
||||
// blend between local position and server snapshots
|
||||
else if (state == ForecastState.BLENDING)
|
||||
{
|
||||
// DEBUG: force FOLLOW for now
|
||||
// snapshot interpolation: get the interpolated remote position at this time.
|
||||
// if there is no snapshot yet, just use lastReceived
|
||||
Vector3 targetPosition = lastReceivedState.position;
|
||||
Quaternion targetRotation = lastReceivedState.rotation;
|
||||
if (clientSnapshots.Count > 0)
|
||||
{
|
||||
// step the interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
clientSnapshots,
|
||||
NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
targetPosition = computed.position;
|
||||
targetRotation = computed.rotation;
|
||||
// scale is ignored
|
||||
}
|
||||
Vector3 targetPosition = interpolated.position;
|
||||
Quaternion targetRotation = interpolated.rotation;
|
||||
|
||||
// blend between local and remote position
|
||||
// set debug color
|
||||
#if UNITY_EDITOR // PERF: only access .material in Editor, as it may instantiate!
|
||||
if (debugColors)
|
||||
{
|
||||
rend.material.color = blendingColor;
|
||||
}
|
||||
#endif
|
||||
|
||||
// sample the blending curve to find out how much to blend right now
|
||||
|
||||
float blendingElapsed = (float)(NetworkTime.localTime - blendingStartTime);
|
||||
float relativeElapsed = blendingElapsed / blendingTime;
|
||||
float p = blendingCurve.Evaluate(relativeElapsed);
|
||||
@ -356,12 +269,54 @@ void UpdateClient()
|
||||
Quaternion newRotation = Quaternion.Slerp(currentRotation, targetRotation, p).normalized;
|
||||
|
||||
// assign position and rotation together. faster than accessing manually.
|
||||
// TODO reuse ApplySnapshot for consistency?
|
||||
tf.SetPositionAndRotation(newPosition, newRotation);
|
||||
//predictedRigidbody.MovePosition(newPosition); // smooth
|
||||
//predictedRigidbody.MoveRotation(newRotation); // smooth
|
||||
}
|
||||
// directly apply server snapshots while following
|
||||
else if (state == ForecastState.FOLLOWING)
|
||||
{
|
||||
// BEGIN CUSTOM CHANGE MAGIC: -98% => -1% bots benchmark
|
||||
// PERF: only set if changed in order to not trigger physics updates while kinematic(!)
|
||||
// EPSILON: simply needs to be small enough so we can't perceive jitter.
|
||||
const float epsilon = 0.00001f;
|
||||
if (Vector3.Distance(interpolated.position, lastSetPosition) >= epsilon)
|
||||
{
|
||||
base.ApplySnapshot(interpolated);
|
||||
lastSetPosition = interpolated.position;
|
||||
}
|
||||
// END CUSTOM CHANGE
|
||||
}
|
||||
}
|
||||
|
||||
// Prediction uses a Rigidbody, which needs to be moved in FixedUpdate() even while kinematic.
|
||||
double lastReceivedRemoteTime = 0;
|
||||
void UpdateClient()
|
||||
{
|
||||
// PREDICTING checks state, which happens in update()
|
||||
if (state == ForecastState.PREDICTING)
|
||||
{
|
||||
// we want to predict until the first server state came in.
|
||||
// -> our [Command] AddForce is sent locally.
|
||||
// -> predictionStartTime was set to NetworkTime.predictedTime,
|
||||
// which is the time on server when the [Command] will arrive.
|
||||
// -> we want to wait until the last received is at least >= start time.
|
||||
// TODO add a safety buffer on top to make sure it's the state after [Command]?
|
||||
// but technically doesn't make a difference if it just barely moved anyway.
|
||||
if (lastReceivedRemoteTime > predictionStartTime)
|
||||
{
|
||||
// Debug.Log($"{name} END PREDICTING because received state = {lastReceivedState.timestamp:F2} > prediction start = {predictionStartTime:F2}");
|
||||
BeginBlending();
|
||||
}
|
||||
}
|
||||
else if (state == ForecastState.BLENDING)
|
||||
{
|
||||
// transition to FOLLOWING after blending is done.
|
||||
// we could check 'if p >= 1' but if the user's curve never
|
||||
// reaches a value of '1' then we would never transition.
|
||||
// best to reach if elapsed time > blend time.
|
||||
float blendingElapsed = (float)(NetworkTime.localTime - blendingStartTime);
|
||||
if (blendingElapsed >= blendingTime)
|
||||
{
|
||||
// Debug.Log($"{name} END BLENDING");
|
||||
@ -371,50 +326,15 @@ void UpdateClient()
|
||||
// FOLLOWING sets Transform, which happens in Update().
|
||||
else if (state == ForecastState.FOLLOWING)
|
||||
{
|
||||
// hard set position & rotation.
|
||||
// in theory we must always set rigidbody.position/rotation instead of transform:
|
||||
// https://forum.unity.com/threads/how-expensive-is-physics-synctransforms.1366146/#post-9557491
|
||||
// however, tf.SetPositionAndRotation is faster in our Prediction Benchmark.
|
||||
// predictedRigidbody.position = lastReceivedState.position;
|
||||
// predictedRigidbody.rotation = lastReceivedState.rotation;
|
||||
// tf.SetPositionAndRotation(lastReceivedState.position, lastReceivedState.rotation);
|
||||
|
||||
// only while we have snapshots
|
||||
if (clientSnapshots.Count > 0)
|
||||
{
|
||||
// step the interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
clientSnapshots,
|
||||
NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply to transform.
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
tf.SetPositionAndRotation(computed.position, computed.rotation); // scale is ignored
|
||||
}
|
||||
// NetworkTransform sync happens while FOLLOWING
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if UNITY_EDITOR // PERF: only run gizmos in editor
|
||||
void OnDrawGizmos()
|
||||
{
|
||||
// draw server state while blending
|
||||
if (state == ForecastState.BLENDING)
|
||||
{
|
||||
Gizmos.color = blendingColor;
|
||||
Gizmos.DrawWireCube(lastReceivedState.position, col.bounds.size);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// while predicting on client, if we hit another object then we need to
|
||||
// start predicting this one too.
|
||||
// otherwise the collision response would be delayed until next server
|
||||
// state to follow comes in.
|
||||
/*
|
||||
[ClientCallback]
|
||||
void OnCollisionEnter(Collision collision)
|
||||
{
|
||||
@ -450,6 +370,7 @@ void OnCollisionEnter(Collision collision)
|
||||
|
||||
other.AddPredictedForceChain(impulse, ForceMode.Impulse, remainingCollisionChainDepth - 1);
|
||||
}
|
||||
*/
|
||||
|
||||
// optional user callbacks, in case people need to know about events.
|
||||
protected virtual void OnBeginPrediction() {}
|
||||
@ -458,103 +379,12 @@ protected virtual void OnBeginFollow() {}
|
||||
|
||||
// process a received server state.
|
||||
// compares it against our history and applies corrections if needed.
|
||||
ForecastRigidbodyState lastReceivedState;
|
||||
void OnReceivedState(ForecastRigidbodyState data)//, bool sleeping)
|
||||
protected override void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale)
|
||||
{
|
||||
base.OnServerToClientSync(position, rotation, scale);
|
||||
|
||||
// store last time
|
||||
lastReceivedState = data;
|
||||
|
||||
// add to snapshot interpolation for smooth following.
|
||||
// add a small timeline offset to account for decoupled arrival of
|
||||
// NetworkTime and NetworkTransform snapshots.
|
||||
// needs to be sendInterval. half sendInterval doesn't solve it.
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3427
|
||||
// remove this after LocalWorldState.
|
||||
|
||||
// insert transform transform snapshot.
|
||||
// ignore while predicting since they'll be from old server state.
|
||||
if (state != ForecastState.PREDICTING)
|
||||
{
|
||||
SnapshotInterpolation.InsertIfNotExists(
|
||||
clientSnapshots,
|
||||
NetworkClient.snapshotSettings.bufferLimit,
|
||||
new TransformSnapshot(
|
||||
NetworkClient.connection.remoteTimeStamp, // TODO use Ninja's offset from NT-R?: + timeStampAdjustment + offset, // arrival remote timestamp. NOT remote time.
|
||||
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
|
||||
data.position,
|
||||
data.rotation,
|
||||
Vector3.zero // scale is unused
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// send state to clients every sendInterval.
|
||||
// reliable for now.
|
||||
public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
{
|
||||
// Time.time was at the beginning of this frame.
|
||||
// NetworkLateUpdate->Broadcast->OnSerialize is at the end of the frame.
|
||||
// as result, client should use this to correct the _next_ frame.
|
||||
// otherwise we see noticeable resets that seem off by one frame.
|
||||
//
|
||||
// to solve this, we can send the current deltaTime.
|
||||
// server is technically supposed to be at a fixed frame rate, but this can vary.
|
||||
// sending server's current deltaTime is the safest option.
|
||||
// client then applies it on top of remoteTimestamp.
|
||||
|
||||
|
||||
// FAST VERSION: this shows in profiler a lot, so cache EVERYTHING!
|
||||
tf.GetPositionAndRotation(out Vector3 position, out Quaternion rotation); // faster than tf.position + tf.rotation. server's rigidbody is on the same transform.
|
||||
|
||||
// simple but slow write:
|
||||
// writer.WriteFloat(Time.deltaTime);
|
||||
// writer.WriteVector3(position);
|
||||
// writer.WriteQuaternion(rotation);
|
||||
// writer.WriteVector3(predictedRigidbody.velocity);
|
||||
// writer.WriteVector3(predictedRigidbody.angularVelocity);
|
||||
|
||||
// performance optimization: write a whole struct at once via blittable:
|
||||
ForecastSyncData data = new ForecastSyncData(
|
||||
Time.deltaTime,
|
||||
position,
|
||||
rotation);
|
||||
writer.WriteForecastSyncData(data);
|
||||
}
|
||||
|
||||
// read the server's state, compare with client state & correct if necessary.
|
||||
public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
{
|
||||
// deserialize data
|
||||
// we want to know the time on the server when this was sent, which is remoteTimestamp.
|
||||
double timestamp = NetworkClient.connection.remoteTimeStamp;
|
||||
|
||||
// simple but slow read:
|
||||
// double serverDeltaTime = reader.ReadFloat();
|
||||
// Vector3 position = reader.ReadVector3();
|
||||
// Quaternion rotation = reader.ReadQuaternion();
|
||||
// Vector3 velocity = reader.ReadVector3();
|
||||
// Vector3 angularVelocity = reader.ReadVector3();
|
||||
|
||||
// performance optimization: read a whole struct at once via blittable:
|
||||
ForecastSyncData data = reader.ReadForecastSyncData();
|
||||
double serverDeltaTime = data.deltaTime;
|
||||
Vector3 position = data.position;
|
||||
Quaternion rotation = data.rotation;
|
||||
|
||||
// server sends state at the end of the frame.
|
||||
// parse and apply the server's delta time to our timestamp.
|
||||
// otherwise we see noticeable resets that seem off by one frame.
|
||||
timestamp += serverDeltaTime;
|
||||
|
||||
// however, adding yet one more frame delay gives much(!) better results.
|
||||
// we don't know why yet, so keep this as an option for now.
|
||||
// possibly because client captures at the beginning of the frame,
|
||||
// with physics happening at the end of the frame?
|
||||
if (oneFrameAhead) timestamp += serverDeltaTime;
|
||||
|
||||
// process received state
|
||||
OnReceivedState(new ForecastRigidbodyState(timestamp, position, rotation));
|
||||
lastReceivedRemoteTime = NetworkClient.connection.remoteTimeStamp;
|
||||
}
|
||||
|
||||
protected override void OnValidate()
|
||||
|
922
Assets/Mirror/Components/ForecastRigidbody/NetworkTransform.cs
Normal file
922
Assets/Mirror/Components/ForecastRigidbody/NetworkTransform.cs
Normal file
@ -0,0 +1,922 @@
|
||||
// NetworkTransform V2 by mischa (2021-07)
|
||||
// Snapshot Interpolation: https://gafferongames.com/post/snapshot_interpolation/
|
||||
//
|
||||
// Base class for NetworkTransform and NetworkTransformChild.
|
||||
// => simple unreliable sync without any interpolation for now.
|
||||
// => which means we don't need teleport detection either
|
||||
//
|
||||
// NOTE: several functions are virtual in case someone needs to modify a part.
|
||||
//
|
||||
// Channel: uses UNRELIABLE at all times.
|
||||
// -> out of order packets are dropped automatically
|
||||
// -> it's better than RELIABLE for several reasons:
|
||||
// * head of line blocking would add delay
|
||||
// * resending is mostly pointless
|
||||
// * bigger data race:
|
||||
// -> if we use a Cmd() at position X over reliable
|
||||
// -> client gets Cmd() and X at the same time, but buffers X for bufferTime
|
||||
// -> for unreliable, it would get X before the reliable Cmd(), still
|
||||
// buffer for bufferTime but end up closer to the original time
|
||||
// comment out the below line to quickly revert the onlySyncOnChange feature
|
||||
#define onlySyncOnChange_BANDWIDTH_SAVING
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// BIGBOX CHANGE: using NetworkClient time interpolation breaks bots' DropPods. keep this for now. /////////////////
|
||||
[AddComponentMenu("Network/Network Transform (Unreliable)")]
|
||||
public class NetworkTransformOld : NetworkBehaviour
|
||||
{
|
||||
// target transform to sync. can be on a child.
|
||||
[Header("Target")]
|
||||
[Tooltip("The Transform component to sync. May be on on this GameObject, or on a child.")]
|
||||
public Transform target;
|
||||
|
||||
// TODO SyncDirection { CLIENT_TO_SERVER, SERVER_TO_CLIENT } is easier?
|
||||
[Header("Authority")]
|
||||
[Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
|
||||
public bool clientAuthority;
|
||||
|
||||
// Is this a client with authority over this transform?
|
||||
// This component could be on the player object or any object that has been assigned authority to this client.
|
||||
protected bool IsClientWithAuthority => isClient && authority;
|
||||
|
||||
[Header("Synchronization")]
|
||||
[Tooltip("Send N snapshots per second. Multiples of frame rate make sense.")]
|
||||
public int sendRate = 30; // in Hz. easier to work with as int for EMA. easier to display '30' than '0.333333333'
|
||||
public float sendInterval => 1f / sendRate;
|
||||
|
||||
// decrease bufferTime at runtime to see the catchup effect.
|
||||
// increase to see slowdown.
|
||||
// 'double' so we can have very precise dynamic adjustment without rounding
|
||||
[Header("Buffering")]
|
||||
[Tooltip("Local simulation is behind by sendInterval * multiplier seconds.\n\nThis guarantees that we always have enough snapshots in the buffer to mitigate lags & jitter.\n\nIncrease this if the simulation isn't smooth. By default, it should be around 2.")]
|
||||
public double bufferTimeMultiplier = 2;
|
||||
public double bufferTime => sendInterval * bufferTimeMultiplier;
|
||||
|
||||
[Tooltip("Buffer size limit to avoid ever growing list memory consumption attacks.")]
|
||||
public int bufferSizeLimit = 64;
|
||||
|
||||
// catchup /////////////////////////////////////////////////////////////
|
||||
// catchup thresholds in 'frames'.
|
||||
// half a frame might be too aggressive.
|
||||
[Header("Catchup / Slowdown")]
|
||||
[Tooltip("Slowdown begins when the local timeline is moving too fast towards remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be negative.\n\nDon't modify unless you know what you are doing.")]
|
||||
public float catchupNegativeThreshold = -1; // careful, don't want to run out of snapshots
|
||||
|
||||
[Tooltip("Catchup begins when the local timeline is moving too slow and getting too far away from remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be positive.\n\nDon't modify unless you know what you are doing.")]
|
||||
public float catchupPositiveThreshold = 1;
|
||||
|
||||
[Tooltip("Local timeline acceleration in % while catching up.")]
|
||||
[Range(0, 1)]
|
||||
public double catchupSpeed = 0.01f; // 1%
|
||||
|
||||
[Tooltip("Local timeline slowdown in % while slowing down.")]
|
||||
[Range(0, 1)]
|
||||
public double slowdownSpeed = 0.01f; // 1%
|
||||
|
||||
[Tooltip("Catchup/Slowdown is adjusted over n-second exponential moving average.")]
|
||||
public int driftEmaDuration = 1; // shouldn't need to modify this, but expose it anyway
|
||||
|
||||
// we use EMA to average the last second worth of snapshot time diffs.
|
||||
// manually averaging the last second worth of values with a for loop
|
||||
// would be the same, but a moving average is faster because we only
|
||||
// ever add one value.
|
||||
ExponentialMovingAverage serverDriftEma;
|
||||
ExponentialMovingAverage clientDriftEma;
|
||||
|
||||
// dynamic buffer time adjustment //////////////////////////////////////
|
||||
// dynamically adjusts bufferTimeMultiplier for smooth results.
|
||||
// to understand how this works, try this manually:
|
||||
//
|
||||
// - disable dynamic adjustment
|
||||
// - set jitter = 0.2 (20% is a lot!)
|
||||
// - notice some stuttering
|
||||
// - disable interpolation to see just how much jitter this really is(!)
|
||||
// - enable interpolation again
|
||||
// - manually increase bufferTimeMultiplier to 3-4
|
||||
// ... the cube slows down (blue) until it's smooth
|
||||
// - with dynamic adjustment enabled, it will set 4 automatically
|
||||
// ... the cube slows down (blue) until it's smooth as well
|
||||
//
|
||||
// note that 20% jitter is extreme.
|
||||
// for this to be perfectly smooth, set the safety tolerance to '2'.
|
||||
// but realistically this is not necessary, and '1' is enough.
|
||||
[Header("Dynamic Adjustment")]
|
||||
[Tooltip("Automatically adjust bufferTimeMultiplier for smooth results.\nSets a low multiplier on stable connections, and a high multiplier on jittery connections.")]
|
||||
public bool dynamicAdjustment = true;
|
||||
|
||||
[Tooltip("Safety buffer that is always added to the dynamic bufferTimeMultiplier adjustment.")]
|
||||
public float dynamicAdjustmentTolerance = 1; // 1 is realistically just fine, 2 is very very safe even for 20% jitter. can be half a frame too. (see above comments)
|
||||
|
||||
[Tooltip("Dynamic adjustment is computed over n-second exponential moving average standard deviation.")]
|
||||
public int deliveryTimeEmaDuration = 2; // 1-2s recommended to capture average delivery time
|
||||
|
||||
ExponentialMovingAverage serverDeliveryTimeEma; // average delivery time (standard deviation gives average jitter)
|
||||
ExponentialMovingAverage clientDeliveryTimeEma; // average delivery time (standard deviation gives average jitter)
|
||||
|
||||
// buffers & time //////////////////////////////////////////////////////
|
||||
// snapshots sorted by timestamp
|
||||
// in the original article, glenn fiedler drops any snapshots older than
|
||||
// the last received snapshot.
|
||||
// -> instead, we insert into a sorted buffer
|
||||
// -> the higher the buffer information density, the better
|
||||
// -> we still drop anything older than the first element in the buffer
|
||||
// => internal for testing
|
||||
//
|
||||
// IMPORTANT: of explicit 'NTSnapshot' type instead of 'Snapshot'
|
||||
// interface because List<interface> allocates through boxing
|
||||
internal SortedList<double, NTSnapshot> serverSnapshots = new SortedList<double, NTSnapshot>();
|
||||
internal SortedList<double, NTSnapshot> clientSnapshots = new SortedList<double, NTSnapshot>();
|
||||
|
||||
// only convert the static Interpolation function to Func<T> once to
|
||||
// avoid allocations
|
||||
Func<NTSnapshot, NTSnapshot, double, NTSnapshot> Interpolate = NTSnapshot.Interpolate;
|
||||
|
||||
// for smooth interpolation, we need to interpolate along server time.
|
||||
// any other time (arrival on client, client local time, etc.) is not
|
||||
// going to give smooth results.
|
||||
double serverTimeline;
|
||||
double serverTimescale;
|
||||
|
||||
// catchup / slowdown adjustments are applied to timescale,
|
||||
// to be adjusted in every update instead of when receiving messages.
|
||||
double clientTimeline;
|
||||
double clientTimescale;
|
||||
|
||||
// only sync when changed hack /////////////////////////////////////////
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
[Header("Sync Only If Changed")]
|
||||
[Tooltip("When true, changes are not sent unless greater than sensitivity values below.")]
|
||||
public bool onlySyncOnChange = true;
|
||||
|
||||
// 3 was original, but testing under really bad network conditions, 2%-5% packet loss and 250-1200ms ping, 5 proved to eliminate any twitching.
|
||||
[Tooltip("How much time, as a multiple of send interval, has passed before clearing buffers.")]
|
||||
public float bufferResetMultiplier = 5;
|
||||
|
||||
[Header("Sensitivity"), Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
|
||||
public float positionSensitivity = 0.01f;
|
||||
public float rotationSensitivity = 0.01f;
|
||||
public float scaleSensitivity = 0.01f;
|
||||
|
||||
protected bool positionChanged;
|
||||
protected bool rotationChanged;
|
||||
protected bool scaleChanged;
|
||||
|
||||
// Used to store last sent snapshots
|
||||
protected NTSnapshot lastSnapshot;
|
||||
protected bool cachedSnapshotComparison;
|
||||
protected bool hasSentUnchangedPosition;
|
||||
#endif
|
||||
// selective sync //////////////////////////////////////////////////////
|
||||
[Header("Selective Sync & interpolation")]
|
||||
public bool syncPosition = true;
|
||||
public bool syncRotation = true;
|
||||
public bool syncScale = false; // rare. off by default.
|
||||
|
||||
double lastClientSendTime;
|
||||
double lastServerSendTime;
|
||||
|
||||
// debugging ///////////////////////////////////////////////////////////
|
||||
[Header("Debug")]
|
||||
public bool showGizmos;
|
||||
public bool showOverlay;
|
||||
public Color overlayColor = new Color(0, 0, 0, 0.5f);
|
||||
|
||||
// initialization //////////////////////////////////////////////////////
|
||||
// make sure to call this when inheriting too!
|
||||
protected virtual void Awake()
|
||||
{
|
||||
// initialize EMA with 'emaDuration' seconds worth of history.
|
||||
// 1 second holds 'sendRate' worth of values.
|
||||
// multiplied by emaDuration gives n-seconds.
|
||||
serverDriftEma = new ExponentialMovingAverage(sendRate * driftEmaDuration);
|
||||
clientDriftEma = new ExponentialMovingAverage(sendRate * driftEmaDuration);
|
||||
serverDeliveryTimeEma = new ExponentialMovingAverage(sendRate * deliveryTimeEmaDuration);
|
||||
clientDeliveryTimeEma = new ExponentialMovingAverage(sendRate * deliveryTimeEmaDuration);
|
||||
}
|
||||
|
||||
// BIGBOX CHANGE
|
||||
// need a way to force a sync after a given time duration.
|
||||
// useful if objects send at low frequencies like 100ms, so that we can have the first sync after spawn to be
|
||||
// after 50ms to avoid delays, e.g. for the explosion shards after throwing a water melon to the ground.
|
||||
public void ForceSync()
|
||||
{
|
||||
lastServerSendTime = 0;
|
||||
lastClientSendTime = 0;
|
||||
}
|
||||
// END BIGBOX CHANGE
|
||||
|
||||
// snapshot functions //////////////////////////////////////////////////
|
||||
// construct a snapshot of the current state
|
||||
// => internal for testing
|
||||
protected virtual NTSnapshot ConstructSnapshot()
|
||||
{
|
||||
// NetworkTime.localTime for double precision until Unity has it too
|
||||
return new NTSnapshot(
|
||||
// our local time is what the other end uses as remote time
|
||||
NetworkTime.localTime,
|
||||
// the other end fills out local time itself
|
||||
0,
|
||||
target.localPosition,
|
||||
target.localRotation,
|
||||
target.localScale
|
||||
);
|
||||
}
|
||||
|
||||
// apply a snapshot to the Transform.
|
||||
// -> start, end, interpolated are all passed in caes they are needed
|
||||
// -> a regular game would apply the 'interpolated' snapshot
|
||||
// -> a board game might want to jump to 'goal' directly
|
||||
// (it's easier to always interpolate and then apply selectively,
|
||||
// instead of manually interpolating x, y, z, ... depending on flags)
|
||||
// => internal for testing
|
||||
//
|
||||
// NOTE: stuck detection is unnecessary here.
|
||||
// we always set transform.position anyway, we can't get stuck.
|
||||
protected virtual void ApplySnapshot(NTSnapshot interpolated)
|
||||
{
|
||||
// local position/rotation for VR support
|
||||
//
|
||||
// if syncPosition/Rotation/Scale is disabled then we received nulls
|
||||
// -> current position/rotation/scale would've been added as snapshot
|
||||
// -> we still interpolated
|
||||
// -> but simply don't apply it. if the user doesn't want to sync
|
||||
// scale, then we should not touch scale etc.
|
||||
if (syncPosition)
|
||||
target.localPosition = interpolated.position;
|
||||
|
||||
if (syncRotation)
|
||||
target.localRotation = interpolated.rotation;
|
||||
|
||||
if (syncScale)
|
||||
target.localScale = interpolated.scale;
|
||||
}
|
||||
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
// Returns true if position, rotation AND scale are unchanged, within given sensitivity range.
|
||||
protected virtual bool CompareSnapshots(NTSnapshot currentSnapshot)
|
||||
{
|
||||
positionChanged = Vector3.SqrMagnitude(lastSnapshot.position - currentSnapshot.position) > positionSensitivity * positionSensitivity;
|
||||
rotationChanged = Quaternion.Angle(lastSnapshot.rotation, currentSnapshot.rotation) > rotationSensitivity;
|
||||
scaleChanged = Vector3.SqrMagnitude(lastSnapshot.scale - currentSnapshot.scale) > scaleSensitivity * scaleSensitivity;
|
||||
|
||||
return (!positionChanged && !rotationChanged && !scaleChanged);
|
||||
}
|
||||
#endif
|
||||
// cmd /////////////////////////////////////////////////////////////////
|
||||
// only unreliable. see comment above of this file.
|
||||
[Command(channel = Channels.Unreliable)]
|
||||
void CmdClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale)
|
||||
{
|
||||
OnClientToServerSync(position, rotation, scale);
|
||||
//For client authority, immediately pass on the client snapshot to all other
|
||||
//clients instead of waiting for server to send its snapshots.
|
||||
if (clientAuthority && connectionToClient != null) // BIGBOX CHANGE: DropPods are clientAuthority but don't count bots by excluding connectionToClient==null
|
||||
{
|
||||
RpcServerToClientSync(position, rotation, scale);
|
||||
}
|
||||
}
|
||||
|
||||
// local authority client sends sync message to server for broadcasting
|
||||
protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale)
|
||||
{
|
||||
// only apply if in client authority mode
|
||||
if (!clientAuthority) return;
|
||||
|
||||
// protect against ever growing buffer size attacks
|
||||
if (serverSnapshots.Count >= bufferSizeLimit) return;
|
||||
|
||||
// only player owned objects (with a connection) can send to
|
||||
// server. we can get the timestamp from the connection.
|
||||
double timestamp = connectionToClient.remoteTimeStamp;
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
if (onlySyncOnChange)
|
||||
{
|
||||
double timeIntervalCheck = bufferResetMultiplier * sendInterval;
|
||||
|
||||
if (serverSnapshots.Count > 0 && serverSnapshots.Values[serverSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// position, rotation, scale can have no value if same as last time.
|
||||
// saves bandwidth.
|
||||
// but we still need to feed it to snapshot interpolation. we can't
|
||||
// just have gaps in there if nothing has changed. for example, if
|
||||
// client sends snapshot at t=0
|
||||
// client sends nothing for 10s because not moved
|
||||
// client sends snapshot at t=10
|
||||
// then the server would assume that it's one super slow move and
|
||||
// replay it for 10 seconds.
|
||||
if (!position.HasValue) position = serverSnapshots.Count > 0 ? serverSnapshots.Values[serverSnapshots.Count - 1].position : target.localPosition;
|
||||
if (!rotation.HasValue) rotation = serverSnapshots.Count > 0 ? serverSnapshots.Values[serverSnapshots.Count - 1].rotation : target.localRotation;
|
||||
if (!scale.HasValue) scale = serverSnapshots.Count > 0 ? serverSnapshots.Values[serverSnapshots.Count - 1].scale : target.localScale;
|
||||
|
||||
// construct snapshot with batch timestamp to save bandwidth
|
||||
NTSnapshot snapshot = new NTSnapshot(
|
||||
timestamp,
|
||||
NetworkTime.localTime,
|
||||
position.Value, rotation.Value, scale.Value
|
||||
);
|
||||
|
||||
// (optional) dynamic adjustment
|
||||
if (dynamicAdjustment)
|
||||
{
|
||||
// set bufferTime on the fly.
|
||||
// shows in inspector for easier debugging :)
|
||||
bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
|
||||
sendInterval,
|
||||
serverDeliveryTimeEma.StandardDeviation,
|
||||
dynamicAdjustmentTolerance
|
||||
);
|
||||
// Debug.Log($"[Server]: {name} delivery std={serverDeliveryTimeEma.StandardDeviation} bufferTimeMult := {bufferTimeMultiplier} ");
|
||||
}
|
||||
|
||||
// insert into the server buffer & initialize / adjust / catchup
|
||||
SnapshotInterpolation.InsertAndAdjust(
|
||||
serverSnapshots,
|
||||
bufferSizeLimit,
|
||||
snapshot,
|
||||
ref serverTimeline,
|
||||
ref serverTimescale,
|
||||
sendInterval,
|
||||
bufferTime,
|
||||
catchupSpeed,
|
||||
slowdownSpeed,
|
||||
ref serverDriftEma,
|
||||
catchupNegativeThreshold,
|
||||
catchupPositiveThreshold,
|
||||
ref serverDeliveryTimeEma
|
||||
);
|
||||
}
|
||||
|
||||
// rpc /////////////////////////////////////////////////////////////////
|
||||
// only unreliable. see comment above of this file.
|
||||
[ClientRpc(channel = Channels.Unreliable)]
|
||||
void RpcServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale) =>
|
||||
OnServerToClientSync(position, rotation, scale);
|
||||
|
||||
// server broadcasts sync message to all clients
|
||||
protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale)
|
||||
{
|
||||
// in host mode, the server sends rpcs to all clients.
|
||||
// the host client itself will receive them too.
|
||||
// -> host server is always the source of truth
|
||||
// -> we can ignore any rpc on the host client
|
||||
// => otherwise host objects would have ever growing clientBuffers
|
||||
// (rpc goes to clients. if isServer is true too then we are host)
|
||||
if (isServer) return;
|
||||
|
||||
// don't apply for local player with authority
|
||||
if (IsClientWithAuthority) return;
|
||||
|
||||
// protect against ever growing buffer size attacks
|
||||
if (clientSnapshots.Count >= bufferSizeLimit) return;
|
||||
|
||||
// on the client, we receive rpcs for all entities.
|
||||
// not all of them have a connectionToServer.
|
||||
// but all of them go through NetworkClient.connection.
|
||||
// we can get the timestamp from there.
|
||||
double timestamp = NetworkClient.connection.remoteTimeStamp;
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
if (onlySyncOnChange)
|
||||
{
|
||||
double timeIntervalCheck = bufferResetMultiplier * sendInterval;
|
||||
|
||||
if (clientSnapshots.Count > 0 && clientSnapshots.Values[clientSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// position, rotation, scale can have no value if same as last time.
|
||||
// saves bandwidth.
|
||||
// but we still need to feed it to snapshot interpolation. we can't
|
||||
// just have gaps in there if nothing has changed. for example, if
|
||||
// client sends snapshot at t=0
|
||||
// client sends nothing for 10s because not moved
|
||||
// client sends snapshot at t=10
|
||||
// then the server would assume that it's one super slow move and
|
||||
// replay it for 10 seconds.
|
||||
if (!position.HasValue) position = clientSnapshots.Count > 0 ? clientSnapshots.Values[clientSnapshots.Count - 1].position : target.localPosition;
|
||||
if (!rotation.HasValue) rotation = clientSnapshots.Count > 0 ? clientSnapshots.Values[clientSnapshots.Count - 1].rotation : target.localRotation;
|
||||
if (!scale.HasValue) scale = clientSnapshots.Count > 0 ? clientSnapshots.Values[clientSnapshots.Count - 1].scale : target.localScale;
|
||||
|
||||
// construct snapshot with batch timestamp to save bandwidth
|
||||
NTSnapshot snapshot = new NTSnapshot(
|
||||
timestamp,
|
||||
NetworkTime.localTime,
|
||||
position.Value, rotation.Value, scale.Value
|
||||
);
|
||||
|
||||
// (optional) dynamic adjustment
|
||||
if (dynamicAdjustment)
|
||||
{
|
||||
// set bufferTime on the fly.
|
||||
// shows in inspector for easier debugging :)
|
||||
bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
|
||||
sendInterval,
|
||||
clientDeliveryTimeEma.StandardDeviation,
|
||||
dynamicAdjustmentTolerance
|
||||
);
|
||||
// Debug.Log($"[Client]: {name} delivery std={clientDeliveryTimeEma.StandardDeviation} bufferTimeMult := {bufferTimeMultiplier} ");
|
||||
}
|
||||
|
||||
// insert into the client buffer & initialize / adjust / catchup
|
||||
SnapshotInterpolation.InsertAndAdjust(
|
||||
clientSnapshots,
|
||||
bufferSizeLimit,
|
||||
snapshot,
|
||||
ref clientTimeline,
|
||||
ref clientTimescale,
|
||||
sendInterval,
|
||||
bufferTime,
|
||||
catchupSpeed,
|
||||
slowdownSpeed,
|
||||
ref clientDriftEma,
|
||||
catchupNegativeThreshold,
|
||||
catchupPositiveThreshold,
|
||||
ref clientDeliveryTimeEma
|
||||
);
|
||||
}
|
||||
|
||||
// update //////////////////////////////////////////////////////////////
|
||||
void UpdateServerBroadcast() // V78 improvements
|
||||
{
|
||||
// broadcast to all clients each 'sendInterval'
|
||||
// (client with authority will drop the rpc)
|
||||
// NetworkTime.localTime for double precision until Unity has it too
|
||||
//
|
||||
// IMPORTANT:
|
||||
// snapshot interpolation requires constant sending.
|
||||
// DO NOT only send if position changed. for example:
|
||||
// ---
|
||||
// * client sends first position at t=0
|
||||
// * ... 10s later ...
|
||||
// * client moves again, sends second position at t=10
|
||||
// ---
|
||||
// * server gets first position at t=0
|
||||
// * server gets second position at t=10
|
||||
// * server moves from first to second within a time of 10s
|
||||
// => would be a super slow move, instead of a wait & move.
|
||||
//
|
||||
// IMPORTANT:
|
||||
// DO NOT send nulls if not changed 'since last send' either. we
|
||||
// send unreliable and don't know which 'last send' the other end
|
||||
// received successfully.
|
||||
//
|
||||
// Checks to ensure server only sends snapshots if object is
|
||||
// on server authority(!clientAuthority) mode because on client
|
||||
// authority mode snapshots are broadcasted right after the authoritative
|
||||
// client updates server in the command function(see above), OR,
|
||||
// since host does not send anything to update the server, any client
|
||||
// authoritative movement done by the host will have to be broadcasted
|
||||
// here by checking IsClientWithAuthority.
|
||||
|
||||
if (NetworkTime.localTime >= lastServerSendTime + sendInterval &&
|
||||
(!(clientAuthority && connectionToClient != null) || IsClientWithAuthority)) // BIGBOX CHANGE: DropPods are clientAuthority but don't count bots by excluding connectionToClient==null
|
||||
{
|
||||
// send snapshot without timestamp.
|
||||
// receiver gets it from batch timestamp to save bandwidth.
|
||||
NTSnapshot snapshot = ConstructSnapshot();
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
cachedSnapshotComparison = CompareSnapshots(snapshot);
|
||||
if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; }
|
||||
#endif
|
||||
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
RpcServerToClientSync(
|
||||
// only sync what the user wants to sync
|
||||
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
|
||||
syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?),
|
||||
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
|
||||
);
|
||||
#else
|
||||
RpcServerToClientSync(
|
||||
// only sync what the user wants to sync
|
||||
syncPosition ? snapshot.position : default(Vector3?),
|
||||
syncRotation ? snapshot.rotation : default(Quaternion?),
|
||||
syncScale ? snapshot.scale : default(Vector3?)
|
||||
);
|
||||
#endif
|
||||
|
||||
lastServerSendTime = NetworkTime.localTime;
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
if (cachedSnapshotComparison)
|
||||
{
|
||||
hasSentUnchangedPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasSentUnchangedPosition = false;
|
||||
lastSnapshot = snapshot;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateServerInterpolation() // V78 improvements
|
||||
{
|
||||
// apply buffered snapshots IF client authority
|
||||
// -> in server authority, server moves the object
|
||||
// so no need to apply any snapshots there.
|
||||
// -> don't apply for host mode player objects either, even if in
|
||||
// client authority mode. if it doesn't go over the network,
|
||||
// then we don't need to do anything.
|
||||
if ((clientAuthority && connectionToClient != null) && !isOwned) // BIGBOX CHANGE: DropPods are clientAuthority but don't count bots by excluding connectionToClient==null
|
||||
{
|
||||
if (serverSnapshots.Count > 0)
|
||||
{
|
||||
// compute snapshot interpolation & apply if any was spit out
|
||||
SnapshotInterpolation.Step(
|
||||
serverSnapshots,
|
||||
Time.unscaledDeltaTime,
|
||||
ref serverTimeline,
|
||||
serverTimescale,
|
||||
out NTSnapshot from,
|
||||
out NTSnapshot to,
|
||||
out double t);
|
||||
{
|
||||
NTSnapshot computed = Interpolate(from, to, t);
|
||||
ApplySnapshot(computed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateClientBroadcast() // V78 improvements
|
||||
{
|
||||
// https://github.com/vis2k/Mirror/pull/2992/
|
||||
if (!NetworkClient.ready) return;
|
||||
|
||||
// send to server each 'sendInterval'
|
||||
// NetworkTime.localTime for double precision until Unity has it too
|
||||
//
|
||||
// IMPORTANT:
|
||||
// snapshot interpolation requires constant sending.
|
||||
// DO NOT only send if position changed. for example:
|
||||
// ---
|
||||
// * client sends first position at t=0
|
||||
// * ... 10s later ...
|
||||
// * client moves again, sends second position at t=10
|
||||
// ---
|
||||
// * server gets first position at t=0
|
||||
// * server gets second position at t=10
|
||||
// * server moves from first to second within a time of 10s
|
||||
// => would be a super slow move, instead of a wait & move.
|
||||
//
|
||||
// IMPORTANT:
|
||||
// DO NOT send nulls if not changed 'since last send' either. we
|
||||
// send unreliable and don't know which 'last send' the other end
|
||||
// received successfully.
|
||||
if (NetworkTime.localTime >= lastClientSendTime + sendInterval)
|
||||
{
|
||||
// send snapshot without timestamp.
|
||||
// receiver gets it from batch timestamp to save bandwidth.
|
||||
NTSnapshot snapshot = ConstructSnapshot();
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
cachedSnapshotComparison = CompareSnapshots(snapshot);
|
||||
if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; }
|
||||
#endif
|
||||
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
CmdClientToServerSync(
|
||||
// only sync what the user wants to sync
|
||||
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
|
||||
syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?),
|
||||
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
|
||||
);
|
||||
#else
|
||||
CmdClientToServerSync(
|
||||
// only sync what the user wants to sync
|
||||
syncPosition ? snapshot.position : default(Vector3?),
|
||||
syncRotation ? snapshot.rotation : default(Quaternion?),
|
||||
syncScale ? snapshot.scale : default(Vector3?)
|
||||
);
|
||||
#endif
|
||||
|
||||
lastClientSendTime = NetworkTime.localTime;
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
if (cachedSnapshotComparison)
|
||||
{
|
||||
hasSentUnchangedPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasSentUnchangedPosition = false;
|
||||
lastSnapshot = snapshot;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateClientInterpolation() // V78 improvements
|
||||
{
|
||||
// client authority, and local player (= allowed to move myself)?
|
||||
if (IsClientWithAuthority)
|
||||
return;
|
||||
|
||||
if (clientSnapshots.Count > 0)
|
||||
{
|
||||
// compute snapshot interpolation & apply if any was spit out
|
||||
SnapshotInterpolation.Step(
|
||||
clientSnapshots,
|
||||
Time.unscaledDeltaTime,
|
||||
ref clientTimeline,
|
||||
clientTimescale,
|
||||
out NTSnapshot from,
|
||||
out NTSnapshot to,
|
||||
out double t);
|
||||
{
|
||||
NTSnapshot computed = Interpolate(from, to, t);
|
||||
ApplySnapshot(computed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// V78 improvements
|
||||
// Update applies interpolation
|
||||
protected virtual void Update()
|
||||
{
|
||||
// if server then always sync to others.
|
||||
if (isServer) UpdateServerInterpolation();
|
||||
// 'else if' because host mode shouldn't send anything to server.
|
||||
// it is the server. don't overwrite anything there.
|
||||
else if (isClient) UpdateClientInterpolation();
|
||||
}
|
||||
|
||||
// V78 improvements
|
||||
// LateUpdate broadcasts.
|
||||
// movement scripts may change positions in Update.
|
||||
// use LateUpdate to ensure changes are detected in the same frame.
|
||||
// otherwise this may run before user update, delaying detection until next frame.
|
||||
// this could cause visible jitter.
|
||||
void LateUpdate()
|
||||
{
|
||||
// if server then always sync to others.
|
||||
if (isServer) UpdateServerBroadcast();
|
||||
// client authority, and local player (= allowed to move myself)?
|
||||
// 'else if' because host mode shouldn't send anything to server.
|
||||
// it is the server. don't overwrite anything there.
|
||||
else if (isClient && IsClientWithAuthority) UpdateClientBroadcast();
|
||||
}
|
||||
|
||||
// common Teleport code for client->server and server->client
|
||||
protected virtual void OnTeleport(Vector3 destination)
|
||||
{
|
||||
// reset any in-progress interpolation & buffers
|
||||
Reset();
|
||||
|
||||
// set the new position.
|
||||
// interpolation will automatically continue.
|
||||
target.position = destination;
|
||||
|
||||
// TODO
|
||||
// what if we still receive a snapshot from before the interpolation?
|
||||
// it could easily happen over unreliable.
|
||||
// -> maybe add destination as first entry?
|
||||
}
|
||||
|
||||
// common Teleport code for client->server and server->client
|
||||
protected virtual void OnTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
// reset any in-progress interpolation & buffers
|
||||
Reset();
|
||||
|
||||
// set the new position.
|
||||
// interpolation will automatically continue.
|
||||
target.position = destination;
|
||||
target.rotation = rotation;
|
||||
|
||||
// TODO
|
||||
// what if we still receive a snapshot from before the interpolation?
|
||||
// it could easily happen over unreliable.
|
||||
// -> maybe add destination as first entry?
|
||||
}
|
||||
|
||||
// server->client teleport to force position without interpolation.
|
||||
// otherwise it would interpolate to a (far away) new position.
|
||||
// => manually calling Teleport is the only 100% reliable solution.
|
||||
[ClientRpc]
|
||||
public void RpcTeleport(Vector3 destination)
|
||||
{
|
||||
// NOTE: even in client authority mode, the server is always allowed
|
||||
// to teleport the player. for example:
|
||||
// * CmdEnterPortal() might teleport the player
|
||||
// * Some people use client authority with server sided checks
|
||||
// so the server should be able to reset position if needed.
|
||||
|
||||
// TODO what about host mode?
|
||||
OnTeleport(destination);
|
||||
}
|
||||
|
||||
// server->client teleport to force position and rotation without interpolation.
|
||||
// otherwise it would interpolate to a (far away) new position.
|
||||
// => manually calling Teleport is the only 100% reliable solution.
|
||||
[ClientRpc]
|
||||
public void RpcTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
// NOTE: even in client authority mode, the server is always allowed
|
||||
// to teleport the player. for example:
|
||||
// * CmdEnterPortal() might teleport the player
|
||||
// * Some people use client authority with server sided checks
|
||||
// so the server should be able to reset position if needed.
|
||||
|
||||
// TODO what about host mode?
|
||||
OnTeleport(destination, rotation);
|
||||
}
|
||||
|
||||
// client->server teleport to force position without interpolation.
|
||||
// otherwise it would interpolate to a (far away) new position.
|
||||
// => manually calling Teleport is the only 100% reliable solution.
|
||||
[Command]
|
||||
public void CmdTeleport(Vector3 destination)
|
||||
{
|
||||
// client can only teleport objects that it has authority over.
|
||||
if (!clientAuthority) return;
|
||||
|
||||
// TODO what about host mode?
|
||||
OnTeleport(destination);
|
||||
|
||||
// if a client teleports, we need to broadcast to everyone else too
|
||||
// TODO the teleported client should ignore the rpc though.
|
||||
// otherwise if it already moved again after teleporting,
|
||||
// the rpc would come a little bit later and reset it once.
|
||||
// TODO or not? if client ONLY calls Teleport(pos), the position
|
||||
// would only be set after the rpc. unless the client calls
|
||||
// BOTH Teleport(pos) and target.position=pos
|
||||
RpcTeleport(destination);
|
||||
}
|
||||
|
||||
// client->server teleport to force position and rotation without interpolation.
|
||||
// otherwise it would interpolate to a (far away) new position.
|
||||
// => manually calling Teleport is the only 100% reliable solution.
|
||||
[Command]
|
||||
public void CmdTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
// client can only teleport objects that it has authority over.
|
||||
if (!clientAuthority) return;
|
||||
|
||||
// TODO what about host mode?
|
||||
OnTeleport(destination, rotation);
|
||||
|
||||
// if a client teleports, we need to broadcast to everyone else too
|
||||
// TODO the teleported client should ignore the rpc though.
|
||||
// otherwise if it already moved again after teleporting,
|
||||
// the rpc would come a little bit later and reset it once.
|
||||
// TODO or not? if client ONLY calls Teleport(pos), the position
|
||||
// would only be set after the rpc. unless the client calls
|
||||
// BOTH Teleport(pos) and target.position=pos
|
||||
RpcTeleport(destination, rotation);
|
||||
}
|
||||
|
||||
// BIGBOX CHANGE ///////////////////////////////////////////////////////////
|
||||
// TODO: WHY DOESN'T MIRROR HAVE THIS???
|
||||
[Server]
|
||||
public void ServerTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
this.OnTeleport(destination, rotation);
|
||||
this.RpcTeleport(destination, rotation);
|
||||
}
|
||||
// END BIGBOX CHANGE ///////////////////////////////////////////////////////
|
||||
|
||||
public virtual void Reset()
|
||||
{
|
||||
// disabled objects aren't updated anymore.
|
||||
// so let's clear the buffers.
|
||||
serverSnapshots.Clear();
|
||||
clientSnapshots.Clear();
|
||||
|
||||
// reset interpolation time too so we start at t=0 next time
|
||||
serverTimeline = 0;
|
||||
serverTimescale = 0;
|
||||
clientTimeline = 0;
|
||||
clientTimescale = 0;
|
||||
}
|
||||
|
||||
protected virtual void OnDisable() => Reset();
|
||||
protected virtual void OnEnable() => Reset();
|
||||
|
||||
protected virtual void OnValidate()
|
||||
{
|
||||
// set target to self if none yet
|
||||
if (target == null) target = transform;
|
||||
|
||||
// thresholds need to be <0 and >0
|
||||
catchupNegativeThreshold = Math.Min(catchupNegativeThreshold, 0);
|
||||
catchupPositiveThreshold = Math.Max(catchupPositiveThreshold, 0);
|
||||
|
||||
// buffer limit should be at least multiplier to have enough in there
|
||||
bufferSizeLimit = Mathf.Max((int)bufferTimeMultiplier, bufferSizeLimit);
|
||||
}
|
||||
|
||||
public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
{
|
||||
// sync target component's position on spawn.
|
||||
// fixes https://github.com/vis2k/Mirror/pull/3051/
|
||||
// (Spawn message wouldn't sync NTChild positions either)
|
||||
if (initialState)
|
||||
{
|
||||
if (syncPosition) writer.WriteVector3(target.localPosition);
|
||||
if (syncRotation) writer.WriteQuaternion(target.localRotation);
|
||||
if (syncScale) writer.WriteVector3(target.localScale);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
{
|
||||
// sync target component's position on spawn.
|
||||
// fixes https://github.com/vis2k/Mirror/pull/3051/
|
||||
// (Spawn message wouldn't sync NTChild positions either)
|
||||
if (initialState)
|
||||
{
|
||||
if (syncPosition) target.localPosition = reader.ReadVector3();
|
||||
if (syncRotation) target.localRotation = reader.ReadQuaternion();
|
||||
if (syncScale) target.localScale = reader.ReadVector3();
|
||||
}
|
||||
}
|
||||
|
||||
// OnGUI allocates even if it does nothing. avoid in release.
|
||||
#if UNITY_EDITOR
|
||||
// debug ///////////////////////////////////////////////////////////////
|
||||
protected virtual void OnGUI()
|
||||
{
|
||||
if (!showOverlay) return;
|
||||
|
||||
// show data next to player for easier debugging. this is very useful!
|
||||
// IMPORTANT: this is basically an ESP hack for shooter games.
|
||||
// DO NOT make this available with a hotkey in release builds
|
||||
if (!Debug.isDebugBuild) return;
|
||||
|
||||
// project position to screen
|
||||
Vector3 point = Camera.main.WorldToScreenPoint(target.position);
|
||||
|
||||
// enough alpha, in front of camera and in screen?
|
||||
if (point.z >= 0 && Utils.IsPointInScreen(point))
|
||||
{
|
||||
GUI.color = overlayColor;
|
||||
GUILayout.BeginArea(new Rect(point.x, Screen.height - point.y, 200, 100));
|
||||
|
||||
// always show both client & server buffers so it's super
|
||||
// obvious if we accidentally populate both.
|
||||
if (serverSnapshots.Count > 0)
|
||||
{
|
||||
GUILayout.Label($"Server Buffer:{serverSnapshots.Count}");
|
||||
GUILayout.Label($"Server Timescale:{serverTimescale * 100:F2}%");
|
||||
}
|
||||
|
||||
if (clientSnapshots.Count > 0)
|
||||
{
|
||||
GUILayout.Label($"Client Buffer:{clientSnapshots.Count}");
|
||||
GUILayout.Label($"Client Timescale:{clientTimescale * 100:F2}%");
|
||||
}
|
||||
|
||||
GUILayout.EndArea();
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void DrawGizmos(SortedList<double, NTSnapshot> buffer)
|
||||
{
|
||||
// only draw if we have at least two entries
|
||||
if (buffer.Count < 2) return;
|
||||
|
||||
// calculate threshold for 'old enough' snapshots
|
||||
double threshold = NetworkTime.localTime - bufferTime;
|
||||
Color oldEnoughColor = new Color(0, 1, 0, 0.5f);
|
||||
Color notOldEnoughColor = new Color(0.5f, 0.5f, 0.5f, 0.3f);
|
||||
|
||||
// draw the whole buffer for easier debugging.
|
||||
// it's worth seeing how much we have buffered ahead already
|
||||
for (int i = 0; i < buffer.Count; ++i)
|
||||
{
|
||||
// color depends on if old enough or not
|
||||
NTSnapshot entry = buffer.Values[i];
|
||||
bool oldEnough = entry.localTime <= threshold;
|
||||
Gizmos.color = oldEnough ? oldEnoughColor : notOldEnoughColor;
|
||||
Gizmos.DrawCube(entry.position, Vector3.one);
|
||||
}
|
||||
|
||||
// extra: lines between start<->position<->goal
|
||||
Gizmos.color = Color.green;
|
||||
Gizmos.DrawLine(buffer.Values[0].position, target.position);
|
||||
Gizmos.color = Color.white;
|
||||
Gizmos.DrawLine(target.position, buffer.Values[1].position);
|
||||
}
|
||||
|
||||
protected virtual void OnDrawGizmos()
|
||||
{
|
||||
// This fires in edit mode but that spams NRE's so check isPlaying
|
||||
if (!Application.isPlaying) return;
|
||||
if (!showGizmos) return;
|
||||
|
||||
if (isServer) DrawGizmos(serverSnapshots);
|
||||
if (isClient) DrawGizmos(clientSnapshots);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de975b862c7349b48b588741d69495c9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,64 @@
|
||||
// snapshot for snapshot interpolation
|
||||
// https://gafferongames.com/post/snapshot_interpolation/
|
||||
// position, rotation, scale for compatibility for now.
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// NetworkTransform Snapshot
|
||||
public struct NTSnapshot : Snapshot
|
||||
{
|
||||
// time or sequence are needed to throw away older snapshots.
|
||||
//
|
||||
// glenn fiedler starts with a 16 bit sequence number.
|
||||
// supposedly this is meant as a simplified example.
|
||||
// in the end we need the remote timestamp for accurate interpolation
|
||||
// and buffering over time.
|
||||
//
|
||||
// note: in theory, IF server sends exactly(!) at the same interval then
|
||||
// the 16 bit ushort timestamp would be enough to calculate the
|
||||
// remote time (sequence * sendInterval). but Unity's update is
|
||||
// not guaranteed to run on the exact intervals / do catchup.
|
||||
// => remote timestamp is better for now
|
||||
//
|
||||
// [REMOTE TIME, NOT LOCAL TIME]
|
||||
// => DOUBLE for long term accuracy & batching gives us double anyway
|
||||
public double remoteTime { get; set; }
|
||||
// the local timestamp (when we received it)
|
||||
// used to know if the first two snapshots are old enough to start.
|
||||
public double localTime { get; set; }
|
||||
|
||||
public Vector3 position;
|
||||
public Quaternion rotation;
|
||||
public Vector3 scale;
|
||||
|
||||
public NTSnapshot(double remoteTime, double localTime, Vector3 position, Quaternion rotation, Vector3 scale)
|
||||
{
|
||||
this.remoteTime = remoteTime;
|
||||
this.localTime = localTime;
|
||||
this.position = position;
|
||||
this.rotation = rotation;
|
||||
this.scale = scale;
|
||||
}
|
||||
|
||||
public static NTSnapshot Interpolate(NTSnapshot from, NTSnapshot to, double t)
|
||||
{
|
||||
// NOTE:
|
||||
// Vector3 & Quaternion components are float anyway, so we can
|
||||
// keep using the functions with 't' as float instead of double.
|
||||
return new NTSnapshot(
|
||||
// interpolated snapshot is applied directly. don't need timestamps.
|
||||
0, 0,
|
||||
// lerp position/rotation/scale unclamped in case we ever need
|
||||
// to extrapolate. atm SnapshotInterpolation never does.
|
||||
Vector3.LerpUnclamped(from.position, to.position, (float)t),
|
||||
// IMPORTANT: LerpUnclamped(0, 60, 1.5) extrapolates to ~86.
|
||||
// SlerpUnclamped(0, 60, 1.5) extrapolates to 90!
|
||||
// (0, 90, 1.5) is even worse. for Lerp.
|
||||
// => Slerp works way better for our euler angles.
|
||||
Quaternion.SlerpUnclamped(from.rotation, to.rotation, (float)t),
|
||||
Vector3.LerpUnclamped(from.scale, to.scale, (float)t)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b48b47198bd0f7243be9087c81027633
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
Loading…
Reference in New Issue
Block a user