mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-17 18:40:33 +00:00
Merged master
This commit is contained in:
commit
ceaa63dad1
@ -25,7 +25,7 @@ int GetVisRange(NetworkIdentity identity)
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void Reset()
|
||||
public override void ResetState()
|
||||
{
|
||||
lastRebuildTime = 0D;
|
||||
CustomRanges.Clear();
|
||||
|
@ -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)
|
||||
|
@ -72,7 +72,7 @@ public override void OnRebuildObservers(NetworkIdentity identity, HashSet<Networ
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void Reset()
|
||||
public override void ResetState()
|
||||
{
|
||||
lastRebuildTime = 0D;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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: []
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 62e7e9424c7e48d69b6a3517796142a1
|
||||
timeCreated: 1705235542
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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: []
|
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 04f0b2088c857414393bab3b80356776
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -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 {}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -367,6 +367,7 @@ MonoBehaviour:
|
||||
connectionQualityInterval: 3
|
||||
timeInterpolationGui: 0
|
||||
spawnAmount: 50000
|
||||
spawnAmount: 10000
|
||||
interleave: 2
|
||||
spawnPrefab: {fileID: 449802645721213856, guid: 0ea79775d59804682a8cdd46b3811344,
|
||||
type: 3}
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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?());
|
||||
|
@ -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()
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user