CHANGED TO MIRROR

This commit is contained in:
DerTyp187
2021-10-25 09:20:01 +02:00
parent bd712107b7
commit e509a919b6
611 changed files with 38291 additions and 1216 deletions

View File

@@ -0,0 +1,93 @@
using UnityEngine;
namespace Mirror.Experimental
{
[AddComponentMenu("Network/Experimental/NetworkLerpRigidbody")]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-lerp-rigidbody")]
public class NetworkLerpRigidbody : NetworkBehaviour
{
[Header("Settings")]
[SerializeField] internal Rigidbody target = null;
[Tooltip("How quickly current velocity approaches target velocity")]
[SerializeField] float lerpVelocityAmount = 0.5f;
[Tooltip("How quickly current position approaches target position")]
[SerializeField] float lerpPositionAmount = 0.5f;
[Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
[SerializeField] bool clientAuthority = false;
float nextSyncTime;
[SyncVar()]
Vector3 targetVelocity;
[SyncVar()]
Vector3 targetPosition;
/// <summary>
/// Ignore value if is host or client with Authority
/// </summary>
/// <returns></returns>
bool IgnoreSync => isServer || ClientWithAuthority;
bool ClientWithAuthority => clientAuthority && hasAuthority;
void OnValidate()
{
if (target == null)
{
target = GetComponent<Rigidbody>();
}
}
void Update()
{
if (isServer)
{
SyncToClients();
}
else if (ClientWithAuthority)
{
SendToServer();
}
}
void SyncToClients()
{
targetVelocity = target.velocity;
targetPosition = target.position;
}
void SendToServer()
{
float now = Time.time;
if (now > nextSyncTime)
{
nextSyncTime = now + syncInterval;
CmdSendState(target.velocity, target.position);
}
}
[Command]
void CmdSendState(Vector3 velocity, Vector3 position)
{
target.velocity = velocity;
target.position = position;
targetVelocity = velocity;
targetPosition = position;
}
void FixedUpdate()
{
if (IgnoreSync) { return; }
target.velocity = Vector3.Lerp(target.velocity, targetVelocity, lerpVelocityAmount);
target.position = Vector3.Lerp(target.position, targetPosition, lerpPositionAmount);
// add velocity to position as position would have moved on server at that velocity
targetPosition += target.velocity * Time.fixedDeltaTime;
// TODO does this also need to sync acceleration so and update velocity?
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7f032128052c95a46afb0ddd97d994cc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,361 @@
using UnityEngine;
namespace Mirror.Experimental
{
[AddComponentMenu("Network/Experimental/NetworkRigidbody")]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-rigidbody")]
public class NetworkRigidbody : NetworkBehaviour
{
[Header("Settings")]
[SerializeField] internal Rigidbody target = null;
[Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
public bool clientAuthority = false;
[Header("Velocity")]
[Tooltip("Syncs Velocity every SyncInterval")]
[SerializeField] bool syncVelocity = true;
[Tooltip("Set velocity to 0 each frame (only works if syncVelocity is false")]
[SerializeField] bool clearVelocity = false;
[Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")]
[SerializeField] float velocitySensitivity = 0.1f;
[Header("Angular Velocity")]
[Tooltip("Syncs AngularVelocity every SyncInterval")]
[SerializeField] bool syncAngularVelocity = true;
[Tooltip("Set angularVelocity to 0 each frame (only works if syncAngularVelocity is false")]
[SerializeField] bool clearAngularVelocity = false;
[Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")]
[SerializeField] float angularVelocitySensitivity = 0.1f;
/// <summary>
/// Values sent on client with authority after they are sent to the server
/// </summary>
readonly ClientSyncState previousValue = new ClientSyncState();
void OnValidate()
{
if (target == null)
{
target = GetComponent<Rigidbody>();
}
}
#region Sync vars
[SyncVar(hook = nameof(OnVelocityChanged))]
Vector3 velocity;
[SyncVar(hook = nameof(OnAngularVelocityChanged))]
Vector3 angularVelocity;
[SyncVar(hook = nameof(OnIsKinematicChanged))]
bool isKinematic;
[SyncVar(hook = nameof(OnUseGravityChanged))]
bool useGravity;
[SyncVar(hook = nameof(OnuDragChanged))]
float drag;
[SyncVar(hook = nameof(OnAngularDragChanged))]
float angularDrag;
/// <summary>
/// Ignore value if is host or client with Authority
/// </summary>
/// <returns></returns>
bool IgnoreSync => isServer || ClientWithAuthority;
bool ClientWithAuthority => clientAuthority && hasAuthority;
void OnVelocityChanged(Vector3 _, Vector3 newValue)
{
if (IgnoreSync)
return;
target.velocity = newValue;
}
void OnAngularVelocityChanged(Vector3 _, Vector3 newValue)
{
if (IgnoreSync)
return;
target.angularVelocity = newValue;
}
void OnIsKinematicChanged(bool _, bool newValue)
{
if (IgnoreSync)
return;
target.isKinematic = newValue;
}
void OnUseGravityChanged(bool _, bool newValue)
{
if (IgnoreSync)
return;
target.useGravity = newValue;
}
void OnuDragChanged(float _, float newValue)
{
if (IgnoreSync)
return;
target.drag = newValue;
}
void OnAngularDragChanged(float _, float newValue)
{
if (IgnoreSync)
return;
target.angularDrag = newValue;
}
#endregion
internal void Update()
{
if (isServer)
{
SyncToClients();
}
else if (ClientWithAuthority)
{
SendToServer();
}
}
internal void FixedUpdate()
{
if (clearAngularVelocity && !syncAngularVelocity)
{
target.angularVelocity = Vector3.zero;
}
if (clearVelocity && !syncVelocity)
{
target.velocity = Vector3.zero;
}
}
/// <summary>
/// Updates sync var values on server so that they sync to the client
/// </summary>
[Server]
void SyncToClients()
{
// only update if they have changed more than Sensitivity
Vector3 currentVelocity = syncVelocity ? target.velocity : default;
Vector3 currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default;
bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity);
bool angularVelocityChanged = syncAngularVelocity && ((previousValue.angularVelocity - currentAngularVelocity).sqrMagnitude > angularVelocitySensitivity * angularVelocitySensitivity);
if (velocityChanged)
{
velocity = currentVelocity;
previousValue.velocity = currentVelocity;
}
if (angularVelocityChanged)
{
angularVelocity = currentAngularVelocity;
previousValue.angularVelocity = currentAngularVelocity;
}
// other rigidbody settings
isKinematic = target.isKinematic;
useGravity = target.useGravity;
drag = target.drag;
angularDrag = target.angularDrag;
}
/// <summary>
/// Uses Command to send values to server
/// </summary>
[Client]
void SendToServer()
{
if (!hasAuthority)
{
Debug.LogWarning("SendToServer called without authority");
return;
}
SendVelocity();
SendRigidBodySettings();
}
[Client]
void SendVelocity()
{
float now = Time.time;
if (now < previousValue.nextSyncTime)
return;
Vector3 currentVelocity = syncVelocity ? target.velocity : default;
Vector3 currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default;
bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity);
bool angularVelocityChanged = syncAngularVelocity && ((previousValue.angularVelocity - currentAngularVelocity).sqrMagnitude > angularVelocitySensitivity * angularVelocitySensitivity);
// if angularVelocity has changed it is likely that velocity has also changed so just sync both values
// however if only velocity has changed just send velocity
if (angularVelocityChanged)
{
CmdSendVelocityAndAngular(currentVelocity, currentAngularVelocity);
previousValue.velocity = currentVelocity;
previousValue.angularVelocity = currentAngularVelocity;
}
else if (velocityChanged)
{
CmdSendVelocity(currentVelocity);
previousValue.velocity = currentVelocity;
}
// only update syncTime if either has changed
if (angularVelocityChanged || velocityChanged)
{
previousValue.nextSyncTime = now + syncInterval;
}
}
[Client]
void SendRigidBodySettings()
{
// These shouldn't change often so it is ok to send in their own Command
if (previousValue.isKinematic != target.isKinematic)
{
CmdSendIsKinematic(target.isKinematic);
previousValue.isKinematic = target.isKinematic;
}
if (previousValue.useGravity != target.useGravity)
{
CmdSendUseGravity(target.useGravity);
previousValue.useGravity = target.useGravity;
}
if (previousValue.drag != target.drag)
{
CmdSendDrag(target.drag);
previousValue.drag = target.drag;
}
if (previousValue.angularDrag != target.angularDrag)
{
CmdSendAngularDrag(target.angularDrag);
previousValue.angularDrag = target.angularDrag;
}
}
/// <summary>
/// Called when only Velocity has changed on the client
/// </summary>
[Command]
void CmdSendVelocity(Vector3 velocity)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.velocity = velocity;
target.velocity = velocity;
}
/// <summary>
/// Called when angularVelocity has changed on the client
/// </summary>
[Command]
void CmdSendVelocityAndAngular(Vector3 velocity, Vector3 angularVelocity)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
if (syncVelocity)
{
this.velocity = velocity;
target.velocity = velocity;
}
this.angularVelocity = angularVelocity;
target.angularVelocity = angularVelocity;
}
[Command]
void CmdSendIsKinematic(bool isKinematic)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.isKinematic = isKinematic;
target.isKinematic = isKinematic;
}
[Command]
void CmdSendUseGravity(bool useGravity)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.useGravity = useGravity;
target.useGravity = useGravity;
}
[Command]
void CmdSendDrag(float drag)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.drag = drag;
target.drag = drag;
}
[Command]
void CmdSendAngularDrag(float angularDrag)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.angularDrag = angularDrag;
target.angularDrag = angularDrag;
}
/// <summary>
/// holds previously synced values
/// </summary>
public class ClientSyncState
{
/// <summary>
/// Next sync time that velocity will be synced, based on syncInterval.
/// </summary>
public float nextSyncTime;
public Vector3 velocity;
public Vector3 angularVelocity;
public bool isKinematic;
public bool useGravity;
public float drag;
public float angularDrag;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 83392ae5c1b731446909f252fd494ae4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,360 @@
using UnityEngine;
namespace Mirror.Experimental
{
[AddComponentMenu("Network/Experimental/NetworkRigidbody2D")]
public class NetworkRigidbody2D : NetworkBehaviour
{
[Header("Settings")]
[SerializeField] internal Rigidbody2D target = null;
[Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
public bool clientAuthority = false;
[Header("Velocity")]
[Tooltip("Syncs Velocity every SyncInterval")]
[SerializeField] bool syncVelocity = true;
[Tooltip("Set velocity to 0 each frame (only works if syncVelocity is false")]
[SerializeField] bool clearVelocity = false;
[Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")]
[SerializeField] float velocitySensitivity = 0.1f;
[Header("Angular Velocity")]
[Tooltip("Syncs AngularVelocity every SyncInterval")]
[SerializeField] bool syncAngularVelocity = true;
[Tooltip("Set angularVelocity to 0 each frame (only works if syncAngularVelocity is false")]
[SerializeField] bool clearAngularVelocity = false;
[Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")]
[SerializeField] float angularVelocitySensitivity = 0.1f;
/// <summary>
/// Values sent on client with authority after they are sent to the server
/// </summary>
readonly ClientSyncState previousValue = new ClientSyncState();
void OnValidate()
{
if (target == null)
{
target = GetComponent<Rigidbody2D>();
}
}
#region Sync vars
[SyncVar(hook = nameof(OnVelocityChanged))]
Vector2 velocity;
[SyncVar(hook = nameof(OnAngularVelocityChanged))]
float angularVelocity;
[SyncVar(hook = nameof(OnIsKinematicChanged))]
bool isKinematic;
[SyncVar(hook = nameof(OnGravityScaleChanged))]
float gravityScale;
[SyncVar(hook = nameof(OnuDragChanged))]
float drag;
[SyncVar(hook = nameof(OnAngularDragChanged))]
float angularDrag;
/// <summary>
/// Ignore value if is host or client with Authority
/// </summary>
/// <returns></returns>
bool IgnoreSync => isServer || ClientWithAuthority;
bool ClientWithAuthority => clientAuthority && hasAuthority;
void OnVelocityChanged(Vector2 _, Vector2 newValue)
{
if (IgnoreSync)
return;
target.velocity = newValue;
}
void OnAngularVelocityChanged(float _, float newValue)
{
if (IgnoreSync)
return;
target.angularVelocity = newValue;
}
void OnIsKinematicChanged(bool _, bool newValue)
{
if (IgnoreSync)
return;
target.isKinematic = newValue;
}
void OnGravityScaleChanged(float _, float newValue)
{
if (IgnoreSync)
return;
target.gravityScale = newValue;
}
void OnuDragChanged(float _, float newValue)
{
if (IgnoreSync)
return;
target.drag = newValue;
}
void OnAngularDragChanged(float _, float newValue)
{
if (IgnoreSync)
return;
target.angularDrag = newValue;
}
#endregion
internal void Update()
{
if (isServer)
{
SyncToClients();
}
else if (ClientWithAuthority)
{
SendToServer();
}
}
internal void FixedUpdate()
{
if (clearAngularVelocity && !syncAngularVelocity)
{
target.angularVelocity = 0f;
}
if (clearVelocity && !syncVelocity)
{
target.velocity = Vector2.zero;
}
}
/// <summary>
/// Updates sync var values on server so that they sync to the client
/// </summary>
[Server]
void SyncToClients()
{
// only update if they have changed more than Sensitivity
Vector2 currentVelocity = syncVelocity ? target.velocity : default;
float currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default;
bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity);
bool angularVelocityChanged = syncAngularVelocity && ((previousValue.angularVelocity - currentAngularVelocity) > angularVelocitySensitivity);
if (velocityChanged)
{
velocity = currentVelocity;
previousValue.velocity = currentVelocity;
}
if (angularVelocityChanged)
{
angularVelocity = currentAngularVelocity;
previousValue.angularVelocity = currentAngularVelocity;
}
// other rigidbody settings
isKinematic = target.isKinematic;
gravityScale = target.gravityScale;
drag = target.drag;
angularDrag = target.angularDrag;
}
/// <summary>
/// Uses Command to send values to server
/// </summary>
[Client]
void SendToServer()
{
if (!hasAuthority)
{
Debug.LogWarning("SendToServer called without authority");
return;
}
SendVelocity();
SendRigidBodySettings();
}
[Client]
void SendVelocity()
{
float now = Time.time;
if (now < previousValue.nextSyncTime)
return;
Vector2 currentVelocity = syncVelocity ? target.velocity : default;
float currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default;
bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity);
bool angularVelocityChanged = syncAngularVelocity && previousValue.angularVelocity != currentAngularVelocity;//((previousValue.angularVelocity - currentAngularVelocity).sqrMagnitude > angularVelocitySensitivity * angularVelocitySensitivity);
// if angularVelocity has changed it is likely that velocity has also changed so just sync both values
// however if only velocity has changed just send velocity
if (angularVelocityChanged)
{
CmdSendVelocityAndAngular(currentVelocity, currentAngularVelocity);
previousValue.velocity = currentVelocity;
previousValue.angularVelocity = currentAngularVelocity;
}
else if (velocityChanged)
{
CmdSendVelocity(currentVelocity);
previousValue.velocity = currentVelocity;
}
// only update syncTime if either has changed
if (angularVelocityChanged || velocityChanged)
{
previousValue.nextSyncTime = now + syncInterval;
}
}
[Client]
void SendRigidBodySettings()
{
// These shouldn't change often so it is ok to send in their own Command
if (previousValue.isKinematic != target.isKinematic)
{
CmdSendIsKinematic(target.isKinematic);
previousValue.isKinematic = target.isKinematic;
}
if (previousValue.gravityScale != target.gravityScale)
{
CmdChangeGravityScale(target.gravityScale);
previousValue.gravityScale = target.gravityScale;
}
if (previousValue.drag != target.drag)
{
CmdSendDrag(target.drag);
previousValue.drag = target.drag;
}
if (previousValue.angularDrag != target.angularDrag)
{
CmdSendAngularDrag(target.angularDrag);
previousValue.angularDrag = target.angularDrag;
}
}
/// <summary>
/// Called when only Velocity has changed on the client
/// </summary>
[Command]
void CmdSendVelocity(Vector2 velocity)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.velocity = velocity;
target.velocity = velocity;
}
/// <summary>
/// Called when angularVelocity has changed on the client
/// </summary>
[Command]
void CmdSendVelocityAndAngular(Vector2 velocity, float angularVelocity)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
if (syncVelocity)
{
this.velocity = velocity;
target.velocity = velocity;
}
this.angularVelocity = angularVelocity;
target.angularVelocity = angularVelocity;
}
[Command]
void CmdSendIsKinematic(bool isKinematic)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.isKinematic = isKinematic;
target.isKinematic = isKinematic;
}
[Command]
void CmdChangeGravityScale(float gravityScale)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.gravityScale = gravityScale;
target.gravityScale = gravityScale;
}
[Command]
void CmdSendDrag(float drag)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.drag = drag;
target.drag = drag;
}
[Command]
void CmdSendAngularDrag(float angularDrag)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
this.angularDrag = angularDrag;
target.angularDrag = angularDrag;
}
/// <summary>
/// holds previously synced values
/// </summary>
public class ClientSyncState
{
/// <summary>
/// Next sync time that velocity will be synced, based on syncInterval.
/// </summary>
public float nextSyncTime;
public Vector2 velocity;
public float angularVelocity;
public bool isKinematic;
public float gravityScale;
public float drag;
public float angularDrag;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ab2cbc52526ea384ba280d13cd1a57b9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,12 @@
using UnityEngine;
namespace Mirror.Experimental
{
[DisallowMultipleComponent]
[AddComponentMenu("Network/Experimental/NetworkTransformExperimental")]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-transform")]
public class NetworkTransform : NetworkTransformBase
{
protected override Transform targetTransform => transform;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 741bbe11f5357b44593b15c0d11b16bd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,529 @@
// vis2k:
// base class for NetworkTransform and NetworkTransformChild.
// New method is simple and stupid. No more 1500 lines of code.
//
// Server sends current data.
// Client saves it and interpolates last and latest data points.
// Update handles transform movement / rotation
// FixedUpdate handles rigidbody movement / rotation
//
// Notes:
// * Built-in Teleport detection in case of lags / teleport / obstacles
// * Quaternion > EulerAngles because gimbal lock and Quaternion.Slerp
// * Syncs XYZ. Works 3D and 2D. Saving 4 bytes isn't worth 1000 lines of code.
// * Initial delay might happen if server sends packet immediately after moving
// just 1cm, hence we move 1cm and then wait 100ms for next packet
// * Only way for smooth movement is to use a fixed movement speed during
// interpolation. interpolation over time is never that good.
//
using System;
using UnityEngine;
namespace Mirror.Experimental
{
public abstract class NetworkTransformBase : NetworkBehaviour
{
// target transform to sync. can be on a child.
protected abstract Transform targetTransform { get; }
[Header("Authority")]
[Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
[SyncVar]
public bool clientAuthority;
[Tooltip("Set to true if updates from server should be ignored by owner")]
[SyncVar]
public bool excludeOwnerUpdate = true;
[Header("Synchronization")]
[Tooltip("Set to true if position should be synchronized")]
[SyncVar]
public bool syncPosition = true;
[Tooltip("Set to true if rotation should be synchronized")]
[SyncVar]
public bool syncRotation = true;
[Tooltip("Set to true if scale should be synchronized")]
[SyncVar]
public bool syncScale = true;
[Header("Interpolation")]
[Tooltip("Set to true if position should be interpolated")]
[SyncVar]
public bool interpolatePosition = true;
[Tooltip("Set to true if rotation should be interpolated")]
[SyncVar]
public bool interpolateRotation = true;
[Tooltip("Set to true if scale should be interpolated")]
[SyncVar]
public bool interpolateScale = true;
// Sensitivity is added for VR where human players tend to have micro movements so this can quiet down
// the network traffic. Additionally, rigidbody drift should send less traffic, e.g very slow sliding / rolling.
[Header("Sensitivity")]
[Tooltip("Changes to the transform must exceed these values to be transmitted on the network.")]
[SyncVar]
public float localPositionSensitivity = .01f;
[Tooltip("If rotation exceeds this angle, it will be transmitted on the network")]
[SyncVar]
public float localRotationSensitivity = .01f;
[Tooltip("Changes to the transform must exceed these values to be transmitted on the network.")]
[SyncVar]
public float localScaleSensitivity = .01f;
[Header("Diagnostics")]
// server
public Vector3 lastPosition;
public Quaternion lastRotation;
public Vector3 lastScale;
// client
// use local position/rotation for VR support
[Serializable]
public struct DataPoint
{
public float timeStamp;
public Vector3 localPosition;
public Quaternion localRotation;
public Vector3 localScale;
public float movementSpeed;
public bool isValid => timeStamp != 0;
}
// Is this a client with authority over this transform?
// This component could be on the player object or any object that has been assigned authority to this client.
bool IsOwnerWithClientAuthority => hasAuthority && clientAuthority;
// interpolation start and goal
public DataPoint start = new DataPoint();
public DataPoint goal = new DataPoint();
// We need to store this locally on the server so clients can't request Authority when ever they like
bool clientAuthorityBeforeTeleport;
void FixedUpdate()
{
// if server then always sync to others.
// let the clients know that this has moved
if (isServer && HasEitherMovedRotatedScaled())
{
ServerUpdate();
}
if (isClient)
{
// send to server if we have local authority (and aren't the server)
// -> only if connectionToServer has been initialized yet too
if (IsOwnerWithClientAuthority)
{
ClientAuthorityUpdate();
}
else if (goal.isValid)
{
ClientRemoteUpdate();
}
}
}
void ServerUpdate()
{
RpcMove(targetTransform.localPosition, Compression.CompressQuaternion(targetTransform.localRotation), targetTransform.localScale);
}
void ClientAuthorityUpdate()
{
if (!isServer && HasEitherMovedRotatedScaled())
{
// serialize
// local position/rotation for VR support
// send to server
CmdClientToServerSync(targetTransform.localPosition, Compression.CompressQuaternion(targetTransform.localRotation), targetTransform.localScale);
}
}
void ClientRemoteUpdate()
{
// teleport or interpolate
if (NeedsTeleport())
{
// local position/rotation for VR support
ApplyPositionRotationScale(goal.localPosition, goal.localRotation, goal.localScale);
// reset data points so we don't keep interpolating
start = new DataPoint();
goal = new DataPoint();
}
else
{
// local position/rotation for VR support
ApplyPositionRotationScale(InterpolatePosition(start, goal, targetTransform.localPosition),
InterpolateRotation(start, goal, targetTransform.localRotation),
InterpolateScale(start, goal, targetTransform.localScale));
}
}
// moved or rotated or scaled since last time we checked it?
bool HasEitherMovedRotatedScaled()
{
// Save last for next frame to compare only if change was detected, otherwise
// slow moving objects might never sync because of C#'s float comparison tolerance.
// See also: https://github.com/vis2k/Mirror/pull/428)
bool changed = HasMoved || HasRotated || HasScaled;
if (changed)
{
// local position/rotation for VR support
if (syncPosition) lastPosition = targetTransform.localPosition;
if (syncRotation) lastRotation = targetTransform.localRotation;
if (syncScale) lastScale = targetTransform.localScale;
}
return changed;
}
// local position/rotation for VR support
// SqrMagnitude is faster than Distance per Unity docs
// https://docs.unity3d.com/ScriptReference/Vector3-sqrMagnitude.html
bool HasMoved => syncPosition && Vector3.SqrMagnitude(lastPosition - targetTransform.localPosition) > localPositionSensitivity * localPositionSensitivity;
bool HasRotated => syncRotation && Quaternion.Angle(lastRotation, targetTransform.localRotation) > localRotationSensitivity;
bool HasScaled => syncScale && Vector3.SqrMagnitude(lastScale - targetTransform.localScale) > localScaleSensitivity * localScaleSensitivity;
// teleport / lag / stuck detection
// - checking distance is not enough since there could be just a tiny fence between us and the goal
// - checking time always works, this way we just teleport if we still didn't reach the goal after too much time has elapsed
bool NeedsTeleport()
{
// calculate time between the two data points
float startTime = start.isValid ? start.timeStamp : Time.time - Time.fixedDeltaTime;
float goalTime = goal.isValid ? goal.timeStamp : Time.time;
float difference = goalTime - startTime;
float timeSinceGoalReceived = Time.time - goalTime;
return timeSinceGoalReceived > difference * 5;
}
// local authority client sends sync message to server for broadcasting
[Command(channel = Channels.Unreliable)]
void CmdClientToServerSync(Vector3 position, uint packedRotation, Vector3 scale)
{
// Ignore messages from client if not in client authority mode
if (!clientAuthority)
return;
// deserialize payload
SetGoal(position, Compression.DecompressQuaternion(packedRotation), scale);
// server-only mode does no interpolation to save computations, but let's set the position directly
if (isServer && !isClient)
ApplyPositionRotationScale(goal.localPosition, goal.localRotation, goal.localScale);
RpcMove(position, packedRotation, scale);
}
[ClientRpc(channel = Channels.Unreliable)]
void RpcMove(Vector3 position, uint packedRotation, Vector3 scale)
{
if (hasAuthority && excludeOwnerUpdate) return;
if (!isServer)
SetGoal(position, Compression.DecompressQuaternion(packedRotation), scale);
}
// serialization is needed by OnSerialize and by manual sending from authority
void SetGoal(Vector3 position, Quaternion rotation, Vector3 scale)
{
// put it into a data point immediately
DataPoint temp = new DataPoint
{
// deserialize position
localPosition = position,
localRotation = rotation,
localScale = scale,
timeStamp = Time.time
};
// movement speed: based on how far it moved since last time has to be calculated before 'start' is overwritten
temp.movementSpeed = EstimateMovementSpeed(goal, temp, targetTransform, Time.fixedDeltaTime);
// reassign start wisely
// first ever data point? then make something up for previous one so that we can start interpolation without waiting for next.
if (start.timeStamp == 0)
{
start = new DataPoint
{
timeStamp = Time.time - Time.fixedDeltaTime,
// local position/rotation for VR support
localPosition = targetTransform.localPosition,
localRotation = targetTransform.localRotation,
localScale = targetTransform.localScale,
movementSpeed = temp.movementSpeed
};
}
// second or nth data point? then update previous
// but: we start at where ever we are right now, so that it's perfectly smooth and we don't jump anywhere
//
// example if we are at 'x':
//
// A--x->B
//
// and then receive a new point C:
//
// A--x--B
// |
// |
// C
//
// then we don't want to just jump to B and start interpolation:
//
// x
// |
// |
// C
//
// we stay at 'x' and interpolate from there to C:
//
// x..B
// \ .
// \.
// C
//
else
{
float oldDistance = Vector3.Distance(start.localPosition, goal.localPosition);
float newDistance = Vector3.Distance(goal.localPosition, temp.localPosition);
start = goal;
// local position/rotation for VR support
// teleport / lag / obstacle detection: only continue at current position if we aren't too far away
// XC < AB + BC (see comments above)
if (Vector3.Distance(targetTransform.localPosition, start.localPosition) < oldDistance + newDistance)
{
start.localPosition = targetTransform.localPosition;
start.localRotation = targetTransform.localRotation;
start.localScale = targetTransform.localScale;
}
}
// set new destination in any case. new data is best data.
goal = temp;
}
// try to estimate movement speed for a data point based on how far it moved since the previous one
// - if this is the first time ever then we use our best guess:
// - delta based on transform.localPosition
// - elapsed based on send interval hoping that it roughly matches
static float EstimateMovementSpeed(DataPoint from, DataPoint to, Transform transform, float sendInterval)
{
Vector3 delta = to.localPosition - (from.localPosition != transform.localPosition ? from.localPosition : transform.localPosition);
float elapsed = from.isValid ? to.timeStamp - from.timeStamp : sendInterval;
// avoid NaN
return elapsed > 0 ? delta.magnitude / elapsed : 0;
}
// set position carefully depending on the target component
void ApplyPositionRotationScale(Vector3 position, Quaternion rotation, Vector3 scale)
{
// local position/rotation for VR support
if (syncPosition) targetTransform.localPosition = position;
if (syncRotation) targetTransform.localRotation = rotation;
if (syncScale) targetTransform.localScale = scale;
}
// where are we in the timeline between start and goal? [0,1]
Vector3 InterpolatePosition(DataPoint start, DataPoint goal, Vector3 currentPosition)
{
if (!interpolatePosition)
return currentPosition;
if (start.movementSpeed != 0)
{
// Option 1: simply interpolate based on time, but stutter will happen, it's not that smooth.
// This is especially noticeable if the camera automatically follows the player
// - Tell SonarCloud this isn't really commented code but actual comments and to stfu about it
// - float t = CurrentInterpolationFactor();
// - return Vector3.Lerp(start.position, goal.position, t);
// Option 2: always += speed
// speed is 0 if we just started after idle, so always use max for best results
float speed = Mathf.Max(start.movementSpeed, goal.movementSpeed);
return Vector3.MoveTowards(currentPosition, goal.localPosition, speed * Time.deltaTime);
}
return currentPosition;
}
Quaternion InterpolateRotation(DataPoint start, DataPoint goal, Quaternion defaultRotation)
{
if (!interpolateRotation)
return defaultRotation;
if (start.localRotation != goal.localRotation)
{
float t = CurrentInterpolationFactor(start, goal);
return Quaternion.Slerp(start.localRotation, goal.localRotation, t);
}
return defaultRotation;
}
Vector3 InterpolateScale(DataPoint start, DataPoint goal, Vector3 currentScale)
{
if (!interpolateScale)
return currentScale;
if (start.localScale != goal.localScale)
{
float t = CurrentInterpolationFactor(start, goal);
return Vector3.Lerp(start.localScale, goal.localScale, t);
}
return currentScale;
}
static float CurrentInterpolationFactor(DataPoint start, DataPoint goal)
{
if (start.isValid)
{
float difference = goal.timeStamp - start.timeStamp;
// the moment we get 'goal', 'start' is supposed to start, so elapsed time is based on:
float elapsed = Time.time - goal.timeStamp;
// avoid NaN
return difference > 0 ? elapsed / difference : 1;
}
return 1;
}
#region Server Teleport (force move player)
/// <summary>
/// This method will override this GameObject's current Transform.localPosition to the specified Vector3 and update all clients.
/// <para>NOTE: position must be in LOCAL space if the transform has a parent</para>
/// </summary>
/// <param name="localPosition">Where to teleport this GameObject</param>
[Server]
public void ServerTeleport(Vector3 localPosition)
{
Quaternion localRotation = targetTransform.localRotation;
ServerTeleport(localPosition, localRotation);
}
/// <summary>
/// This method will override this GameObject's current Transform.localPosition and Transform.localRotation
/// to the specified Vector3 and Quaternion and update all clients.
/// <para>NOTE: localPosition must be in LOCAL space if the transform has a parent</para>
/// <para>NOTE: localRotation must be in LOCAL space if the transform has a parent</para>
/// </summary>
/// <param name="localPosition">Where to teleport this GameObject</param>
/// <param name="localRotation">Which rotation to set this GameObject</param>
[Server]
public void ServerTeleport(Vector3 localPosition, Quaternion localRotation)
{
// To prevent applying the position updates received from client (if they have ClientAuth) while being teleported.
// clientAuthorityBeforeTeleport defaults to false when not teleporting, if it is true then it means that teleport
// was previously called but not finished therefore we should keep it as true so that 2nd teleport call doesn't clear authority
clientAuthorityBeforeTeleport = clientAuthority || clientAuthorityBeforeTeleport;
clientAuthority = false;
DoTeleport(localPosition, localRotation);
// tell all clients about new values
RpcTeleport(localPosition, Compression.CompressQuaternion(localRotation), clientAuthorityBeforeTeleport);
}
void DoTeleport(Vector3 newLocalPosition, Quaternion newLocalRotation)
{
targetTransform.localPosition = newLocalPosition;
targetTransform.localRotation = newLocalRotation;
// Since we are overriding the position we don't need a goal and start.
// Reset them to null for fresh start
goal = new DataPoint();
start = new DataPoint();
lastPosition = newLocalPosition;
lastRotation = newLocalRotation;
}
[ClientRpc(channel = Channels.Unreliable)]
void RpcTeleport(Vector3 newPosition, uint newPackedRotation, bool isClientAuthority)
{
DoTeleport(newPosition, Compression.DecompressQuaternion(newPackedRotation));
// only send finished if is owner and is ClientAuthority on server
if (hasAuthority && isClientAuthority)
CmdTeleportFinished();
}
/// <summary>
/// This RPC will be invoked on server after client finishes overriding the position.
/// </summary>
/// <param name="initialAuthority"></param>
[Command(channel = Channels.Unreliable)]
void CmdTeleportFinished()
{
if (clientAuthorityBeforeTeleport)
{
clientAuthority = true;
// reset value so doesn't effect future calls, see note in ServerTeleport
clientAuthorityBeforeTeleport = false;
}
else
{
Debug.LogWarning("Client called TeleportFinished when clientAuthority was false on server", this);
}
}
#endregion
#region Debug Gizmos
// draw the data points for easier debugging
void OnDrawGizmos()
{
// draw start and goal points and a line between them
if (start.localPosition != goal.localPosition)
{
DrawDataPointGizmo(start, Color.yellow);
DrawDataPointGizmo(goal, Color.green);
DrawLineBetweenDataPoints(start, goal, Color.cyan);
}
}
static void DrawDataPointGizmo(DataPoint data, Color color)
{
// use a little offset because transform.localPosition might be in the ground in many cases
Vector3 offset = Vector3.up * 0.01f;
// draw position
Gizmos.color = color;
Gizmos.DrawSphere(data.localPosition + offset, 0.5f);
// draw forward and up like unity move tool
Gizmos.color = Color.blue;
Gizmos.DrawRay(data.localPosition + offset, data.localRotation * Vector3.forward);
Gizmos.color = Color.green;
Gizmos.DrawRay(data.localPosition + offset, data.localRotation * Vector3.up);
}
static void DrawLineBetweenDataPoints(DataPoint data1, DataPoint data2, Color color)
{
Gizmos.color = color;
Gizmos.DrawLine(data1.localPosition, data2.localPosition);
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ea7c690c4fbf8c4439726f4c62eda6d3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,18 @@
using UnityEngine;
namespace Mirror.Experimental
{
/// <summary>
/// A component to synchronize the position of child transforms of networked objects.
/// <para>There must be a NetworkTransform on the root object of the hierarchy. There can be multiple NetworkTransformChild components on an object. This does not use physics for synchronization, it simply synchronizes the localPosition and localRotation of the child transform and lerps towards the received values.</para>
/// </summary>
[AddComponentMenu("Network/Experimental/NetworkTransformChildExperimentalExperimental")]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-transform-child")]
public class NetworkTransformChild : NetworkTransformBase
{
[Header("Target")]
public Transform target;
protected override Transform targetTransform => target;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f65214da13a861f4a8ae309d3daea1c6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant: