Merged master

This commit is contained in:
MrGadget 2024-01-22 06:27:52 -05:00
commit ceaa63dad1
31 changed files with 686 additions and 299 deletions

View File

@ -25,7 +25,7 @@ int GetVisRange(NetworkIdentity identity)
}
[ServerCallback]
public override void Reset()
public override void ResetState()
{
lastRebuildTime = 0D;
CustomRanges.Clear();

View File

@ -11,6 +11,12 @@ public class NetworkMatch : NetworkBehaviour
{
Guid _matchId;
#pragma warning disable IDE0052 // Suppress warning for unused field...this is for debugging purposes
[SerializeField, ReadOnly]
[Tooltip("Match ID is shown here on server for debugging purposes.")]
string MatchID = string.Empty;
#pragma warning restore IDE0052
///<summary>Set this to the same value on all networked objects that belong to a given match</summary>
public Guid matchId
{
@ -25,6 +31,7 @@ public Guid matchId
Guid oldMatch = _matchId;
_matchId = value;
MatchID = value.ToString();
// Only inform the AOI if this netIdentity has been spawned (isServer) and only if using a MatchInterestManagement
if (isServer && NetworkServer.aoi is MatchInterestManagement matchInterestManagement)

View File

@ -72,7 +72,7 @@ public override void OnRebuildObservers(NetworkIdentity identity, HashSet<Networ
}
[ServerCallback]
public override void Reset()
public override void ResetState()
{
lastRebuildTime = 0D;
}

View File

@ -352,7 +352,7 @@ void ReadParameters(NetworkReader reader)
byte parameterCount = reader.ReadByte();
if (parameterCount != parameters.Length)
{
Debug.LogError($"NetworkAnimator: serialized parameter count={parameterCount} does not match expected parameter count={parameters.Length}. Are you changing animators at runtime?");
Debug.LogError($"NetworkAnimator: serialized parameter count={parameterCount} does not match expected parameter count={parameters.Length}. Are you changing animators at runtime?", gameObject);
return;
}
@ -423,7 +423,7 @@ public override void OnDeserialize(NetworkReader reader, bool initialState)
byte layerCount = reader.ReadByte();
if (layerCount != animator.layerCount)
{
Debug.LogError($"NetworkAnimator: serialized layer count={layerCount} does not match expected layer count={animator.layerCount}. Are you changing animators at runtime?");
Debug.LogError($"NetworkAnimator: serialized layer count={layerCount} does not match expected layer count={animator.layerCount}. Are you changing animators at runtime?", gameObject);
return;
}
@ -461,13 +461,13 @@ public void SetTrigger(int hash)
{
if (!isClient)
{
Debug.LogWarning("Tried to set animation in the server for a client-controlled animator");
Debug.LogWarning("Tried to set animation in the server for a client-controlled animator", gameObject);
return;
}
if (!isOwned)
{
Debug.LogWarning("Only the client with authority can set animations");
Debug.LogWarning("Only the client with authority can set animations", gameObject);
return;
}
@ -481,7 +481,7 @@ public void SetTrigger(int hash)
{
if (!isServer)
{
Debug.LogWarning("Tried to set animation in the client for a server-controlled animator");
Debug.LogWarning("Tried to set animation in the client for a server-controlled animator", gameObject);
return;
}
@ -508,13 +508,13 @@ public void ResetTrigger(int hash)
{
if (!isClient)
{
Debug.LogWarning("Tried to reset animation in the server for a client-controlled animator");
Debug.LogWarning("Tried to reset animation in the server for a client-controlled animator", gameObject);
return;
}
if (!isOwned)
{
Debug.LogWarning("Only the client with authority can reset animations");
Debug.LogWarning("Only the client with authority can reset animations", gameObject);
return;
}
@ -528,7 +528,7 @@ public void ResetTrigger(int hash)
{
if (!isServer)
{
Debug.LogWarning("Tried to reset animation in the client for a server-controlled animator");
Debug.LogWarning("Tried to reset animation in the client for a server-controlled animator", gameObject);
return;
}

View File

@ -311,9 +311,9 @@ public void RpcTeleport(Vector3 destination, Quaternion rotation)
}
[ClientRpc]
void RpcReset()
void RpcResetState()
{
Reset();
ResetState();
}
// common Teleport code for client->server and server->client
@ -367,7 +367,7 @@ protected virtual void OnTeleport(Vector3 destination, Quaternion rotation)
// -> maybe add destination as first entry?
}
public virtual void Reset()
public virtual void ResetState()
{
// disabled objects aren't updated anymore.
// so let's clear the buffers.
@ -375,9 +375,16 @@ public virtual void Reset()
clientSnapshots.Clear();
}
public virtual void Reset()
{
ResetState();
// default to ClientToServer so this works immediately for users
syncDirection = SyncDirection.ClientToServer;
}
protected virtual void OnEnable()
{
Reset();
ResetState();
if (NetworkServer.active)
NetworkIdentity.clientAuthorityCallback += OnClientAuthorityChanged;
@ -385,7 +392,7 @@ protected virtual void OnEnable()
protected virtual void OnDisable()
{
Reset();
ResetState();
if (NetworkServer.active)
NetworkIdentity.clientAuthorityCallback -= OnClientAuthorityChanged;
@ -403,8 +410,8 @@ void OnClientAuthorityChanged(NetworkConnectionToClient conn, NetworkIdentity id
if (syncDirection == SyncDirection.ClientToServer)
{
Reset();
RpcReset();
ResetState();
RpcResetState();
}
}

View File

@ -402,9 +402,9 @@ static void RewriteHistory(
// reset state for next session.
// do not ever call this during a session (i.e. after teleport).
// calling this will break delta compression.
public override void Reset()
public override void ResetState()
{
base.Reset();
base.ResetState();
// reset delta
lastSerializedPosition = Vector3Long.zero;

View File

@ -346,7 +346,7 @@ protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotat
double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkClient.sendInterval;
if (serverSnapshots.Count > 0 && serverSnapshots.Values[serverSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
Reset();
ResetState();
}
AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
@ -401,7 +401,7 @@ protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotat
double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkServer.sendInterval;
if (clientSnapshots.Count > 0 && clientSnapshots.Values[clientSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
Reset();
ResetState();
}
AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);

View File

@ -7,8 +7,10 @@ Material:
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: GhostMaterial
m_Name: LocalGhostMaterial
m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords:
- _ALPHAPREMULTIPLY_ON
m_InvalidKeywords: []
@ -19,6 +21,7 @@ Material:
stringTagMap:
RenderType: Transparent
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
@ -77,6 +80,6 @@ Material:
- _UVSec: 0
- _ZWrite: 0
m_Colors:
- _Color: {r: 1, g: 1, b: 1, a: 0.11764706}
- _Color: {r: 1, g: 0, b: 0.067070484, a: 0.15686275}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
m_BuildTextureStacks: []

View File

@ -20,10 +20,10 @@ public enum CorrectionMode
Move, // rigidbody.MovePosition/Rotation
}
[RequireComponent(typeof(Rigidbody))]
// [RequireComponent(typeof(Rigidbody))] <- RB is moved out at runtime, can't require it.
public class PredictedRigidbody : NetworkBehaviour
{
Rigidbody rb;
Rigidbody rb; // own rigidbody on server. this is never moved to a physics copy.
Vector3 lastPosition;
// [Tooltip("Broadcast changes if position changed by more than ... meters.")]
@ -35,6 +35,7 @@ public class PredictedRigidbody : NetworkBehaviour
[Header("State History")]
public int stateHistoryLimit = 32; // 32 x 50 ms = 1.6 seconds is definitely enough
readonly SortedList<double, RigidbodyState> stateHistory = new SortedList<double, RigidbodyState>();
public float recordInterval = 0.050f;
[Tooltip("(Optional) performance optimization where received state is compared to the LAST recorded state first, before sampling the whole history.\n\nThis can save significant traversal overhead for idle objects with a tiny chance of missing corrections for objects which revisisted the same position in the recent history twice.")]
public bool compareLastFirst = true;
@ -52,18 +53,18 @@ public class PredictedRigidbody : NetworkBehaviour
[Tooltip("Configure how to apply the corrected state.")]
public CorrectionMode correctionMode = CorrectionMode.Move;
[Tooltip("Server & Client would sometimes fight over the final position at rest. Instead, hard snap into black below a certain velocity threshold.")]
public float snapThreshold = 0.5f; // adjust with log messages ('snap'). '2' works, but '0.5' is fine too.
[Tooltip("Snap to the server state directly when velocity is < threshold. This is useful to reduce jitter/fighting effects before coming to rest.\nNote this applies position, rotation and velocity(!) so it's still smooth.")]
public float snapThreshold = 2; // 0.5 has too much fighting-at-rest, 2 seems ideal.
[Header("Visual Interpolation")]
[Tooltip("After creating the visual interpolation object, keep showing the original Rigidbody with a ghost (transparent) material for debugging.")]
public bool showGhost = true;
public float ghostDistanceThreshold = 0.1f;
public float ghostEnabledCheckInterval = 0.2f;
double lastGhostEnabledCheckTime = 0;
[Tooltip("After creating the visual interpolation object, replace this object's renderer materials with the ghost (ideally transparent) material.")]
public Material ghostMaterial;
public Material localGhostMaterial;
public Material remoteGhostMaterial;
[Tooltip("How fast to interpolate to the target position, relative to how far we are away from it.\nHigher value will be more jitter but sharper moves, lower value will be less jitter but a little too smooth / rounded moves.")]
public float positionInterpolationSpeed = 15; // 10 is a little too low for billiards at least
@ -75,41 +76,103 @@ public class PredictedRigidbody : NetworkBehaviour
[Header("Debugging")]
public float lineTime = 10;
// visually interpolated GameObject copy for smoothing
protected GameObject visualCopy;
protected MeshRenderer[] originalRenderers;
// Rigidbody & Collider are moved out into a separate object.
// this way the visual object can smoothly follow.
protected GameObject physicsCopy;
Rigidbody physicsCopyRigidbody; // caching to avoid GetComponent
Collider physicsCopyCollider; // caching to avoid GetComponent
// we also create one extra ghost for the exact known server state.
protected GameObject remoteCopy;
void Awake()
{
rb = GetComponent<Rigidbody>();
}
// instantiate a visually-only copy of the gameobject to apply smoothing.
// on clients, where players are watching.
// create & destroy methods are virtual so games with a different
// rendering setup / hierarchy can inject their own copying code here.
protected virtual void CreateVisualCopy()
protected virtual Rigidbody MoveRigidbody(GameObject destination)
{
// create an empty GameObject with the same name + _Visual
visualCopy = new GameObject($"{name}_Visual");
visualCopy.transform.position = transform.position;
visualCopy.transform.rotation = transform.rotation;
visualCopy.transform.localScale = transform.localScale;
Rigidbody source = GetComponent<Rigidbody>();
Rigidbody rigidbodyCopy = destination.AddComponent<Rigidbody>();
rigidbodyCopy.mass = source.mass;
rigidbodyCopy.drag = source.drag;
rigidbodyCopy.angularDrag = source.angularDrag;
rigidbodyCopy.useGravity = source.useGravity;
rigidbodyCopy.isKinematic = source.isKinematic;
rigidbodyCopy.interpolation = source.interpolation;
rigidbodyCopy.collisionDetectionMode = source.collisionDetectionMode;
rigidbodyCopy.constraints = source.constraints;
rigidbodyCopy.sleepThreshold = source.sleepThreshold;
rigidbodyCopy.freezeRotation = source.freezeRotation;
rigidbodyCopy.position = source.position;
rigidbodyCopy.rotation = source.rotation;
rigidbodyCopy.velocity = source.velocity;
Destroy(source);
return rigidbodyCopy;
}
// add the PredictedRigidbodyVisual component
PredictedRigidbodyVisual visualRigidbody = visualCopy.AddComponent<PredictedRigidbodyVisual>();
visualRigidbody.target = this;
visualRigidbody.positionInterpolationSpeed = positionInterpolationSpeed;
visualRigidbody.rotationInterpolationSpeed = rotationInterpolationSpeed;
visualRigidbody.teleportDistanceMultiplier = teleportDistanceMultiplier;
// copy the rendering components
if (TryGetComponent(out MeshRenderer originalMeshRenderer))
protected virtual void MoveBoxColliders(GameObject destination)
{
BoxCollider[] sourceColliders = GetComponents<BoxCollider>();
foreach (BoxCollider sourceCollider in sourceColliders)
{
MeshFilter meshFilter = visualCopy.AddComponent<MeshFilter>();
meshFilter.mesh = GetComponent<MeshFilter>().mesh;
BoxCollider colliderCopy = destination.AddComponent<BoxCollider>();
colliderCopy.center = sourceCollider.center;
colliderCopy.size = sourceCollider.size;
Destroy(sourceCollider);
}
}
MeshRenderer meshRenderer = visualCopy.AddComponent<MeshRenderer>();
protected virtual void MoveSphereColliders(GameObject destination)
{
SphereCollider[] sourceColliders = GetComponents<SphereCollider>();
foreach (SphereCollider sourceCollider in sourceColliders)
{
SphereCollider colliderCopy = destination.AddComponent<SphereCollider>();
colliderCopy.center = sourceCollider.center;
colliderCopy.radius = sourceCollider.radius;
Destroy(sourceCollider);
}
}
protected virtual void MoveCapsuleColliders(GameObject destination)
{
CapsuleCollider[] sourceColliders = GetComponents<CapsuleCollider>();
foreach (CapsuleCollider sourceCollider in sourceColliders)
{
CapsuleCollider colliderCopy = destination.AddComponent<CapsuleCollider>();
colliderCopy.center = sourceCollider.center;
colliderCopy.radius = sourceCollider.radius;
colliderCopy.height = sourceCollider.height;
colliderCopy.direction = sourceCollider.direction;
Destroy(sourceCollider);
}
}
protected virtual void MoveMeshColliders(GameObject destination)
{
MeshCollider[] sourceColliders = GetComponents<MeshCollider>();
foreach (MeshCollider sourceCollider in sourceColliders)
{
MeshCollider colliderCopy = destination.AddComponent<MeshCollider>();
colliderCopy.sharedMesh = sourceCollider.sharedMesh;
colliderCopy.convex = sourceCollider.convex;
colliderCopy.isTrigger = sourceCollider.isTrigger;
Destroy(sourceCollider);
}
}
protected virtual void CopyRenderersAsGhost(GameObject destination, Material material)
{
// find the MeshRenderer component, which sometimes is on a child.
MeshRenderer originalMeshRenderer = GetComponentInChildren<MeshRenderer>(true);
MeshFilter originalMeshFilter = GetComponentInChildren<MeshFilter>(true);
if (originalMeshRenderer != null && originalMeshFilter != null)
{
MeshFilter meshFilter = destination.AddComponent<MeshFilter>();
meshFilter.mesh = originalMeshFilter.mesh;
MeshRenderer meshRenderer = destination.AddComponent<MeshRenderer>();
meshRenderer.material = originalMeshRenderer.material;
// renderers often have multiple materials. copy all.
@ -118,74 +181,120 @@ protected virtual void CreateVisualCopy()
Material[] materials = new Material[originalMeshRenderer.materials.Length];
for (int i = 0; i < materials.Length; ++i)
{
materials[i] = originalMeshRenderer.materials[i];
materials[i] = material;
}
meshRenderer.materials = materials; // need to reassign to see it in effect
}
}
// if we didn't find a renderer, show a warning
else Debug.LogWarning($"PredictedRigidbody: {name} found no renderer to copy onto the visual object. If you are using a custom setup, please overwrite PredictedRigidbody.CreateVisualCopy().");
}
// find renderers in children.
// this will be used a lot later, so only find them once when
// creating the visual copy here.
originalRenderers = GetComponentsInChildren<MeshRenderer>();
// instantiate a physics-only copy of the gameobject to apply corrections.
// this way the main visual object can smoothly follow.
// it's best to separate the physics instead of separating the renderers.
// some projects have complex rendering / animation setups which we can't touch.
// besides, Rigidbody+Collider are two components, where as renders may be many.
protected virtual void CreateGhosts()
{
// skip if already separated
if (physicsCopy != null) return;
// replace this renderer's materials with the ghost (if enabled)
foreach (MeshRenderer rend in originalRenderers)
Debug.Log($"Separating Physics for {name}");
// create an empty GameObject with the same name + _Physical
physicsCopy = new GameObject($"{name}_Physical");
physicsCopy.transform.position = transform.position;
physicsCopy.transform.rotation = transform.rotation;
physicsCopy.transform.localScale = transform.localScale;
// add the PredictedRigidbodyPhysical component
PredictedRigidbodyPhysicsGhost physicsGhostRigidbody = physicsCopy.AddComponent<PredictedRigidbodyPhysicsGhost>();
physicsGhostRigidbody.target = this;
physicsGhostRigidbody.ghostDistanceThreshold = ghostDistanceThreshold;
physicsGhostRigidbody.ghostEnabledCheckInterval = ghostEnabledCheckInterval;
// move the rigidbody component to the physics GameObject
MoveRigidbody(physicsCopy);
// move the collider components to the physics GameObject
MoveBoxColliders(physicsCopy);
MoveSphereColliders(physicsCopy);
MoveCapsuleColliders(physicsCopy);
MoveMeshColliders(physicsCopy);
// show ghost by copying all renderers / materials with ghost material applied
if (showGhost)
{
if (showGhost)
{
// renderers often have multiple materials. replace all.
if (rend.materials != null)
{
Material[] materials = rend.materials;
for (int i = 0; i < materials.Length; ++i)
{
materials[i] = ghostMaterial;
}
rend.materials = materials; // need to reassign to see it in effect
}
}
else
{
rend.enabled = false;
}
// one for the locally predicted rigidbody
CopyRenderersAsGhost(physicsCopy, localGhostMaterial);
physicsGhostRigidbody.ghostDistanceThreshold = ghostDistanceThreshold;
physicsGhostRigidbody.ghostEnabledCheckInterval = ghostEnabledCheckInterval;
// one for the latest remote state for comparison
remoteCopy = new GameObject($"{name}_Remote");
PredictedRigidbodyRemoteGhost predictedGhost = remoteCopy.AddComponent<PredictedRigidbodyRemoteGhost>();
predictedGhost.target = this;
predictedGhost.ghostDistanceThreshold = ghostDistanceThreshold;
predictedGhost.ghostEnabledCheckInterval = ghostEnabledCheckInterval;
CopyRenderersAsGhost(remoteCopy, remoteGhostMaterial);
}
// cache components to avoid GetComponent calls at runtime
physicsCopyRigidbody = physicsCopy.GetComponent<Rigidbody>();
physicsCopyCollider = physicsCopy.GetComponent<Collider>();
if (physicsGhostRigidbody == null) throw new Exception("SeparatePhysics: couldn't find final Rigidbody.");
if (physicsCopyCollider == null) throw new Exception("SeparatePhysics: couldn't find final Collider.");
}
protected virtual void DestroyVisualCopy()
protected virtual void DestroyCopies()
{
if (visualCopy != null) Destroy(visualCopy);
if (physicsCopy != null) Destroy(physicsCopy);
if (remoteCopy != null) Destroy(remoteCopy);
}
protected virtual void UpdateVisualCopy()
protected virtual void SmoothFollowPhysicsCopy()
{
// only if visual copy was already created
if (visualCopy == null || originalRenderers == null) return;
// hard follow:
// transform.position = physicsCopyCollider.position;
// transform.rotation = physicsCopyCollider.rotation;
// enough to run this in a certain interval.
// doing this every update would be overkill.
// this is only for debug purposes anyway.
if (NetworkTime.localTime < lastGhostEnabledCheckTime + ghostEnabledCheckInterval) return;
lastGhostEnabledCheckTime = NetworkTime.localTime;
// if we are further than N colliders sizes behind, then teleport
float colliderSize = physicsCopyCollider.bounds.size.magnitude;
float threshold = colliderSize * teleportDistanceMultiplier;
float distance = Vector3.Distance(transform.position, physicsCopyRigidbody.position);
if (distance > threshold)
{
transform.position = physicsCopyRigidbody.position;
transform.rotation = physicsCopyRigidbody.rotation;
Debug.Log($"[PredictedRigidbody] Teleported because distance to physics copy = {distance:F2} > threshold {threshold:F2}");
return;
}
// only show ghost while interpolating towards the object.
// if we are 'inside' the object then don't show ghost.
// otherwise it just looks like z-fighting the whole time.
// => iterated the renderers we found when creating the visual copy.
// we don't want to GetComponentsInChildren every time here!
bool insideTarget = Vector3.Distance(transform.position, visualCopy.transform.position) <= ghostDistanceThreshold;
foreach (MeshRenderer rend in originalRenderers)
rend.enabled = !insideTarget;
// smoothly interpolate to the target position.
// speed relative to how far away we are
float positionStep = distance * positionInterpolationSpeed;
// speed relative to how far away we are.
// => speed increases by distance² because the further away, the
// sooner we need to catch the fuck up
// float positionStep = (distance * distance) * interpolationSpeed;
transform.position = Vector3.MoveTowards(transform.position, physicsCopyRigidbody.position, positionStep * Time.deltaTime);
// smoothly interpolate to the target rotation.
// Quaternion.RotateTowards doesn't seem to work at all, so let's use SLerp.
transform.rotation = Quaternion.Slerp(transform.rotation, physicsCopyRigidbody.rotation, rotationInterpolationSpeed * Time.deltaTime);
}
// creater visual copy only on clients, where players are watching.
public override void OnStartClient() => CreateVisualCopy();
public override void OnStartClient()
{
// OnDeserialize may have already created this
if (physicsCopy == null) CreateGhosts();
}
// destroy visual copy only in OnStopClient().
// OnDestroy() wouldn't be called for scene objects that are only disabled instead of destroyed.
public override void OnStopClient() => DestroyVisualCopy();
public override void OnStopClient() => DestroyCopies();
void UpdateServer()
{
@ -203,15 +312,14 @@ void UpdateServer()
SetDirty();
}
void UpdateClient()
{
UpdateVisualCopy();
}
void Update()
{
if (isServer) UpdateServer();
if (isClient) UpdateClient();
}
void LateUpdate()
{
if (isClient) SmoothFollowPhysicsCopy();
}
void FixedUpdate()
@ -224,8 +332,14 @@ void FixedUpdate()
// manually store last recorded so we can easily check against this
// without traversing the SortedList.
RigidbodyState lastRecorded;
double lastRecordTime;
void RecordState()
{
// instead of recording every fixedupdate, let's record in an interval.
// we don't want to record every tiny move and correct too hard.
if (NetworkTime.time < lastRecordTime + recordInterval) return;
lastRecordTime = NetworkTime.time;
// NetworkTime.time is always behind by bufferTime.
// prediction aims to be on the exact same server time (immediately).
// use predictedTime to record state, otherwise we would record in the past.
@ -250,20 +364,20 @@ void RecordState()
if (stateHistory.Count > 0)
{
RigidbodyState last = stateHistory.Values[stateHistory.Count - 1];
positionDelta = rb.position - last.position;
velocityDelta = rb.velocity - last.velocity;
rotationDelta = rb.rotation * Quaternion.Inverse(last.rotation); // this is how you calculate a quaternion delta
positionDelta = physicsCopyRigidbody.position - last.position;
velocityDelta = physicsCopyRigidbody.velocity - last.velocity;
rotationDelta = physicsCopyRigidbody.rotation * Quaternion.Inverse(last.rotation); // this is how you calculate a quaternion delta
// debug draw the recorded state
Debug.DrawLine(last.position, rb.position, Color.red, lineTime);
Debug.DrawLine(last.position, physicsCopyRigidbody.position, Color.red, lineTime);
}
// create state to insert
RigidbodyState state = new RigidbodyState(
predictedTime,
positionDelta, rb.position,
rotationDelta, rb.rotation,
velocityDelta, rb.velocity
positionDelta, physicsCopyRigidbody.position,
rotationDelta, physicsCopyRigidbody.rotation,
velocityDelta, physicsCopyRigidbody.velocity
);
// add state to history
@ -277,18 +391,31 @@ void RecordState()
protected virtual void OnSnappedIntoPlace() {}
protected virtual void OnCorrected() {}
void ApplyState(Vector3 position, Quaternion rotation, Vector3 velocity)
void ApplyState(double timestamp, Vector3 position, Quaternion rotation, Vector3 velocity)
{
// fix rigidbodies seemingly dancing in place instead of coming to rest.
// hard snap to the position below a threshold velocity.
// this is fine because the visual object still smoothly interpolates to it.
if (rb.velocity.magnitude <= snapThreshold)
if (physicsCopyRigidbody.velocity.magnitude <= snapThreshold)
{
Debug.Log($"Prediction: snapped {name} into place because velocity {rb.velocity.magnitude:F3} <= {snapThreshold:F3}");
Debug.Log($"Prediction: snapped {name} into place because velocity {physicsCopyRigidbody.velocity.magnitude:F3} <= {snapThreshold:F3}");
// apply server state immediately.
// important to apply velocity as well, instead of Vector3.zero.
// in case an object is still slightly moving, we don't want it
// to stop and start moving again on client - slide as well here.
physicsCopyRigidbody.position = position;
physicsCopyRigidbody.rotation = rotation;
physicsCopyRigidbody.velocity = velocity;
// clear history and insert the exact state we just applied.
// this makes future corrections more accurate.
stateHistory.Clear();
rb.position = position;
rb.rotation = rotation;
rb.velocity = Vector3.zero;
stateHistory.Add(timestamp, new RigidbodyState(
timestamp,
Vector3.zero, position,
Quaternion.identity, rotation,
Vector3.zero, velocity
));
// user callback
OnSnappedIntoPlace();
@ -299,23 +426,31 @@ void ApplyState(Vector3 position, Quaternion rotation, Vector3 velocity)
// TODO is this a good idea? what about next capture while it's interpolating?
if (correctionMode == CorrectionMode.Move)
{
rb.MovePosition(position);
rb.MoveRotation(rotation);
physicsCopyRigidbody.MovePosition(position);
physicsCopyRigidbody.MoveRotation(rotation);
}
else if (correctionMode == CorrectionMode.Set)
{
rb.position = position;
rb.rotation = rotation;
physicsCopyRigidbody.position = position;
physicsCopyRigidbody.rotation = rotation;
}
// there's only one way to set velocity
rb.velocity = velocity;
physicsCopyRigidbody.velocity = velocity;
}
// process a received server state.
// compares it against our history and applies corrections if needed.
void OnReceivedState(double timestamp, RigidbodyState state)
{
// always update remote state ghost
if (remoteCopy != null)
{
remoteCopy.transform.position = state.position;
remoteCopy.transform.rotation = state.rotation;
remoteCopy.transform.localScale = transform.localScale;
}
// OPTIONAL performance optimization when comparing idle objects.
// even idle objects will have a history of ~32 entries.
// sampling & traversing through them is unnecessarily costly.
@ -335,8 +470,8 @@ void OnReceivedState(double timestamp, RigidbodyState state)
//
// if this ever causes issues, feel free to disable it.
if (compareLastFirst &&
Vector3.Distance(state.position, rb.position) < positionCorrectionThreshold &&
Quaternion.Angle(state.rotation, rb.rotation) < rotationCorrectionThreshold)
Vector3.Distance(state.position, physicsCopyRigidbody.position) < positionCorrectionThreshold &&
Quaternion.Angle(state.rotation, physicsCopyRigidbody.rotation) < rotationCorrectionThreshold)
{
// Debug.Log($"OnReceivedState for {name}: taking optimized early return!");
return;
@ -364,7 +499,7 @@ void OnReceivedState(double timestamp, RigidbodyState state)
if (state.timestamp < oldest.timestamp)
{
Debug.LogWarning($"Hard correcting client because the client is too far behind the server. History of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This would cause the client to be out of sync as long as it's behind.");
ApplyState(state.position, state.rotation, state.velocity);
ApplyState(state.timestamp, state.position, state.rotation, state.velocity);
return;
}
@ -381,11 +516,11 @@ void OnReceivedState(double timestamp, RigidbodyState state)
// we clamp it to 'now'.
// but only correct if off by threshold.
// TODO maybe we should interpolate this back to 'now'?
if (Vector3.Distance(state.position, rb.position) >= positionCorrectionThreshold)
if (Vector3.Distance(state.position, physicsCopyRigidbody.position) >= positionCorrectionThreshold)
{
double ahead = state.timestamp - newest.timestamp;
Debug.Log($"Hard correction because the client is ahead of the server by {(ahead*1000):F1}ms. History of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This can happen when latency is near zero, and is fine unless it shows jitter.");
ApplyState(state.position, state.rotation, state.velocity);
ApplyState(state.timestamp, state.position, state.rotation, state.velocity);
}
return;
}
@ -396,7 +531,7 @@ void OnReceivedState(double timestamp, RigidbodyState state)
// something went very wrong. sampling should've worked.
// hard correct to recover the error.
Debug.LogError($"Failed to sample history of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This should never happen because the timestamp is within history.");
ApplyState(state.position, state.rotation, state.velocity);
ApplyState(state.timestamp, state.position, state.rotation, state.velocity);
return;
}
@ -428,8 +563,8 @@ void OnReceivedState(double timestamp, RigidbodyState state)
// for example, on same machine with near zero latency.
// int correctedAmount = stateHistory.Count - afterIndex;
// Debug.Log($"Correcting {name}: {correctedAmount} / {stateHistory.Count} states to final position from: {rb.position} to: {last.position}");
Debug.DrawLine(rb.position, recomputed.position, Color.green, lineTime);
ApplyState(recomputed.position, recomputed.rotation, recomputed.velocity);
Debug.DrawLine(physicsCopyRigidbody.position, recomputed.position, Color.green, lineTime);
ApplyState(recomputed.timestamp, recomputed.position, recomputed.rotation, recomputed.velocity);
// user callback
OnCorrected();
@ -451,14 +586,18 @@ public override void OnSerialize(NetworkWriter writer, bool initialState)
// sending server's current deltaTime is the safest option.
// client then applies it on top of remoteTimestamp.
writer.WriteFloat(Time.deltaTime);
writer.WriteVector3(rb.position);
writer.WriteQuaternion(rb.rotation);
writer.WriteVector3(rb.velocity);
writer.WriteVector3(rb.position); // own rigidbody on server, it's never moved to physics copy
writer.WriteQuaternion(rb.rotation); // own rigidbody on server, it's never moved to physics copy
writer.WriteVector3(rb.velocity); // own rigidbody on server, it's never moved to physics copy
}
// read the server's state, compare with client state & correct if necessary.
public override void OnDeserialize(NetworkReader reader, bool initialState)
{
// this may be called before OnStartClient.
// in that case, separate physics first before applying state.
if (physicsCopy == null) CreateGhosts();
// deserialize data
// we want to know the time on the server when this was sent, which is remoteTimestamp.
double timestamp = NetworkClient.connection.remoteTimeStamp;
@ -490,6 +629,11 @@ protected override void OnValidate()
// force syncDirection to be ServerToClient
syncDirection = SyncDirection.ServerToClient;
// state should be synced immediately for now.
// later when we have prediction fully dialed in,
// then we can maybe relax this a bit.
syncInterval = 0;
}
}
}

View File

@ -4,7 +4,10 @@ MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences:
- ghostMaterial: {fileID: 2100000, guid: 411a48b4a197d4924bec3e3809bc9320, type: 2}
- localGhostMaterial: {fileID: 2100000, guid: 411a48b4a197d4924bec3e3809bc9320,
type: 2}
- remoteGhostMaterial: {fileID: 2100000, guid: 04f0b2088c857414393bab3b80356776,
type: 2}
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:

View File

@ -0,0 +1,72 @@
// Prediction moves out the Rigidbody & Collider into a separate object.
// This way the main (visual) object can smoothly follow it, instead of hard.
using System;
using UnityEngine;
namespace Mirror
{
public class PredictedRigidbodyPhysicsGhost : MonoBehaviour
{
[Tooltip("The predicted rigidbody owner.")]
public PredictedRigidbody target;
// ghost (settings are copyed from PredictedRigidbody)
MeshRenderer ghost;
public float ghostDistanceThreshold = 0.1f;
public float ghostEnabledCheckInterval = 0.2f;
double lastGhostEnabledCheckTime = 0;
// cache
Collider co;
// we add this component manually from PredictedRigidbody.
// so assign this in Start. target isn't set in Awake yet.
void Start()
{
co = GetComponent<Collider>();
ghost = GetComponent<MeshRenderer>();
}
void UpdateGhostRenderers()
{
// only if a ghost renderer was given
if (ghost == null) return;
// enough to run this in a certain interval.
// doing this every update would be overkill.
// this is only for debug purposes anyway.
if (NetworkTime.localTime < lastGhostEnabledCheckTime + ghostEnabledCheckInterval) return;
lastGhostEnabledCheckTime = NetworkTime.localTime;
// only show ghost while interpolating towards the object.
// if we are 'inside' the object then don't show ghost.
// otherwise it just looks like z-fighting the whole time.
// => iterated the renderers we found when creating the visual copy.
// we don't want to GetComponentsInChildren every time here!
bool insideTarget = Vector3.Distance(transform.position, target.transform.position) <= ghostDistanceThreshold;
ghost.enabled = !insideTarget;
}
void Update() => UpdateGhostRenderers();
// always follow in late update, after update modified positions
void LateUpdate()
{
// if owner gets network destroyed for any reason, destroy visual
if (target == null || target.gameObject == null) Destroy(gameObject);
}
// also show a yellow gizmo for the predicted & corrected physics.
// in case we can't renderer ghosts, at least we have this.
void OnDrawGizmos()
{
if (co != null)
{
// show the client's predicted & corrected physics in yellow
Bounds bounds = co.bounds;
Gizmos.color = Color.yellow;
Gizmos.DrawWireCube(bounds.center, bounds.size);
}
}
}
}

View File

@ -0,0 +1,53 @@
// simply ghost object that always follows last received server state.
using UnityEngine;
namespace Mirror
{
public class PredictedRigidbodyRemoteGhost : MonoBehaviour
{
[Tooltip("The predicted rigidbody owner.")]
public PredictedRigidbody target;
// ghost (settings are copyed from PredictedRigidbody)
MeshRenderer ghost;
public float ghostDistanceThreshold = 0.1f;
public float ghostEnabledCheckInterval = 0.2f;
double lastGhostEnabledCheckTime = 0;
// we add this component manually from PredictedRigidbody.
// so assign this in Start. target isn't set in Awake yet.
void Start()
{
ghost = GetComponent<MeshRenderer>();
}
void UpdateGhostRenderers()
{
// only if a ghost renderer was given
if (ghost == null) return;
// enough to run this in a certain interval.
// doing this every update would be overkill.
// this is only for debug purposes anyway.
if (NetworkTime.localTime < lastGhostEnabledCheckTime + ghostEnabledCheckInterval) return;
lastGhostEnabledCheckTime = NetworkTime.localTime;
// only show ghost while interpolating towards the object.
// if we are 'inside' the object then don't show ghost.
// otherwise it just looks like z-fighting the whole time.
// => iterated the renderers we found when creating the visual copy.
// we don't want to GetComponentsInChildren every time here!
bool insideTarget = Vector3.Distance(transform.position, target.transform.position) <= ghostDistanceThreshold;
ghost.enabled = !insideTarget;
}
void Update() => UpdateGhostRenderers();
// always follow in late update, after update modified positions
void LateUpdate()
{
// if owner gets network destroyed for any reason, destroy visual
if (target == null || target.gameObject == null) Destroy(gameObject);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 62e7e9424c7e48d69b6a3517796142a1
timeCreated: 1705235542

View File

@ -1,64 +0,0 @@
using System;
using UnityEngine;
namespace Mirror
{
public class PredictedRigidbodyVisual : MonoBehaviour
{
[Tooltip("The predicted rigidbody to follow.")]
public PredictedRigidbody target;
Rigidbody targetRigidbody;
// settings are applied in the other PredictedRigidbody component and then copied here.
[HideInInspector] public float positionInterpolationSpeed = 15; // 10 is a little too low for billiards at least
[HideInInspector] public float rotationInterpolationSpeed = 10;
[HideInInspector] public float teleportDistanceMultiplier = 10;
// we add this component manually from PredictedRigidbody.
// so assign this in Start. target isn't set in Awake yet.
void Start()
{
targetRigidbody = target.GetComponent<Rigidbody>();
}
// always follow in late update, after update modified positions
void LateUpdate()
{
// if target gets network destroyed for any reason, destroy visual
if (targetRigidbody == null || target.gameObject == null)
{
Destroy(gameObject);
return;
}
// hard follow:
// transform.position = targetRigidbody.position;
// transform.rotation = targetRigidbody.rotation;
// if we are further than N colliders sizes behind, then teleport
float colliderSize = target.GetComponent<Collider>().bounds.size.magnitude;
float threshold = colliderSize * teleportDistanceMultiplier;
float distance = Vector3.Distance(transform.position, targetRigidbody.position);
if (distance > threshold)
{
transform.position = targetRigidbody.position;
transform.rotation = targetRigidbody.rotation;
Debug.Log($"[PredictedRigidbodyVisual] Teleported because distance {distance:F2} > threshold {threshold:F2}");
return;
}
// smoothly interpolate to the target position.
// speed relative to how far away we are
float positionStep = distance * positionInterpolationSpeed;
// speed relative to how far away we are.
// => speed increases by distance² because the further away, the
// sooner we need to catch the fuck up
// float positionStep = (distance * distance) * interpolationSpeed;
transform.position = Vector3.MoveTowards(transform.position, targetRigidbody.position, positionStep * Time.deltaTime);
// smoothly interpolate to the target rotation.
// Quaternion.RotateTowards doesn't seem to work at all, so let's use SLerp.
transform.rotation = Quaternion.Slerp(transform.rotation, targetRigidbody.rotation, rotationInterpolationSpeed * Time.deltaTime);
}
}
}

View File

@ -0,0 +1,85 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: RemoteGhostMaterial
m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords:
- _ALPHAPREMULTIPLY_ON
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: 3000
stringTagMap:
RenderType: Transparent
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BumpMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailAlbedoMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailMask:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailNormalMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _EmissionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MetallicGlossMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _OcclusionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ParallaxMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _BumpScale: 1
- _Cutoff: 0.5
- _DetailNormalMapScale: 1
- _DstBlend: 10
- _GlossMapScale: 1
- _Glossiness: 0.92
- _GlossyReflections: 1
- _Metallic: 0
- _Mode: 3
- _OcclusionStrength: 1
- _Parallax: 0.02
- _SmoothnessTextureChannel: 0
- _SpecularHighlights: 1
- _SrcBlend: 1
- _UVSec: 0
- _ZWrite: 0
m_Colors:
- _Color: {r: 0.09849727, g: 1, b: 0, a: 0.15686275}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
m_BuildTextureStacks: []

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 04f0b2088c857414393bab3b80356776
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -87,5 +87,5 @@ public class ShowInInspectorAttribute : Attribute {}
/// Used to make a field readonly in the inspector
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class ReadOnlyAttribute : PropertyAttribute { }
public class ReadOnlyAttribute : PropertyAttribute {}
}

View File

@ -23,7 +23,7 @@ protected virtual void OnEnable()
}
[ServerCallback]
public virtual void Reset() {}
public virtual void ResetState() {}
// Callback used by the visibility system to determine if an observer
// (player) can see the NetworkIdentity. If this function returns true,

View File

@ -318,8 +318,14 @@ internal static void OnTransportData(ArraySegment<byte> data, int channelId)
// always process all messages in the batch.
if (!unbatcher.AddBatch(data))
{
Debug.LogWarning($"NetworkClient: failed to add batch, disconnecting.");
connection.Disconnect();
if (exceptionsDisconnect)
{
Debug.LogError($"NetworkClient: failed to add batch, disconnecting.");
connection.Disconnect();
}
else
Debug.LogWarning($"NetworkClient: failed to add batch.");
return;
}
@ -355,17 +361,27 @@ internal static void OnTransportData(ArraySegment<byte> data, int channelId)
// so we need to disconnect.
// -> return to avoid the below unbatches.count error.
// we already disconnected and handled it.
Debug.LogWarning($"NetworkClient: failed to unpack and invoke message. Disconnecting.");
connection.Disconnect();
if (exceptionsDisconnect)
{
Debug.LogError($"NetworkClient: failed to unpack and invoke message. Disconnecting.");
connection.Disconnect();
}
else
Debug.LogWarning($"NetworkClient: failed to unpack and invoke message.");
return;
}
}
// otherwise disconnect
else
{
// WARNING, not error. can happen if attacker sends random data.
Debug.LogWarning($"NetworkClient: received Message was too short (messages should start with message id)");
connection.Disconnect();
if (exceptionsDisconnect)
{
Debug.LogError($"NetworkClient: received Message was too short (messages should start with message id). Disconnecting.");
connection.Disconnect();
}
else
Debug.LogWarning("NetworkClient: received Message was too short (messages should start with message id)");
return;
}
}
@ -536,14 +552,17 @@ public static void RegisterHandler<T>(Action<T, int> handler, bool requireAuthen
handlers[msgType] = NetworkMessages.WrapHandler((Action<NetworkConnection, T, int>)HandlerWrapped, requireAuthentication, exceptionsDisconnect);
}
/// <summary>Replace a handler for a particular message type. Should require authentication by default.</summary>
// RegisterHandler throws a warning (as it should) if a handler is assigned twice
// Use of ReplaceHandler makes it clear the user intended to replace the handler
// Deprecated 2024-01-21
[Obsolete("Use ReplaceHandler without the NetworkConnection parameter instead. This version is obsolete and will be removed soon.")]
public static void ReplaceHandler<T>(Action<NetworkConnection, T> handler, bool requireAuthentication = true)
where T : struct, NetworkMessage
{
// we use the same WrapHandler function for server and client.
// so let's wrap it to ignore the NetworkConnection parameter.
// it's not needed on client. it's always NetworkClient.connection.
ushort msgType = NetworkMessageId<T>.Id;
handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication, exceptionsDisconnect);
void HandlerWrapped(NetworkConnection _, T value) => handler(_, value);
handlers[msgType] = NetworkMessages.WrapHandler((Action<NetworkConnection, T>)HandlerWrapped, requireAuthentication, exceptionsDisconnect);
}
/// <summary>Replace a handler for a particular message type. Should require authentication by default.</summary>
@ -552,7 +571,26 @@ public static void ReplaceHandler<T>(Action<NetworkConnection, T> handler, bool
public static void ReplaceHandler<T>(Action<T> handler, bool requireAuthentication = true)
where T : struct, NetworkMessage
{
ReplaceHandler((NetworkConnection _, T value) => { handler(value); }, requireAuthentication);
// we use the same WrapHandler function for server and client.
// so let's wrap it to ignore the NetworkConnection parameter.
// it's not needed on client. it's always NetworkClient.connection.
ushort msgType = NetworkMessageId<T>.Id;
void HandlerWrapped(NetworkConnection _, T value) => handler(value);
handlers[msgType] = NetworkMessages.WrapHandler((Action<NetworkConnection, T>)HandlerWrapped, requireAuthentication, exceptionsDisconnect);
}
/// <summary>Replace a handler for a particular message type. Should require authentication by default. This version passes channelId to the handler.</summary>
// RegisterHandler throws a warning (as it should) if a handler is assigned twice
// Use of ReplaceHandler makes it clear the user intended to replace the handler
public static void ReplaceHandler<T>(Action<T, int> handler, bool requireAuthentication = true)
where T : struct, NetworkMessage
{
// we use the same WrapHandler function for server and client.
// so let's wrap it to ignore the NetworkConnection parameter.
// it's not needed on client. it's always NetworkClient.connection.
ushort msgType = NetworkMessageId<T>.Id;
void HandlerWrapped(NetworkConnection _, T value, int channelId) => handler(value, channelId);
handlers[msgType] = NetworkMessages.WrapHandler((Action<NetworkConnection, T, int>)HandlerWrapped, requireAuthentication, exceptionsDisconnect);
}
/// <summary>Unregister a message handler of type T.</summary>
@ -1634,7 +1672,7 @@ public static void DestroyAllClientObjects()
// unspawned objects should be reset for reuse later.
if (wasUnspawned)
{
identity.Reset();
identity.ResetState();
}
// without unspawn handler, we need to disable/destroy.
else
@ -1643,7 +1681,7 @@ public static void DestroyAllClientObjects()
// they always stay in the scene, we don't destroy them.
if (identity.sceneId != 0)
{
identity.Reset();
identity.ResetState();
identity.gameObject.SetActive(false);
}
// spawned objects are destroyed
@ -1679,7 +1717,7 @@ static void DestroyObject(uint netId)
if (InvokeUnSpawnHandler(identity.assetId, identity.gameObject))
{
// reset object after user's handler
identity.Reset();
identity.ResetState();
}
// otherwise fall back to default Destroy
else if (identity.sceneId == 0)
@ -1693,7 +1731,7 @@ static void DestroyObject(uint netId)
identity.gameObject.SetActive(false);
spawnableObjects[identity.sceneId] = identity;
// reset for scene objects
identity.Reset();
identity.ResetState();
}
// remove from dictionary no matter how it is unspawned

View File

@ -1324,7 +1324,7 @@ public void RemoveClientAuthority()
// the identity during destroy as people might want to be able to read
// the members inside OnDestroy(), and we have no way of invoking reset
// after OnDestroy is called.
internal void Reset()
internal void ResetState()
{
hasSpawned = false;
clientStarted = false;

View File

@ -161,7 +161,7 @@ static void Initialize()
// reset Interest Management so that rebuild intervals
// start at 0 when starting again.
if (aoi != null) aoi.Reset();
if (aoi != null) aoi.ResetState();
// reset NetworkTime
NetworkTime.ResetStatics();
@ -244,7 +244,7 @@ public static void Shutdown()
OnDisconnectedEvent = null;
OnErrorEvent = null;
if (aoi != null) aoi.Reset();
if (aoi != null) aoi.ResetState();
}
static void RemoveTransportHandlers()
@ -384,8 +384,13 @@ static void OnEntityStateMessage(NetworkConnectionToClient connection, EntitySta
// failure to deserialize disconnects to prevent exploits.
if (!identity.DeserializeServer(reader))
{
Debug.LogWarning($"Server failed to deserialize client state for {identity.name} with netId={identity.netId}, Disconnecting.");
connection.Disconnect();
if (exceptionsDisconnect)
{
Debug.LogError($"Server failed to deserialize client state for {identity.name} with netId={identity.netId}, Disconnecting.");
connection.Disconnect();
}
else
Debug.LogWarning($"Server failed to deserialize client state for {identity.name} with netId={identity.netId}.");
}
}
}
@ -725,8 +730,14 @@ internal static void OnTransportData(int connectionId, ArraySegment<byte> data,
// always process all messages in the batch.
if (!connection.unbatcher.AddBatch(data))
{
Debug.LogWarning($"NetworkServer: received Message was too short (messages should start with message id)");
connection.Disconnect();
if (exceptionsDisconnect)
{
Debug.LogError($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id). Disconnecting.");
connection.Disconnect();
}
else
Debug.LogWarning($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id).");
return;
}
@ -762,17 +773,28 @@ internal static void OnTransportData(int connectionId, ArraySegment<byte> data,
// so we need to disconnect.
// -> return to avoid the below unbatches.count error.
// we already disconnected and handled it.
Debug.LogWarning($"NetworkServer: failed to unpack and invoke message. Disconnecting {connectionId}.");
connection.Disconnect();
if (exceptionsDisconnect)
{
Debug.LogError($"NetworkServer: failed to unpack and invoke message. Disconnecting {connectionId}.");
connection.Disconnect();
}
else
Debug.LogWarning($"NetworkServer: failed to unpack and invoke message from connectionId:{connectionId}.");
return;
}
}
// otherwise disconnect
else
{
// WARNING, not error. can happen if attacker sends random data.
Debug.LogWarning($"NetworkServer: received Message was too short (messages should start with message id). Disconnecting {connectionId}");
connection.Disconnect();
if (exceptionsDisconnect)
{
Debug.LogError($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id). Disconnecting.");
connection.Disconnect();
}
else
Debug.LogWarning($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id).");
return;
}
}
@ -908,6 +930,14 @@ public static void ReplaceHandler<T>(Action<NetworkConnectionToClient, T> handle
ushort msgType = NetworkMessageId<T>.Id;
handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication, exceptionsDisconnect);
}
/// <summary>Replace a handler for message type T. Most should require authentication.</summary>
public static void ReplaceHandler<T>(Action<NetworkConnectionToClient, T, int> handler, bool requireAuthentication = true)
where T : struct, NetworkMessage
{
ushort msgType = NetworkMessageId<T>.Id;
handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication, exceptionsDisconnect);
}
/// <summary>Unregister a handler for a message type T.</summary>
public static void UnregisterHandler<T>()
@ -1635,7 +1665,7 @@ static void DestroyObject(NetworkIdentity identity, DestroyMode mode)
// otherwise simply .Reset() and set inactive again
else if (mode == DestroyMode.Reset)
{
identity.Reset();
identity.ResetState();
}
}

View File

@ -367,6 +367,7 @@ MonoBehaviour:
connectionQualityInterval: 3
timeInterpolationGui: 0
spawnAmount: 50000
spawnAmount: 10000
interleave: 2
spawnPrefab: {fileID: 449802645721213856, guid: 0ea79775d59804682a8cdd46b3811344,
type: 3}

View File

@ -5,9 +5,13 @@ namespace Mirror.Examples.NetworkRoom
[RequireComponent(typeof(Common.RandomColor))]
public class Reward : NetworkBehaviour
{
public bool available = true;
[Header("Components")]
public Common.RandomColor randomColor;
[Header("Diagnostics")]
[ReadOnly, SerializeField]
bool available = true;
protected override void OnValidate()
{
base.OnValidate();

View File

@ -140,7 +140,7 @@ public void TooManyComponents()
// let's reset and initialize again with the added ones.
// this should show the 'too many components' error
LogAssert.Expect(LogType.Error, new Regex(".*too many NetworkBehaviour.*"));
serverIdentity.Reset();
serverIdentity.ResetState();
// clientIdentity.Reset();
serverIdentity.Awake();
// clientIdentity.Awake();

View File

@ -624,7 +624,7 @@ public void ClearAllComponentsDirtyBits()
}
[Test]
public void Reset()
public void ResetState()
{
CreateNetworked(out GameObject _, out NetworkIdentity identity);
@ -637,7 +637,7 @@ public void Reset()
identity.observers[43] = new NetworkConnectionToClient(2);
// mark for reset and reset
identity.Reset();
identity.ResetState();
Assert.That(identity.isServer, Is.False);
Assert.That(identity.isClient, Is.False);
Assert.That(identity.isLocalPlayer, Is.False);

View File

@ -632,6 +632,7 @@ public void Send_ClientToServerMessage_UnknownMessageIdDisconnects()
// send a message without a registered handler
NetworkClient.Send(new TestMessage1());
LogAssert.Expect(LogType.Error, $"NetworkServer: failed to unpack and invoke message. Disconnecting 1.");
ProcessMessages();
// should have been disconnected
@ -684,6 +685,7 @@ public void Send_ServerToClientMessage_UnknownMessageIdDisconnects()
// send a message without a registered handler
connectionToClient.Send(new TestMessage1());
LogAssert.Expect(LogType.Error, $"NetworkClient: failed to unpack and invoke message. Disconnecting.");
ProcessMessages();
// should have been disconnected
@ -866,6 +868,7 @@ public void UnregisterHandler()
// unregister, send again, should not be called again
NetworkServer.UnregisterHandler<TestMessage1>();
NetworkClient.Send(new TestMessage1());
LogAssert.Expect(LogType.Error, $"NetworkServer: failed to unpack and invoke message. Disconnecting 1.");
ProcessMessages();
Assert.That(variant1Called, Is.EqualTo(1));
}
@ -889,6 +892,7 @@ public void ClearHandler()
// clear handlers, send again, should not be called again
NetworkServer.ClearHandlers();
NetworkClient.Send(new TestMessage1());
LogAssert.Expect(LogType.Error, $"NetworkServer: failed to unpack and invoke message. Disconnecting 1.");
ProcessMessages();
Assert.That(variant1Called, Is.EqualTo(1));
}

View File

@ -314,6 +314,9 @@ public void OnServerToClientSync_WithClientAuthority_Nullables_Uses_Last()
component.netIdentity.isClient = true;
component.netIdentity.isLocalPlayer = true;
// client authority has to be disabled
component.syncDirection = SyncDirection.ServerToClient;
// call OnClientToServerSync with authority and nullable types
// to make sure it uses the last valid position then.
component.OnServerToClientSync(new Vector3?(), new Quaternion?(), new Vector3?());

View File

@ -75,7 +75,7 @@ public class #SCRIPTNAME# : InterestManagement
/// Called by NetworkServer in Initialize and Shutdown
/// </summary>
[ServerCallback]
public override void Reset() { }
public override void ResetState() { }
[ServerCallback]
void Update()

View File

@ -10,20 +10,10 @@ using Mirror;
public class #SCRIPTNAME# : NetworkTransformBase
{
protected override Transform targetComponent => transform;
// If you need this template to reference a child target,
// replace the line above with the code below.
/*
[Header("Target")]
public Transform target;
protected override Transform targetComponent => target;
*/
#region Unity Callbacks
protected override void Awake() { }
protected override void OnValidate()
{
base.OnValidate();
@ -45,44 +35,56 @@ public class #SCRIPTNAME# : NetworkTransformBase
base.OnDisable();
}
/// <summary>
/// Buffers are cleared and interpolation times are reset to zero here.
/// This may be called when you are implementing some system of not sending
/// if nothing changed, or just plain resetting if you have not received data
/// for some time, as this will prevent a long interpolation period between old
/// and just received data, as it will look like a lag. Reset() should also be
/// called when authority is changed to another client or server, to prevent
/// old buffers bugging out the interpolation if authority is changed back.
/// </summary>
public override void Reset()
{
base.Reset();
}
#endregion
#region NT Base Callbacks
/// <summary>
/// NTSnapshot struct is created from incoming data from server
/// and added to SnapshotInterpolation sorted list.
/// You may want to skip calling the base method for the local player
/// if doing client-side prediction, or perhaps pass altered values,
/// or compare the server data to local values and correct large differences.
/// NTSnapshot struct is created here
/// </summary>
protected override void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale)
protected override TransformSnapshot Construct()
{
base.OnServerToClientSync(position, rotation, scale);
return base.Construct();
}
protected override Vector3 GetPosition()
{
return base.GetPosition();
}
protected override Quaternion GetRotation()
{
return base.GetRotation();
}
protected override Vector3 GetScale()
{
return base.GetScale();
}
protected override void SetPosition(Vector3 position)
{
base.SetPosition(position);
}
protected override void SetRotation(Quaternion rotation)
{
base.SetRotation(rotation);
}
protected override void SetScale(Vector3 scale)
{
base.SetScale(scale);
}
/// <summary>
/// NTSnapshot struct is created from incoming data from client
/// and added to SnapshotInterpolation sorted list.
/// You may want to implement anti-cheat checks here in client authority mode.
/// localPosition, localRotation, and localScale are set here:
/// interpolated values are used if interpolation is enabled.
/// goal values are used if interpolation is disabled.
/// </summary>
protected override void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale)
protected override void Apply(TransformSnapshot interpolated, TransformSnapshot endGoal)
{
base.OnClientToServerSync(position, rotation, scale);
base.Apply(interpolated, endGoal);
}
/// <summary>
@ -106,48 +108,32 @@ public class #SCRIPTNAME# : NetworkTransformBase
}
/// <summary>
/// NTSnapshot struct is created here
/// Buffers are cleared and interpolation times are reset to zero here.
/// This may be called when you are implementing some system of not sending
/// if nothing changed, or just plain resetting if you have not received data
/// for some time, as this will prevent a long interpolation period between old
/// and just received data, as it will look like a lag. Reset() should also be
/// called when authority is changed to another client or server, to prevent
/// old buffers bugging out the interpolation if authority is changed back.
/// </summary>
protected override NTSnapshot ConstructSnapshot()
public override void ResetState()
{
return base.ConstructSnapshot();
base.ResetState();
}
/// <summary>
/// localPosition, localRotation, and localScale are set here:
/// interpolated values are used if interpolation is enabled.
/// goal values are used if interpolation is disabled.
/// </summary>
protected override void Apply(TransformSnapshot interpolated, TransformSnapshot endGoal)
{
base.Apply(interpolated, endGoal);
}
#if onlySyncOnChange_BANDWIDTH_SAVING
/// <summary>
/// Returns true if position, rotation AND scale are unchanged, within given sensitivity range.
/// </summary>
protected override bool CompareSnapshots(NTSnapshot currentSnapshot)
{
return base.CompareSnapshots(currentSnapshot);
}
#endif
#endregion
#region GUI
#if UNITY_EDITOR || DEVELOPMENT_BUILD
// OnGUI allocates even if it does nothing. avoid in release.
#if UNITY_EDITOR || DEVELOPMENT_BUILD
protected override void OnGUI()
{
base.OnGUI();
}
protected override void DrawGizmos(SortedList<double, NTSnapshot> buffer)
protected override void DrawGizmos(SortedList<double, TransformSnapshot> buffer)
{
base.DrawGizmos(buffer);
}