aha
This commit is contained in:
@ -0,0 +1,547 @@
|
||||
// Snapshot Interpolation: https://gafferongames.com/post/snapshot_interpolation/
|
||||
//
|
||||
// Base class for NetworkTransform and NetworkTransformChild.
|
||||
// => simple unreliable sync without any interpolation for now.
|
||||
// => which means we don't need teleport detection either
|
||||
//
|
||||
// NOTE: several functions are virtual in case someone needs to modify a part.
|
||||
//
|
||||
// Channel: uses UNRELIABLE at all times.
|
||||
// -> out of order packets are dropped automatically
|
||||
// -> it's better than RELIABLE for several reasons:
|
||||
// * head of line blocking would add delay
|
||||
// * resending is mostly pointless
|
||||
// * bigger data race:
|
||||
// -> if we use a Cmd() at position X over reliable
|
||||
// -> client gets Cmd() and X at the same time, but buffers X for bufferTime
|
||||
// -> for unreliable, it would get X before the reliable Cmd(), still
|
||||
// buffer for bufferTime but end up closer to the original time
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public enum CoordinateSpace { Local, World }
|
||||
|
||||
public abstract class NetworkTransformBase : NetworkBehaviour
|
||||
{
|
||||
// target transform to sync. can be on a child.
|
||||
// TODO this field is kind of unnecessary since we now support child NetworkBehaviours
|
||||
[Header("Target")]
|
||||
[Tooltip("The Transform component to sync. May be on on this GameObject, or on a child.")]
|
||||
public Transform target;
|
||||
|
||||
// Is this a client with authority over this transform?
|
||||
// This component could be on the player object or any object that has been assigned authority to this client.
|
||||
protected bool IsClientWithAuthority => isClient && authority;
|
||||
|
||||
// snapshots with initial capacity to avoid early resizing & allocations: see NetworkRigidbodyBenchmark example.
|
||||
public readonly SortedList<double, TransformSnapshot> clientSnapshots = new SortedList<double, TransformSnapshot>(16);
|
||||
public readonly SortedList<double, TransformSnapshot> serverSnapshots = new SortedList<double, TransformSnapshot>(16);
|
||||
|
||||
// selective sync //////////////////////////////////////////////////////
|
||||
[Header("Selective Sync\nDon't change these at Runtime")]
|
||||
public bool syncPosition = true; // do not change at runtime!
|
||||
public bool syncRotation = true; // do not change at runtime!
|
||||
public bool syncScale = false; // do not change at runtime! rare. off by default.
|
||||
|
||||
[Header("Bandwidth Savings")]
|
||||
[Tooltip("When true, changes are not sent unless greater than sensitivity values below.")]
|
||||
public bool onlySyncOnChange = true;
|
||||
[Tooltip("Apply smallest-three quaternion compression. This is lossy, you can disable it if the small rotation inaccuracies are noticeable in your project.")]
|
||||
public bool compressRotation = true;
|
||||
|
||||
// interpolation is on by default, but can be disabled to jump to
|
||||
// the destination immediately. some projects need this.
|
||||
[Header("Interpolation")]
|
||||
[Tooltip("Set to false to have a snap-like effect on position movement.")]
|
||||
public bool interpolatePosition = true;
|
||||
[Tooltip("Set to false to have a snap-like effect on rotations.")]
|
||||
public bool interpolateRotation = true;
|
||||
[Tooltip("Set to false to remove scale smoothing. Example use-case: Instant flipping of sprites that use -X and +X for direction.")]
|
||||
public bool interpolateScale = true;
|
||||
|
||||
// CoordinateSpace ///////////////////////////////////////////////////////////
|
||||
[Header("Coordinate Space")]
|
||||
[Tooltip("Local by default. World may be better when changing hierarchy, or non-NetworkTransforms root position/rotation/scale values.")]
|
||||
public CoordinateSpace coordinateSpace = CoordinateSpace.Local;
|
||||
|
||||
// convert syncInterval to sendIntervalMultiplier.
|
||||
// in the future this can be moved into core to support tick aligned Sync,
|
||||
public uint sendIntervalMultiplier
|
||||
{
|
||||
get
|
||||
{
|
||||
if (syncInterval > 0)
|
||||
{
|
||||
// if syncInterval is > 0, calculate how many multiples of NetworkManager.sendRate it is
|
||||
//
|
||||
// for example:
|
||||
// NetworkServer.sendInterval is 1/60 = 0.16
|
||||
// NetworkTransform.syncInterval is 0.5 (500ms).
|
||||
// 0.5 / 0.16 = 3.125
|
||||
// in other words: 3.125 x sendInterval
|
||||
//
|
||||
// note that NetworkServer.sendInterval is usually set on start.
|
||||
// to make this work in Edit mode, make sure that NetworkManager
|
||||
// OnValidate sets NetworkServer.sendInterval immediately.
|
||||
float multiples = syncInterval / NetworkServer.sendInterval;
|
||||
|
||||
// syncInterval is always supposed to sync at a minimum of 1 x sendInterval.
|
||||
// that's what we do for every other NetworkBehaviour since
|
||||
// we only sync in Broadcast() which is called @ sendInterval.
|
||||
return multiples > 1 ? (uint)Mathf.RoundToInt(multiples) : 1;
|
||||
}
|
||||
|
||||
// if syncInterval is 0, use NetworkManager.sendRate (x1)
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
[Header("Timeline Offset")]
|
||||
[Tooltip("Add a small timeline offset to account for decoupled arrival of NetworkTime and NetworkTransform snapshots.\nfixes: https://github.com/MirrorNetworking/Mirror/issues/3427")]
|
||||
public bool timelineOffset = true;
|
||||
|
||||
// Ninja's Notes on offset & mulitplier:
|
||||
//
|
||||
// In a no multiplier scenario:
|
||||
// 1. Snapshots are sent every frame (frame being 1 NM send interval).
|
||||
// 2. Time Interpolation is set to be 'behind' by 2 frames times.
|
||||
// In theory where everything works, we probably have around 2 snapshots before we need to interpolate snapshots. From NT perspective, we should always have around 2 snapshots ready, so no stutter.
|
||||
//
|
||||
// In a multiplier scenario:
|
||||
// 1. Snapshots are sent every 10 frames.
|
||||
// 2. Time Interpolation remains 'behind by 2 frames'.
|
||||
// When everything works, we are receiving NT snapshots every 10 frames, but start interpolating after 2.
|
||||
// Even if I assume we had 2 snapshots to begin with to start interpolating (which we don't), by the time we reach 13th frame, we are out of snapshots, and have to wait 7 frames for next snapshot to come. This is the reason why we absolutely need the timestamp adjustment. We are starting way too early to interpolate.
|
||||
//
|
||||
protected double timeStampAdjustment => NetworkServer.sendInterval * (sendIntervalMultiplier - 1);
|
||||
protected double offset => timelineOffset ? NetworkServer.sendInterval * sendIntervalMultiplier : 0;
|
||||
|
||||
// velocity for convenience (animators etc.)
|
||||
// this isn't technically NetworkTransforms job, but it's needed by so many projects that we just provide it anyway.
|
||||
public Vector3 velocity { get; private set; }
|
||||
public Vector3 angularVelocity { get; private set; }
|
||||
|
||||
// debugging ///////////////////////////////////////////////////////////
|
||||
[Header("Debug")]
|
||||
public bool showGizmos;
|
||||
public bool showOverlay;
|
||||
public Color overlayColor = new Color(0, 0, 0, 0.5f);
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
// Skip if Editor is in Play mode
|
||||
if (Application.isPlaying) return;
|
||||
|
||||
base.OnValidate();
|
||||
|
||||
// configure in awake
|
||||
Configure();
|
||||
}
|
||||
|
||||
// initialization //////////////////////////////////////////////////////
|
||||
// forcec configuration of some settings
|
||||
protected virtual void Configure()
|
||||
{
|
||||
// set target to self if none yet
|
||||
if (target == null) target = transform;
|
||||
|
||||
// Unity doesn't support setting world scale.
|
||||
// OnValidate force disables syncScale in world mode.
|
||||
if (coordinateSpace == CoordinateSpace.World) syncScale = false;
|
||||
}
|
||||
|
||||
// make sure to call this when inheriting too!
|
||||
protected virtual void Awake()
|
||||
{
|
||||
// sometimes OnValidate() doesn't run before launching a project.
|
||||
// need to guarantee configuration runs.
|
||||
Configure();
|
||||
}
|
||||
|
||||
// snapshot functions //////////////////////////////////////////////////
|
||||
// get local/world position
|
||||
protected virtual Vector3 GetPosition() =>
|
||||
coordinateSpace == CoordinateSpace.Local ? target.localPosition : target.position;
|
||||
|
||||
// get local/world rotation
|
||||
protected virtual Quaternion GetRotation() =>
|
||||
coordinateSpace == CoordinateSpace.Local ? target.localRotation : target.rotation;
|
||||
|
||||
// get local/world scale
|
||||
protected virtual Vector3 GetScale() =>
|
||||
coordinateSpace == CoordinateSpace.Local ? target.localScale : target.lossyScale;
|
||||
|
||||
// set local/world position
|
||||
protected virtual void SetPosition(Vector3 position)
|
||||
{
|
||||
if (coordinateSpace == CoordinateSpace.Local)
|
||||
target.localPosition = position;
|
||||
else
|
||||
target.position = position;
|
||||
}
|
||||
|
||||
// set local/world rotation
|
||||
protected virtual void SetRotation(Quaternion rotation)
|
||||
{
|
||||
if (coordinateSpace == CoordinateSpace.Local)
|
||||
target.localRotation = rotation;
|
||||
else
|
||||
target.rotation = rotation;
|
||||
}
|
||||
|
||||
// set local/world position
|
||||
protected virtual void SetScale(Vector3 scale)
|
||||
{
|
||||
if (coordinateSpace == CoordinateSpace.Local)
|
||||
target.localScale = scale;
|
||||
// Unity doesn't support setting world scale.
|
||||
// OnValidate disables syncScale in world mode.
|
||||
// else
|
||||
// target.lossyScale = scale; // TODO
|
||||
}
|
||||
|
||||
// construct a snapshot of the current state
|
||||
// => internal for testing
|
||||
protected virtual TransformSnapshot Construct()
|
||||
{
|
||||
// NetworkTime.localTime for double precision until Unity has it too
|
||||
return new TransformSnapshot(
|
||||
// our local time is what the other end uses as remote time
|
||||
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
|
||||
0, // the other end fills out local time itself
|
||||
GetPosition(),
|
||||
GetRotation(),
|
||||
GetScale()
|
||||
);
|
||||
}
|
||||
|
||||
protected void AddSnapshot(SortedList<double, TransformSnapshot> snapshots, double timeStamp, Vector3? position, Quaternion? rotation, Vector3? scale)
|
||||
{
|
||||
// position, rotation, scale can have no value if same as last time.
|
||||
// saves bandwidth.
|
||||
// but we still need to feed it to snapshot interpolation. we can't
|
||||
// just have gaps in there if nothing has changed. for example, if
|
||||
// client sends snapshot at t=0
|
||||
// client sends nothing for 10s because not moved
|
||||
// client sends snapshot at t=10
|
||||
// then the server would assume that it's one super slow move and
|
||||
// replay it for 10 seconds.
|
||||
|
||||
if (!position.HasValue) position = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position : GetPosition();
|
||||
if (!rotation.HasValue) rotation = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : GetRotation();
|
||||
if (!scale.HasValue) scale = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : GetScale();
|
||||
|
||||
// insert transform snapshot
|
||||
SnapshotInterpolation.InsertIfNotExists(
|
||||
snapshots,
|
||||
NetworkClient.snapshotSettings.bufferLimit,
|
||||
new TransformSnapshot(
|
||||
timeStamp, // arrival remote timestamp. NOT remote time.
|
||||
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
|
||||
position.Value,
|
||||
rotation.Value,
|
||||
scale.Value
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// apply a snapshot to the Transform.
|
||||
// -> start, end, interpolated are all passed in caes they are needed
|
||||
// -> a regular game would apply the 'interpolated' snapshot
|
||||
// -> a board game might want to jump to 'goal' directly
|
||||
// (it's easier to always interpolate and then apply selectively,
|
||||
// instead of manually interpolating x, y, z, ... depending on flags)
|
||||
// => internal for testing
|
||||
//
|
||||
// NOTE: stuck detection is unnecessary here.
|
||||
// we always set transform.position anyway, we can't get stuck.
|
||||
protected virtual void Apply(TransformSnapshot interpolated, TransformSnapshot endGoal)
|
||||
{
|
||||
// local position/rotation for VR support
|
||||
//
|
||||
// if syncPosition/Rotation/Scale is disabled then we received nulls
|
||||
// -> current position/rotation/scale would've been added as snapshot
|
||||
// -> we still interpolated
|
||||
// -> but simply don't apply it. if the user doesn't want to sync
|
||||
// scale, then we should not touch scale etc.
|
||||
|
||||
// calculate the velocity and angular velocity for the object
|
||||
// these can be used to drive animations or other behaviours
|
||||
if (!isOwned && Time.deltaTime > 0)
|
||||
{
|
||||
velocity = (transform.localPosition - interpolated.position) / Time.deltaTime;
|
||||
angularVelocity = (transform.localRotation.eulerAngles - interpolated.rotation.eulerAngles) / Time.deltaTime;
|
||||
}
|
||||
|
||||
// interpolate parts
|
||||
if (syncPosition) SetPosition(interpolatePosition ? interpolated.position : endGoal.position);
|
||||
if (syncRotation) SetRotation(interpolateRotation ? interpolated.rotation : endGoal.rotation);
|
||||
if (syncScale) SetScale(interpolateScale ? interpolated.scale : endGoal.scale);
|
||||
}
|
||||
|
||||
// client->server teleport to force position without interpolation.
|
||||
// otherwise it would interpolate to a (far away) new position.
|
||||
// => manually calling Teleport is the only 100% reliable solution.
|
||||
[Command]
|
||||
public void CmdTeleport(Vector3 destination)
|
||||
{
|
||||
// client can only teleport objects that it has authority over.
|
||||
if (syncDirection != SyncDirection.ClientToServer) return;
|
||||
|
||||
// TODO what about host mode?
|
||||
OnTeleport(destination);
|
||||
|
||||
// if a client teleports, we need to broadcast to everyone else too
|
||||
// TODO the teleported client should ignore the rpc though.
|
||||
// otherwise if it already moved again after teleporting,
|
||||
// the rpc would come a little bit later and reset it once.
|
||||
// TODO or not? if client ONLY calls Teleport(pos), the position
|
||||
// would only be set after the rpc. unless the client calls
|
||||
// BOTH Teleport(pos) and target.position=pos
|
||||
RpcTeleport(destination);
|
||||
}
|
||||
|
||||
// client->server teleport to force position and rotation without interpolation.
|
||||
// otherwise it would interpolate to a (far away) new position.
|
||||
// => manually calling Teleport is the only 100% reliable solution.
|
||||
[Command]
|
||||
public void CmdTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
// client can only teleport objects that it has authority over.
|
||||
if (syncDirection != SyncDirection.ClientToServer) return;
|
||||
|
||||
// TODO what about host mode?
|
||||
OnTeleport(destination, rotation);
|
||||
|
||||
// if a client teleports, we need to broadcast to everyone else too
|
||||
// TODO the teleported client should ignore the rpc though.
|
||||
// otherwise if it already moved again after teleporting,
|
||||
// the rpc would come a little bit later and reset it once.
|
||||
// TODO or not? if client ONLY calls Teleport(pos), the position
|
||||
// would only be set after the rpc. unless the client calls
|
||||
// BOTH Teleport(pos) and target.position=pos
|
||||
RpcTeleport(destination, rotation);
|
||||
}
|
||||
|
||||
// server->client teleport to force position without interpolation.
|
||||
// otherwise it would interpolate to a (far away) new position.
|
||||
// => manually calling Teleport is the only 100% reliable solution.
|
||||
[ClientRpc]
|
||||
public void RpcTeleport(Vector3 destination)
|
||||
{
|
||||
// NOTE: even in client authority mode, the server is always allowed
|
||||
// to teleport the player. for example:
|
||||
// * CmdEnterPortal() might teleport the player
|
||||
// * Some people use client authority with server sided checks
|
||||
// so the server should be able to reset position if needed.
|
||||
|
||||
// TODO what about host mode?
|
||||
OnTeleport(destination);
|
||||
}
|
||||
|
||||
// server->client teleport to force position and rotation without interpolation.
|
||||
// otherwise it would interpolate to a (far away) new position.
|
||||
// => manually calling Teleport is the only 100% reliable solution.
|
||||
[ClientRpc]
|
||||
public void RpcTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
// NOTE: even in client authority mode, the server is always allowed
|
||||
// to teleport the player. for example:
|
||||
// * CmdEnterPortal() might teleport the player
|
||||
// * Some people use client authority with server sided checks
|
||||
// so the server should be able to reset position if needed.
|
||||
|
||||
// TODO what about host mode?
|
||||
OnTeleport(destination, rotation);
|
||||
}
|
||||
|
||||
// teleport on server, broadcast to clients.
|
||||
[Server]
|
||||
public void ServerTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
OnTeleport(destination, rotation);
|
||||
RpcTeleport(destination, rotation);
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
void RpcResetState()
|
||||
{
|
||||
ResetState();
|
||||
}
|
||||
|
||||
// common Teleport code for client->server and server->client
|
||||
protected virtual void OnTeleport(Vector3 destination)
|
||||
{
|
||||
// set the new position.
|
||||
// interpolation will automatically continue.
|
||||
target.position = destination;
|
||||
|
||||
// reset interpolation to immediately jump to the new position.
|
||||
// do not call Reset() here, this would cause delta compression to
|
||||
// get out of sync for NetworkTransformReliable because NTReliable's
|
||||
// 'override Reset()' resets lastDe/SerializedPosition:
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3588
|
||||
// because client's next OnSerialize() will delta compress,
|
||||
// but server's last delta will have been reset, causing offsets.
|
||||
//
|
||||
// instead, simply clear snapshots.
|
||||
ResetState();
|
||||
|
||||
// TODO
|
||||
// what if we still receive a snapshot from before the interpolation?
|
||||
// it could easily happen over unreliable.
|
||||
// -> maybe add destination as first entry?
|
||||
}
|
||||
|
||||
// common Teleport code for client->server and server->client
|
||||
protected virtual void OnTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
// set the new position.
|
||||
// interpolation will automatically continue.
|
||||
target.position = destination;
|
||||
target.rotation = rotation;
|
||||
|
||||
// reset interpolation to immediately jump to the new position.
|
||||
// do not call Reset() here, this would cause delta compression to
|
||||
// get out of sync for NetworkTransformReliable because NTReliable's
|
||||
// 'override Reset()' resets lastDe/SerializedPosition:
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3588
|
||||
// because client's next OnSerialize() will delta compress,
|
||||
// but server's last delta will have been reset, causing offsets.
|
||||
//
|
||||
// instead, simply clear snapshots.
|
||||
ResetState();
|
||||
|
||||
// TODO
|
||||
// what if we still receive a snapshot from before the interpolation?
|
||||
// it could easily happen over unreliable.
|
||||
// -> maybe add destination as first entry?
|
||||
}
|
||||
|
||||
public virtual void ResetState()
|
||||
{
|
||||
// disabled objects aren't updated anymore.
|
||||
// so let's clear the buffers.
|
||||
serverSnapshots.Clear();
|
||||
clientSnapshots.Clear();
|
||||
|
||||
// Prevent resistance from CharacterController
|
||||
// or non-knematic Rigidbodies when teleporting.
|
||||
Physics.SyncTransforms();
|
||||
}
|
||||
|
||||
public virtual void Reset()
|
||||
{
|
||||
ResetState();
|
||||
// default to ClientToServer so this works immediately for users
|
||||
syncDirection = SyncDirection.ClientToServer;
|
||||
}
|
||||
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
ResetState();
|
||||
|
||||
if (NetworkServer.active)
|
||||
NetworkIdentity.clientAuthorityCallback += OnClientAuthorityChanged;
|
||||
}
|
||||
|
||||
protected virtual void OnDisable()
|
||||
{
|
||||
ResetState();
|
||||
|
||||
if (NetworkServer.active)
|
||||
NetworkIdentity.clientAuthorityCallback -= OnClientAuthorityChanged;
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
void OnClientAuthorityChanged(NetworkConnectionToClient conn, NetworkIdentity identity, bool authorityState)
|
||||
{
|
||||
if (identity != netIdentity) return;
|
||||
|
||||
// If server gets authority or syncdirection is server to client,
|
||||
// we don't reset buffers.
|
||||
// This is because if syncdirection is S to C, we will never have
|
||||
// snapshot issues since there is only ever 1 source.
|
||||
|
||||
if (syncDirection == SyncDirection.ClientToServer)
|
||||
{
|
||||
ResetState();
|
||||
RpcResetState();
|
||||
}
|
||||
}
|
||||
|
||||
// OnGUI allocates even if it does nothing. avoid in release.
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
// debug ///////////////////////////////////////////////////////////////
|
||||
protected virtual void OnGUI()
|
||||
{
|
||||
if (!showOverlay) return;
|
||||
if (!Camera.main) return;
|
||||
|
||||
// show data next to player for easier debugging. this is very useful!
|
||||
// IMPORTANT: this is basically an ESP hack for shooter games.
|
||||
// DO NOT make this available with a hotkey in release builds
|
||||
if (!Debug.isDebugBuild) return;
|
||||
|
||||
// project position to screen
|
||||
Vector3 point = Camera.main.WorldToScreenPoint(target.position);
|
||||
|
||||
// enough alpha, in front of camera and in screen?
|
||||
if (point.z >= 0 && Utils.IsPointInScreen(point))
|
||||
{
|
||||
GUI.color = overlayColor;
|
||||
GUILayout.BeginArea(new Rect(point.x, Screen.height - point.y, 200, 100));
|
||||
|
||||
// always show both client & server buffers so it's super
|
||||
// obvious if we accidentally populate both.
|
||||
GUILayout.Label($"Server Buffer:{serverSnapshots.Count}");
|
||||
GUILayout.Label($"Client Buffer:{clientSnapshots.Count}");
|
||||
|
||||
GUILayout.EndArea();
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void DrawGizmos(SortedList<double, TransformSnapshot> buffer)
|
||||
{
|
||||
// only draw if we have at least two entries
|
||||
if (buffer.Count < 2) return;
|
||||
|
||||
// calculate threshold for 'old enough' snapshots
|
||||
double threshold = NetworkTime.localTime - NetworkClient.bufferTime;
|
||||
Color oldEnoughColor = new Color(0, 1, 0, 0.5f);
|
||||
Color notOldEnoughColor = new Color(0.5f, 0.5f, 0.5f, 0.3f);
|
||||
|
||||
// draw the whole buffer for easier debugging.
|
||||
// it's worth seeing how much we have buffered ahead already
|
||||
for (int i = 0; i < buffer.Count; ++i)
|
||||
{
|
||||
// color depends on if old enough or not
|
||||
TransformSnapshot entry = buffer.Values[i];
|
||||
bool oldEnough = entry.localTime <= threshold;
|
||||
Gizmos.color = oldEnough ? oldEnoughColor : notOldEnoughColor;
|
||||
Gizmos.DrawWireCube(entry.position, Vector3.one);
|
||||
}
|
||||
|
||||
// extra: lines between start<->position<->goal
|
||||
Gizmos.color = Color.green;
|
||||
Gizmos.DrawLine(buffer.Values[0].position, target.position);
|
||||
Gizmos.color = Color.white;
|
||||
Gizmos.DrawLine(target.position, buffer.Values[1].position);
|
||||
}
|
||||
|
||||
protected virtual void OnDrawGizmos()
|
||||
{
|
||||
// This fires in edit mode but that spams NRE's so check isPlaying
|
||||
if (!Application.isPlaying) return;
|
||||
if (!showGizmos) return;
|
||||
|
||||
if (isServer) DrawGizmos(serverSnapshots);
|
||||
if (isClient) DrawGizmos(clientSnapshots);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c44135fde488424eaf28566206ce473
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,717 @@
|
||||
// Quake NetworkTransform based on 2022 NetworkTransformUnreliable.
|
||||
// Snapshot Interpolation: https://gafferongames.com/post/snapshot_interpolation/
|
||||
// Quake: https://www.jfedor.org/quake3/
|
||||
//
|
||||
// Base class for NetworkTransform and NetworkTransformChild.
|
||||
// => simple unreliable sync without any interpolation for now.
|
||||
// => which means we don't need teleport detection either
|
||||
//
|
||||
// several functions are virtual in case someone needs to modify a part.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/Network Transform Hybrid")]
|
||||
public class NetworkTransformHybrid : NetworkBehaviourHybrid
|
||||
{
|
||||
public bool useFixedUpdate;
|
||||
TransformSnapshot? pendingSnapshot;
|
||||
|
||||
// target transform to sync. can be on a child.
|
||||
[Header("Target")]
|
||||
[Tooltip("The Transform component to sync. May be on this GameObject, or on a child.")]
|
||||
public Transform target;
|
||||
|
||||
[Tooltip("Buffer size limit to avoid ever growing list memory consumption attacks.")]
|
||||
public int bufferSizeLimit = 64;
|
||||
internal SortedList<double, TransformSnapshot> clientSnapshots = new SortedList<double, TransformSnapshot>();
|
||||
internal SortedList<double, TransformSnapshot> serverSnapshots = new SortedList<double, TransformSnapshot>();
|
||||
|
||||
// CUSTOM CHANGE: bring back sendRate. this will probably be ported to Mirror.
|
||||
// TODO but use built in syncInterval instead of the extra field here!
|
||||
[Header("Synchronization")]
|
||||
[Tooltip("Send N snapshots per second. Multiples of frame rate make sense.")]
|
||||
public int sendRate = 30; // in Hz. easier to work with as int for EMA. easier to display '30' than '0.333333333'
|
||||
public float sendInterval => 1f / sendRate;
|
||||
// END CUSTOM CHANGE
|
||||
|
||||
// delta compression needs to remember 'last' to compress against.
|
||||
// this is from reliable full state serializations, not from last
|
||||
// unreliable delta since that isn't guaranteed to be delivered.
|
||||
Vector3 lastSerializedBaselinePosition = Vector3.zero;
|
||||
Quaternion lastSerializedBaselineRotation = Quaternion.identity;
|
||||
Vector3 lastSerializedBaselineScale = Vector3.one;
|
||||
|
||||
// save last deserialized baseline to delta decompress against
|
||||
Vector3 lastDeserializedBaselinePosition = Vector3.zero; // unused, but keep for delta
|
||||
Quaternion lastDeserializedBaselineRotation = Quaternion.identity; // unused, but keep for delta
|
||||
Vector3 lastDeserializedBaselineScale = Vector3.one; // unused, but keep for delta
|
||||
|
||||
// sensitivity is for changed-detection,
|
||||
// this is != precision, which is for quantization and delta compression.
|
||||
[Header("Sensitivity"), Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
|
||||
public float positionSensitivity = 0.01f;
|
||||
public float rotationSensitivity = 0.01f;
|
||||
public float scaleSensitivity = 0.01f;
|
||||
|
||||
// selective sync //////////////////////////////////////////////////////
|
||||
[Header("Selective Sync & interpolation")]
|
||||
public bool syncPosition = true;
|
||||
public bool syncRotation = true;
|
||||
public bool syncScale = false;
|
||||
|
||||
// velocity for convenience (animators etc.)
|
||||
// this isn't technically NetworkTransforms job, but it's needed by so many projects that we just provide it anyway.
|
||||
public Vector3 velocity { get; private set; }
|
||||
public Vector3 angularVelocity { get; private set; }
|
||||
|
||||
// debugging ///////////////////////////////////////////////////////////
|
||||
[Header("Debug")]
|
||||
public bool debugDraw;
|
||||
public bool showGizmos;
|
||||
public bool showOverlay;
|
||||
public Color overlayColor = new Color(0, 0, 0, 0.5f);
|
||||
|
||||
// initialization //////////////////////////////////////////////////////
|
||||
// make sure to call this when inheriting too!
|
||||
protected virtual void Awake() {}
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
// Skip if Editor is in Play mode
|
||||
if (Application.isPlaying) return;
|
||||
|
||||
base.OnValidate();
|
||||
Reset();
|
||||
}
|
||||
|
||||
void Reset()
|
||||
{
|
||||
// set target to self if none yet
|
||||
if (target == null) target = transform;
|
||||
|
||||
// we use sendRate for convenience.
|
||||
// but project it to syncInterval for NetworkTransformHybrid to work properly.
|
||||
syncInterval = sendInterval;
|
||||
|
||||
// default to ClientToServer so this works immediately for users
|
||||
syncDirection = SyncDirection.ClientToServer;
|
||||
}
|
||||
|
||||
// apply a snapshot to the Transform.
|
||||
// -> start, end, interpolated are all passed in caes they are needed
|
||||
// -> a regular game would apply the 'interpolated' snapshot
|
||||
// -> a board game might want to jump to 'goal' directly
|
||||
// (it's easier to always interpolate and then apply selectively,
|
||||
// instead of manually interpolating x, y, z, ... depending on flags)
|
||||
// => internal for testing
|
||||
//
|
||||
// NOTE: stuck detection is unnecessary here.
|
||||
// we always set transform.position anyway, we can't get stuck.
|
||||
protected virtual void ApplySnapshot(TransformSnapshot interpolated)
|
||||
{
|
||||
// local position/rotation for VR support
|
||||
//
|
||||
// if syncPosition/Rotation/Scale is disabled then we received nulls
|
||||
// -> current position/rotation/scale would've been added as snapshot
|
||||
// -> we still interpolated
|
||||
// -> but simply don't apply it. if the user doesn't want to sync
|
||||
// scale, then we should not touch scale etc.
|
||||
|
||||
// calculate the velocity and angular velocity for the object
|
||||
// these can be used to drive animations or other behaviours
|
||||
if (!isOwned && Time.deltaTime > 0)
|
||||
{
|
||||
velocity = (transform.localPosition - interpolated.position) / Time.deltaTime;
|
||||
angularVelocity = (transform.localRotation.eulerAngles - interpolated.rotation.eulerAngles) / Time.deltaTime;
|
||||
}
|
||||
|
||||
if (syncPosition) target.localPosition = interpolated.position;
|
||||
if (syncRotation) target.localRotation = interpolated.rotation;
|
||||
if (syncScale) target.localScale = interpolated.scale;
|
||||
}
|
||||
|
||||
// store state after baseline sync
|
||||
protected override void StoreState()
|
||||
{
|
||||
target.GetLocalPositionAndRotation(out lastSerializedBaselinePosition, out lastSerializedBaselineRotation);
|
||||
lastSerializedBaselineScale = target.localScale;
|
||||
}
|
||||
|
||||
// check if position / rotation / scale changed since last _full reliable_ sync.
|
||||
// squared comparisons for performance
|
||||
protected override bool StateChanged()
|
||||
{
|
||||
target.GetLocalPositionAndRotation(out Vector3 position, out Quaternion rotation);
|
||||
Vector3 scale = target.localScale;
|
||||
|
||||
if (syncPosition)
|
||||
{
|
||||
float positionDelta = Vector3.Distance(position, lastSerializedBaselinePosition);
|
||||
if (positionDelta >= positionSensitivity)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (syncRotation)
|
||||
{
|
||||
float rotationDelta = Quaternion.Angle(lastSerializedBaselineRotation, rotation);
|
||||
if (rotationDelta >= rotationSensitivity)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (syncScale)
|
||||
{
|
||||
float scaleDelta = Vector3.Distance(scale, lastSerializedBaselineScale);
|
||||
if (scaleDelta >= scaleSensitivity)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// serialization ///////////////////////////////////////////////////////
|
||||
// called on server and on client, depending on SyncDirection
|
||||
protected override void OnSerializeBaseline(NetworkWriter writer)
|
||||
{
|
||||
// perf: get position/rotation directly. TransformSnapshot is too expensive.
|
||||
// TransformSnapshot snapshot = ConstructSnapshot();
|
||||
target.GetLocalPositionAndRotation(out Vector3 position, out Quaternion rotation);
|
||||
Vector3 scale = target.localScale;
|
||||
|
||||
if (syncPosition) writer.WriteVector3(position);
|
||||
if (syncRotation) writer.WriteQuaternion(rotation);
|
||||
if (syncScale) writer.WriteVector3(scale);
|
||||
}
|
||||
|
||||
// called on server and on client, depending on SyncDirection
|
||||
protected override void OnDeserializeBaseline(NetworkReader reader, byte baselineTick)
|
||||
{
|
||||
// deserialize
|
||||
Vector3? position = null;
|
||||
Quaternion? rotation = null;
|
||||
Vector3? scale = null;
|
||||
|
||||
if (syncPosition)
|
||||
{
|
||||
position = reader.ReadVector3();
|
||||
lastDeserializedBaselinePosition = position.Value;
|
||||
}
|
||||
if (syncRotation)
|
||||
{
|
||||
rotation = reader.ReadQuaternion();
|
||||
lastDeserializedBaselineRotation = rotation.Value;
|
||||
}
|
||||
if (syncScale)
|
||||
{
|
||||
scale = reader.ReadVector3();
|
||||
lastDeserializedBaselineScale = scale.Value;
|
||||
}
|
||||
|
||||
// debug draw: baseline = yellow
|
||||
if (debugDraw && position.HasValue) Debug.DrawLine(position.Value, position.Value + Vector3.up, Color.yellow, 10f);
|
||||
|
||||
// if baseline counts as delta, insert it into snapshot buffer too
|
||||
if (baselineIsDelta)
|
||||
{
|
||||
if (isServer)
|
||||
{
|
||||
OnClientToServerDeltaSync(position, rotation, scale);
|
||||
}
|
||||
else if (isClient)
|
||||
{
|
||||
OnServerToClientDeltaSync(position, rotation, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// called on server and on client, depending on SyncDirection
|
||||
protected override void OnSerializeDelta(NetworkWriter writer)
|
||||
{
|
||||
// perf: get position/rotation directly. TransformSnapshot is too expensive.
|
||||
// TransformSnapshot snapshot = ConstructSnapshot();
|
||||
target.GetLocalPositionAndRotation(out Vector3 position, out Quaternion rotation);
|
||||
Vector3 scale = target.localScale;
|
||||
|
||||
if (syncPosition) writer.WriteVector3(position);
|
||||
if (syncRotation) writer.WriteQuaternion(rotation);
|
||||
if (syncScale) writer.WriteVector3(scale);
|
||||
}
|
||||
|
||||
// called on server and on client, depending on SyncDirection
|
||||
protected override void OnDeserializeDelta(NetworkReader reader, byte baselineTick)
|
||||
{
|
||||
Vector3? position = null;
|
||||
Quaternion? rotation = null;
|
||||
Vector3? scale = null;
|
||||
|
||||
if (syncPosition) position = reader.ReadVector3();
|
||||
if (syncRotation) rotation = reader.ReadQuaternion();
|
||||
if (syncScale) scale = reader.ReadVector3();
|
||||
|
||||
// debug draw: delta = white
|
||||
if (debugDraw && position.HasValue) Debug.DrawLine(position.Value, position.Value + Vector3.up, Color.white, 10f);
|
||||
|
||||
if (isServer)
|
||||
{
|
||||
OnClientToServerDeltaSync(position, rotation, scale);
|
||||
}
|
||||
else if (isClient)
|
||||
{
|
||||
OnServerToClientDeltaSync(position, rotation, scale);
|
||||
}
|
||||
}
|
||||
|
||||
// processing //////////////////////////////////////////////////////////
|
||||
// local authority client sends sync message to server for broadcasting
|
||||
protected virtual void OnClientToServerDeltaSync(Vector3? position, Quaternion? rotation, Vector3? scale)
|
||||
{
|
||||
// only apply if in client authority mode
|
||||
if (syncDirection != SyncDirection.ClientToServer) return;
|
||||
|
||||
// protect against ever-growing buffer size attacks
|
||||
if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return;
|
||||
|
||||
// only player owned objects (with a connection) can send to
|
||||
// server. we can get the timestamp from the connection.
|
||||
double timestamp = connectionToClient.remoteTimeStamp;
|
||||
|
||||
// insert transform snapshot
|
||||
SnapshotInterpolation.InsertIfNotExists(
|
||||
serverSnapshots,
|
||||
bufferSizeLimit,
|
||||
new TransformSnapshot(
|
||||
timestamp, // arrival remote timestamp. NOT remote time.
|
||||
NetworkTime.localTime, // Unity 2019 doesn't have Time.timeAsDouble yet
|
||||
position.HasValue ? position.Value : Vector3.zero,
|
||||
rotation.HasValue ? rotation.Value : Quaternion.identity,
|
||||
scale.HasValue ? scale.Value : Vector3.one
|
||||
));
|
||||
}
|
||||
|
||||
// server broadcasts sync message to all clients
|
||||
protected virtual void OnServerToClientDeltaSync(Vector3? position, Quaternion? rotation, Vector3? scale)
|
||||
{
|
||||
// in host mode, the server sends rpcs to all clients.
|
||||
// the host client itself will receive them too.
|
||||
// -> host server is always the source of truth
|
||||
// -> we can ignore any rpc on the host client
|
||||
// => otherwise host objects would have ever growing clientBuffers
|
||||
// (rpc goes to clients. if isServer is true too then we are host)
|
||||
if (isServer) return;
|
||||
|
||||
// don't apply for local player with authority
|
||||
if (IsClientWithAuthority) return;
|
||||
|
||||
// Debug.Log($"[{name}] Client: received delta for baseline #{baselineTick}");
|
||||
|
||||
// on the client, we receive rpcs for all entities.
|
||||
// not all of them have a connectionToServer.
|
||||
// but all of them go through NetworkClient.connection.
|
||||
// we can get the timestamp from there.
|
||||
double timestamp = NetworkClient.connection.remoteTimeStamp;
|
||||
|
||||
// position, rotation, scale can have no value if same as last time.
|
||||
// saves bandwidth.
|
||||
// but we still need to feed it to snapshot interpolation. we can't
|
||||
// just have gaps in there if nothing has changed. for example, if
|
||||
// client sends snapshot at t=0
|
||||
// client sends nothing for 10s because not moved
|
||||
// client sends snapshot at t=10
|
||||
// then the server would assume that it's one super slow move and
|
||||
// replay it for 10 seconds.
|
||||
// if (!syncPosition) position = clientSnapshots.Count > 0 ? clientSnapshots.Values[clientSnapshots.Count - 1].position : target.localPosition;
|
||||
// if (!syncRotation) rotation = clientSnapshots.Count > 0 ? clientSnapshots.Values[clientSnapshots.Count - 1].rotation : target.localRotation;
|
||||
// if (!syncScale) scale = clientSnapshots.Count > 0 ? clientSnapshots.Values[clientSnapshots.Count - 1].scale : target.localScale;
|
||||
|
||||
// insert snapshot
|
||||
SnapshotInterpolation.InsertIfNotExists(
|
||||
clientSnapshots,
|
||||
bufferSizeLimit,
|
||||
new TransformSnapshot(
|
||||
timestamp, // arrival remote timestamp. NOT remote time.
|
||||
NetworkTime.localTime, // Unity 2019 doesn't have Time.timeAsDouble yet
|
||||
position.HasValue ? position.Value : Vector3.zero,
|
||||
rotation.HasValue ? rotation.Value : Quaternion.identity,
|
||||
scale.HasValue ? scale.Value : Vector3.one
|
||||
));
|
||||
}
|
||||
|
||||
// update server ///////////////////////////////////////////////////////
|
||||
void UpdateServerInterpolation()
|
||||
{
|
||||
// apply buffered snapshots IF client authority
|
||||
// -> in server authority, server moves the object
|
||||
// so no need to apply any snapshots there.
|
||||
// -> don't apply for host mode player objects either, even if in
|
||||
// client authority mode. if it doesn't go over the network,
|
||||
// then we don't need to do anything.
|
||||
if (syncDirection == SyncDirection.ClientToServer && !isOwned)
|
||||
{
|
||||
if (serverSnapshots.Count > 0)
|
||||
{
|
||||
// step the transform interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
serverSnapshots,
|
||||
// CUSTOM CHANGE: allow for custom sendRate+sendInterval again.
|
||||
// for example, if the object is moving @ 1 Hz, always put it back by 1s.
|
||||
// that's how we still get smooth movement even with a global timeline.
|
||||
connectionToClient.remoteTimeline - sendInterval,
|
||||
// END CUSTOM CHANGE
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
if (useFixedUpdate)
|
||||
pendingSnapshot = computed;
|
||||
else
|
||||
ApplySnapshot(computed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update client ///////////////////////////////////////////////////////
|
||||
void UpdateClientInterpolation()
|
||||
{
|
||||
// only while we have snapshots
|
||||
if (clientSnapshots.Count > 0)
|
||||
{
|
||||
// step the interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
clientSnapshots,
|
||||
// CUSTOM CHANGE: allow for custom sendRate+sendInterval again.
|
||||
// for example, if the object is moving @ 1 Hz, always put it back by 1s.
|
||||
// that's how we still get smooth movement even with a global timeline.
|
||||
NetworkTime.time - sendInterval, // == NetworkClient.localTimeline from snapshot interpolation
|
||||
// END CUSTOM CHANGE
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
if (useFixedUpdate)
|
||||
pendingSnapshot = computed;
|
||||
else
|
||||
ApplySnapshot(computed);
|
||||
}
|
||||
}
|
||||
|
||||
// Update() without LateUpdate() split: otherwise perf. is cut in half!
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update(); // NetworkBehaviourHybrid
|
||||
|
||||
if (isServer)
|
||||
{
|
||||
// interpolate remote clients
|
||||
UpdateServerInterpolation();
|
||||
}
|
||||
// 'else if' because host mode shouldn't update both.
|
||||
else if (isClient)
|
||||
{
|
||||
// interpolate remote client (and local player if no authority)
|
||||
if (!IsClientWithAuthority) UpdateClientInterpolation();
|
||||
}
|
||||
}
|
||||
|
||||
void FixedUpdate()
|
||||
{
|
||||
if (!useFixedUpdate) return;
|
||||
|
||||
if (pendingSnapshot.HasValue)
|
||||
{
|
||||
ApplySnapshot(pendingSnapshot.Value);
|
||||
pendingSnapshot = null;
|
||||
}
|
||||
}
|
||||
|
||||
// common Teleport code for client->server and server->client
|
||||
protected virtual void OnTeleport(Vector3 destination)
|
||||
{
|
||||
// reset any in-progress interpolation & buffers
|
||||
ResetState();
|
||||
|
||||
// set the new position.
|
||||
// interpolation will automatically continue.
|
||||
target.position = destination;
|
||||
|
||||
// TODO
|
||||
// what if we still receive a snapshot from before the interpolation?
|
||||
// it could easily happen over unreliable.
|
||||
// -> maybe add destination as first entry?
|
||||
}
|
||||
|
||||
// common Teleport code for client->server and server->client
|
||||
protected virtual void OnTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
// reset any in-progress interpolation & buffers
|
||||
ResetState();
|
||||
|
||||
// set the new position.
|
||||
// interpolation will automatically continue.
|
||||
target.position = destination;
|
||||
target.rotation = rotation;
|
||||
|
||||
// TODO
|
||||
// what if we still receive a snapshot from before the interpolation?
|
||||
// it could easily happen over unreliable.
|
||||
// -> maybe add destination as first entry?
|
||||
}
|
||||
|
||||
// server->client teleport to force position without interpolation.
|
||||
// otherwise it would interpolate to a (far away) new position.
|
||||
// => manually calling Teleport is the only 100% reliable solution.
|
||||
[ClientRpc]
|
||||
public void RpcTeleport(Vector3 destination)
|
||||
{
|
||||
// NOTE: even in client authority mode, the server is always allowed
|
||||
// to teleport the player. for example:
|
||||
// * CmdEnterPortal() might teleport the player
|
||||
// * Some people use client authority with server sided checks
|
||||
// so the server should be able to reset position if needed.
|
||||
|
||||
// TODO what about host mode?
|
||||
OnTeleport(destination);
|
||||
}
|
||||
|
||||
// server->client teleport to force position and rotation without interpolation.
|
||||
// otherwise it would interpolate to a (far away) new position.
|
||||
// => manually calling Teleport is the only 100% reliable solution.
|
||||
[ClientRpc]
|
||||
public void RpcTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
// NOTE: even in client authority mode, the server is always allowed
|
||||
// to teleport the player. for example:
|
||||
// * CmdEnterPortal() might teleport the player
|
||||
// * Some people use client authority with server sided checks
|
||||
// so the server should be able to reset position if needed.
|
||||
|
||||
// TODO what about host mode?
|
||||
OnTeleport(destination, rotation);
|
||||
}
|
||||
|
||||
// client->server teleport to force position without interpolation.
|
||||
// otherwise it would interpolate to a (far away) new position.
|
||||
// => manually calling Teleport is the only 100% reliable solution.
|
||||
[Command]
|
||||
public void CmdTeleport(Vector3 destination)
|
||||
{
|
||||
// client can only teleport objects that it has authority over.
|
||||
if (syncDirection != SyncDirection.ClientToServer) return;
|
||||
|
||||
// TODO what about host mode?
|
||||
OnTeleport(destination);
|
||||
|
||||
// if a client teleports, we need to broadcast to everyone else too
|
||||
// TODO the teleported client should ignore the rpc though.
|
||||
// otherwise if it already moved again after teleporting,
|
||||
// the rpc would come a little bit later and reset it once.
|
||||
// TODO or not? if client ONLY calls Teleport(pos), the position
|
||||
// would only be set after the rpc. unless the client calls
|
||||
// BOTH Teleport(pos) and target.position=pos
|
||||
RpcTeleport(destination);
|
||||
}
|
||||
|
||||
// client->server teleport to force position and rotation without interpolation.
|
||||
// otherwise it would interpolate to a (far away) new position.
|
||||
// => manually calling Teleport is the only 100% reliable solution.
|
||||
[Command]
|
||||
public void CmdTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
// client can only teleport objects that it has authority over.
|
||||
if (syncDirection != SyncDirection.ClientToServer) return;
|
||||
|
||||
// TODO what about host mode?
|
||||
OnTeleport(destination, rotation);
|
||||
|
||||
// if a client teleports, we need to broadcast to everyone else too
|
||||
// TODO the teleported client should ignore the rpc though.
|
||||
// otherwise if it already moved again after teleporting,
|
||||
// the rpc would come a little bit later and reset it once.
|
||||
// TODO or not? if client ONLY calls Teleport(pos), the position
|
||||
// would only be set after the rpc. unless the client calls
|
||||
// BOTH Teleport(pos) and target.position=pos
|
||||
RpcTeleport(destination, rotation);
|
||||
}
|
||||
|
||||
[Server]
|
||||
public void ServerTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
OnTeleport(destination, rotation);
|
||||
RpcTeleport(destination, rotation);
|
||||
}
|
||||
|
||||
public override void ResetState()
|
||||
{
|
||||
base.ResetState(); // NetworkBehaviourHybrid
|
||||
|
||||
// disabled objects aren't updated anymore so let's clear the buffers.
|
||||
serverSnapshots.Clear();
|
||||
clientSnapshots.Clear();
|
||||
|
||||
// reset baseline
|
||||
lastSerializedBaselinePosition = Vector3.zero;
|
||||
lastSerializedBaselineRotation = Quaternion.identity;
|
||||
lastSerializedBaselineScale = Vector3.one;
|
||||
|
||||
lastDeserializedBaselinePosition = Vector3.zero;
|
||||
lastDeserializedBaselineRotation = Quaternion.identity;
|
||||
lastDeserializedBaselineScale = Vector3.one;
|
||||
|
||||
// Prevent resistance from CharacterController
|
||||
// or non-knematic Rigidbodies when teleporting.
|
||||
Physics.SyncTransforms();
|
||||
|
||||
// Debug.Log($"[{name}] ResetState to baselineTick=0");
|
||||
}
|
||||
|
||||
protected virtual void OnDisable() => ResetState();
|
||||
protected virtual void OnEnable() => ResetState();
|
||||
|
||||
public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
{
|
||||
// OnSerialize(initial) is called every time when a player starts observing us.
|
||||
// note this is _not_ called just once on spawn.
|
||||
|
||||
base.OnSerialize(writer, initialState); // NetworkBehaviourHybrid
|
||||
|
||||
// sync target component's position on spawn.
|
||||
// fixes https://github.com/vis2k/Mirror/pull/3051/
|
||||
// (Spawn message wouldn't sync NTChild positions either)
|
||||
if (initialState)
|
||||
{
|
||||
// spawn message is used as first baseline.
|
||||
// perf: get position/rotation directly. TransformSnapshot is too expensive.
|
||||
// TransformSnapshot snapshot = ConstructSnapshot();
|
||||
target.GetLocalPositionAndRotation(out Vector3 position, out Quaternion rotation);
|
||||
Vector3 scale = target.localScale;
|
||||
|
||||
if (syncPosition) writer.WriteVector3(position);
|
||||
if (syncRotation) writer.WriteQuaternion(rotation);
|
||||
if (syncScale) writer.WriteVector3(scale);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
{
|
||||
base.OnDeserialize(reader, initialState); // NetworkBehaviourHybrid
|
||||
|
||||
// sync target component's position on spawn.
|
||||
// fixes https://github.com/vis2k/Mirror/pull/3051/
|
||||
// (Spawn message wouldn't sync NTChild positions either)
|
||||
if (initialState)
|
||||
{
|
||||
// save last deserialized baseline tick number to compare deltas against
|
||||
Vector3 position = Vector3.zero;
|
||||
Quaternion rotation = Quaternion.identity;
|
||||
Vector3 scale = Vector3.one;
|
||||
|
||||
if (syncPosition)
|
||||
{
|
||||
position = reader.ReadVector3();
|
||||
lastDeserializedBaselinePosition = position;
|
||||
}
|
||||
if (syncRotation)
|
||||
{
|
||||
rotation = reader.ReadQuaternion();
|
||||
lastDeserializedBaselineRotation = rotation;
|
||||
}
|
||||
if (syncScale)
|
||||
{
|
||||
scale = reader.ReadVector3();
|
||||
lastDeserializedBaselineScale = scale;
|
||||
}
|
||||
|
||||
// if baseline counts as delta, insert it into snapshot buffer too
|
||||
if (baselineIsDelta)
|
||||
OnServerToClientDeltaSync(position, rotation, scale);
|
||||
}
|
||||
}
|
||||
// CUSTOM CHANGE ///////////////////////////////////////////////////////////
|
||||
// Don't run OnGUI or draw gizmos in debug builds.
|
||||
// OnGUI allocates even if it does nothing. avoid in release.
|
||||
//#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
#if UNITY_EDITOR
|
||||
// debug ///////////////////////////////////////////////////////////////
|
||||
// END CUSTOM CHANGE ///////////////////////////////////////////////////////
|
||||
protected virtual void OnGUI()
|
||||
{
|
||||
if (!showOverlay) return;
|
||||
|
||||
// show data next to player for easier debugging. this is very useful!
|
||||
// IMPORTANT: this is basically an ESP hack for shooter games.
|
||||
// DO NOT make this available with a hotkey in release builds
|
||||
if (!Debug.isDebugBuild) return;
|
||||
|
||||
// project position to screen
|
||||
Vector3 point = Camera.main.WorldToScreenPoint(target.position);
|
||||
|
||||
// enough alpha, in front of camera and in screen?
|
||||
if (point.z >= 0 && Utils.IsPointInScreen(point))
|
||||
{
|
||||
GUI.color = overlayColor;
|
||||
GUILayout.BeginArea(new Rect(point.x, Screen.height - point.y, 200, 100));
|
||||
|
||||
// always show both client & server buffers so it's super
|
||||
// obvious if we accidentally populate both.
|
||||
GUILayout.Label($"Server Buffer:{serverSnapshots.Count}");
|
||||
GUILayout.Label($"Client Buffer:{clientSnapshots.Count}");
|
||||
|
||||
GUILayout.EndArea();
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void DrawGizmos(SortedList<double, TransformSnapshot> buffer)
|
||||
{
|
||||
// only draw if we have at least two entries
|
||||
if (buffer.Count < 2) return;
|
||||
|
||||
// calculate threshold for 'old enough' snapshots
|
||||
double threshold = NetworkTime.localTime - NetworkClient.bufferTime;
|
||||
Color oldEnoughColor = new Color(0, 1, 0, 0.5f);
|
||||
Color notOldEnoughColor = new Color(0.5f, 0.5f, 0.5f, 0.3f);
|
||||
|
||||
// draw the whole buffer for easier debugging.
|
||||
// it's worth seeing how much we have buffered ahead already
|
||||
for (int i = 0; i < buffer.Count; ++i)
|
||||
{
|
||||
// color depends on if old enough or not
|
||||
TransformSnapshot entry = buffer.Values[i];
|
||||
bool oldEnough = entry.localTime <= threshold;
|
||||
Gizmos.color = oldEnough ? oldEnoughColor : notOldEnoughColor;
|
||||
Gizmos.DrawCube(entry.position, Vector3.one);
|
||||
}
|
||||
|
||||
// extra: lines between start<->position<->goal
|
||||
Gizmos.color = Color.green;
|
||||
Gizmos.DrawLine(buffer.Values[0].position, target.position);
|
||||
Gizmos.color = Color.white;
|
||||
Gizmos.DrawLine(target.position, buffer.Values[1].position);
|
||||
}
|
||||
|
||||
protected virtual void OnDrawGizmos()
|
||||
{
|
||||
// This fires in edit mode but that spams NRE's so check isPlaying
|
||||
if (!Application.isPlaying) return;
|
||||
if (!showGizmos) return;
|
||||
|
||||
if (isServer) DrawGizmos(serverSnapshots);
|
||||
if (isClient) DrawGizmos(clientSnapshots);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f63ea2e505fd484193fb31c5c55ca73
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,448 @@
|
||||
// NetworkTransform V3 (reliable) by mischa (2022-10)
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/Network Transform (Reliable)")]
|
||||
public class NetworkTransformReliable : NetworkTransformBase
|
||||
{
|
||||
uint sendIntervalCounter = 0;
|
||||
double lastSendIntervalTime = double.MinValue;
|
||||
TransformSnapshot? pendingSnapshot;
|
||||
|
||||
[Header("Additional Settings")]
|
||||
[Tooltip("If we only sync on change, then we need to correct old snapshots if more time than sendInterval * multiplier has elapsed.\n\nOtherwise the first move will always start interpolating from the last move sequence's time, which will make it stutter when starting every time.")]
|
||||
public float onlySyncOnChangeCorrectionMultiplier = 2;
|
||||
public bool useFixedUpdate;
|
||||
|
||||
[Header("Rotation")]
|
||||
[Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
|
||||
public float rotationSensitivity = 0.01f;
|
||||
|
||||
// delta compression is capable of detecting byte-level changes.
|
||||
// if we scale float position to bytes,
|
||||
// then small movements will only change one byte.
|
||||
// this gives optimal bandwidth.
|
||||
// benchmark with 0.01 precision: 130 KB/s => 60 KB/s
|
||||
// benchmark with 0.1 precision: 130 KB/s => 30 KB/s
|
||||
[Header("Precision")]
|
||||
[Tooltip("Position is rounded in order to drastically minimize bandwidth.\n\nFor example, a precision of 0.01 rounds to a centimeter. In other words, sub-centimeter movements aren't synced until they eventually exceeded an actual centimeter.\n\nDepending on how important the object is, a precision of 0.01-0.10 (1-10 cm) is recommended.\n\nFor example, even a 1cm precision combined with delta compression cuts the Benchmark demo's bandwidth in half, compared to sending every tiny change.")]
|
||||
[Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range.
|
||||
public float positionPrecision = 0.01f; // 1 cm
|
||||
[Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range.
|
||||
public float scalePrecision = 0.01f; // 1 cm
|
||||
|
||||
// delta compression needs to remember 'last' to compress against
|
||||
protected Vector3Long lastSerializedPosition = Vector3Long.zero;
|
||||
protected Vector3Long lastDeserializedPosition = Vector3Long.zero;
|
||||
|
||||
protected Vector3Long lastSerializedScale = Vector3Long.zero;
|
||||
protected Vector3Long lastDeserializedScale = Vector3Long.zero;
|
||||
|
||||
// Used to store last sent snapshots
|
||||
protected TransformSnapshot last;
|
||||
|
||||
// update //////////////////////////////////////////////////////////////
|
||||
void Update()
|
||||
{
|
||||
// if server then always sync to others.
|
||||
if (isServer) UpdateServer();
|
||||
// 'else if' because host mode shouldn't send anything to server.
|
||||
// it is the server. don't overwrite anything there.
|
||||
else if (isClient) UpdateClient();
|
||||
}
|
||||
|
||||
void FixedUpdate()
|
||||
{
|
||||
if (!useFixedUpdate) return;
|
||||
|
||||
if (pendingSnapshot.HasValue && !IsClientWithAuthority)
|
||||
{
|
||||
// Apply via base method, but in FixedUpdate
|
||||
Apply(pendingSnapshot.Value, pendingSnapshot.Value);
|
||||
pendingSnapshot = null;
|
||||
}
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
// set dirty to trigger OnSerialize. either always, or only if changed.
|
||||
// It has to be checked in LateUpdate() for onlySyncOnChange to avoid
|
||||
// the possibility of Update() running first before the object's movement
|
||||
// script's Update(), which then causes NT to send every alternate frame
|
||||
// instead.
|
||||
if (isServer || (IsClientWithAuthority && NetworkClient.ready))
|
||||
{
|
||||
if (sendIntervalCounter == sendIntervalMultiplier && (!onlySyncOnChange || Changed(Construct())))
|
||||
SetDirty();
|
||||
|
||||
CheckLastSendTime();
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void UpdateServer()
|
||||
{
|
||||
// apply buffered snapshots IF client authority
|
||||
// -> in server authority, server moves the object
|
||||
// so no need to apply any snapshots there.
|
||||
// -> don't apply for host mode player objects either, even if in
|
||||
// client authority mode. if it doesn't go over the network,
|
||||
// then we don't need to do anything.
|
||||
// -> connectionToClient is briefly null after scene changes:
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3329
|
||||
if (syncDirection == SyncDirection.ClientToServer &&
|
||||
connectionToClient != null &&
|
||||
!isOwned)
|
||||
{
|
||||
if (serverSnapshots.Count > 0)
|
||||
{
|
||||
// step the transform interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
serverSnapshots,
|
||||
connectionToClient.remoteTimeline,
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
Apply(computed, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void UpdateClient()
|
||||
{
|
||||
if (useFixedUpdate)
|
||||
{
|
||||
if (!IsClientWithAuthority && clientSnapshots.Count > 0)
|
||||
{
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
clientSnapshots,
|
||||
NetworkTime.time,
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t
|
||||
);
|
||||
pendingSnapshot = TransformSnapshot.Interpolate(from, to, t);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// client authority, and local player (= allowed to move myself)?
|
||||
if (!IsClientWithAuthority)
|
||||
{
|
||||
// only while we have snapshots
|
||||
if (clientSnapshots.Count > 0)
|
||||
{
|
||||
// step the interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
clientSnapshots,
|
||||
NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
Apply(computed, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void CheckLastSendTime()
|
||||
{
|
||||
// timeAsDouble not available in older Unity versions.
|
||||
if (AccurateInterval.Elapsed(NetworkTime.localTime, NetworkServer.sendInterval, ref lastSendIntervalTime))
|
||||
{
|
||||
if (sendIntervalCounter == sendIntervalMultiplier)
|
||||
sendIntervalCounter = 0;
|
||||
sendIntervalCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
// check if position / rotation / scale changed since last sync
|
||||
protected virtual bool Changed(TransformSnapshot current) =>
|
||||
// position is quantized and delta compressed.
|
||||
// only consider it changed if the quantized representation is changed.
|
||||
// careful: don't use 'serialized / deserialized last'. as it depends on sync mode etc.
|
||||
QuantizedChanged(last.position, current.position, positionPrecision) ||
|
||||
// rotation isn't quantized / delta compressed.
|
||||
// check with sensitivity.
|
||||
Quaternion.Angle(last.rotation, current.rotation) > rotationSensitivity ||
|
||||
// scale is quantized and delta compressed.
|
||||
// only consider it changed if the quantized representation is changed.
|
||||
// careful: don't use 'serialized / deserialized last'. as it depends on sync mode etc.
|
||||
QuantizedChanged(last.scale, current.scale, scalePrecision);
|
||||
|
||||
// helper function to compare quantized representations of a Vector3
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected bool QuantizedChanged(Vector3 u, Vector3 v, float precision)
|
||||
{
|
||||
Compression.ScaleToLong(u, precision, out Vector3Long uQuantized);
|
||||
Compression.ScaleToLong(v, precision, out Vector3Long vQuantized);
|
||||
return uQuantized != vQuantized;
|
||||
}
|
||||
|
||||
// NT may be used on client/server/host to Owner/Observers with
|
||||
// ServerToClient or ClientToServer.
|
||||
// however, OnSerialize should always delta against last.
|
||||
public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
{
|
||||
// get current snapshot for broadcasting.
|
||||
TransformSnapshot snapshot = Construct();
|
||||
|
||||
// ClientToServer optimization:
|
||||
// for interpolated client owned identities,
|
||||
// always broadcast the latest known snapshot so other clients can
|
||||
// interpolate immediately instead of catching up too
|
||||
|
||||
// TODO dirty mask? [compression is very good w/o it already]
|
||||
// each vector's component is delta compressed.
|
||||
// an unchanged component would still require 1 byte.
|
||||
// let's use a dirty bit mask to filter those out as well.
|
||||
|
||||
// initial
|
||||
if (initialState)
|
||||
{
|
||||
// If there is a last serialized snapshot, we use it.
|
||||
// This prevents the new client getting a snapshot that is different
|
||||
// from what the older clients last got. If this happens, and on the next
|
||||
// regular serialisation the delta compression will get wrong values.
|
||||
// Notes:
|
||||
// 1. Interestingly only the older clients have it wrong, because at the end
|
||||
// of this function, last = snapshot which is the initial state's snapshot
|
||||
// 2. Regular NTR gets by this bug because it sends every frame anyway so initialstate
|
||||
// snapshot constructed would have been the same as the last anyway.
|
||||
if (last.remoteTime > 0) snapshot = last;
|
||||
if (syncPosition) writer.WriteVector3(snapshot.position);
|
||||
if (syncRotation)
|
||||
{
|
||||
// (optional) smallest three compression for now. no delta.
|
||||
if (compressRotation)
|
||||
writer.WriteUInt(Compression.CompressQuaternion(snapshot.rotation));
|
||||
else
|
||||
writer.WriteQuaternion(snapshot.rotation);
|
||||
}
|
||||
if (syncScale) writer.WriteVector3(snapshot.scale);
|
||||
}
|
||||
// delta
|
||||
else
|
||||
{
|
||||
// int before = writer.Position;
|
||||
|
||||
if (syncPosition)
|
||||
{
|
||||
// quantize -> delta -> varint
|
||||
Compression.ScaleToLong(snapshot.position, positionPrecision, out Vector3Long quantized);
|
||||
DeltaCompression.Compress(writer, lastSerializedPosition, quantized);
|
||||
}
|
||||
if (syncRotation)
|
||||
{
|
||||
// (optional) smallest three compression for now. no delta.
|
||||
if (compressRotation)
|
||||
writer.WriteUInt(Compression.CompressQuaternion(snapshot.rotation));
|
||||
else
|
||||
writer.WriteQuaternion(snapshot.rotation);
|
||||
}
|
||||
if (syncScale)
|
||||
{
|
||||
// quantize -> delta -> varint
|
||||
Compression.ScaleToLong(snapshot.scale, scalePrecision, out Vector3Long quantized);
|
||||
DeltaCompression.Compress(writer, lastSerializedScale, quantized);
|
||||
}
|
||||
}
|
||||
|
||||
// save serialized as 'last' for next delta compression
|
||||
if (syncPosition) Compression.ScaleToLong(snapshot.position, positionPrecision, out lastSerializedPosition);
|
||||
if (syncScale) Compression.ScaleToLong(snapshot.scale, scalePrecision, out lastSerializedScale);
|
||||
|
||||
// set 'last'
|
||||
last = snapshot;
|
||||
}
|
||||
|
||||
public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
{
|
||||
Vector3? position = null;
|
||||
Quaternion? rotation = null;
|
||||
Vector3? scale = null;
|
||||
|
||||
// initial
|
||||
if (initialState)
|
||||
{
|
||||
if (syncPosition) position = reader.ReadVector3();
|
||||
if (syncRotation)
|
||||
{
|
||||
// (optional) smallest three compression for now. no delta.
|
||||
if (compressRotation)
|
||||
rotation = Compression.DecompressQuaternion(reader.ReadUInt());
|
||||
else
|
||||
rotation = reader.ReadQuaternion();
|
||||
}
|
||||
if (syncScale) scale = reader.ReadVector3();
|
||||
}
|
||||
// delta
|
||||
else
|
||||
{
|
||||
// varint -> delta -> quantize
|
||||
if (syncPosition)
|
||||
{
|
||||
Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedPosition);
|
||||
position = Compression.ScaleToFloat(quantized, positionPrecision);
|
||||
}
|
||||
if (syncRotation)
|
||||
{
|
||||
// (optional) smallest three compression for now. no delta.
|
||||
if (compressRotation)
|
||||
rotation = Compression.DecompressQuaternion(reader.ReadUInt());
|
||||
else
|
||||
rotation = reader.ReadQuaternion();
|
||||
}
|
||||
if (syncScale)
|
||||
{
|
||||
Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedScale);
|
||||
scale = Compression.ScaleToFloat(quantized, scalePrecision);
|
||||
}
|
||||
}
|
||||
|
||||
// handle depending on server / client / host.
|
||||
// server has priority for host mode.
|
||||
if (isServer) OnClientToServerSync(position, rotation, scale);
|
||||
else if (isClient) OnServerToClientSync(position, rotation, scale);
|
||||
|
||||
// save deserialized as 'last' for next delta compression
|
||||
if (syncPosition) Compression.ScaleToLong(position.Value, positionPrecision, out lastDeserializedPosition);
|
||||
if (syncScale) Compression.ScaleToLong(scale.Value, scalePrecision, out lastDeserializedScale);
|
||||
}
|
||||
|
||||
// sync ////////////////////////////////////////////////////////////////
|
||||
|
||||
// local authority client sends sync message to server for broadcasting
|
||||
protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale)
|
||||
{
|
||||
// only apply if in client authority mode
|
||||
if (syncDirection != SyncDirection.ClientToServer) return;
|
||||
|
||||
// protect against ever growing buffer size attacks
|
||||
if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return;
|
||||
|
||||
// 'only sync on change' needs a correction on every new move sequence.
|
||||
if (onlySyncOnChange &&
|
||||
NeedsCorrection(serverSnapshots, connectionToClient.remoteTimeStamp, NetworkServer.sendInterval * sendIntervalMultiplier, onlySyncOnChangeCorrectionMultiplier))
|
||||
{
|
||||
RewriteHistory(
|
||||
serverSnapshots,
|
||||
connectionToClient.remoteTimeStamp,
|
||||
NetworkTime.localTime, // arrival remote timestamp. NOT remote timeline.
|
||||
NetworkServer.sendInterval * sendIntervalMultiplier, // Unity 2019 doesn't have timeAsDouble yet
|
||||
GetPosition(),
|
||||
GetRotation(),
|
||||
GetScale());
|
||||
}
|
||||
|
||||
// add a small timeline offset to account for decoupled arrival of
|
||||
// NetworkTime and NetworkTransform snapshots.
|
||||
// needs to be sendInterval. half sendInterval doesn't solve it.
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3427
|
||||
// remove this after LocalWorldState.
|
||||
AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
|
||||
}
|
||||
|
||||
// server broadcasts sync message to all clients
|
||||
protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale)
|
||||
{
|
||||
// don't apply for local player with authority
|
||||
if (IsClientWithAuthority) return;
|
||||
|
||||
// 'only sync on change' needs a correction on every new move sequence.
|
||||
if (onlySyncOnChange &&
|
||||
NeedsCorrection(clientSnapshots, NetworkClient.connection.remoteTimeStamp, NetworkClient.sendInterval * sendIntervalMultiplier, onlySyncOnChangeCorrectionMultiplier))
|
||||
{
|
||||
RewriteHistory(
|
||||
clientSnapshots,
|
||||
NetworkClient.connection.remoteTimeStamp, // arrival remote timestamp. NOT remote timeline.
|
||||
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
|
||||
NetworkClient.sendInterval * sendIntervalMultiplier,
|
||||
GetPosition(),
|
||||
GetRotation(),
|
||||
GetScale());
|
||||
}
|
||||
|
||||
// add a small timeline offset to account for decoupled arrival of
|
||||
// NetworkTime and NetworkTransform snapshots.
|
||||
// needs to be sendInterval. half sendInterval doesn't solve it.
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3427
|
||||
// remove this after LocalWorldState.
|
||||
AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
|
||||
}
|
||||
|
||||
// only sync on change /////////////////////////////////////////////////
|
||||
// snap interp. needs a continous flow of packets.
|
||||
// 'only sync on change' interrupts it while not changed.
|
||||
// once it restarts, snap interp. will interp from the last old position.
|
||||
// this will cause very noticeable stutter for the first move each time.
|
||||
// the fix is quite simple.
|
||||
|
||||
// 1. detect if the remaining snapshot is too old from a past move.
|
||||
static bool NeedsCorrection(
|
||||
SortedList<double, TransformSnapshot> snapshots,
|
||||
double remoteTimestamp,
|
||||
double bufferTime,
|
||||
double toleranceMultiplier) =>
|
||||
snapshots.Count == 1 &&
|
||||
remoteTimestamp - snapshots.Keys[0] >= bufferTime * toleranceMultiplier;
|
||||
|
||||
// 2. insert a fake snapshot at current position,
|
||||
// exactly one 'sendInterval' behind the newly received one.
|
||||
static void RewriteHistory(
|
||||
SortedList<double, TransformSnapshot> snapshots,
|
||||
// timestamp of packet arrival, not interpolated remote time!
|
||||
double remoteTimeStamp,
|
||||
double localTime,
|
||||
double sendInterval,
|
||||
Vector3 position,
|
||||
Quaternion rotation,
|
||||
Vector3 scale)
|
||||
{
|
||||
// clear the previous snapshot
|
||||
snapshots.Clear();
|
||||
|
||||
// insert a fake one at where we used to be,
|
||||
// 'sendInterval' behind the new one.
|
||||
SnapshotInterpolation.InsertIfNotExists(
|
||||
snapshots,
|
||||
NetworkClient.snapshotSettings.bufferLimit,
|
||||
new TransformSnapshot(
|
||||
remoteTimeStamp - sendInterval, // arrival remote timestamp. NOT remote time.
|
||||
localTime - sendInterval, // Unity 2019 doesn't have timeAsDouble yet
|
||||
position,
|
||||
rotation,
|
||||
scale
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// 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 ResetState()
|
||||
{
|
||||
base.ResetState();
|
||||
|
||||
// reset delta
|
||||
lastSerializedPosition = Vector3Long.zero;
|
||||
lastDeserializedPosition = Vector3Long.zero;
|
||||
|
||||
lastSerializedScale = Vector3Long.zero;
|
||||
lastDeserializedScale = Vector3Long.zero;
|
||||
|
||||
// reset 'last' for delta too
|
||||
last = new TransformSnapshot(0, 0, Vector3.zero, Quaternion.identity, Vector3.zero);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8ff3ba0becae47b8b9381191598957c8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,462 @@
|
||||
// NetworkTransform V2 by mischa (2021-07)
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/Network Transform (Unreliable)")]
|
||||
public class NetworkTransformUnreliable : NetworkTransformBase
|
||||
{
|
||||
uint sendIntervalCounter = 0;
|
||||
double lastSendIntervalTime = double.MinValue;
|
||||
TransformSnapshot? pendingSnapshot;
|
||||
|
||||
[Header("Additional Settings")]
|
||||
// Testing under really bad network conditions, 2%-5% packet loss and 250-1200ms ping, 5 proved to eliminate any twitching, however this should not be the default as it is a rare case Developers may want to cover.
|
||||
[Tooltip("How much time, as a multiple of send interval, has passed before clearing buffers.\nA larger buffer means more delay, but results in smoother movement.\nExample: 1 for faster responses minimal smoothing, 5 covers bad pings but has noticable delay, 3 is recommended for balanced results.")]
|
||||
public float bufferResetMultiplier = 3;
|
||||
public bool useFixedUpdate;
|
||||
|
||||
[Header("Sensitivity"), Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
|
||||
public float positionSensitivity = 0.01f;
|
||||
public float rotationSensitivity = 0.01f;
|
||||
public float scaleSensitivity = 0.01f;
|
||||
|
||||
// Used to store last sent snapshots
|
||||
protected TransformSnapshot lastSnapshot;
|
||||
protected Changed cachedChangedComparison;
|
||||
protected bool hasSentUnchangedPosition;
|
||||
|
||||
// update //////////////////////////////////////////////////////////////
|
||||
// Update applies interpolation
|
||||
void Update()
|
||||
{
|
||||
if (isServer) UpdateServerInterpolation();
|
||||
// for all other clients (and for local player if !authority),
|
||||
// we need to apply snapshots from the buffer.
|
||||
// 'else if' because host mode shouldn't interpolate client
|
||||
else if (isClient && !IsClientWithAuthority) UpdateClientInterpolation();
|
||||
}
|
||||
|
||||
void FixedUpdate()
|
||||
{
|
||||
if (!useFixedUpdate) return;
|
||||
|
||||
if (pendingSnapshot.HasValue)
|
||||
{
|
||||
Apply(pendingSnapshot.Value, pendingSnapshot.Value);
|
||||
pendingSnapshot = null;
|
||||
}
|
||||
}
|
||||
|
||||
// LateUpdate broadcasts.
|
||||
// movement scripts may change positions in Update.
|
||||
// use LateUpdate to ensure changes are detected in the same frame.
|
||||
// otherwise this may run before user update, delaying detection until next frame.
|
||||
// this could cause visible jitter.
|
||||
void LateUpdate()
|
||||
{
|
||||
// if server then always sync to others.
|
||||
if (isServer) UpdateServerBroadcast();
|
||||
// client authority, and local player (= allowed to move myself)?
|
||||
// 'else if' because host mode shouldn't send anything to server.
|
||||
// it is the server. don't overwrite anything there.
|
||||
else if (isClient && IsClientWithAuthority) UpdateClientBroadcast();
|
||||
}
|
||||
|
||||
protected virtual void CheckLastSendTime()
|
||||
{
|
||||
// We check interval every frame, and then send if interval is reached.
|
||||
// So by the time sendIntervalCounter == sendIntervalMultiplier, data is sent,
|
||||
// thus we reset the counter here.
|
||||
// This fixes previous issue of, if sendIntervalMultiplier = 1, we send every frame,
|
||||
// because intervalCounter is always = 1 in the previous version.
|
||||
|
||||
// Changing == to >= https://github.com/MirrorNetworking/Mirror/issues/3571
|
||||
|
||||
if (sendIntervalCounter >= sendIntervalMultiplier)
|
||||
sendIntervalCounter = 0;
|
||||
|
||||
// timeAsDouble not available in older Unity versions.
|
||||
if (AccurateInterval.Elapsed(NetworkTime.localTime, NetworkServer.sendInterval, ref lastSendIntervalTime))
|
||||
sendIntervalCounter++;
|
||||
}
|
||||
|
||||
void UpdateServerBroadcast()
|
||||
{
|
||||
// broadcast to all clients each 'sendInterval'
|
||||
// (client with authority will drop the rpc)
|
||||
// NetworkTime.localTime for double precision until Unity has it too
|
||||
//
|
||||
// IMPORTANT:
|
||||
// snapshot interpolation requires constant sending.
|
||||
// DO NOT only send if position changed. for example:
|
||||
// ---
|
||||
// * client sends first position at t=0
|
||||
// * ... 10s later ...
|
||||
// * client moves again, sends second position at t=10
|
||||
// ---
|
||||
// * server gets first position at t=0
|
||||
// * server gets second position at t=10
|
||||
// * server moves from first to second within a time of 10s
|
||||
// => would be a super slow move, instead of a wait & move.
|
||||
//
|
||||
// IMPORTANT:
|
||||
// DO NOT send nulls if not changed 'since last send' either. we
|
||||
// send unreliable and don't know which 'last send' the other end
|
||||
// received successfully.
|
||||
//
|
||||
// Checks to ensure server only sends snapshots if object is
|
||||
// on server authority(!clientAuthority) mode because on client
|
||||
// authority mode snapshots are broadcasted right after the authoritative
|
||||
// client updates server in the command function(see above), OR,
|
||||
// since host does not send anything to update the server, any client
|
||||
// authoritative movement done by the host will have to be broadcasted
|
||||
// here by checking IsClientWithAuthority.
|
||||
// TODO send same time that NetworkServer sends time snapshot?
|
||||
CheckLastSendTime();
|
||||
|
||||
if (sendIntervalCounter == sendIntervalMultiplier && // same interval as time interpolation!
|
||||
(syncDirection == SyncDirection.ServerToClient || IsClientWithAuthority))
|
||||
{
|
||||
// send snapshot without timestamp.
|
||||
// receiver gets it from batch timestamp to save bandwidth.
|
||||
TransformSnapshot snapshot = Construct();
|
||||
|
||||
cachedChangedComparison = CompareChangedSnapshots(snapshot);
|
||||
|
||||
if ((cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) && hasSentUnchangedPosition && onlySyncOnChange) { return; }
|
||||
|
||||
SyncData syncData = new SyncData(cachedChangedComparison, snapshot);
|
||||
|
||||
RpcServerToClientSync(syncData);
|
||||
|
||||
if (cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot)
|
||||
{
|
||||
hasSentUnchangedPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasSentUnchangedPosition = false;
|
||||
UpdateLastSentSnapshot(cachedChangedComparison, snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateServerInterpolation()
|
||||
{
|
||||
// apply buffered snapshots IF client authority
|
||||
// -> in server authority, server moves the object
|
||||
// so no need to apply any snapshots there.
|
||||
// -> don't apply for host mode player objects either, even if in
|
||||
// client authority mode. if it doesn't go over the network,
|
||||
// then we don't need to do anything.
|
||||
// -> connectionToClient is briefly null after scene changes:
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3329
|
||||
if (syncDirection == SyncDirection.ClientToServer &&
|
||||
connectionToClient != null &&
|
||||
!isOwned)
|
||||
{
|
||||
if (serverSnapshots.Count == 0) return;
|
||||
|
||||
// step the transform interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
serverSnapshots,
|
||||
connectionToClient.remoteTimeline,
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
if (useFixedUpdate)
|
||||
pendingSnapshot = computed;
|
||||
else
|
||||
Apply(computed, to);
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateClientBroadcast()
|
||||
{
|
||||
// https://github.com/vis2k/Mirror/pull/2992/
|
||||
if (!NetworkClient.ready) return;
|
||||
|
||||
// send to server each 'sendInterval'
|
||||
// NetworkTime.localTime for double precision until Unity has it too
|
||||
//
|
||||
// IMPORTANT:
|
||||
// snapshot interpolation requires constant sending.
|
||||
// DO NOT only send if position changed. for example:
|
||||
// ---
|
||||
// * client sends first position at t=0
|
||||
// * ... 10s later ...
|
||||
// * client moves again, sends second position at t=10
|
||||
// ---
|
||||
// * server gets first position at t=0
|
||||
// * server gets second position at t=10
|
||||
// * server moves from first to second within a time of 10s
|
||||
// => would be a super slow move, instead of a wait & move.
|
||||
//
|
||||
// IMPORTANT:
|
||||
// DO NOT send nulls if not changed 'since last send' either. we
|
||||
// send unreliable and don't know which 'last send' the other end
|
||||
// received successfully.
|
||||
CheckLastSendTime();
|
||||
if (sendIntervalCounter == sendIntervalMultiplier) // same interval as time interpolation!
|
||||
{
|
||||
// send snapshot without timestamp.
|
||||
// receiver gets it from batch timestamp to save bandwidth.
|
||||
TransformSnapshot snapshot = Construct();
|
||||
|
||||
cachedChangedComparison = CompareChangedSnapshots(snapshot);
|
||||
|
||||
if ((cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) && hasSentUnchangedPosition && onlySyncOnChange) { return; }
|
||||
|
||||
SyncData syncData = new SyncData(cachedChangedComparison, snapshot);
|
||||
|
||||
CmdClientToServerSync(syncData);
|
||||
|
||||
if (cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot)
|
||||
{
|
||||
hasSentUnchangedPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasSentUnchangedPosition = false;
|
||||
UpdateLastSentSnapshot(cachedChangedComparison, snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateClientInterpolation()
|
||||
{
|
||||
// only while we have snapshots
|
||||
if (clientSnapshots.Count == 0) return;
|
||||
|
||||
// step the interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
clientSnapshots,
|
||||
NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
if (useFixedUpdate)
|
||||
pendingSnapshot = computed;
|
||||
else
|
||||
Apply(computed, to);
|
||||
}
|
||||
|
||||
public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
{
|
||||
// sync target component's position on spawn.
|
||||
// fixes https://github.com/vis2k/Mirror/pull/3051/
|
||||
// (Spawn message wouldn't sync NTChild positions either)
|
||||
if (initialState)
|
||||
{
|
||||
if (syncPosition) writer.WriteVector3(GetPosition());
|
||||
if (syncRotation) writer.WriteQuaternion(GetRotation());
|
||||
if (syncScale) writer.WriteVector3(GetScale());
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
{
|
||||
// sync target component's position on spawn.
|
||||
// fixes https://github.com/vis2k/Mirror/pull/3051/
|
||||
// (Spawn message wouldn't sync NTChild positions either)
|
||||
if (initialState)
|
||||
{
|
||||
if (syncPosition) SetPosition(reader.ReadVector3());
|
||||
if (syncRotation) SetRotation(reader.ReadQuaternion());
|
||||
if (syncScale) SetScale(reader.ReadVector3());
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void UpdateLastSentSnapshot(Changed change, TransformSnapshot currentSnapshot)
|
||||
{
|
||||
if (change == Changed.None || change == Changed.CompressRot) return;
|
||||
|
||||
if ((change & Changed.PosX) > 0) lastSnapshot.position.x = currentSnapshot.position.x;
|
||||
if ((change & Changed.PosY) > 0) lastSnapshot.position.y = currentSnapshot.position.y;
|
||||
if ((change & Changed.PosZ) > 0) lastSnapshot.position.z = currentSnapshot.position.z;
|
||||
|
||||
if (compressRotation)
|
||||
{
|
||||
if ((change & Changed.Rot) > 0) lastSnapshot.rotation = currentSnapshot.rotation;
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector3 newRotation;
|
||||
newRotation.x = (change & Changed.RotX) > 0 ? currentSnapshot.rotation.eulerAngles.x : lastSnapshot.rotation.eulerAngles.x;
|
||||
newRotation.y = (change & Changed.RotY) > 0 ? currentSnapshot.rotation.eulerAngles.y : lastSnapshot.rotation.eulerAngles.y;
|
||||
newRotation.z = (change & Changed.RotZ) > 0 ? currentSnapshot.rotation.eulerAngles.z : lastSnapshot.rotation.eulerAngles.z;
|
||||
|
||||
lastSnapshot.rotation = Quaternion.Euler(newRotation);
|
||||
}
|
||||
|
||||
if ((change & Changed.Scale) > 0) lastSnapshot.scale = currentSnapshot.scale;
|
||||
}
|
||||
|
||||
// Returns true if position, rotation AND scale are unchanged, within given sensitivity range.
|
||||
// Note the sensitivity comparison are different for pos, rot and scale.
|
||||
protected virtual Changed CompareChangedSnapshots(TransformSnapshot currentSnapshot)
|
||||
{
|
||||
Changed change = Changed.None;
|
||||
|
||||
if (syncPosition)
|
||||
{
|
||||
bool positionChanged = Vector3.SqrMagnitude(lastSnapshot.position - currentSnapshot.position) > positionSensitivity * positionSensitivity;
|
||||
if (positionChanged)
|
||||
{
|
||||
if (Mathf.Abs(lastSnapshot.position.x - currentSnapshot.position.x) > positionSensitivity) change |= Changed.PosX;
|
||||
if (Mathf.Abs(lastSnapshot.position.y - currentSnapshot.position.y) > positionSensitivity) change |= Changed.PosY;
|
||||
if (Mathf.Abs(lastSnapshot.position.z - currentSnapshot.position.z) > positionSensitivity) change |= Changed.PosZ;
|
||||
}
|
||||
}
|
||||
|
||||
if (syncRotation)
|
||||
{
|
||||
if (compressRotation)
|
||||
{
|
||||
bool rotationChanged = Quaternion.Angle(lastSnapshot.rotation, currentSnapshot.rotation) > rotationSensitivity;
|
||||
if (rotationChanged)
|
||||
{
|
||||
// Here we set all Rot enum flags, to tell us if there was a change in rotation
|
||||
// when using compression. If no change, we don't write the compressed Quat.
|
||||
change |= Changed.CompressRot;
|
||||
change |= Changed.Rot;
|
||||
}
|
||||
else
|
||||
{
|
||||
change |= Changed.CompressRot;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Mathf.Abs(lastSnapshot.rotation.eulerAngles.x - currentSnapshot.rotation.eulerAngles.x) > rotationSensitivity) change |= Changed.RotX;
|
||||
if (Mathf.Abs(lastSnapshot.rotation.eulerAngles.y - currentSnapshot.rotation.eulerAngles.y) > rotationSensitivity) change |= Changed.RotY;
|
||||
if (Mathf.Abs(lastSnapshot.rotation.eulerAngles.z - currentSnapshot.rotation.eulerAngles.z) > rotationSensitivity) change |= Changed.RotZ;
|
||||
}
|
||||
}
|
||||
|
||||
if (syncScale)
|
||||
{
|
||||
if (Vector3.SqrMagnitude(lastSnapshot.scale - currentSnapshot.scale) > scaleSensitivity * scaleSensitivity) change |= Changed.Scale;
|
||||
}
|
||||
|
||||
return change;
|
||||
}
|
||||
|
||||
[Command(channel = Channels.Unreliable)]
|
||||
void CmdClientToServerSync(SyncData syncData)
|
||||
{
|
||||
OnClientToServerSync(syncData);
|
||||
//For client authority, immediately pass on the client snapshot to all other
|
||||
//clients instead of waiting for server to send its snapshots.
|
||||
if (syncDirection == SyncDirection.ClientToServer)
|
||||
RpcServerToClientSync(syncData);
|
||||
}
|
||||
|
||||
protected virtual void OnClientToServerSync(SyncData syncData)
|
||||
{
|
||||
// only apply if in client authority mode
|
||||
if (syncDirection != SyncDirection.ClientToServer) return;
|
||||
|
||||
// protect against ever growing buffer size attacks
|
||||
if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return;
|
||||
|
||||
// only player owned objects (with a connection) can send to
|
||||
// server. we can get the timestamp from the connection.
|
||||
double timestamp = connectionToClient.remoteTimeStamp;
|
||||
|
||||
if (onlySyncOnChange)
|
||||
{
|
||||
double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkClient.sendInterval;
|
||||
|
||||
if (serverSnapshots.Count > 0 && serverSnapshots.Values[serverSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
|
||||
ResetState();
|
||||
}
|
||||
|
||||
UpdateSyncData(ref syncData, serverSnapshots);
|
||||
|
||||
AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, syncData.position, syncData.quatRotation, syncData.scale);
|
||||
}
|
||||
|
||||
[ClientRpc(channel = Channels.Unreliable)]
|
||||
void RpcServerToClientSync(SyncData syncData) =>
|
||||
OnServerToClientSync(syncData);
|
||||
|
||||
protected virtual void OnServerToClientSync(SyncData syncData)
|
||||
{
|
||||
// in host mode, the server sends rpcs to all clients.
|
||||
// the host client itself will receive them too.
|
||||
// -> host server is always the source of truth
|
||||
// -> we can ignore any rpc on the host client
|
||||
// => otherwise host objects would have ever growing clientBuffers
|
||||
// (rpc goes to clients. if isServer is true too then we are host)
|
||||
if (isServer) return;
|
||||
|
||||
// don't apply for local player with authority
|
||||
if (IsClientWithAuthority) return;
|
||||
|
||||
// on the client, we receive rpcs for all entities.
|
||||
// not all of them have a connectionToServer.
|
||||
// but all of them go through NetworkClient.connection.
|
||||
// we can get the timestamp from there.
|
||||
double timestamp = NetworkClient.connection.remoteTimeStamp;
|
||||
|
||||
if (onlySyncOnChange)
|
||||
{
|
||||
double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkServer.sendInterval;
|
||||
|
||||
if (clientSnapshots.Count > 0 && clientSnapshots.Values[clientSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
|
||||
ResetState();
|
||||
}
|
||||
|
||||
UpdateSyncData(ref syncData, clientSnapshots);
|
||||
|
||||
AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, syncData.position, syncData.quatRotation, syncData.scale);
|
||||
}
|
||||
|
||||
protected virtual void UpdateSyncData(ref SyncData syncData, SortedList<double, TransformSnapshot> snapshots)
|
||||
{
|
||||
if (syncData.changedDataByte == Changed.None || syncData.changedDataByte == Changed.CompressRot)
|
||||
{
|
||||
syncData.position = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position : GetPosition();
|
||||
syncData.quatRotation = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : GetRotation();
|
||||
syncData.scale = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : GetScale();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Just going to update these without checking if syncposition or not,
|
||||
// because if not syncing position, NT will not apply any position data
|
||||
// to the target during Apply().
|
||||
|
||||
syncData.position.x = (syncData.changedDataByte & Changed.PosX) > 0 ? syncData.position.x : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position.x : GetPosition().x);
|
||||
syncData.position.y = (syncData.changedDataByte & Changed.PosY) > 0 ? syncData.position.y : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position.y : GetPosition().y);
|
||||
syncData.position.z = (syncData.changedDataByte & Changed.PosZ) > 0 ? syncData.position.z : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position.z : GetPosition().z);
|
||||
|
||||
// If compressRot is true, we already have the Quat in syncdata.
|
||||
if ((syncData.changedDataByte & Changed.CompressRot) == 0)
|
||||
{
|
||||
syncData.vecRotation.x = (syncData.changedDataByte & Changed.RotX) > 0 ? syncData.vecRotation.x : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation.eulerAngles.x : GetRotation().eulerAngles.x);
|
||||
syncData.vecRotation.y = (syncData.changedDataByte & Changed.RotY) > 0 ? syncData.vecRotation.y : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation.eulerAngles.y : GetRotation().eulerAngles.y); ;
|
||||
syncData.vecRotation.z = (syncData.changedDataByte & Changed.RotZ) > 0 ? syncData.vecRotation.z : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation.eulerAngles.z : GetRotation().eulerAngles.z);
|
||||
|
||||
syncData.quatRotation = Quaternion.Euler(syncData.vecRotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
syncData.quatRotation = (syncData.changedDataByte & Changed.Rot) > 0 ? syncData.quatRotation : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : GetRotation());
|
||||
}
|
||||
|
||||
syncData.scale = (syncData.changedDataByte & Changed.Scale) > 0 ? syncData.scale : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : GetScale());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a553cb17010b2403e8523b558bffbc14
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,68 @@
|
||||
// snapshot for snapshot interpolation
|
||||
// https://gafferongames.com/post/snapshot_interpolation/
|
||||
// position, rotation, scale for compatibility for now.
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// NetworkTransform Snapshot
|
||||
public struct TransformSnapshot : Snapshot
|
||||
{
|
||||
// time or sequence are needed to throw away older snapshots.
|
||||
//
|
||||
// glenn fiedler starts with a 16 bit sequence number.
|
||||
// supposedly this is meant as a simplified example.
|
||||
// in the end we need the remote timestamp for accurate interpolation
|
||||
// and buffering over time.
|
||||
//
|
||||
// note: in theory, IF server sends exactly(!) at the same interval then
|
||||
// the 16 bit ushort timestamp would be enough to calculate the
|
||||
// remote time (sequence * sendInterval). but Unity's update is
|
||||
// not guaranteed to run on the exact intervals / do catchup.
|
||||
// => remote timestamp is better for now
|
||||
//
|
||||
// [REMOTE TIME, NOT LOCAL TIME]
|
||||
// => DOUBLE for long term accuracy & batching gives us double anyway
|
||||
public double remoteTime { get; set; }
|
||||
|
||||
// the local timestamp (when we received it)
|
||||
// used to know if the first two snapshots are old enough to start.
|
||||
public double localTime { get; set; }
|
||||
|
||||
public Vector3 position;
|
||||
public Quaternion rotation;
|
||||
public Vector3 scale;
|
||||
|
||||
public TransformSnapshot(double remoteTime, double localTime, Vector3 position, Quaternion rotation, Vector3 scale)
|
||||
{
|
||||
this.remoteTime = remoteTime;
|
||||
this.localTime = localTime;
|
||||
this.position = position;
|
||||
this.rotation = rotation;
|
||||
this.scale = scale;
|
||||
}
|
||||
|
||||
public static TransformSnapshot Interpolate(TransformSnapshot from, TransformSnapshot to, double t)
|
||||
{
|
||||
// NOTE:
|
||||
// Vector3 & Quaternion components are float anyway, so we can
|
||||
// keep using the functions with 't' as float instead of double.
|
||||
return new TransformSnapshot(
|
||||
// interpolated snapshot is applied directly. don't need timestamps.
|
||||
0, 0,
|
||||
// lerp position/rotation/scale unclamped in case we ever need
|
||||
// to extrapolate. atm SnapshotInterpolation never does.
|
||||
Vector3.LerpUnclamped(from.position, to.position, (float)t),
|
||||
// IMPORTANT: LerpUnclamped(0, 60, 1.5) extrapolates to ~86.
|
||||
// SlerpUnclamped(0, 60, 1.5) extrapolates to 90!
|
||||
// (0, 90, 1.5) is even worse. for Lerp.
|
||||
// => Slerp works way better for our euler angles.
|
||||
Quaternion.SlerpUnclamped(from.rotation, to.rotation, (float)t),
|
||||
Vector3.LerpUnclamped(from.scale, to.scale, (float)t)
|
||||
);
|
||||
}
|
||||
|
||||
public override string ToString() =>
|
||||
$"TransformSnapshot(remoteTime={remoteTime:F2}, localTime={localTime:F2}, pos={position}, rot={rotation}, scale={scale})";
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d3dae77b43dc4e1dbb2012924b2da79c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Components/NetworkTransform/TransformSnapshot.cs
|
||||
uploadId: 736421
|
156
Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs
Normal file
156
Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs
Normal file
@ -0,0 +1,156 @@
|
||||
using UnityEngine;
|
||||
using System;
|
||||
using Mirror;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[Serializable]
|
||||
public struct SyncData
|
||||
{
|
||||
public Changed changedDataByte;
|
||||
public Vector3 position;
|
||||
public Quaternion quatRotation;
|
||||
public Vector3 vecRotation;
|
||||
public Vector3 scale;
|
||||
|
||||
public SyncData(Changed _dataChangedByte, Vector3 _position, Quaternion _rotation, Vector3 _scale)
|
||||
{
|
||||
this.changedDataByte = _dataChangedByte;
|
||||
this.position = _position;
|
||||
this.quatRotation = _rotation;
|
||||
this.vecRotation = quatRotation.eulerAngles;
|
||||
this.scale = _scale;
|
||||
}
|
||||
|
||||
public SyncData(Changed _dataChangedByte, TransformSnapshot _snapshot)
|
||||
{
|
||||
this.changedDataByte = _dataChangedByte;
|
||||
this.position = _snapshot.position;
|
||||
this.quatRotation = _snapshot.rotation;
|
||||
this.vecRotation = quatRotation.eulerAngles;
|
||||
this.scale = _snapshot.scale;
|
||||
}
|
||||
|
||||
public SyncData(Changed _dataChangedByte, Vector3 _position, Vector3 _vecRotation, Vector3 _scale)
|
||||
{
|
||||
this.changedDataByte = _dataChangedByte;
|
||||
this.position = _position;
|
||||
this.vecRotation = _vecRotation;
|
||||
this.quatRotation = Quaternion.Euler(vecRotation);
|
||||
this.scale = _scale;
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum Changed : byte
|
||||
{
|
||||
None = 0,
|
||||
PosX = 1 << 0,
|
||||
PosY = 1 << 1,
|
||||
PosZ = 1 << 2,
|
||||
CompressRot = 1 << 3,
|
||||
RotX = 1 << 4,
|
||||
RotY = 1 << 5,
|
||||
RotZ = 1 << 6,
|
||||
Scale = 1 << 7,
|
||||
|
||||
Pos = PosX | PosY | PosZ,
|
||||
Rot = RotX | RotY | RotZ
|
||||
}
|
||||
|
||||
|
||||
public static class SyncDataReaderWriter
|
||||
{
|
||||
public static void WriteSyncData(this NetworkWriter writer, SyncData syncData)
|
||||
{
|
||||
writer.WriteByte((byte)syncData.changedDataByte);
|
||||
|
||||
// Write position
|
||||
if ((syncData.changedDataByte & Changed.PosX) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.position.x);
|
||||
}
|
||||
|
||||
if ((syncData.changedDataByte & Changed.PosY) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.position.y);
|
||||
}
|
||||
|
||||
if ((syncData.changedDataByte & Changed.PosZ) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.position.z);
|
||||
}
|
||||
|
||||
// Write rotation
|
||||
if ((syncData.changedDataByte & Changed.CompressRot) > 0)
|
||||
{
|
||||
if((syncData.changedDataByte & Changed.Rot) > 0)
|
||||
{
|
||||
writer.WriteUInt(Compression.CompressQuaternion(syncData.quatRotation));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((syncData.changedDataByte & Changed.RotX) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.quatRotation.eulerAngles.x);
|
||||
}
|
||||
|
||||
if ((syncData.changedDataByte & Changed.RotY) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.quatRotation.eulerAngles.y);
|
||||
}
|
||||
|
||||
if ((syncData.changedDataByte & Changed.RotZ) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.quatRotation.eulerAngles.z);
|
||||
}
|
||||
}
|
||||
|
||||
// Write scale
|
||||
if ((syncData.changedDataByte & Changed.Scale) > 0)
|
||||
{
|
||||
writer.WriteVector3(syncData.scale);
|
||||
}
|
||||
}
|
||||
|
||||
public static SyncData ReadSyncData(this NetworkReader reader)
|
||||
{
|
||||
Changed changedData = (Changed)reader.ReadByte();
|
||||
|
||||
// If we have nothing to read here, let's say because posX is unchanged, then we can write anything
|
||||
// for now, but in the NT, we will need to check changedData again, to put the right values of the axis
|
||||
// back. We don't have it here.
|
||||
|
||||
Vector3 position =
|
||||
new Vector3(
|
||||
(changedData & Changed.PosX) > 0 ? reader.ReadFloat() : 0,
|
||||
(changedData & Changed.PosY) > 0 ? reader.ReadFloat() : 0,
|
||||
(changedData & Changed.PosZ) > 0 ? reader.ReadFloat() : 0
|
||||
);
|
||||
|
||||
Vector3 vecRotation = new Vector3();
|
||||
Quaternion quatRotation = new Quaternion();
|
||||
|
||||
if ((changedData & Changed.CompressRot) > 0)
|
||||
{
|
||||
quatRotation = (changedData & Changed.RotX) > 0 ? Compression.DecompressQuaternion(reader.ReadUInt()) : new Quaternion();
|
||||
}
|
||||
else
|
||||
{
|
||||
vecRotation =
|
||||
new Vector3(
|
||||
(changedData & Changed.RotX) > 0 ? reader.ReadFloat() : 0,
|
||||
(changedData & Changed.RotY) > 0 ? reader.ReadFloat() : 0,
|
||||
(changedData & Changed.RotZ) > 0 ? reader.ReadFloat() : 0
|
||||
);
|
||||
}
|
||||
|
||||
Vector3 scale = (changedData & Changed.Scale) == Changed.Scale ? reader.ReadVector3() : new Vector3();
|
||||
|
||||
SyncData _syncData = (changedData & Changed.CompressRot) > 0 ? new SyncData(changedData, position, quatRotation, scale) : new SyncData(changedData, position, vecRotation, scale);
|
||||
|
||||
return _syncData;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a1c0832ca88e749ff96fe04cebb617ef
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs
|
||||
uploadId: 736421
|
Reference in New Issue
Block a user