The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com). ## [1.10.0] - 2024-07-22 ### Added - Added `NetworkBehaviour.OnNetworkPreSpawn` and `NetworkBehaviour.OnNetworkPostSpawn` methods that provide the ability to handle pre and post spawning actions during the `NetworkObject` spawn sequence. (#2906) - Added a client-side only `NetworkBehaviour.OnNetworkSessionSynchronized` convenience method that is invoked on all `NetworkBehaviour`s after a newly joined client has finished synchronizing with the network session in progress. (#2906) - Added `NetworkBehaviour.OnInSceneObjectsSpawned` convenience method that is invoked when all in-scene `NetworkObject`s have been spawned after a scene has been loaded or upon a host or server starting. (#2906) ### Fixed - Fixed issue where the realtime network stats monitor was not able to display RPC traffic in release builds due to those stats being only available in development builds or the editor. (#2980) - Fixed issue where `NetworkManager.ScenesLoaded` was not being updated if `PostSynchronizationSceneUnloading` was set and any loaded scenes not used during synchronization were unloaded.(#2977) - Fixed issue where internal delta serialization could not have a byte serializer defined when serializing deltas for other types. Added `[GenerateSerializationForType(typeof(byte))]` to both the `NetworkVariable` and `AnticipatedNetworkVariable` classes to assure a byte serializer is defined. (#2953) - Fixed issue with the client count not being correct on the host or server side when a client disconnects itself from a session. (#2941) - Fixed issue with the host trying to send itself a message that it has connected when first starting up. (#2941) - Fixed issue where in-scene placed NetworkObjects could be destroyed if a client disconnects early and/or before approval. (#2923) - Fixed issue where `NetworkDeltaPosition` would "jitter" periodically if both unreliable delta state updates and half-floats were used together. (#2922) - Fixed issue where `NetworkRigidbody2D` would not properly change body type based on the instance's authority when spawned. (#2916) - Fixed issue where a `NetworkObject` component's associated `NetworkBehaviour` components would not be detected if scene loading is disabled in the editor and the currently loaded scene has in-scene placed `NetworkObject`s. (#2906) - Fixed issue where an in-scene placed `NetworkObject` with `NetworkTransform` that is also parented under a `GameObject` would not properly synchronize when the parent `GameObject` had a world space position other than 0,0,0. (#2895) ### Changed
3370 lines
148 KiB
C#
3370 lines
148 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.CompilerServices;
|
|
using Unity.Mathematics;
|
|
using UnityEngine;
|
|
|
|
namespace Unity.Netcode.Components
|
|
{
|
|
/// <summary>
|
|
/// A component for syncing transforms.
|
|
/// NetworkTransform will read the underlying transform and replicate it to clients.
|
|
/// The replicated value will be automatically be interpolated (if active) and applied to the underlying GameObject's transform.
|
|
/// </summary>
|
|
[DisallowMultipleComponent]
|
|
[AddComponentMenu("Netcode/Network Transform")]
|
|
[DefaultExecutionOrder(100000)] // this is needed to catch the update time after the transform was updated by user scripts
|
|
public class NetworkTransform : NetworkBehaviour
|
|
{
|
|
/// <summary>
|
|
/// The default position change threshold value.
|
|
/// Any changes above this threshold will be replicated.
|
|
/// </summary>
|
|
public const float PositionThresholdDefault = 0.001f;
|
|
|
|
/// <summary>
|
|
/// The default rotation angle change threshold value.
|
|
/// Any changes above this threshold will be replicated.
|
|
/// </summary>
|
|
public const float RotAngleThresholdDefault = 0.01f;
|
|
|
|
/// <summary>
|
|
/// The default scale change threshold value.
|
|
/// Any changes above this threshold will be replicated.
|
|
/// </summary>
|
|
public const float ScaleThresholdDefault = 0.01f;
|
|
|
|
/// <summary>
|
|
/// The handler delegate type that takes client requested changes and returns resulting changes handled by the server.
|
|
/// </summary>
|
|
/// <param name="pos">The position requested by the client.</param>
|
|
/// <param name="rot">The rotation requested by the client.</param>
|
|
/// <param name="scale">The scale requested by the client.</param>
|
|
/// <returns>The resulting position, rotation and scale changes after handling.</returns>
|
|
public delegate (Vector3 pos, Quaternion rotOut, Vector3 scale) OnClientRequestChangeDelegate(Vector3 pos, Quaternion rot, Vector3 scale);
|
|
|
|
/// <summary>
|
|
/// The handler that gets invoked when server receives a change from a client.
|
|
/// This handler would be useful for server to modify pos/rot/scale before applying client's request.
|
|
/// </summary>
|
|
public OnClientRequestChangeDelegate OnClientRequestChange;
|
|
|
|
/// <summary>
|
|
/// When set each state update will contain a state identifier
|
|
/// </summary>
|
|
internal static bool TrackByStateId;
|
|
|
|
/// <summary>
|
|
/// Enabled by default.
|
|
/// When set (enabled by default), NetworkTransform will send common state updates using unreliable network delivery
|
|
/// to provide a higher tolerance to poor network conditions (especially packet loss). When disabled, all state updates
|
|
/// are sent using a reliable fragmented sequenced network delivery.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The following more critical state updates are still sent as reliable fragmented sequenced:
|
|
/// - The initial synchronization state update
|
|
/// - The teleporting state update.
|
|
/// - When using half float precision and the `NetworkDeltaPosition` delta exceeds the maximum delta forcing the axis in
|
|
/// question to be collapsed into the core base position, this state update will be sent as reliable fragmented sequenced.
|
|
///
|
|
/// In order to preserve a continual consistency of axial values when unreliable delta messaging is enabled (due to the
|
|
/// possibility of dropping packets), NetworkTransform instances will send 1 axial frame synchronization update per
|
|
/// second (only for the axis marked to synchronize are sent as reliable fragmented sequenced) as long as a delta state
|
|
/// update had been previously sent. When a NetworkObject is at rest, axial frame synchronization updates are not sent.
|
|
/// </remarks>
|
|
[Tooltip("When set, NetworkTransform will send common state updates using unreliable network delivery " +
|
|
"to provide a higher tolerance to poor network conditions (especially packet loss). When disabled, all state updates are " +
|
|
"sent using reliable fragmented sequenced network delivery.")]
|
|
public bool UseUnreliableDeltas = false;
|
|
|
|
/// <summary>
|
|
/// Data structure used to synchronize the <see cref="NetworkTransform"/>
|
|
/// </summary>
|
|
public struct NetworkTransformState : INetworkSerializable
|
|
{
|
|
private const int k_InLocalSpaceBit = 0x00000001; // Persists between state updates (authority dictates if this is set)
|
|
private const int k_PositionXBit = 0x00000002;
|
|
private const int k_PositionYBit = 0x00000004;
|
|
private const int k_PositionZBit = 0x00000008;
|
|
private const int k_RotAngleXBit = 0x00000010;
|
|
private const int k_RotAngleYBit = 0x00000020;
|
|
private const int k_RotAngleZBit = 0x00000040;
|
|
private const int k_ScaleXBit = 0x00000080;
|
|
private const int k_ScaleYBit = 0x00000100;
|
|
private const int k_ScaleZBit = 0x00000200;
|
|
private const int k_TeleportingBit = 0x00000400;
|
|
private const int k_Interpolate = 0x00000800; // Persists between state updates (authority dictates if this is set)
|
|
private const int k_QuaternionSync = 0x00001000; // Persists between state updates (authority dictates if this is set)
|
|
private const int k_QuaternionCompress = 0x00002000; // Persists between state updates (authority dictates if this is set)
|
|
private const int k_UseHalfFloats = 0x00004000; // Persists between state updates (authority dictates if this is set)
|
|
private const int k_Synchronization = 0x00008000;
|
|
private const int k_PositionSlerp = 0x00010000; // Persists between state updates (authority dictates if this is set)
|
|
private const int k_IsParented = 0x00020000; // When parented and synchronizing, we need to have both lossy and local scale due to varying spawn order
|
|
private const int k_SynchBaseHalfFloat = 0x00040000;
|
|
private const int k_ReliableSequenced = 0x00080000;
|
|
private const int k_UseUnreliableDeltas = 0x00100000;
|
|
private const int k_UnreliableFrameSync = 0x00200000;
|
|
private const int k_TrackStateId = 0x10000000; // (Internal Debugging) When set each state update will contain a state identifier
|
|
|
|
// Stores persistent and state relative flags
|
|
private uint m_Bitset;
|
|
internal uint BitSet
|
|
{
|
|
get { return m_Bitset; }
|
|
set { m_Bitset = value; }
|
|
}
|
|
|
|
// Used to store the tick calculated sent time
|
|
internal double SentTime;
|
|
|
|
// Used for full precision position updates
|
|
internal float PositionX, PositionY, PositionZ;
|
|
|
|
// Used for full precision Euler updates
|
|
internal float RotAngleX, RotAngleY, RotAngleZ;
|
|
|
|
// Used for full precision quaternion updates
|
|
internal Quaternion Rotation;
|
|
|
|
// Used for full precision scale updates
|
|
internal float ScaleX, ScaleY, ScaleZ;
|
|
|
|
// Used for half precision delta position updates
|
|
internal Vector3 CurrentPosition;
|
|
internal Vector3 DeltaPosition;
|
|
internal NetworkDeltaPosition NetworkDeltaPosition;
|
|
|
|
// Used for half precision scale
|
|
internal HalfVector3 HalfVectorScale;
|
|
internal Vector3 Scale;
|
|
internal Vector3 LossyScale;
|
|
|
|
// Used for half precision quaternion
|
|
internal HalfVector4 HalfVectorRotation;
|
|
|
|
// Used to store a compressed quaternion
|
|
internal uint QuaternionCompressed;
|
|
|
|
// Authoritative and non-authoritative sides use this to determine if a NetworkTransformState is
|
|
// dirty or not.
|
|
internal bool IsDirty { get; set; }
|
|
|
|
/// <summary>
|
|
/// The last byte size of the <see cref="NetworkTransformState"/> updated.
|
|
/// </summary>
|
|
public int LastSerializedSize { get; internal set; }
|
|
|
|
// Used for NetworkDeltaPosition delta position synchronization
|
|
internal int NetworkTick;
|
|
|
|
// Used when tracking by state ID is enabled
|
|
internal int StateId;
|
|
|
|
// Set when a state has been explicitly set (i.e. SetState)
|
|
internal bool ExplicitSet;
|
|
|
|
// Used during serialization
|
|
private FastBufferReader m_Reader;
|
|
private FastBufferWriter m_Writer;
|
|
|
|
/// <summary>
|
|
/// When set, the <see cref="NetworkTransform"/> is operates in local space
|
|
/// </summary>
|
|
public bool InLocalSpace
|
|
{
|
|
get => GetFlag(k_InLocalSpaceBit);
|
|
internal set
|
|
{
|
|
SetFlag(value, k_InLocalSpaceBit);
|
|
}
|
|
}
|
|
|
|
// Position
|
|
/// <summary>
|
|
/// When set, the X-Axis position value has changed
|
|
/// </summary>
|
|
public bool HasPositionX
|
|
{
|
|
get => GetFlag(k_PositionXBit);
|
|
internal set
|
|
{
|
|
SetFlag(value, k_PositionXBit);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When set, the Y-Axis position value has changed
|
|
/// </summary>
|
|
public bool HasPositionY
|
|
{
|
|
get => GetFlag(k_PositionYBit);
|
|
internal set
|
|
{
|
|
SetFlag(value, k_PositionYBit);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When set, the Z-Axis position value has changed
|
|
/// </summary>
|
|
public bool HasPositionZ
|
|
{
|
|
get => GetFlag(k_PositionZBit);
|
|
internal set
|
|
{
|
|
SetFlag(value, k_PositionZBit);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When set, at least one of the position axis values has changed.
|
|
/// </summary>
|
|
public bool HasPositionChange
|
|
{
|
|
get
|
|
{
|
|
return HasPositionX | HasPositionY | HasPositionZ;
|
|
}
|
|
}
|
|
|
|
// RotAngles
|
|
/// <summary>
|
|
/// When set, the Euler rotation X-Axis value has changed.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// When quaternion synchronization is enabled all axis are always updated.
|
|
/// </remarks>
|
|
public bool HasRotAngleX
|
|
{
|
|
get => GetFlag(k_RotAngleXBit);
|
|
internal set
|
|
{
|
|
SetFlag(value, k_RotAngleXBit);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When set, the Euler rotation Y-Axis value has changed.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// When quaternion synchronization is enabled all axis are always updated.
|
|
/// </remarks>
|
|
public bool HasRotAngleY
|
|
{
|
|
get => GetFlag(k_RotAngleYBit);
|
|
internal set
|
|
{
|
|
SetFlag(value, k_RotAngleYBit);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When set, the Euler rotation Z-Axis value has changed.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// When quaternion synchronization is enabled all axis are always updated.
|
|
/// </remarks>
|
|
public bool HasRotAngleZ
|
|
{
|
|
get => GetFlag(k_RotAngleZBit);
|
|
internal set
|
|
{
|
|
SetFlag(value, k_RotAngleZBit);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When set, at least one of the rotation axis values has changed.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// When quaternion synchronization is enabled all axis are always updated.
|
|
/// </remarks>
|
|
public bool HasRotAngleChange
|
|
{
|
|
get
|
|
{
|
|
return HasRotAngleX | HasRotAngleY | HasRotAngleZ;
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
internal bool HasScale(int axisIndex)
|
|
{
|
|
return GetFlag(k_ScaleXBit << axisIndex);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
internal void SetHasScale(int axisIndex, bool isSet)
|
|
{
|
|
SetFlag(isSet, k_ScaleXBit << axisIndex);
|
|
}
|
|
|
|
// Scale
|
|
/// <summary>
|
|
/// When set, the X-Axis scale value has changed.
|
|
/// </summary>
|
|
public bool HasScaleX
|
|
{
|
|
get => GetFlag(k_ScaleXBit);
|
|
internal set
|
|
{
|
|
SetFlag(value, k_ScaleXBit);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When set, the Y-Axis scale value has changed.
|
|
/// </summary>
|
|
public bool HasScaleY
|
|
{
|
|
get => GetFlag(k_ScaleYBit);
|
|
internal set
|
|
{
|
|
SetFlag(value, k_ScaleYBit);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When set, the Z-Axis scale value has changed.
|
|
/// </summary>
|
|
public bool HasScaleZ
|
|
{
|
|
get => GetFlag(k_ScaleZBit);
|
|
internal set
|
|
{
|
|
SetFlag(value, k_ScaleZBit);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When set, at least one of the scale axis values has changed.
|
|
/// </summary>
|
|
public bool HasScaleChange
|
|
{
|
|
get
|
|
{
|
|
return HasScaleX | HasScaleY | HasScaleZ;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When set, the current state will be treated as a teleport.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// When teleporting:
|
|
/// - Interpolation is reset.
|
|
/// - If using half precision, full precision values are used.
|
|
/// - All axis marked to be synchronized will be updated.
|
|
/// </remarks>
|
|
public bool IsTeleportingNextFrame
|
|
{
|
|
get => GetFlag(k_TeleportingBit);
|
|
internal set
|
|
{
|
|
SetFlag(value, k_TeleportingBit);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When set the <see cref="NetworkTransform"/> is uses interpolation.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Authority does not apply interpolation via <see cref="NetworkTransform"/>.
|
|
/// Authority should handle its own motion/rotation/scale smoothing locally.
|
|
/// </remarks>
|
|
public bool UseInterpolation
|
|
{
|
|
get => GetFlag(k_Interpolate);
|
|
internal set
|
|
{
|
|
SetFlag(value, k_Interpolate);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When enabled, this <see cref="NetworkTransform"/> instance uses <see cref="Quaternion"/> synchronization.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Use quaternion synchronization if you are nesting <see cref="NetworkTransform"/>s and rotation can occur on both the parent and child.
|
|
/// When quaternion synchronization is enabled, the entire quaternion is updated when there are any changes to any axial values.
|
|
/// You can use half float precision or quaternion compression to reduce the bandwidth cost.
|
|
/// </remarks>
|
|
public bool QuaternionSync
|
|
{
|
|
get => GetFlag(k_QuaternionSync);
|
|
internal set
|
|
{
|
|
SetFlag(value, k_QuaternionSync);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When set <see cref="Quaternion"/>s will be compressed down to 4 bytes using a smallest three implementation.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This only will be applied when <see cref="QuaternionSync"/> is enabled.
|
|
/// Half float precision provides a higher precision than quaternion compression but at the cost of 4 additional bytes per update.
|
|
/// - Quaternion Compression: 4 bytes per delta update
|
|
/// - Half float precision: 8 bytes per delta update
|
|
/// </remarks>
|
|
public bool QuaternionCompression
|
|
{
|
|
get => GetFlag(k_QuaternionCompress);
|
|
internal set
|
|
{
|
|
SetFlag(value, k_QuaternionCompress);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When set, the <see cref="NetworkTransform"/> will use half float precision for position, rotation, and scale.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Postion is synchronized through delta position updates in order to reduce precision loss/drift and to extend to positions beyond the limitation of half float maximum values.
|
|
/// Rotation and scale both use half float precision (<see cref="HalfVector4"/> and <see cref="HalfVector3"/>)
|
|
/// </remarks>
|
|
public bool UseHalfFloatPrecision
|
|
{
|
|
get => GetFlag(k_UseHalfFloats);
|
|
internal set
|
|
{
|
|
SetFlag(value, k_UseHalfFloats);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When set, this indicates it is the first state being synchronized.
|
|
/// Typically when the associate <see cref="NetworkObject"/> is spawned or a client is being synchronized after connecting to a network session in progress.
|
|
/// </summary>
|
|
public bool IsSynchronizing
|
|
{
|
|
get => GetFlag(k_Synchronization);
|
|
internal set
|
|
{
|
|
SetFlag(value, k_Synchronization);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if position interpolation will Slerp towards its target position.
|
|
/// This is only really useful if you are moving around a point in a circular pattern.
|
|
/// </summary>
|
|
public bool UsePositionSlerp
|
|
{
|
|
get => GetFlag(k_PositionSlerp);
|
|
internal set
|
|
{
|
|
SetFlag(value, k_PositionSlerp);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns whether this state update was a frame synchronization when
|
|
/// UseUnreliableDeltas is enabled. When set, the entire transform will
|
|
/// be or has been synchronized.
|
|
/// </summary>
|
|
public bool IsUnreliableFrameSync()
|
|
{
|
|
return UnreliableFrameSync;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if this state was sent with reliable delivery.
|
|
/// If false, then it was sent with unreliable delivery.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Unreliable delivery will only be used if <see cref="UseUnreliableDeltas"/> is set.
|
|
/// </remarks>
|
|
public bool IsReliableStateUpdate()
|
|
{
|
|
return ReliableSequenced;
|
|
}
|
|
|
|
internal bool IsParented
|
|
{
|
|
get => GetFlag(k_IsParented);
|
|
set
|
|
{
|
|
SetFlag(value, k_IsParented);
|
|
}
|
|
}
|
|
|
|
internal bool SynchronizeBaseHalfFloat
|
|
{
|
|
get => GetFlag(k_SynchBaseHalfFloat);
|
|
set
|
|
{
|
|
SetFlag(value, k_SynchBaseHalfFloat);
|
|
}
|
|
}
|
|
|
|
internal bool ReliableSequenced
|
|
{
|
|
get => GetFlag(k_ReliableSequenced);
|
|
set
|
|
{
|
|
SetFlag(value, k_ReliableSequenced);
|
|
}
|
|
}
|
|
|
|
internal bool UseUnreliableDeltas
|
|
{
|
|
get => GetFlag(k_UseUnreliableDeltas);
|
|
set
|
|
{
|
|
SetFlag(value, k_UseUnreliableDeltas);
|
|
}
|
|
}
|
|
|
|
internal bool UnreliableFrameSync
|
|
{
|
|
get => GetFlag(k_UnreliableFrameSync);
|
|
set
|
|
{
|
|
SetFlag(value, k_UnreliableFrameSync);
|
|
}
|
|
}
|
|
|
|
internal bool TrackByStateId
|
|
{
|
|
get => GetFlag(k_TrackStateId);
|
|
set
|
|
{
|
|
SetFlag(value, k_TrackStateId);
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private bool GetFlag(int flag)
|
|
{
|
|
return (m_Bitset & flag) != 0;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void SetFlag(bool set, int flag)
|
|
{
|
|
if (set) { m_Bitset = m_Bitset | (uint)flag; }
|
|
else { m_Bitset = m_Bitset & (uint)~flag; }
|
|
}
|
|
|
|
internal void ClearBitSetForNextTick()
|
|
{
|
|
// Clear everything but flags that should persist between state updates until changed by authority
|
|
m_Bitset &= k_InLocalSpaceBit | k_Interpolate | k_UseHalfFloats | k_QuaternionSync | k_QuaternionCompress | k_PositionSlerp | k_UseUnreliableDeltas;
|
|
IsDirty = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the current state's rotation. If there is no change in the rotation,
|
|
/// then it will return <see cref="Quaternion.identity"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// When there is no change in an updated state's rotation then there are no values to return.
|
|
/// Checking for <see cref="HasRotAngleChange"/> is one way to detect this.
|
|
/// </remarks>
|
|
/// <returns><see cref="Quaternion"/></returns>
|
|
public Quaternion GetRotation()
|
|
{
|
|
if (HasRotAngleChange)
|
|
{
|
|
if (QuaternionSync)
|
|
{
|
|
return Rotation;
|
|
}
|
|
else
|
|
{
|
|
return Quaternion.Euler(RotAngleX, RotAngleY, RotAngleZ);
|
|
}
|
|
}
|
|
return Quaternion.identity;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the current state's position. If there is no change in position,
|
|
/// then it returns <see cref="Vector3.zero"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// When there is no change in an updated state's position then there are no values to return.
|
|
/// Checking for <see cref="HasPositionChange"/> is one way to detect this.
|
|
/// When used with half precision it returns the half precision delta position state update
|
|
/// which will not be the full position.
|
|
/// To get a NettworkTransform's full position, use <see cref="GetSpaceRelativePosition(bool)"/> and
|
|
/// pass true as the parameter.
|
|
/// </remarks>
|
|
/// <returns><see cref="Vector3"/></returns>
|
|
public Vector3 GetPosition()
|
|
{
|
|
if (HasPositionChange)
|
|
{
|
|
if (UseHalfFloatPrecision)
|
|
{
|
|
if (IsTeleportingNextFrame)
|
|
{
|
|
return CurrentPosition;
|
|
}
|
|
else
|
|
{
|
|
return NetworkDeltaPosition.GetFullPosition();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return new Vector3(PositionX, PositionY, PositionZ);
|
|
}
|
|
}
|
|
return Vector3.zero;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the current state's scale. If there is no change in scale,
|
|
/// then it returns <see cref="Vector3.zero"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// When there is no change in an updated state's scale then there are no values to return.
|
|
/// Checking for <see cref="HasScaleChange"/> is one way to detect this.
|
|
/// </remarks>
|
|
/// <returns><see cref="Vector3"/></returns>
|
|
public Vector3 GetScale()
|
|
{
|
|
if (HasScaleChange)
|
|
{
|
|
if (UseHalfFloatPrecision)
|
|
{
|
|
if (IsTeleportingNextFrame)
|
|
{
|
|
return Scale;
|
|
}
|
|
else
|
|
{
|
|
return HalfVectorScale.ToVector3();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return new Vector3(ScaleX, ScaleY, ScaleZ);
|
|
}
|
|
}
|
|
return Vector3.zero;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The network tick that this state was sent by the authoritative instance.
|
|
/// </summary>
|
|
/// <returns><see cref="int"/></returns>
|
|
public int GetNetworkTick()
|
|
{
|
|
return NetworkTick;
|
|
}
|
|
|
|
internal HalfVector3 HalfEulerRotation;
|
|
|
|
/// <summary>
|
|
/// Serializes this <see cref="NetworkTransformState"/>
|
|
/// </summary>
|
|
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
|
|
{
|
|
// Used to calculate the LastSerializedSize value
|
|
var positionStart = 0;
|
|
var isWriting = serializer.IsWriter;
|
|
if (isWriting)
|
|
{
|
|
m_Writer = serializer.GetFastBufferWriter();
|
|
positionStart = m_Writer.Position;
|
|
}
|
|
else
|
|
{
|
|
m_Reader = serializer.GetFastBufferReader();
|
|
positionStart = m_Reader.Position;
|
|
}
|
|
|
|
// Synchronize State Flags and Network Tick
|
|
{
|
|
if (isWriting)
|
|
{
|
|
if (UseUnreliableDeltas)
|
|
{
|
|
// If teleporting, synchronizing, doing an axial frame sync, or using half float precision and we collapsed a delta into the base position
|
|
if (IsTeleportingNextFrame || IsSynchronizing || UnreliableFrameSync || (UseHalfFloatPrecision && NetworkDeltaPosition.CollapsedDeltaIntoBase))
|
|
{
|
|
// Send the message reliably
|
|
ReliableSequenced = true;
|
|
}
|
|
else
|
|
{
|
|
ReliableSequenced = false;
|
|
}
|
|
}
|
|
else // If not using UseUnreliableDeltas, then always use reliable fragmented sequenced
|
|
{
|
|
ReliableSequenced = true;
|
|
}
|
|
|
|
BytePacker.WriteValueBitPacked(m_Writer, m_Bitset);
|
|
// We use network ticks as opposed to absolute time as the authoritative
|
|
// side updates on every new tick.
|
|
BytePacker.WriteValueBitPacked(m_Writer, NetworkTick);
|
|
|
|
}
|
|
else
|
|
{
|
|
ByteUnpacker.ReadValueBitPacked(m_Reader, out m_Bitset);
|
|
// We use network ticks as opposed to absolute time as the authoritative
|
|
// side updates on every new tick.
|
|
ByteUnpacker.ReadValueBitPacked(m_Reader, out NetworkTick);
|
|
}
|
|
}
|
|
|
|
// If debugging states and track by state identifier is enabled, serialize the current state identifier
|
|
if (TrackByStateId)
|
|
{
|
|
serializer.SerializeValue(ref StateId);
|
|
}
|
|
|
|
// Synchronize Position
|
|
if (HasPositionChange)
|
|
{
|
|
if (UseHalfFloatPrecision)
|
|
{
|
|
NetworkDeltaPosition.SynchronizeBase = SynchronizeBaseHalfFloat;
|
|
|
|
// Apply which axis should be updated for both write/read (teleporting, synchronizing, or just updating)
|
|
NetworkDeltaPosition.HalfVector3.AxisToSynchronize[0] = HasPositionX;
|
|
NetworkDeltaPosition.HalfVector3.AxisToSynchronize[1] = HasPositionY;
|
|
NetworkDeltaPosition.HalfVector3.AxisToSynchronize[2] = HasPositionZ;
|
|
|
|
if (IsTeleportingNextFrame)
|
|
{
|
|
// **Always use full precision when teleporting and UseHalfFloatPrecision is enabled**
|
|
serializer.SerializeValue(ref CurrentPosition);
|
|
// If we are synchronizing, then include the half vector position's delta offset
|
|
if (IsSynchronizing)
|
|
{
|
|
serializer.SerializeValue(ref DeltaPosition);
|
|
if (!isWriting)
|
|
{
|
|
NetworkDeltaPosition.NetworkTick = NetworkTick;
|
|
NetworkDeltaPosition.NetworkSerialize(serializer);
|
|
}
|
|
else
|
|
{
|
|
serializer.SerializeNetworkSerializable(ref NetworkDeltaPosition);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!isWriting)
|
|
{
|
|
NetworkDeltaPosition.NetworkTick = NetworkTick;
|
|
NetworkDeltaPosition.NetworkSerialize(serializer);
|
|
}
|
|
else
|
|
{
|
|
serializer.SerializeNetworkSerializable(ref NetworkDeltaPosition);
|
|
}
|
|
}
|
|
}
|
|
else // Full precision axis specific position synchronization
|
|
{
|
|
if (HasPositionX)
|
|
{
|
|
serializer.SerializeValue(ref PositionX);
|
|
}
|
|
|
|
if (HasPositionY)
|
|
{
|
|
serializer.SerializeValue(ref PositionY);
|
|
}
|
|
|
|
if (HasPositionZ)
|
|
{
|
|
serializer.SerializeValue(ref PositionZ);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Synchronize Rotation
|
|
if (HasRotAngleChange)
|
|
{
|
|
if (QuaternionSync)
|
|
{
|
|
// Always use the full quaternion if teleporting
|
|
if (IsTeleportingNextFrame)
|
|
{
|
|
serializer.SerializeValue(ref Rotation);
|
|
}
|
|
else
|
|
{
|
|
// Use the quaternion compressor if enabled
|
|
if (QuaternionCompression)
|
|
{
|
|
if (isWriting)
|
|
{
|
|
QuaternionCompressed = QuaternionCompressor.CompressQuaternion(ref Rotation);
|
|
}
|
|
|
|
serializer.SerializeValue(ref QuaternionCompressed);
|
|
|
|
if (!isWriting)
|
|
{
|
|
QuaternionCompressor.DecompressQuaternion(ref Rotation, QuaternionCompressed);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (UseHalfFloatPrecision)
|
|
{
|
|
if (isWriting)
|
|
{
|
|
HalfVectorRotation.UpdateFrom(ref Rotation);
|
|
}
|
|
|
|
serializer.SerializeNetworkSerializable(ref HalfVectorRotation);
|
|
|
|
if (!isWriting)
|
|
{
|
|
Rotation = HalfVectorRotation.ToQuaternion();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
serializer.SerializeValue(ref Rotation);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else // Euler Rotation Synchronization
|
|
{
|
|
// Half float precision (full precision when teleporting)
|
|
if (UseHalfFloatPrecision && !IsTeleportingNextFrame)
|
|
{
|
|
if (HasRotAngleChange)
|
|
{
|
|
// Apply which axis should be updated for both write/read
|
|
HalfEulerRotation.AxisToSynchronize[0] = HasRotAngleX;
|
|
HalfEulerRotation.AxisToSynchronize[1] = HasRotAngleY;
|
|
HalfEulerRotation.AxisToSynchronize[2] = HasRotAngleZ;
|
|
|
|
if (isWriting)
|
|
{
|
|
HalfEulerRotation.Set(RotAngleX, RotAngleY, RotAngleZ);
|
|
}
|
|
|
|
serializer.SerializeValue(ref HalfEulerRotation);
|
|
|
|
if (!isWriting)
|
|
{
|
|
var eulerRotation = HalfEulerRotation.ToVector3();
|
|
if (HasRotAngleX)
|
|
{
|
|
RotAngleX = eulerRotation.x;
|
|
}
|
|
|
|
if (HasRotAngleY)
|
|
{
|
|
RotAngleY = eulerRotation.y;
|
|
}
|
|
|
|
if (HasRotAngleZ)
|
|
{
|
|
RotAngleZ = eulerRotation.z;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else // Full precision Euler
|
|
{
|
|
// RotAngle Values
|
|
if (HasRotAngleX)
|
|
{
|
|
serializer.SerializeValue(ref RotAngleX);
|
|
}
|
|
|
|
if (HasRotAngleY)
|
|
{
|
|
serializer.SerializeValue(ref RotAngleY);
|
|
}
|
|
|
|
if (HasRotAngleZ)
|
|
{
|
|
serializer.SerializeValue(ref RotAngleZ);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Synchronize Scale
|
|
if (HasScaleChange)
|
|
{
|
|
// If we are teleporting (which includes synchronizing) and the associated NetworkObject has a parent
|
|
// then we want to serialize the LossyScale since NetworkObject spawn order is not guaranteed
|
|
if (IsTeleportingNextFrame && IsParented)
|
|
{
|
|
serializer.SerializeValue(ref LossyScale);
|
|
}
|
|
// Half precision scale synchronization
|
|
if (UseHalfFloatPrecision)
|
|
{
|
|
if (IsTeleportingNextFrame)
|
|
{
|
|
serializer.SerializeValue(ref Scale);
|
|
}
|
|
else
|
|
{
|
|
// Apply which axis should be updated for both write/read
|
|
HalfVectorScale.AxisToSynchronize[0] = HasScaleX;
|
|
HalfVectorScale.AxisToSynchronize[1] = HasScaleY;
|
|
HalfVectorScale.AxisToSynchronize[2] = HasScaleZ;
|
|
|
|
// For scale, when half precision is enabled we can still only send the axis with deltas
|
|
if (isWriting)
|
|
{
|
|
HalfVectorScale.Set(Scale[0], Scale[1], Scale[2]);
|
|
}
|
|
|
|
serializer.SerializeValue(ref HalfVectorScale);
|
|
|
|
if (!isWriting)
|
|
{
|
|
Scale = HalfVectorScale.ToVector3();
|
|
if (HasScaleX)
|
|
{
|
|
ScaleX = Scale.x;
|
|
}
|
|
|
|
if (HasScaleY)
|
|
{
|
|
ScaleY = Scale.y;
|
|
}
|
|
|
|
if (HasScaleZ)
|
|
{
|
|
ScaleZ = Scale.x;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else // Full precision scale synchronization
|
|
{
|
|
if (HasScaleX)
|
|
{
|
|
serializer.SerializeValue(ref ScaleX);
|
|
}
|
|
|
|
if (HasScaleY)
|
|
{
|
|
serializer.SerializeValue(ref ScaleY);
|
|
}
|
|
|
|
if (HasScaleZ)
|
|
{
|
|
serializer.SerializeValue(ref ScaleZ);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Only if we are receiving state
|
|
if (!isWriting)
|
|
{
|
|
// Go ahead and mark the local state dirty
|
|
IsDirty = HasPositionChange || HasRotAngleChange || HasScaleChange;
|
|
LastSerializedSize = m_Reader.Position - positionStart;
|
|
}
|
|
else
|
|
{
|
|
LastSerializedSize = m_Writer.Position - positionStart;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When enabled (default), the x component of position will be synchronized by authority.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Changes to this on non-authoritative instances has no effect.
|
|
/// </remarks>
|
|
public bool SyncPositionX = true;
|
|
|
|
/// <summary>
|
|
/// When enabled (default), the y component of position will be synchronized by authority.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Changes to this on non-authoritative instances has no effect.
|
|
/// </remarks>
|
|
public bool SyncPositionY = true;
|
|
|
|
/// <summary>
|
|
/// When enabled (default), the z component of position will be synchronized by authority.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Changes to this on non-authoritative instances has no effect.
|
|
/// </remarks>
|
|
public bool SyncPositionZ = true;
|
|
|
|
private bool SynchronizePosition
|
|
{
|
|
get
|
|
{
|
|
return SyncPositionX || SyncPositionY || SyncPositionZ;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When enabled (default), the x component of rotation will be synchronized by authority.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// When <see cref="UseQuaternionSynchronization"/> is enabled this does not apply.
|
|
/// Changes to this on non-authoritative instances has no effect.
|
|
/// </remarks>
|
|
public bool SyncRotAngleX = true;
|
|
|
|
/// <summary>
|
|
/// When enabled (default), the y component of rotation will be synchronized by authority.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// When <see cref="UseQuaternionSynchronization"/> is enabled this does not apply.
|
|
/// Changes to this on non-authoritative instances has no effect.
|
|
/// </remarks>
|
|
public bool SyncRotAngleY = true;
|
|
|
|
/// <summary>
|
|
/// When enabled (default), the z component of rotation will be synchronized by authority.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// When <see cref="UseQuaternionSynchronization"/> is enabled this does not apply.
|
|
/// Changes to this on non-authoritative instances has no effect.
|
|
/// </remarks>
|
|
public bool SyncRotAngleZ = true;
|
|
|
|
private bool SynchronizeRotation
|
|
{
|
|
get
|
|
{
|
|
return SyncRotAngleX || SyncRotAngleY || SyncRotAngleZ;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When enabled (default), the x component of scale will be synchronized by authority.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Changes to this on non-authoritative instances has no effect.
|
|
/// </remarks>
|
|
public bool SyncScaleX = true;
|
|
|
|
/// <summary>
|
|
/// When enabled (default), the y component of scale will be synchronized by authority.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Changes to this on non-authoritative instances has no effect.
|
|
/// </remarks>
|
|
public bool SyncScaleY = true;
|
|
|
|
/// <summary>
|
|
/// When enabled (default), the z component of scale will be synchronized by authority.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Changes to this on non-authoritative instances has no effect.
|
|
/// </remarks>
|
|
public bool SyncScaleZ = true;
|
|
|
|
private bool SynchronizeScale
|
|
{
|
|
get
|
|
{
|
|
return SyncScaleX || SyncScaleY || SyncScaleZ;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The position threshold value that triggers a delta state update by the authoritative instance.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Note: setting this to zero will update position every network tick whether it changed or not.
|
|
/// </remarks>
|
|
public float PositionThreshold = PositionThresholdDefault;
|
|
|
|
/// <summary>
|
|
/// The rotation threshold value that triggers a delta state update by the authoritative instance.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Minimum Value: 0.00001
|
|
/// Maximum Value: 360.0
|
|
/// </remarks>
|
|
[Range(0.00001f, 360.0f)]
|
|
public float RotAngleThreshold = RotAngleThresholdDefault;
|
|
|
|
/// <summary>
|
|
/// The scale threshold value that triggers a delta state update by the authoritative instance.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Note: setting this to zero will update position every network tick whether it changed or not.
|
|
/// </remarks>
|
|
public float ScaleThreshold = ScaleThresholdDefault;
|
|
|
|
/// <summary>
|
|
/// Enable this on the authority side for quaternion synchronization
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is synchronized by authority. During runtime, this should only be changed by the
|
|
/// authoritative side. Non-authoritative instances will be overridden by the next
|
|
/// authoritative state update.
|
|
/// </remarks>
|
|
[Tooltip("When enabled, this will synchronize the full Quaternion (i.e. all Euler rotation axis are updated if one axis has a delta)")]
|
|
public bool UseQuaternionSynchronization = false;
|
|
|
|
/// <summary>
|
|
/// Enabled this on the authority side for quaternion compression
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This has a lower precision than half float precision. Recommended only for low precision
|
|
/// scenarios. <see cref="UseHalfFloatPrecision"/> provides better precision at roughly half
|
|
/// the cost of a full quaternion update.
|
|
/// This is synchronized by authority. During runtime, this should only be changed by the
|
|
/// authoritative side. Non-authoritative instances will be overridden by the next
|
|
/// authoritative state update.
|
|
/// </remarks>
|
|
[Tooltip("When enabled, this uses a smallest three implementation that reduces full Quaternion updates down to the size of an unsigned integer (ignores half float precision settings).")]
|
|
public bool UseQuaternionCompression = false;
|
|
|
|
/// <summary>
|
|
/// Enable this to use half float precision for position, rotation, and scale.
|
|
/// When enabled, delta position synchronization is used.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is synchronized by authority. During runtime, this should only be changed by the
|
|
/// authoritative side. Non-authoritative instances will be overridden by the next
|
|
/// authoritative state update.
|
|
/// </remarks>
|
|
[Tooltip("When enabled, this will use half float precision values for position (uses delta position updating), rotation (except when Quaternion compression is enabled), and scale.")]
|
|
public bool UseHalfFloatPrecision = false;
|
|
|
|
/// <summary>
|
|
/// Sets whether the transform should be treated as local (true) or world (false) space.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is synchronized by authority. During runtime, this should only be changed by the
|
|
/// authoritative side. Non-authoritative instances will be overridden by the next
|
|
/// authoritative state update.
|
|
/// </remarks>
|
|
[Tooltip("Sets whether this transform should sync in local space or in world space")]
|
|
public bool InLocalSpace = false;
|
|
|
|
/// <summary>
|
|
/// When enabled (default) interpolation is applied.
|
|
/// When disabled interpolation is disabled.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is synchronized by authority and changes to interpolation during runtime forces a
|
|
/// teleport/full update. During runtime, this should only be changed by the authoritative
|
|
/// side. Non-authoritative instances will be overridden by the next authoritative state update.
|
|
/// </remarks>
|
|
public bool Interpolate = true;
|
|
|
|
/// <summary>
|
|
/// When true and interpolation is enabled, this will Slerp to the target position.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is synchronized by authority and only applies to position interpolation.
|
|
/// During runtime, this should only be changed by the authoritative side. Non-authoritative
|
|
/// instances will be overridden by the next authoritative state update.
|
|
/// </remarks>
|
|
[Tooltip("When enabled the position interpolator will Slerp towards its current target position.")]
|
|
public bool SlerpPosition = false;
|
|
|
|
/// <summary>
|
|
/// Used to determine who can write to this transform. Server only for this transform.
|
|
/// Changing this value alone in a child implementation will not allow you to create a NetworkTransform which can be written to by clients. See the ClientNetworkTransform Sample
|
|
/// in the package samples for how to implement a NetworkTransform with client write support.
|
|
/// If using different values, please use RPCs to write to the server. Netcode doesn't support client side network variable writing
|
|
/// </summary>
|
|
public bool CanCommitToTransform { get; protected set; }
|
|
|
|
/// <summary>
|
|
/// Internally used by <see cref="NetworkTransform"/> to keep track of whether this <see cref="NetworkBehaviour"/> derived class instance
|
|
/// was instantiated on the server side or not.
|
|
/// </summary>
|
|
protected bool m_CachedIsServer; // Note: we no longer use this and are only keeping it until we decide to deprecate it
|
|
|
|
/// <summary>
|
|
/// Internally used by <see cref="NetworkTransform"/> to keep track of the <see cref="NetworkManager"/> instance assigned to this
|
|
/// this <see cref="NetworkBehaviour"/> derived class instance.
|
|
/// </summary>
|
|
protected NetworkManager m_CachedNetworkManager;
|
|
|
|
/// <summary>
|
|
/// Helper method that returns the space relative position of the transform.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If InLocalSpace is <see cref="true"/> then it returns the transform.localPosition
|
|
/// If InLocalSpace is <see cref="false"/> then it returns the transform.position
|
|
/// When invoked on the non-authority side:
|
|
/// If <see cref="getCurrentState"/> is true then it will return the most
|
|
/// current authority position from the most recent state update. This can be useful
|
|
/// if interpolation is enabled and you need to determine the final target position.
|
|
/// When invoked on the authority side:
|
|
/// It will always return the space relative position.
|
|
/// </remarks>
|
|
/// <param name="getCurrentState">
|
|
/// Authority always returns the space relative transform position (whether true or false).
|
|
/// Non-authority:
|
|
/// When false (default): returns the space relative transform position
|
|
/// When true: returns the authority position from the most recent state update.
|
|
/// </param>
|
|
/// <returns><see cref="Vector3"/></returns>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public Vector3 GetSpaceRelativePosition(bool getCurrentState = false)
|
|
{
|
|
if (!getCurrentState || CanCommitToTransform)
|
|
{
|
|
return InLocalSpace ? transform.localPosition : transform.position;
|
|
}
|
|
else
|
|
{
|
|
// When half float precision is enabled, get the NetworkDeltaPosition's full position
|
|
if (UseHalfFloatPrecision)
|
|
{
|
|
return m_HalfPositionState.GetFullPosition();
|
|
}
|
|
else
|
|
{
|
|
// Otherwise, just get the current position
|
|
return m_CurrentPosition;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method that returns the space relative rotation of the transform.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If InLocalSpace is <see cref="true"/> then it returns the transform.localRotation
|
|
/// If InLocalSpace is <see cref="false"/> then it returns the transform.rotation
|
|
/// When invoked on the non-authority side:
|
|
/// If <see cref="getCurrentState"/> is true then it will return the most
|
|
/// current authority rotation from the most recent state update. This can be useful
|
|
/// if interpolation is enabled and you need to determine the final target rotation.
|
|
/// When invoked on the authority side:
|
|
/// It will always return the space relative rotation.
|
|
/// </remarks>
|
|
/// <param name="getCurrentState">
|
|
/// Authority always returns the space relative transform rotation (whether true or false).
|
|
/// Non-authority:
|
|
/// When false (default): returns the space relative transform rotation
|
|
/// When true: returns the authority rotation from the most recent state update.
|
|
/// </param>
|
|
/// <returns><see cref="Quaternion"/></returns>
|
|
public Quaternion GetSpaceRelativeRotation(bool getCurrentState = false)
|
|
{
|
|
if (!getCurrentState || CanCommitToTransform)
|
|
{
|
|
return InLocalSpace ? transform.localRotation : transform.rotation;
|
|
}
|
|
else
|
|
{
|
|
return m_CurrentRotation;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method that returns the scale of the transform.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// When invoked on the non-authority side:
|
|
/// If <see cref="getCurrentState"/> is true then it will return the most
|
|
/// current authority scale from the most recent state update. This can be useful
|
|
/// if interpolation is enabled and you need to determine the final target scale.
|
|
/// When invoked on the authority side:
|
|
/// It will always return the space relative scale.
|
|
/// </remarks>
|
|
/// <param name="getCurrentState">
|
|
/// Authority always returns the space relative transform scale (whether true or false).
|
|
/// Non-authority:
|
|
/// When false (default): returns the space relative transform scale
|
|
/// When true: returns the authority scale from the most recent state update.
|
|
/// </param>
|
|
/// <returns><see cref="Vector3"/></returns>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public Vector3 GetScale(bool getCurrentState = false)
|
|
{
|
|
if (!getCurrentState || CanCommitToTransform)
|
|
{
|
|
return transform.localScale;
|
|
}
|
|
else
|
|
{
|
|
return m_CurrentScale;
|
|
}
|
|
}
|
|
|
|
// Used by both authoritative and non-authoritative instances.
|
|
// This represents the most recent local authoritative state.
|
|
private NetworkTransformState m_LocalAuthoritativeNetworkState;
|
|
|
|
internal NetworkTransformState LocalAuthoritativeNetworkState
|
|
{
|
|
get
|
|
{
|
|
return m_LocalAuthoritativeNetworkState;
|
|
}
|
|
set
|
|
{
|
|
m_LocalAuthoritativeNetworkState = value;
|
|
}
|
|
}
|
|
|
|
private ClientRpcParams m_ClientRpcParams = new ClientRpcParams() { Send = new ClientRpcSendParams() };
|
|
private List<ulong> m_ClientIds = new List<ulong>() { 0 };
|
|
|
|
private BufferedLinearInterpolatorVector3 m_PositionInterpolator;
|
|
private BufferedLinearInterpolatorVector3 m_ScaleInterpolator;
|
|
private BufferedLinearInterpolatorQuaternion m_RotationInterpolator; // rotation is a single Quaternion since each Euler axis will affect the quaternion's final value
|
|
|
|
// Non-Authoritative's current position, scale, and rotation that is used to assure the non-authoritative side cannot make adjustments to
|
|
// the portions of the transform being synchronized.
|
|
private Vector3 m_CurrentPosition;
|
|
private Vector3 m_TargetPosition;
|
|
private Vector3 m_CurrentScale;
|
|
private Vector3 m_TargetScale;
|
|
private Quaternion m_CurrentRotation;
|
|
private Vector3 m_TargetRotation;
|
|
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
internal void UpdatePositionInterpolator(Vector3 position, double time, bool resetInterpolator = false)
|
|
{
|
|
if (!CanCommitToTransform)
|
|
{
|
|
if (resetInterpolator)
|
|
{
|
|
m_PositionInterpolator.ResetTo(position, time);
|
|
}
|
|
else
|
|
{
|
|
m_PositionInterpolator.AddMeasurement(position, time);
|
|
}
|
|
}
|
|
}
|
|
|
|
#if DEBUG_NETWORKTRANSFORM || UNITY_INCLUDE_TESTS
|
|
/// <summary>
|
|
/// For debugging delta position and half vector3
|
|
/// </summary>
|
|
protected delegate void AddLogEntryHandler(ref NetworkTransformState networkTransformState, ulong targetClient, bool preUpdate = false);
|
|
protected AddLogEntryHandler m_AddLogEntry;
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void AddLogEntry(ref NetworkTransformState networkTransformState, ulong targetClient, bool preUpdate = false)
|
|
{
|
|
m_AddLogEntry?.Invoke(ref networkTransformState, targetClient, preUpdate);
|
|
}
|
|
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
protected int GetStateId(ref NetworkTransformState state)
|
|
{
|
|
return state.StateId;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
protected NetworkDeltaPosition GetHalfPositionState()
|
|
{
|
|
return m_HalfPositionState;
|
|
}
|
|
|
|
#else
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void AddLogEntry(ref NetworkTransformState networkTransformState, ulong targetClient, bool preUpdate = false)
|
|
{
|
|
}
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// Only used when UseHalfFloatPrecision is enabled
|
|
/// </summary>
|
|
private NetworkDeltaPosition m_HalfPositionState = new NetworkDeltaPosition(Vector3.zero, 0);
|
|
|
|
internal void UpdatePositionSlerp()
|
|
{
|
|
if (m_PositionInterpolator != null)
|
|
{
|
|
m_PositionInterpolator.IsSlerp = SlerpPosition;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if synchronization is needed.
|
|
/// Basically only if we are running in owner authoritative mode and it
|
|
/// is the owner being synchronized we don't want to synchronize with
|
|
/// the exception of the NetworkObject being owned by the server.
|
|
/// </summary>
|
|
private bool ShouldSynchronizeHalfFloat(ulong targetClientId)
|
|
{
|
|
if (!IsServerAuthoritative() && NetworkObject.OwnerClientId == targetClientId)
|
|
{
|
|
// Return false for all client owners but return true for the server
|
|
return NetworkObject.IsOwnedByServer;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// For test logging purposes
|
|
internal NetworkTransformState SynchronizeState;
|
|
|
|
/// <summary>
|
|
/// This is invoked when a new client joins (server and client sides)
|
|
/// Server Side: Serializes as if we were teleporting (everything is sent via NetworkTransformState)
|
|
/// Client Side: Adds the interpolated state which applies the NetworkTransformState as well
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If a derived class overrides this, then make sure to invoke this base method!
|
|
/// </remarks>
|
|
/// <typeparam name="T"></typeparam>
|
|
/// <param name="serializer"></param>
|
|
/// <param name="targetClientId">the clientId being synchronized (both reading and writing)</param>
|
|
protected override void OnSynchronize<T>(ref BufferSerializer<T> serializer)
|
|
{
|
|
m_CachedNetworkManager = NetworkManager;
|
|
var targetClientId = m_TargetIdBeingSynchronized;
|
|
var synchronizationState = new NetworkTransformState()
|
|
{
|
|
HalfEulerRotation = new HalfVector3(),
|
|
HalfVectorRotation = new HalfVector4(),
|
|
HalfVectorScale = new HalfVector3(),
|
|
NetworkDeltaPosition = new NetworkDeltaPosition(),
|
|
};
|
|
|
|
if (serializer.IsWriter)
|
|
{
|
|
synchronizationState.IsTeleportingNextFrame = true;
|
|
var transformToCommit = transform;
|
|
// If we are using Half Float Precision, then we want to only synchronize the authority's m_HalfPositionState.FullPosition in order for
|
|
// for the non-authority side to be able to properly synchronize delta position updates.
|
|
ApplyTransformToNetworkStateWithInfo(ref synchronizationState, ref transformToCommit, true, targetClientId);
|
|
synchronizationState.NetworkSerialize(serializer);
|
|
SynchronizeState = synchronizationState;
|
|
}
|
|
else
|
|
{
|
|
synchronizationState.NetworkSerialize(serializer);
|
|
// Set the transform's synchronization modes
|
|
InLocalSpace = synchronizationState.InLocalSpace;
|
|
Interpolate = synchronizationState.UseInterpolation;
|
|
UseQuaternionSynchronization = synchronizationState.QuaternionSync;
|
|
UseHalfFloatPrecision = synchronizationState.UseHalfFloatPrecision;
|
|
UseQuaternionCompression = synchronizationState.QuaternionCompression;
|
|
SlerpPosition = synchronizationState.UsePositionSlerp;
|
|
UpdatePositionSlerp();
|
|
|
|
// Teleport/Fully Initialize based on the state
|
|
ApplyTeleportingState(synchronizationState);
|
|
SynchronizeState = synchronizationState;
|
|
m_LocalAuthoritativeNetworkState = synchronizationState;
|
|
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false;
|
|
m_LocalAuthoritativeNetworkState.IsSynchronizing = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This will try to send/commit the current transform delta states (if any)
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Only client owners or the server should invoke this method
|
|
/// </remarks>
|
|
/// <param name="transformToCommit">the transform to be committed</param>
|
|
/// <param name="dirtyTime">time it was marked dirty</param>
|
|
protected void TryCommitTransformToServer(Transform transformToCommit, double dirtyTime)
|
|
{
|
|
if (!IsSpawned)
|
|
{
|
|
NetworkLog.LogError($"Cannot commit transform when not spawned!");
|
|
return;
|
|
}
|
|
|
|
// Only the server or the owner is allowed to commit a transform
|
|
if (!IsServer && !IsOwner)
|
|
{
|
|
var errorMessage = gameObject != NetworkObject.gameObject ?
|
|
$"Non-authority instance of {NetworkObject.gameObject.name} is trying to commit a transform on {gameObject.name}!" :
|
|
$"Non-authority instance of {NetworkObject.gameObject.name} is trying to commit a transform!";
|
|
NetworkLog.LogError(errorMessage);
|
|
return;
|
|
}
|
|
|
|
// If we are authority, update the authoritative state
|
|
if (CanCommitToTransform)
|
|
{
|
|
OnUpdateAuthoritativeState(ref transformToCommit);
|
|
}
|
|
else // Non-Authority
|
|
{
|
|
var position = InLocalSpace ? transformToCommit.localPosition : transformToCommit.position;
|
|
var rotation = InLocalSpace ? transformToCommit.localRotation : transformToCommit.rotation;
|
|
// We are an owner requesting to update our state
|
|
if (!IsServer)
|
|
{
|
|
SetStateServerRpc(position, rotation, transformToCommit.localScale, false);
|
|
}
|
|
else // Server is always authoritative (including owner authoritative)
|
|
{
|
|
SetStateClientRpc(position, rotation, transformToCommit.localScale, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked just prior to being pushed to non-authority instances.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is useful to know the exact position, rotation, or scale values sent
|
|
/// to non-authoritative instances. This is only invoked on the authoritative
|
|
/// instance.
|
|
/// </remarks>
|
|
/// <param name="networkTransformState">the state being pushed</param>
|
|
protected virtual void OnAuthorityPushTransformState(ref NetworkTransformState networkTransformState)
|
|
{
|
|
}
|
|
|
|
// Only set if a delta has been sent, this is reset after an axial synch has been sent
|
|
// to assure the instance doesn't continue to send axial synchs when an object is at rest.
|
|
private bool m_DeltaSynch;
|
|
|
|
/// <summary>
|
|
/// Authoritative side only
|
|
/// If there are any transform delta states, this method will synchronize the
|
|
/// state with all non-authority instances.
|
|
/// </summary>
|
|
private void TryCommitTransform(ref Transform transformToCommit, bool synchronize = false, bool settingState = false)
|
|
{
|
|
// Only the server or the owner is allowed to commit a transform
|
|
if (!IsServer && !IsOwner)
|
|
{
|
|
NetworkLog.LogError($"[{name}] is trying to commit the transform without authority!");
|
|
return;
|
|
}
|
|
|
|
// If the transform has deltas (returns dirty) or if an explicitly set state is pending
|
|
if (m_LocalAuthoritativeNetworkState.ExplicitSet || ApplyTransformToNetworkStateWithInfo(ref m_LocalAuthoritativeNetworkState, ref transformToCommit, synchronize))
|
|
{
|
|
m_LocalAuthoritativeNetworkState.LastSerializedSize = m_OldState.LastSerializedSize;
|
|
|
|
// If the state was explicitly set, then update the network tick to match the locally calculate tick
|
|
if (m_LocalAuthoritativeNetworkState.ExplicitSet)
|
|
{
|
|
m_LocalAuthoritativeNetworkState.NetworkTick = m_CachedNetworkManager.NetworkTickSystem.ServerTime.Tick;
|
|
}
|
|
|
|
// Send the state update
|
|
UpdateTransformState();
|
|
|
|
// Mark the last tick and the old state (for next ticks)
|
|
m_OldState = m_LocalAuthoritativeNetworkState;
|
|
|
|
// Reset the teleport and explicit state flags after we have sent the state update.
|
|
// These could be set again in the below OnAuthorityPushTransformState virtual method
|
|
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false;
|
|
m_LocalAuthoritativeNetworkState.ExplicitSet = false;
|
|
|
|
try
|
|
{
|
|
// Notify of the pushed state update
|
|
OnAuthorityPushTransformState(ref m_LocalAuthoritativeNetworkState);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogException(ex);
|
|
}
|
|
|
|
// The below is part of assuring we only send a frame synch, when sending unreliable deltas, if
|
|
// we have already sent at least one unreliable delta state update. At this point in the callstack,
|
|
// a delta state update has just been sent in the above UpdateTransformState() call and as long as
|
|
// we didn't send a frame synch and we are not synchronizing then we know at least one unreliable
|
|
// delta has been sent. Under this scenario, we should start checking for this instance's alloted
|
|
// frame synch "tick slot". Once we send a frame synch, if no other deltas occur after that
|
|
// (i.e. the object is at rest) then we will stop sending frame synch's until the object begins
|
|
// moving, rotating, or scaling again.
|
|
if (UseUnreliableDeltas && !m_LocalAuthoritativeNetworkState.UnreliableFrameSync && !synchronize)
|
|
{
|
|
m_DeltaSynch = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the interpolators with the current transform values
|
|
/// </summary>
|
|
private void ResetInterpolatedStateToCurrentAuthoritativeState()
|
|
{
|
|
var serverTime = NetworkManager.ServerTime.Time;
|
|
|
|
UpdatePositionInterpolator(GetSpaceRelativePosition(), serverTime, true);
|
|
UpdatePositionSlerp();
|
|
|
|
m_ScaleInterpolator.ResetTo(transform.localScale, serverTime);
|
|
m_RotationInterpolator.ResetTo(GetSpaceRelativeRotation(), serverTime);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used for integration testing:
|
|
/// Will apply the transform to the LocalAuthoritativeNetworkState and get detailed dirty information returned
|
|
/// in the <see cref="NetworkTransformState"/> returned.
|
|
/// </summary>
|
|
/// <param name="transform">transform to apply</param>
|
|
/// <returns>NetworkTransformState</returns>
|
|
internal NetworkTransformState ApplyLocalNetworkState(Transform transform)
|
|
{
|
|
// Since we never commit these changes, we need to simulate that any changes were committed previously and the bitset
|
|
// value would already be reset prior to having the state applied
|
|
m_LocalAuthoritativeNetworkState.ClearBitSetForNextTick();
|
|
|
|
// Now check the transform for any threshold value changes
|
|
ApplyTransformToNetworkStateWithInfo(ref m_LocalAuthoritativeNetworkState, ref transform);
|
|
|
|
// Return the entire state to be used by the integration test
|
|
return m_LocalAuthoritativeNetworkState;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used for integration testing
|
|
/// </summary>
|
|
internal bool ApplyTransformToNetworkState(ref NetworkTransformState networkState, double dirtyTime, Transform transformToUse)
|
|
{
|
|
m_CachedNetworkManager = NetworkManager;
|
|
// Apply the interpolate and PostionDeltaCompression flags, otherwise we get false positives whether something changed or not.
|
|
networkState.UseInterpolation = Interpolate;
|
|
networkState.QuaternionSync = UseQuaternionSynchronization;
|
|
networkState.UseHalfFloatPrecision = UseHalfFloatPrecision;
|
|
networkState.QuaternionCompression = UseQuaternionCompression;
|
|
networkState.UseUnreliableDeltas = UseUnreliableDeltas;
|
|
m_HalfPositionState = new NetworkDeltaPosition(Vector3.zero, 0, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ));
|
|
|
|
return ApplyTransformToNetworkStateWithInfo(ref networkState, ref transformToUse);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies the transform to the <see cref="NetworkTransformState"/> specified.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState networkState, ref Transform transformToUse, bool isSynchronization = false, ulong targetClientId = 0)
|
|
{
|
|
// As long as we are not doing our first synchronization and we are sending unreliable deltas, each
|
|
// NetworkTransform will stagger its full transfom synchronization over a 1 second period based on the
|
|
// assigned tick slot (m_TickSync).
|
|
// More about m_DeltaSynch:
|
|
// If we have not sent any deltas since our last frame synch, then this will prevent us from sending
|
|
// frame synch's when the object is at rest. If this is false and a state update is detected and sent,
|
|
// then it will be set to true and each subsequent tick will do this check to determine if it should
|
|
// send a full frame synch.
|
|
var isAxisSync = false;
|
|
// We compare against the NetworkTickSystem version since ServerTime is set when updating ticks
|
|
if (UseUnreliableDeltas && !isSynchronization && m_DeltaSynch && m_NextTickSync <= m_CachedNetworkManager.NetworkTickSystem.ServerTime.Tick)
|
|
{
|
|
// Increment to the next frame synch tick position for this instance
|
|
m_NextTickSync += (int)m_CachedNetworkManager.NetworkConfig.TickRate;
|
|
// If we are teleporting, we do not need to send a frame synch for this tick slot
|
|
// as a "frame synch" really is effectively just a teleport.
|
|
isAxisSync = !networkState.IsTeleportingNextFrame;
|
|
// Reset our delta synch trigger so we don't send another frame synch until we
|
|
// send at least 1 unreliable state update after this fame synch or teleport
|
|
m_DeltaSynch = false;
|
|
}
|
|
// This is used to determine if we need to send the state update reliably (if we are doing an axial sync)
|
|
networkState.UnreliableFrameSync = isAxisSync;
|
|
|
|
var isTeleportingAndNotSynchronizing = networkState.IsTeleportingNextFrame && !isSynchronization;
|
|
var isDirty = false;
|
|
var isPositionDirty = isTeleportingAndNotSynchronizing ? networkState.HasPositionChange : false;
|
|
var isRotationDirty = isTeleportingAndNotSynchronizing ? networkState.HasRotAngleChange : false;
|
|
var isScaleDirty = isTeleportingAndNotSynchronizing ? networkState.HasScaleChange : false;
|
|
|
|
var position = InLocalSpace ? transformToUse.localPosition : transformToUse.position;
|
|
var rotAngles = InLocalSpace ? transformToUse.localEulerAngles : transformToUse.eulerAngles;
|
|
var scale = transformToUse.localScale;
|
|
networkState.IsSynchronizing = isSynchronization;
|
|
|
|
// All of the checks below, up to the delta position checking portion, are to determine if the
|
|
// authority changed a property during runtime that requires a full synchronizing.
|
|
if (InLocalSpace != networkState.InLocalSpace)
|
|
{
|
|
networkState.InLocalSpace = InLocalSpace;
|
|
isDirty = true;
|
|
networkState.IsTeleportingNextFrame = true;
|
|
}
|
|
|
|
// Check for parenting when synchronizing and/or teleporting
|
|
if (isSynchronization || networkState.IsTeleportingNextFrame)
|
|
{
|
|
// This all has to do with complex nested hierarchies and how it impacts scale
|
|
// when set for the first time or teleporting and depends upon whether the
|
|
// NetworkObject is parented (or "de-parented") at the same time any scale
|
|
// values are applied.
|
|
var hasParentNetworkObject = false;
|
|
|
|
var parentNetworkObject = (NetworkObject)null;
|
|
|
|
// If the NetworkObject belonging to this NetworkTransform instance has a parent
|
|
// (i.e. this handles nested NetworkTransforms under a parent at some layer above)
|
|
if (NetworkObject.transform.parent != null)
|
|
{
|
|
parentNetworkObject = NetworkObject.transform.parent.GetComponent<NetworkObject>();
|
|
|
|
// In-scene placed NetworkObjects parented under a GameObject with no
|
|
// NetworkObject preserve their lossyScale when synchronizing.
|
|
if (parentNetworkObject == null && NetworkObject.IsSceneObject != false)
|
|
{
|
|
hasParentNetworkObject = true;
|
|
}
|
|
else
|
|
{
|
|
// Or if the relative NetworkObject has a parent NetworkObject
|
|
hasParentNetworkObject = parentNetworkObject != null;
|
|
}
|
|
}
|
|
|
|
networkState.IsParented = hasParentNetworkObject;
|
|
|
|
// When synchronizing with a parent, world position stays impacts position whether
|
|
// the NetworkTransform is using world or local space synchronization.
|
|
// WorldPositionStays: (always use world space)
|
|
// !WorldPositionStays: (always use local space)
|
|
// Exception: If it is an in-scene placed NetworkObject and it is parented under a GameObject
|
|
// then always use local space unless AutoObjectParentSync is disabled and the NetworkTransform
|
|
// is synchronizing in world space.
|
|
if (isSynchronization && networkState.IsParented)
|
|
{
|
|
var parentedUnderGameObject = NetworkObject.transform.parent != null && !parentNetworkObject && NetworkObject.IsSceneObject.Value;
|
|
if (NetworkObject.WorldPositionStays() && (!parentedUnderGameObject || (parentedUnderGameObject && !NetworkObject.AutoObjectParentSync && !InLocalSpace)))
|
|
{
|
|
position = transformToUse.position;
|
|
networkState.InLocalSpace = false;
|
|
}
|
|
else
|
|
{
|
|
position = transformToUse.localPosition;
|
|
networkState.InLocalSpace = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Interpolate != networkState.UseInterpolation)
|
|
{
|
|
networkState.UseInterpolation = Interpolate;
|
|
isDirty = true;
|
|
// When we change from interpolating to not interpolating (or vice versa) we need to synchronize/reset everything
|
|
networkState.IsTeleportingNextFrame = true;
|
|
}
|
|
|
|
if (UseQuaternionSynchronization != networkState.QuaternionSync)
|
|
{
|
|
networkState.QuaternionSync = UseQuaternionSynchronization;
|
|
isDirty = true;
|
|
networkState.IsTeleportingNextFrame = true;
|
|
}
|
|
|
|
if (UseQuaternionCompression != networkState.QuaternionCompression)
|
|
{
|
|
networkState.QuaternionCompression = UseQuaternionCompression;
|
|
isDirty = true;
|
|
networkState.IsTeleportingNextFrame = true;
|
|
}
|
|
|
|
if (UseHalfFloatPrecision != networkState.UseHalfFloatPrecision)
|
|
{
|
|
networkState.UseHalfFloatPrecision = UseHalfFloatPrecision;
|
|
isDirty = true;
|
|
networkState.IsTeleportingNextFrame = true;
|
|
}
|
|
|
|
if (SlerpPosition != networkState.UsePositionSlerp)
|
|
{
|
|
networkState.UsePositionSlerp = SlerpPosition;
|
|
isDirty = true;
|
|
networkState.IsTeleportingNextFrame = true;
|
|
}
|
|
|
|
if (UseUnreliableDeltas != networkState.UseUnreliableDeltas)
|
|
{
|
|
networkState.UseUnreliableDeltas = UseUnreliableDeltas;
|
|
isDirty = true;
|
|
networkState.IsTeleportingNextFrame = true;
|
|
}
|
|
|
|
// Begin delta checks against last sent state update
|
|
if (!UseHalfFloatPrecision)
|
|
{
|
|
if (SyncPositionX && (Mathf.Abs(networkState.PositionX - position.x) >= PositionThreshold || networkState.IsTeleportingNextFrame || isAxisSync))
|
|
{
|
|
networkState.PositionX = position.x;
|
|
networkState.HasPositionX = true;
|
|
isPositionDirty = true;
|
|
}
|
|
|
|
if (SyncPositionY && (Mathf.Abs(networkState.PositionY - position.y) >= PositionThreshold || networkState.IsTeleportingNextFrame || isAxisSync))
|
|
{
|
|
networkState.PositionY = position.y;
|
|
networkState.HasPositionY = true;
|
|
isPositionDirty = true;
|
|
}
|
|
|
|
if (SyncPositionZ && (Mathf.Abs(networkState.PositionZ - position.z) >= PositionThreshold || networkState.IsTeleportingNextFrame || isAxisSync))
|
|
{
|
|
networkState.PositionZ = position.z;
|
|
networkState.HasPositionZ = true;
|
|
isPositionDirty = true;
|
|
}
|
|
}
|
|
else if (SynchronizePosition)
|
|
{
|
|
// If we are teleporting then we can skip the delta threshold check
|
|
isPositionDirty = networkState.IsTeleportingNextFrame || isAxisSync;
|
|
if (m_HalfFloatTargetTickOwnership > m_CachedNetworkManager.ServerTime.Tick)
|
|
{
|
|
isPositionDirty = true;
|
|
}
|
|
|
|
// For NetworkDeltaPosition, if any axial value is dirty then we always send a full update
|
|
if (!isPositionDirty)
|
|
{
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
if (Math.Abs(position[i] - m_HalfPositionState.PreviousPosition[i]) >= PositionThreshold)
|
|
{
|
|
isPositionDirty = i == 0 ? SyncPositionX : i == 1 ? SyncPositionY : SyncPositionZ;
|
|
if (!isPositionDirty)
|
|
{
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the position is dirty or we are teleporting (which includes synchronization)
|
|
// then determine what parts of the NetworkDeltaPosition should be updated
|
|
if (isPositionDirty)
|
|
{
|
|
// If we are not synchronizing the transform state for the first time
|
|
if (!isSynchronization)
|
|
{
|
|
// With global teleporting (broadcast to all non-authority instances)
|
|
// we re-initialize authority's NetworkDeltaPosition and synchronize all
|
|
// non-authority instances with the new full precision position
|
|
if (networkState.IsTeleportingNextFrame)
|
|
{
|
|
m_HalfPositionState = new NetworkDeltaPosition(position, networkState.NetworkTick, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ));
|
|
networkState.CurrentPosition = position;
|
|
}
|
|
else // Otherwise, just synchronize the delta position value
|
|
{
|
|
m_HalfPositionState.HalfVector3.AxisToSynchronize = math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ);
|
|
m_HalfPositionState.UpdateFrom(ref position, networkState.NetworkTick);
|
|
}
|
|
|
|
networkState.NetworkDeltaPosition = m_HalfPositionState;
|
|
|
|
// If ownership offset is greater or we are doing an axial synchronization then synchronize the base position
|
|
if ((m_HalfFloatTargetTickOwnership > m_CachedNetworkManager.ServerTime.Tick || isAxisSync) && !networkState.IsTeleportingNextFrame)
|
|
{
|
|
networkState.SynchronizeBaseHalfFloat = true;
|
|
}
|
|
else
|
|
{
|
|
networkState.SynchronizeBaseHalfFloat = UseUnreliableDeltas ? m_HalfPositionState.CollapsedDeltaIntoBase : false;
|
|
}
|
|
}
|
|
else // If synchronizing is set, then use the current full position value on the server side
|
|
{
|
|
if (ShouldSynchronizeHalfFloat(targetClientId))
|
|
{
|
|
// If we have a NetworkDeltaPosition that has a state applied, then we want to determine
|
|
// what needs to be synchronized. For owner authoritative mode, the server side
|
|
// will have no valid state yet.
|
|
if (m_HalfPositionState.NetworkTick > 0)
|
|
{
|
|
// Always synchronize the base position and the ushort values of the
|
|
// current m_HalfPositionState
|
|
networkState.CurrentPosition = m_HalfPositionState.CurrentBasePosition;
|
|
networkState.NetworkDeltaPosition = m_HalfPositionState;
|
|
// If the server is the owner, in both server and owner authoritative modes,
|
|
// or we are running in server authoritative mode, then we use the
|
|
// HalfDeltaConvertedBack value as the delta position
|
|
if (NetworkObject.IsOwnedByServer || IsServerAuthoritative())
|
|
{
|
|
networkState.DeltaPosition = m_HalfPositionState.HalfDeltaConvertedBack;
|
|
}
|
|
else
|
|
{
|
|
// Otherwise, we are in owner authoritative mode and the server's NetworkDeltaPosition
|
|
// state is "non-authoritative" relative so we use the DeltaPosition.
|
|
networkState.DeltaPosition = m_HalfPositionState.DeltaPosition;
|
|
}
|
|
}
|
|
else // Reset everything and just send the current position
|
|
{
|
|
networkState.NetworkDeltaPosition = new NetworkDeltaPosition(Vector3.zero, 0, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ));
|
|
networkState.DeltaPosition = Vector3.zero;
|
|
networkState.CurrentPosition = position;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
networkState.NetworkDeltaPosition = new NetworkDeltaPosition(Vector3.zero, 0, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ));
|
|
networkState.CurrentPosition = position;
|
|
}
|
|
// Add log entry for this update relative to the client being synchronized
|
|
AddLogEntry(ref networkState, targetClientId, true);
|
|
}
|
|
networkState.HasPositionX = SyncPositionX;
|
|
networkState.HasPositionY = SyncPositionY;
|
|
networkState.HasPositionZ = SyncPositionZ;
|
|
}
|
|
}
|
|
|
|
if (!UseQuaternionSynchronization)
|
|
{
|
|
if (SyncRotAngleX && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleX, rotAngles.x)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame || isAxisSync))
|
|
{
|
|
networkState.RotAngleX = rotAngles.x;
|
|
networkState.HasRotAngleX = true;
|
|
isRotationDirty = true;
|
|
}
|
|
|
|
if (SyncRotAngleY && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleY, rotAngles.y)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame || isAxisSync))
|
|
{
|
|
networkState.RotAngleY = rotAngles.y;
|
|
networkState.HasRotAngleY = true;
|
|
isRotationDirty = true;
|
|
}
|
|
|
|
if (SyncRotAngleZ && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleZ, rotAngles.z)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame || isAxisSync))
|
|
{
|
|
networkState.RotAngleZ = rotAngles.z;
|
|
networkState.HasRotAngleZ = true;
|
|
isRotationDirty = true;
|
|
}
|
|
}
|
|
else if (SynchronizeRotation)
|
|
{
|
|
// If we are teleporting then we can skip the delta threshold check
|
|
isRotationDirty = networkState.IsTeleportingNextFrame || isAxisSync;
|
|
// For quaternion synchronization, if one angle is dirty we send a full update
|
|
if (!isRotationDirty)
|
|
{
|
|
var previousRotation = networkState.Rotation.eulerAngles;
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
if (Mathf.Abs(Mathf.DeltaAngle(previousRotation[i], rotAngles[i])) >= RotAngleThreshold)
|
|
{
|
|
isRotationDirty = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (isRotationDirty)
|
|
{
|
|
networkState.Rotation = InLocalSpace ? transformToUse.localRotation : transformToUse.rotation;
|
|
networkState.HasRotAngleX = true;
|
|
networkState.HasRotAngleY = true;
|
|
networkState.HasRotAngleZ = true;
|
|
}
|
|
}
|
|
|
|
// For scale, we need to check for parenting when synchronizing and/or teleporting
|
|
if (isSynchronization || networkState.IsTeleportingNextFrame)
|
|
{
|
|
// If we are synchronizing and the associated NetworkObject has a parent then we want to send the
|
|
// LossyScale if the NetworkObject has a parent since NetworkObject spawn order is not guaranteed
|
|
if (networkState.IsParented)
|
|
{
|
|
networkState.LossyScale = transform.lossyScale;
|
|
}
|
|
}
|
|
|
|
// Checking scale deltas when not synchronizing
|
|
if (!isSynchronization)
|
|
{
|
|
if (!UseHalfFloatPrecision)
|
|
{
|
|
if (SyncScaleX && (Mathf.Abs(networkState.ScaleX - scale.x) >= ScaleThreshold || networkState.IsTeleportingNextFrame || isAxisSync))
|
|
{
|
|
networkState.ScaleX = scale.x;
|
|
networkState.HasScaleX = true;
|
|
isScaleDirty = true;
|
|
}
|
|
|
|
if (SyncScaleY && (Mathf.Abs(networkState.ScaleY - scale.y) >= ScaleThreshold || networkState.IsTeleportingNextFrame || isAxisSync))
|
|
{
|
|
networkState.ScaleY = scale.y;
|
|
networkState.HasScaleY = true;
|
|
isScaleDirty = true;
|
|
}
|
|
|
|
if (SyncScaleZ && (Mathf.Abs(networkState.ScaleZ - scale.z) >= ScaleThreshold || networkState.IsTeleportingNextFrame || isAxisSync))
|
|
{
|
|
networkState.ScaleZ = scale.z;
|
|
networkState.HasScaleZ = true;
|
|
isScaleDirty = true;
|
|
}
|
|
}
|
|
else if (SynchronizeScale)
|
|
{
|
|
var previousScale = networkState.Scale;
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
if (Mathf.Abs(scale[i] - previousScale[i]) >= ScaleThreshold || networkState.IsTeleportingNextFrame || isAxisSync)
|
|
{
|
|
isScaleDirty = true;
|
|
networkState.Scale[i] = scale[i];
|
|
networkState.SetHasScale(i, i == 0 ? SyncScaleX : i == 1 ? SyncScaleY : SyncScaleZ);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else // Just apply the full local scale when synchronizing
|
|
if (SynchronizeScale)
|
|
{
|
|
if (!UseHalfFloatPrecision)
|
|
{
|
|
networkState.ScaleX = transform.localScale.x;
|
|
networkState.ScaleY = transform.localScale.y;
|
|
networkState.ScaleZ = transform.localScale.z;
|
|
}
|
|
else
|
|
{
|
|
networkState.Scale = transform.localScale;
|
|
}
|
|
networkState.HasScaleX = true;
|
|
networkState.HasScaleY = true;
|
|
networkState.HasScaleZ = true;
|
|
isScaleDirty = true;
|
|
}
|
|
isDirty |= isPositionDirty || isRotationDirty || isScaleDirty;
|
|
|
|
if (isDirty)
|
|
{
|
|
// Some integration/unit tests disable the NetworkTransform and there is no
|
|
// NetworkManager
|
|
if (enabled)
|
|
{
|
|
// We use the NetworkTickSystem version since ServerTime is set when updating ticks
|
|
networkState.NetworkTick = m_CachedNetworkManager.NetworkTickSystem.ServerTime.Tick;
|
|
}
|
|
}
|
|
|
|
// Mark the state dirty for the next network tick update to clear out the bitset values
|
|
networkState.IsDirty |= isDirty;
|
|
return isDirty;
|
|
}
|
|
|
|
protected virtual void OnTransformUpdated()
|
|
{
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies the authoritative state to the transform
|
|
/// </summary>
|
|
protected internal void ApplyAuthoritativeState()
|
|
{
|
|
var networkState = m_LocalAuthoritativeNetworkState;
|
|
// The m_CurrentPosition, m_CurrentRotation, and m_CurrentScale values are continually updated
|
|
// at the end of this method and assure that when not interpolating the non-authoritative side
|
|
// cannot make adjustments to any portions the transform not being synchronized.
|
|
var adjustedPosition = m_CurrentPosition;
|
|
var adjustedRotation = m_CurrentRotation;
|
|
var adjustedRotAngles = adjustedRotation.eulerAngles;
|
|
var adjustedScale = m_CurrentScale;
|
|
|
|
// Non-Authority Preservers the authority's transform state update modes
|
|
InLocalSpace = networkState.InLocalSpace;
|
|
Interpolate = networkState.UseInterpolation;
|
|
UseHalfFloatPrecision = networkState.UseHalfFloatPrecision;
|
|
UseQuaternionSynchronization = networkState.QuaternionSync;
|
|
UseQuaternionCompression = networkState.QuaternionCompression;
|
|
UseUnreliableDeltas = networkState.UseUnreliableDeltas;
|
|
|
|
if (SlerpPosition != networkState.UsePositionSlerp)
|
|
{
|
|
SlerpPosition = networkState.UsePositionSlerp;
|
|
UpdatePositionSlerp();
|
|
}
|
|
|
|
// NOTE ABOUT INTERPOLATING AND THE CODE BELOW:
|
|
// We always apply the interpolated state for any axis we are synchronizing even when the state has no deltas
|
|
// to assure we fully interpolate to our target even after we stop extrapolating 1 tick later.
|
|
if (Interpolate)
|
|
{
|
|
if (SynchronizePosition)
|
|
{
|
|
var interpolatedPosition = m_PositionInterpolator.GetInterpolatedValue();
|
|
if (UseHalfFloatPrecision)
|
|
{
|
|
adjustedPosition = interpolatedPosition;
|
|
}
|
|
else
|
|
{
|
|
if (SyncPositionX) { adjustedPosition.x = interpolatedPosition.x; }
|
|
if (SyncPositionY) { adjustedPosition.y = interpolatedPosition.y; }
|
|
if (SyncPositionZ) { adjustedPosition.z = interpolatedPosition.z; }
|
|
}
|
|
}
|
|
|
|
if (SynchronizeScale)
|
|
{
|
|
if (UseHalfFloatPrecision)
|
|
{
|
|
adjustedScale = m_ScaleInterpolator.GetInterpolatedValue();
|
|
}
|
|
else
|
|
{
|
|
var interpolatedScale = m_ScaleInterpolator.GetInterpolatedValue();
|
|
if (SyncScaleX) { adjustedScale.x = interpolatedScale.x; }
|
|
if (SyncScaleY) { adjustedScale.y = interpolatedScale.y; }
|
|
if (SyncScaleZ) { adjustedScale.z = interpolatedScale.z; }
|
|
}
|
|
}
|
|
|
|
if (SynchronizeRotation)
|
|
{
|
|
var interpolatedRotation = m_RotationInterpolator.GetInterpolatedValue();
|
|
if (UseQuaternionSynchronization)
|
|
{
|
|
adjustedRotation = interpolatedRotation;
|
|
}
|
|
else
|
|
{
|
|
var interpolatedEulerAngles = interpolatedRotation.eulerAngles;
|
|
if (SyncRotAngleX) { adjustedRotAngles.x = interpolatedEulerAngles.x; }
|
|
if (SyncRotAngleY) { adjustedRotAngles.y = interpolatedEulerAngles.y; }
|
|
if (SyncRotAngleZ) { adjustedRotAngles.z = interpolatedEulerAngles.z; }
|
|
adjustedRotation.eulerAngles = adjustedRotAngles;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Non-Interpolated Position and Scale
|
|
if (UseHalfFloatPrecision)
|
|
{
|
|
if (networkState.HasPositionChange && SynchronizePosition)
|
|
{
|
|
adjustedPosition = m_TargetPosition;
|
|
}
|
|
|
|
if (networkState.HasScaleChange && SynchronizeScale)
|
|
{
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
if (m_LocalAuthoritativeNetworkState.HasScale(i))
|
|
{
|
|
adjustedScale[i] = m_LocalAuthoritativeNetworkState.Scale[i];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (networkState.HasPositionX) { adjustedPosition.x = networkState.PositionX; }
|
|
if (networkState.HasPositionY) { adjustedPosition.y = networkState.PositionY; }
|
|
if (networkState.HasPositionZ) { adjustedPosition.z = networkState.PositionZ; }
|
|
if (networkState.HasScaleX) { adjustedScale.x = networkState.ScaleX; }
|
|
if (networkState.HasScaleY) { adjustedScale.y = networkState.ScaleY; }
|
|
if (networkState.HasScaleZ) { adjustedScale.z = networkState.ScaleZ; }
|
|
}
|
|
|
|
// Non-interpolated rotation
|
|
if (SynchronizeRotation)
|
|
{
|
|
if (networkState.QuaternionSync && networkState.HasRotAngleChange)
|
|
{
|
|
adjustedRotation = networkState.Rotation;
|
|
}
|
|
else
|
|
{
|
|
if (networkState.HasRotAngleX) { adjustedRotAngles.x = networkState.RotAngleX; }
|
|
if (networkState.HasRotAngleY) { adjustedRotAngles.y = networkState.RotAngleY; }
|
|
if (networkState.HasRotAngleZ) { adjustedRotAngles.z = networkState.RotAngleZ; }
|
|
adjustedRotation.eulerAngles = adjustedRotAngles;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply the position if we are synchronizing position
|
|
if (SynchronizePosition)
|
|
{
|
|
// Update our current position if it changed or we are interpolating
|
|
if (networkState.HasPositionChange || Interpolate)
|
|
{
|
|
m_CurrentPosition = adjustedPosition;
|
|
}
|
|
if (InLocalSpace)
|
|
{
|
|
transform.localPosition = m_CurrentPosition;
|
|
}
|
|
else
|
|
{
|
|
transform.position = m_CurrentPosition;
|
|
}
|
|
}
|
|
|
|
// Apply the rotation if we are synchronizing rotation
|
|
if (SynchronizeRotation)
|
|
{
|
|
// Update our current rotation if it changed or we are interpolating
|
|
if (networkState.HasRotAngleChange || Interpolate)
|
|
{
|
|
m_CurrentRotation = adjustedRotation;
|
|
}
|
|
if (InLocalSpace)
|
|
{
|
|
transform.localRotation = m_CurrentRotation;
|
|
}
|
|
else
|
|
{
|
|
transform.rotation = m_CurrentRotation;
|
|
}
|
|
}
|
|
|
|
// Apply the scale if we are synchronizing scale
|
|
if (SynchronizeScale)
|
|
{
|
|
// Update our current scale if it changed or we are interpolating
|
|
if (networkState.HasScaleChange || Interpolate)
|
|
{
|
|
m_CurrentScale = adjustedScale;
|
|
}
|
|
transform.localScale = m_CurrentScale;
|
|
}
|
|
OnTransformUpdated();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles applying the full authoritative state (i.e. teleporting)
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Only non-authoritative instances should invoke this
|
|
/// </remarks>
|
|
private void ApplyTeleportingState(NetworkTransformState newState)
|
|
{
|
|
if (!newState.IsTeleportingNextFrame)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var sentTime = newState.SentTime;
|
|
var currentPosition = GetSpaceRelativePosition();
|
|
var currentRotation = GetSpaceRelativeRotation();
|
|
var currentEulerAngles = currentRotation.eulerAngles;
|
|
var currentScale = transform.localScale;
|
|
|
|
var isSynchronization = newState.IsSynchronizing;
|
|
|
|
// Clear all interpolators
|
|
m_ScaleInterpolator.Clear();
|
|
m_PositionInterpolator.Clear();
|
|
m_RotationInterpolator.Clear();
|
|
|
|
if (newState.HasPositionChange)
|
|
{
|
|
if (!UseHalfFloatPrecision)
|
|
{
|
|
// Adjust based on which axis changed
|
|
if (newState.HasPositionX)
|
|
{
|
|
currentPosition.x = newState.PositionX;
|
|
}
|
|
|
|
if (newState.HasPositionY)
|
|
{
|
|
currentPosition.y = newState.PositionY;
|
|
}
|
|
|
|
if (newState.HasPositionZ)
|
|
{
|
|
currentPosition.z = newState.PositionZ;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// With delta position teleport updates or synchronization, we create a new instance and provide the current network tick.
|
|
m_HalfPositionState = new NetworkDeltaPosition(newState.CurrentPosition, newState.NetworkTick, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ));
|
|
|
|
// When first synchronizing we determine if we need to apply the current delta position
|
|
// offset or not. This is specific to owner authoritative mode on the owner side only
|
|
if (isSynchronization)
|
|
{
|
|
// Need to use NetworkManager vs m_CachedNetworkManager here since we are yet to be spawned
|
|
if (ShouldSynchronizeHalfFloat(NetworkManager.LocalClientId))
|
|
{
|
|
m_HalfPositionState.HalfVector3.Axis = newState.NetworkDeltaPosition.HalfVector3.Axis;
|
|
m_HalfPositionState.DeltaPosition = newState.DeltaPosition;
|
|
currentPosition = m_HalfPositionState.ToVector3(newState.NetworkTick);
|
|
}
|
|
else
|
|
{
|
|
currentPosition = newState.CurrentPosition;
|
|
}
|
|
// Before the state is applied add a log entry if AddLogEntry is assigned
|
|
AddLogEntry(ref newState, NetworkObject.OwnerClientId, true);
|
|
}
|
|
else
|
|
{
|
|
// If we are just teleporting, then we already created a new NetworkDeltaPosition value.
|
|
// set the current position to the state's current position
|
|
currentPosition = newState.CurrentPosition;
|
|
}
|
|
}
|
|
|
|
m_CurrentPosition = currentPosition;
|
|
m_TargetPosition = currentPosition;
|
|
|
|
// Apply the position
|
|
if (newState.InLocalSpace)
|
|
{
|
|
transform.localPosition = currentPosition;
|
|
}
|
|
else
|
|
{
|
|
transform.position = currentPosition;
|
|
}
|
|
|
|
if (Interpolate)
|
|
{
|
|
UpdatePositionInterpolator(currentPosition, sentTime, true);
|
|
}
|
|
}
|
|
|
|
if (newState.HasScaleChange)
|
|
{
|
|
bool shouldUseLossy = false;
|
|
if (newState.IsParented)
|
|
{
|
|
if (transform.parent == null)
|
|
{
|
|
shouldUseLossy = NetworkObject.WorldPositionStays();
|
|
}
|
|
else
|
|
{
|
|
shouldUseLossy = !NetworkObject.WorldPositionStays();
|
|
}
|
|
}
|
|
|
|
if (UseHalfFloatPrecision)
|
|
{
|
|
currentScale = shouldUseLossy ? newState.LossyScale : newState.Scale;
|
|
}
|
|
else
|
|
{
|
|
// Adjust based on which axis changed
|
|
if (newState.HasScaleX)
|
|
{
|
|
currentScale.x = shouldUseLossy ? newState.LossyScale.x : newState.ScaleX;
|
|
}
|
|
|
|
if (newState.HasScaleY)
|
|
{
|
|
currentScale.y = shouldUseLossy ? newState.LossyScale.y : newState.ScaleY;
|
|
}
|
|
|
|
if (newState.HasScaleZ)
|
|
{
|
|
currentScale.z = shouldUseLossy ? newState.LossyScale.z : newState.ScaleZ;
|
|
}
|
|
}
|
|
|
|
m_CurrentScale = currentScale;
|
|
m_TargetScale = currentScale;
|
|
|
|
// Apply the adjusted scale
|
|
transform.localScale = currentScale;
|
|
|
|
if (Interpolate)
|
|
{
|
|
m_ScaleInterpolator.ResetTo(currentScale, sentTime);
|
|
}
|
|
}
|
|
|
|
if (newState.HasRotAngleChange)
|
|
{
|
|
if (newState.QuaternionSync)
|
|
{
|
|
currentRotation = newState.Rotation;
|
|
}
|
|
else
|
|
{
|
|
// Adjust based on which axis changed
|
|
if (newState.HasRotAngleX)
|
|
{
|
|
currentEulerAngles.x = newState.RotAngleX;
|
|
}
|
|
|
|
if (newState.HasRotAngleY)
|
|
{
|
|
currentEulerAngles.y = newState.RotAngleY;
|
|
}
|
|
|
|
if (newState.HasRotAngleZ)
|
|
{
|
|
currentEulerAngles.z = newState.RotAngleZ;
|
|
}
|
|
currentRotation.eulerAngles = currentEulerAngles;
|
|
}
|
|
|
|
m_CurrentRotation = currentRotation;
|
|
m_TargetRotation = currentRotation.eulerAngles;
|
|
|
|
if (InLocalSpace)
|
|
{
|
|
transform.localRotation = currentRotation;
|
|
}
|
|
else
|
|
{
|
|
transform.rotation = currentRotation;
|
|
}
|
|
|
|
if (Interpolate)
|
|
{
|
|
m_RotationInterpolator.ResetTo(currentRotation, sentTime);
|
|
}
|
|
}
|
|
|
|
// Add log after to applying the update if AddLogEntry is defined
|
|
if (isSynchronization)
|
|
{
|
|
AddLogEntry(ref newState, NetworkObject.OwnerClientId);
|
|
}
|
|
OnTransformUpdated();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the new state's values to their respective interpolator
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Only non-authoritative instances should invoke this
|
|
/// </remarks>
|
|
private void ApplyUpdatedState(NetworkTransformState newState)
|
|
{
|
|
// Set the transforms's synchronization modes
|
|
InLocalSpace = newState.InLocalSpace;
|
|
Interpolate = newState.UseInterpolation;
|
|
UseQuaternionSynchronization = newState.QuaternionSync;
|
|
UseQuaternionCompression = newState.QuaternionCompression;
|
|
UseHalfFloatPrecision = newState.UseHalfFloatPrecision;
|
|
UseUnreliableDeltas = newState.UseUnreliableDeltas;
|
|
|
|
if (SlerpPosition != newState.UsePositionSlerp)
|
|
{
|
|
SlerpPosition = newState.UsePositionSlerp;
|
|
UpdatePositionSlerp();
|
|
}
|
|
|
|
m_LocalAuthoritativeNetworkState = newState;
|
|
if (m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame)
|
|
{
|
|
ApplyTeleportingState(m_LocalAuthoritativeNetworkState);
|
|
return;
|
|
}
|
|
|
|
var sentTime = newState.SentTime;
|
|
var currentRotation = GetSpaceRelativeRotation();
|
|
var currentEulerAngles = currentRotation.eulerAngles;
|
|
|
|
// Only if using half float precision and our position had changed last update then
|
|
if (UseHalfFloatPrecision && m_LocalAuthoritativeNetworkState.HasPositionChange)
|
|
{
|
|
if (m_LocalAuthoritativeNetworkState.SynchronizeBaseHalfFloat)
|
|
{
|
|
m_HalfPositionState = m_LocalAuthoritativeNetworkState.NetworkDeltaPosition;
|
|
}
|
|
else
|
|
{
|
|
// assure our local NetworkDeltaPosition state is updated
|
|
m_HalfPositionState.HalfVector3.Axis = m_LocalAuthoritativeNetworkState.NetworkDeltaPosition.HalfVector3.Axis;
|
|
m_LocalAuthoritativeNetworkState.NetworkDeltaPosition.CurrentBasePosition = m_HalfPositionState.CurrentBasePosition;
|
|
|
|
// This is to assure when you get the position of the state it is the correct position
|
|
m_LocalAuthoritativeNetworkState.NetworkDeltaPosition.ToVector3(0);
|
|
}
|
|
// Update our target position
|
|
m_TargetPosition = m_HalfPositionState.ToVector3(newState.NetworkTick);
|
|
m_LocalAuthoritativeNetworkState.CurrentPosition = m_TargetPosition;
|
|
}
|
|
|
|
if (!Interpolate)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Apply axial changes from the new state
|
|
// Either apply the delta position target position or the current state's delta position
|
|
// depending upon whether UsePositionDeltaCompression is enabled
|
|
if (m_LocalAuthoritativeNetworkState.HasPositionChange)
|
|
{
|
|
if (!m_LocalAuthoritativeNetworkState.UseHalfFloatPrecision)
|
|
{
|
|
var newTargetPosition = m_TargetPosition;
|
|
if (m_LocalAuthoritativeNetworkState.HasPositionX)
|
|
{
|
|
newTargetPosition.x = m_LocalAuthoritativeNetworkState.PositionX;
|
|
}
|
|
|
|
if (m_LocalAuthoritativeNetworkState.HasPositionY)
|
|
{
|
|
newTargetPosition.y = m_LocalAuthoritativeNetworkState.PositionY;
|
|
}
|
|
|
|
if (m_LocalAuthoritativeNetworkState.HasPositionZ)
|
|
{
|
|
newTargetPosition.z = m_LocalAuthoritativeNetworkState.PositionZ;
|
|
}
|
|
m_TargetPosition = newTargetPosition;
|
|
}
|
|
UpdatePositionInterpolator(m_TargetPosition, sentTime);
|
|
}
|
|
|
|
if (m_LocalAuthoritativeNetworkState.HasScaleChange)
|
|
{
|
|
var currentScale = m_TargetScale;
|
|
if (UseHalfFloatPrecision)
|
|
{
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
if (m_LocalAuthoritativeNetworkState.HasScale(i))
|
|
{
|
|
currentScale[i] = m_LocalAuthoritativeNetworkState.Scale[i];
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (m_LocalAuthoritativeNetworkState.HasScaleX)
|
|
{
|
|
currentScale.x = m_LocalAuthoritativeNetworkState.ScaleX;
|
|
}
|
|
|
|
if (m_LocalAuthoritativeNetworkState.HasScaleY)
|
|
{
|
|
currentScale.y = m_LocalAuthoritativeNetworkState.ScaleY;
|
|
}
|
|
|
|
if (m_LocalAuthoritativeNetworkState.HasScaleZ)
|
|
{
|
|
currentScale.z = m_LocalAuthoritativeNetworkState.ScaleZ;
|
|
}
|
|
}
|
|
m_TargetScale = currentScale;
|
|
m_ScaleInterpolator.AddMeasurement(currentScale, sentTime);
|
|
}
|
|
|
|
// With rotation, we check if there are any changes first and
|
|
// if so then apply the changes to the current Euler rotation
|
|
// values.
|
|
if (m_LocalAuthoritativeNetworkState.HasRotAngleChange)
|
|
{
|
|
if (m_LocalAuthoritativeNetworkState.QuaternionSync)
|
|
{
|
|
currentRotation = m_LocalAuthoritativeNetworkState.Rotation;
|
|
}
|
|
else
|
|
{
|
|
currentEulerAngles = m_TargetRotation;
|
|
// Adjust based on which axis changed
|
|
// (both half precision and full precision apply Eulers to the RotAngle properties when reading the update)
|
|
if (m_LocalAuthoritativeNetworkState.HasRotAngleX)
|
|
{
|
|
currentEulerAngles.x = m_LocalAuthoritativeNetworkState.RotAngleX;
|
|
}
|
|
|
|
if (m_LocalAuthoritativeNetworkState.HasRotAngleY)
|
|
{
|
|
currentEulerAngles.y = m_LocalAuthoritativeNetworkState.RotAngleY;
|
|
}
|
|
|
|
if (m_LocalAuthoritativeNetworkState.HasRotAngleZ)
|
|
{
|
|
currentEulerAngles.z = m_LocalAuthoritativeNetworkState.RotAngleZ;
|
|
}
|
|
m_TargetRotation = currentEulerAngles;
|
|
currentRotation.eulerAngles = currentEulerAngles;
|
|
}
|
|
|
|
m_RotationInterpolator.AddMeasurement(currentRotation, sentTime);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked on the non-authoritative side when the NetworkTransformState has been updated
|
|
/// </summary>
|
|
/// <param name="oldState">the previous <see cref="NetworkTransformState"/></param>
|
|
/// <param name="newState">the new <see cref="NetworkTransformState"/></param>
|
|
protected virtual void OnNetworkTransformStateUpdated(ref NetworkTransformState oldState, ref NetworkTransformState newState)
|
|
{
|
|
|
|
}
|
|
|
|
protected virtual void OnBeforeUpdateTransformState()
|
|
{
|
|
|
|
}
|
|
|
|
private NetworkTransformState m_OldState = new NetworkTransformState();
|
|
|
|
/// <summary>
|
|
/// Only non-authoritative instances should invoke this method
|
|
/// </summary>
|
|
private void OnNetworkStateChanged(NetworkTransformState oldState, NetworkTransformState newState)
|
|
{
|
|
if (!NetworkObject.IsSpawned || CanCommitToTransform)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// If we are using UseUnreliableDeltas and our old state tick is greater than the new state tick,
|
|
// then just ignore the newstate. This avoids any scenario where the new state is out of order
|
|
// from the old state (with unreliable traffic and/or mixed unreliable and reliable)
|
|
if (UseUnreliableDeltas && oldState.NetworkTick > newState.NetworkTick && !newState.IsTeleportingNextFrame && !newState.UnreliableFrameSync)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get the time when this new state was sent
|
|
newState.SentTime = new NetworkTime(m_CachedNetworkManager.NetworkConfig.TickRate, newState.NetworkTick).Time;
|
|
|
|
OnBeforeUpdateTransformState();
|
|
|
|
// Apply the new state
|
|
ApplyUpdatedState(newState);
|
|
|
|
// Provide notifications when the state has been updated
|
|
// We use the m_LocalAuthoritativeNetworkState because newState has been applied and adjustments could have
|
|
// been made (i.e. half float precision position values will have been updated)
|
|
OnNetworkTransformStateUpdated(ref oldState, ref m_LocalAuthoritativeNetworkState);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Will set the maximum interpolation boundary for the interpolators of this <see cref="NetworkTransform"/> instance.
|
|
/// This value roughly translates to the maximum value of 't' in <see cref="Vector3.Lerp(Vector3, Vector3, float)"/> and
|
|
/// <see cref="Quaternion.Lerp(Quaternion, Quaternion, float)"/> for all transform elements being monitored by
|
|
/// <see cref="NetworkTransform"/> (i.e. Position, Scale, and Rotation)
|
|
/// </summary>
|
|
/// <param name="maxInterpolationBound">Maximum time boundary that can be used in a frame when interpolating between two values</param>
|
|
public void SetMaxInterpolationBound(float maxInterpolationBound)
|
|
{
|
|
m_RotationInterpolator.MaxInterpolationBound = maxInterpolationBound;
|
|
m_PositionInterpolator.MaxInterpolationBound = maxInterpolationBound;
|
|
m_ScaleInterpolator.MaxInterpolationBound = maxInterpolationBound;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create interpolators when first instantiated to avoid memory allocations if the
|
|
/// associated NetworkObject persists (i.e. despawned but not destroyed or pools)
|
|
/// </summary>
|
|
protected virtual void Awake()
|
|
{
|
|
// Rotation is a single Quaternion since each Euler axis will affect the quaternion's final value
|
|
m_RotationInterpolator = new BufferedLinearInterpolatorQuaternion();
|
|
m_PositionInterpolator = new BufferedLinearInterpolatorVector3();
|
|
m_ScaleInterpolator = new BufferedLinearInterpolatorVector3();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks for changes in the axis to synchronize. If one or more did change it
|
|
/// then determines if the axis were enabled and if the delta between the last known
|
|
/// delta position and the current position for the axis exceeds the adjustment range
|
|
/// before it is collapsed into the base position.
|
|
/// If it does exceed the adjustment range, then we have to teleport the object so
|
|
/// a full position synchronization takes place and the NetworkDeltaPosition is
|
|
/// reset with the updated base position that it then will generating a new delta position from.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This only happens if a user disables an axis, continues to update the disabled axis,
|
|
/// and then later enables the axis. (which will not be a recommended best practice)
|
|
/// </remarks>
|
|
private void AxisChangedDeltaPositionCheck()
|
|
{
|
|
if (UseHalfFloatPrecision && SynchronizePosition)
|
|
{
|
|
var synAxis = m_HalfPositionState.HalfVector3.AxisToSynchronize;
|
|
if (SyncPositionX != synAxis.x || SyncPositionY != synAxis.y || SyncPositionZ != synAxis.z)
|
|
{
|
|
var positionState = m_HalfPositionState.GetFullPosition();
|
|
var relativePosition = GetSpaceRelativePosition();
|
|
bool needsToTeleport = false;
|
|
// Only if the synchronization of an axis is turned on do we need to
|
|
// check if a teleport is required due to the delta from the last known
|
|
// to the currently known axis value exceeds MaxDeltaBeforeAdjustment.
|
|
if (SyncPositionX && SyncPositionX != synAxis.x)
|
|
{
|
|
needsToTeleport = Mathf.Abs(relativePosition.x - positionState.x) >= NetworkDeltaPosition.MaxDeltaBeforeAdjustment;
|
|
}
|
|
if (SyncPositionY && SyncPositionY != synAxis.y)
|
|
{
|
|
needsToTeleport = Mathf.Abs(relativePosition.y - positionState.y) >= NetworkDeltaPosition.MaxDeltaBeforeAdjustment;
|
|
}
|
|
if (SyncPositionZ && SyncPositionZ != synAxis.z)
|
|
{
|
|
needsToTeleport = Mathf.Abs(relativePosition.z - positionState.z) >= NetworkDeltaPosition.MaxDeltaBeforeAdjustment;
|
|
}
|
|
// If needed, force a teleport as the delta is outside of the valid delta boundary
|
|
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = needsToTeleport;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called by authority to check for deltas and update non-authoritative instances
|
|
/// if any are found.
|
|
/// </summary>
|
|
internal void OnUpdateAuthoritativeState(ref Transform transformSource)
|
|
{
|
|
// If our replicated state is not dirty and our local authority state is dirty, clear it.
|
|
if (!m_LocalAuthoritativeNetworkState.ExplicitSet && m_LocalAuthoritativeNetworkState.IsDirty && !m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame)
|
|
{
|
|
// Now clear our bitset and prepare for next network tick state update
|
|
m_LocalAuthoritativeNetworkState.ClearBitSetForNextTick();
|
|
if (TrackByStateId)
|
|
{
|
|
m_LocalAuthoritativeNetworkState.TrackByStateId = true;
|
|
m_LocalAuthoritativeNetworkState.StateId++;
|
|
}
|
|
else
|
|
{
|
|
m_LocalAuthoritativeNetworkState.TrackByStateId = false;
|
|
}
|
|
}
|
|
|
|
AxisChangedDeltaPositionCheck();
|
|
|
|
TryCommitTransform(ref transformSource);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Authority subscribes to network tick events and will invoke
|
|
/// <see cref="OnUpdateAuthoritativeState(ref Transform)"/> each network tick.
|
|
/// </summary>
|
|
private void NetworkTickSystem_Tick()
|
|
{
|
|
// As long as we are still authority
|
|
if (CanCommitToTransform)
|
|
{
|
|
// Update any changes to the transform
|
|
var transformSource = transform;
|
|
OnUpdateAuthoritativeState(ref transformSource);
|
|
|
|
m_CurrentPosition = GetSpaceRelativePosition();
|
|
m_TargetPosition = GetSpaceRelativePosition();
|
|
}
|
|
else // If we are no longer authority, unsubscribe to the tick event
|
|
if (NetworkManager != null && NetworkManager.NetworkTickSystem != null)
|
|
{
|
|
NetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override void OnNetworkSpawn()
|
|
{
|
|
///////////////////////////////////////////////////////////////
|
|
// NOTE: Legacy and no longer used (candidates for deprecation)
|
|
m_CachedIsServer = IsServer;
|
|
///////////////////////////////////////////////////////////////
|
|
|
|
// Started using this again to avoid the getter processing cost of NetworkBehaviour.NetworkManager
|
|
m_CachedNetworkManager = NetworkManager;
|
|
|
|
Initialize();
|
|
|
|
if (CanCommitToTransform && UseHalfFloatPrecision)
|
|
{
|
|
SetState(GetSpaceRelativePosition(), GetSpaceRelativeRotation(), GetScale(), false);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override void OnNetworkDespawn()
|
|
{
|
|
DeregisterForTickUpdate(this);
|
|
|
|
CanCommitToTransform = false;
|
|
if (NetworkManager != null && NetworkManager.NetworkTickSystem != null)
|
|
{
|
|
NetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override void OnDestroy()
|
|
{
|
|
// During destroy, use NetworkBehaviour.NetworkManager as opposed to m_CachedNetworkManager
|
|
if (NetworkManager != null && NetworkManager.NetworkTickSystem != null)
|
|
{
|
|
NetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick;
|
|
}
|
|
CanCommitToTransform = false;
|
|
base.OnDestroy();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override void OnLostOwnership()
|
|
{
|
|
base.OnLostOwnership();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override void OnGainedOwnership()
|
|
{
|
|
base.OnGainedOwnership();
|
|
}
|
|
|
|
protected override void OnOwnershipChanged(ulong previous, ulong current)
|
|
{
|
|
// If we were the previous owner or the newly assigned owner then reinitialize
|
|
if (current == m_CachedNetworkManager.LocalClientId || previous == m_CachedNetworkManager.LocalClientId)
|
|
{
|
|
InternalInitialization(true);
|
|
}
|
|
base.OnOwnershipChanged(previous, current);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked when first spawned and when ownership changes.
|
|
/// </summary>
|
|
/// <param name="replicatedState">the current <see cref="NetworkTransformState"/> after initializing</param>
|
|
protected virtual void OnInitialize(ref NetworkTransformState replicatedState)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// An owner read and owner write NetworkVariable so it doesn't generate any messages
|
|
/// </summary>
|
|
private NetworkVariable<NetworkTransformState> m_InternalStatNetVar = new NetworkVariable<NetworkTransformState>(default, NetworkVariableReadPermission.Owner, NetworkVariableWritePermission.Owner);
|
|
/// <summary>
|
|
/// This method is only invoked by the owner
|
|
/// Use: OnInitialize(ref NetworkTransformState replicatedState) to be notified on all instances
|
|
/// </summary>
|
|
/// <param name="replicatedState"></param>
|
|
protected virtual void OnInitialize(ref NetworkVariable<NetworkTransformState> replicatedState)
|
|
{
|
|
|
|
}
|
|
|
|
private int m_HalfFloatTargetTickOwnership;
|
|
|
|
/// <summary>
|
|
/// The internal initialzation method to allow for internal API adjustments
|
|
/// </summary>
|
|
/// <param name="isOwnershipChange"></param>
|
|
private void InternalInitialization(bool isOwnershipChange = false)
|
|
{
|
|
if (!IsSpawned)
|
|
{
|
|
return;
|
|
}
|
|
|
|
CanCommitToTransform = IsServerAuthoritative() ? IsServer : IsOwner;
|
|
var currentPosition = GetSpaceRelativePosition();
|
|
var currentRotation = GetSpaceRelativeRotation();
|
|
|
|
if (CanCommitToTransform)
|
|
{
|
|
if (UseHalfFloatPrecision)
|
|
{
|
|
m_HalfPositionState = new NetworkDeltaPosition(currentPosition, m_CachedNetworkManager.ServerTime.Tick, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ));
|
|
}
|
|
m_CurrentPosition = currentPosition;
|
|
m_TargetPosition = currentPosition;
|
|
|
|
RegisterForTickUpdate(this);
|
|
|
|
m_LocalAuthoritativeNetworkState.SynchronizeBaseHalfFloat = false;
|
|
if (UseHalfFloatPrecision && isOwnershipChange && !IsServerAuthoritative() && Interpolate)
|
|
{
|
|
m_HalfFloatTargetTickOwnership = m_CachedNetworkManager.ServerTime.Tick;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Remove this instance from the tick update
|
|
DeregisterForTickUpdate(this);
|
|
|
|
ResetInterpolatedStateToCurrentAuthoritativeState();
|
|
m_LocalAuthoritativeNetworkState.SynchronizeBaseHalfFloat = false;
|
|
m_CurrentPosition = currentPosition;
|
|
m_TargetPosition = currentPosition;
|
|
m_CurrentScale = transform.localScale;
|
|
m_TargetScale = transform.localScale;
|
|
m_CurrentRotation = currentRotation;
|
|
m_TargetRotation = currentRotation.eulerAngles;
|
|
|
|
}
|
|
OnInitialize(ref m_LocalAuthoritativeNetworkState);
|
|
|
|
if (IsOwner)
|
|
{
|
|
m_InternalStatNetVar.Value = m_LocalAuthoritativeNetworkState;
|
|
OnInitialize(ref m_InternalStatNetVar);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes NetworkTransform when spawned and ownership changes.
|
|
/// </summary>
|
|
protected void Initialize()
|
|
{
|
|
InternalInitialization();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
/// <remarks>
|
|
/// When a parent changes, non-authoritative instances should:
|
|
/// - Apply the resultant position, rotation, and scale from the parenting action.
|
|
/// - Clear interpolators (even if not enabled on this frame)
|
|
/// - Reset the interpolators to the position, rotation, and scale resultant values.
|
|
/// This prevents interpolation visual anomalies and issues during initial synchronization
|
|
/// </remarks>
|
|
public override void OnNetworkObjectParentChanged(NetworkObject parentNetworkObject)
|
|
{
|
|
// Only if we are not authority
|
|
if (!CanCommitToTransform)
|
|
{
|
|
m_TargetPosition = m_CurrentPosition = GetSpaceRelativePosition();
|
|
m_CurrentRotation = GetSpaceRelativeRotation();
|
|
m_TargetRotation = m_CurrentRotation.eulerAngles;
|
|
m_TargetScale = m_CurrentScale = GetScale();
|
|
|
|
if (Interpolate)
|
|
{
|
|
m_ScaleInterpolator.Clear();
|
|
m_PositionInterpolator.Clear();
|
|
m_RotationInterpolator.Clear();
|
|
|
|
// Always use NetworkManager here as this can be invoked prior to spawning
|
|
var tempTime = new NetworkTime(NetworkManager.NetworkConfig.TickRate, NetworkManager.ServerTime.Tick).Time;
|
|
UpdatePositionInterpolator(m_CurrentPosition, tempTime, true);
|
|
m_ScaleInterpolator.ResetTo(m_CurrentScale, tempTime);
|
|
m_RotationInterpolator.ResetTo(m_CurrentRotation, tempTime);
|
|
}
|
|
}
|
|
base.OnNetworkObjectParentChanged(parentNetworkObject);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Directly sets a state on the authoritative transform.
|
|
/// Owner clients can directly set the state on a server authoritative transform
|
|
/// This will override any changes made previously to the transform
|
|
/// This isn't resistant to network jitter. Server side changes due to this method won't be interpolated.
|
|
/// The parameters are broken up into pos / rot / scale on purpose so that the caller can perturb
|
|
/// just the desired one(s)
|
|
/// </summary>
|
|
/// <param name="posIn"></param> new position to move to. Can be null
|
|
/// <param name="rotIn"></param> new rotation to rotate to. Can be null
|
|
/// <param name="scaleIn">new scale to scale to. Can be null</param>
|
|
/// <param name="teleportDisabled">When true (the default) the <see cref="NetworkObject"/> will not be teleported and, if enabled, will interpolate. When false the <see cref="NetworkObject"/> will teleport/apply the parameters provided immediately.</param>
|
|
/// <exception cref="Exception"></exception>
|
|
public void SetState(Vector3? posIn = null, Quaternion? rotIn = null, Vector3? scaleIn = null, bool teleportDisabled = true)
|
|
{
|
|
if (!IsSpawned)
|
|
{
|
|
NetworkLog.LogError($"Cannot commit transform when not spawned!");
|
|
return;
|
|
}
|
|
|
|
// Only the server or the owner is allowed to commit a transform
|
|
if (!IsServer && !IsOwner)
|
|
{
|
|
var errorMessage = gameObject != NetworkObject.gameObject ?
|
|
$"Non-authority instance of {NetworkObject.gameObject.name} is trying to commit a transform on {gameObject.name}!" :
|
|
$"Non-authority instance of {NetworkObject.gameObject.name} is trying to commit a transform!";
|
|
NetworkLog.LogError(errorMessage);
|
|
return;
|
|
}
|
|
|
|
Vector3 pos = posIn == null ? GetSpaceRelativePosition() : posIn.Value;
|
|
Quaternion rot = rotIn == null ? GetSpaceRelativeRotation() : rotIn.Value;
|
|
Vector3 scale = scaleIn == null ? transform.localScale : scaleIn.Value;
|
|
|
|
if (!CanCommitToTransform)
|
|
{
|
|
// Preserving the ability for owner authoritative mode to accept state changes from server
|
|
if (IsServer)
|
|
{
|
|
m_ClientIds[0] = OwnerClientId;
|
|
m_ClientRpcParams.Send.TargetClientIds = m_ClientIds;
|
|
SetStateClientRpc(pos, rot, scale, !teleportDisabled, m_ClientRpcParams);
|
|
}
|
|
else // Preserving the ability for server authoritative mode to accept state changes from owner
|
|
{
|
|
SetStateServerRpc(pos, rot, scale, !teleportDisabled);
|
|
}
|
|
return;
|
|
}
|
|
|
|
SetStateInternal(pos, rot, scale, !teleportDisabled);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Authoritative only method
|
|
/// Sets the internal state (teleporting or just set state) of the authoritative
|
|
/// transform directly.
|
|
/// </summary>
|
|
private void SetStateInternal(Vector3 pos, Quaternion rot, Vector3 scale, bool shouldTeleport)
|
|
{
|
|
if (InLocalSpace)
|
|
{
|
|
transform.localPosition = pos;
|
|
transform.localRotation = rot;
|
|
}
|
|
else
|
|
{
|
|
transform.SetPositionAndRotation(pos, rot);
|
|
}
|
|
transform.localScale = scale;
|
|
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = shouldTeleport;
|
|
|
|
var transformToCommit = transform;
|
|
|
|
// Explicit set states are cumulative during a fractional tick period of time (i.e. each SetState invocation will
|
|
// update the axial deltas to whatever changes are applied). As such, we need to preserve the dirty and explicit
|
|
// state flags.
|
|
var stateWasDirty = m_LocalAuthoritativeNetworkState.IsDirty;
|
|
var explicitState = m_LocalAuthoritativeNetworkState.ExplicitSet;
|
|
|
|
// Apply any delta states to the m_LocalAuthoritativeNetworkState
|
|
var isDirty = ApplyTransformToNetworkStateWithInfo(ref m_LocalAuthoritativeNetworkState, ref transformToCommit);
|
|
|
|
// If we were dirty and the explicit state was set (prior to checking for deltas) or the current explicit state is dirty,
|
|
// then we set the explicit state flag.
|
|
m_LocalAuthoritativeNetworkState.ExplicitSet = (stateWasDirty && explicitState) || isDirty;
|
|
|
|
// If the current explicit set flag is set, then we are dirty. This assures if more than one explicit set state is invoked
|
|
// in between a fractional tick period and the current explicit set state did not find any deltas that we preserve any
|
|
// previous dirty state.
|
|
m_LocalAuthoritativeNetworkState.IsDirty = m_LocalAuthoritativeNetworkState.ExplicitSet;
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked by <see cref="SetState"/>, allows a non-owner server to update the transform state
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Continued support for client-driven server authority model
|
|
/// </remarks>
|
|
[ClientRpc]
|
|
private void SetStateClientRpc(Vector3 pos, Quaternion rot, Vector3 scale, bool shouldTeleport, ClientRpcParams clientRpcParams = default)
|
|
{
|
|
// Server dictated state is always applied
|
|
SetStateInternal(pos, rot, scale, shouldTeleport);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked by <see cref="SetState"/>, allows an owner-client update the transform state
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Continued support for client-driven server authority model
|
|
/// </remarks>
|
|
[ServerRpc]
|
|
private void SetStateServerRpc(Vector3 pos, Quaternion rot, Vector3 scale, bool shouldTeleport)
|
|
{
|
|
// server has received this RPC request to move change transform. give the server a chance to modify or even reject the move
|
|
if (OnClientRequestChange != null)
|
|
{
|
|
(pos, rot, scale) = OnClientRequestChange(pos, rot, scale);
|
|
}
|
|
SetStateInternal(pos, rot, scale, shouldTeleport);
|
|
}
|
|
|
|
|
|
private void UpdateInterpolation()
|
|
{
|
|
// Non-Authority
|
|
if (Interpolate)
|
|
{
|
|
var serverTime = m_CachedNetworkManager.ServerTime;
|
|
var cachedDeltaTime = m_CachedNetworkManager.RealTimeProvider.DeltaTime;
|
|
var cachedServerTime = serverTime.Time;
|
|
|
|
// With owner authoritative mode, non-authority clients can lag behind
|
|
// by more than 1 tick period of time. The current "solution" for now
|
|
// is to make their cachedRenderTime run 2 ticks behind.
|
|
var ticksAgo = !IsServerAuthoritative() && !IsServer ? 2 : 1;
|
|
var cachedRenderTime = serverTime.TimeTicksAgo(ticksAgo).Time;
|
|
|
|
// Now only update the interpolators for the portions of the transform being synchronized
|
|
if (SynchronizePosition)
|
|
{
|
|
m_PositionInterpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime);
|
|
}
|
|
|
|
if (SynchronizeRotation)
|
|
{
|
|
// When using half precision Lerp towards the target rotation.
|
|
// When using full precision Slerp towards the target rotation.
|
|
/// <see cref="BufferedLinearInterpolatorQuaternion.IsSlerp"/>
|
|
m_RotationInterpolator.IsSlerp = !UseHalfFloatPrecision;
|
|
m_RotationInterpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime);
|
|
}
|
|
|
|
if (SynchronizeScale)
|
|
{
|
|
m_ScaleInterpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
/// <remarks>
|
|
/// If you override this method, be sure that:
|
|
/// - Non-authority always invokes this base class method.
|
|
/// </remarks>
|
|
protected virtual void Update()
|
|
{
|
|
// If not spawned or this instance has authority, exit early
|
|
if (!IsSpawned || CanCommitToTransform)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Non-Authority
|
|
UpdateInterpolation();
|
|
|
|
// Apply the current authoritative state
|
|
ApplyAuthoritativeState();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Teleport the transform to the given values without interpolating
|
|
/// </summary>
|
|
/// <param name="newPosition"></param> new position to move to.
|
|
/// <param name="newRotation"></param> new rotation to rotate to.
|
|
/// <param name="newScale">new scale to scale to.</param>
|
|
/// <exception cref="Exception"></exception>
|
|
public void Teleport(Vector3 newPosition, Quaternion newRotation, Vector3 newScale)
|
|
{
|
|
if (!CanCommitToTransform)
|
|
{
|
|
throw new Exception("Teleporting on non-authoritative side is not allowed!");
|
|
}
|
|
|
|
// Teleporting now is as simple as setting the internal state and passing the teleport flag
|
|
SetStateInternal(newPosition, newRotation, newScale, true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Override this method and return false to switch to owner authoritative mode
|
|
/// </summary>
|
|
/// <returns>(<see cref="true"/> or <see cref="false"/>) where when false it runs as owner-client authoritative</returns>
|
|
protected virtual bool OnIsServerAuthoritative()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Method to determine if this <see cref="NetworkTransform"/> instance is owner or server authoritative.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Used by <see cref="NetworkRigidbody"/> to determines if this is server or owner authoritative.
|
|
/// </remarks>
|
|
/// <returns><see cref="true"/> or <see cref="false"/></returns>
|
|
public bool IsServerAuthoritative()
|
|
{
|
|
return OnIsServerAuthoritative();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked by <see cref="NetworkTransformMessage"/> to update the transform state
|
|
/// </summary>
|
|
/// <param name="networkTransformState"></param>
|
|
internal void TransformStateUpdate(ref NetworkTransformState networkTransformState)
|
|
{
|
|
// Store the previous/old state
|
|
m_OldState = m_LocalAuthoritativeNetworkState;
|
|
|
|
// Assign the new incoming state
|
|
m_LocalAuthoritativeNetworkState = networkTransformState;
|
|
|
|
// Apply the state update
|
|
OnNetworkStateChanged(m_OldState, m_LocalAuthoritativeNetworkState);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked by the authoritative instance to sends a <see cref="NetworkTransformMessage"/> containing the <see cref="NetworkTransformState"/>
|
|
/// </summary>
|
|
private void UpdateTransformState()
|
|
{
|
|
if (m_CachedNetworkManager.ShutdownInProgress)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool isServerAuthoritative = OnIsServerAuthoritative();
|
|
if (isServerAuthoritative && !IsServer)
|
|
{
|
|
Debug.LogError($"Server authoritative {nameof(NetworkTransform)} can only be updated by the server!");
|
|
}
|
|
else if (!isServerAuthoritative && !IsServer && !IsOwner)
|
|
{
|
|
Debug.LogError($"Owner authoritative {nameof(NetworkTransform)} can only be updated by the owner!");
|
|
}
|
|
var customMessageManager = m_CachedNetworkManager.CustomMessagingManager;
|
|
|
|
var networkTransformMessage = new NetworkTransformMessage()
|
|
{
|
|
NetworkObjectId = NetworkObjectId,
|
|
NetworkBehaviourId = NetworkBehaviourId,
|
|
State = m_LocalAuthoritativeNetworkState
|
|
};
|
|
|
|
// Determine what network delivery method to use:
|
|
// When to send reliable packets:
|
|
// - If UsUnrealiable is not enabled
|
|
// - If teleporting or synchronizing
|
|
// - If sending an UnrealiableFrameSync or synchronizing the base position of the NetworkDeltaPosition
|
|
var networkDelivery = !UseUnreliableDeltas | m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame | m_LocalAuthoritativeNetworkState.IsSynchronizing
|
|
| m_LocalAuthoritativeNetworkState.UnreliableFrameSync | m_LocalAuthoritativeNetworkState.SynchronizeBaseHalfFloat
|
|
? NetworkDelivery.ReliableSequenced : NetworkDelivery.UnreliableSequenced;
|
|
|
|
// Server-host always sends updates to all clients (but itself)
|
|
if (IsServer)
|
|
{
|
|
var clientCount = m_CachedNetworkManager.ConnectionManager.ConnectedClientsList.Count;
|
|
for (int i = 0; i < clientCount; i++)
|
|
{
|
|
var clientId = m_CachedNetworkManager.ConnectionManager.ConnectedClientsList[i].ClientId;
|
|
if (NetworkManager.ServerClientId == clientId)
|
|
{
|
|
continue;
|
|
}
|
|
if (!NetworkObject.Observers.Contains(clientId))
|
|
{
|
|
continue;
|
|
}
|
|
NetworkManager.MessageManager.SendMessage(ref networkTransformMessage, networkDelivery, clientId);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Clients (owner authoritative) send messages to the server-host
|
|
NetworkManager.MessageManager.SendMessage(ref networkTransformMessage, networkDelivery, NetworkManager.ServerClientId);
|
|
}
|
|
}
|
|
|
|
#region Network Tick Registration and Handling
|
|
private static Dictionary<NetworkManager, NetworkTransformTickRegistration> s_NetworkTickRegistration = new Dictionary<NetworkManager, NetworkTransformTickRegistration>();
|
|
|
|
private static void RemoveTickUpdate(NetworkManager networkManager)
|
|
{
|
|
s_NetworkTickRegistration.Remove(networkManager);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Having the tick update once and cycling through registered instances to update is evidently less processor
|
|
/// intensive than having each instance subscribe and update individually.
|
|
/// </summary>
|
|
private class NetworkTransformTickRegistration
|
|
{
|
|
private Action m_NetworkTickUpdate;
|
|
private NetworkManager m_NetworkManager;
|
|
public HashSet<NetworkTransform> NetworkTransforms = new HashSet<NetworkTransform>();
|
|
|
|
private int m_LastTick;
|
|
private void OnNetworkManagerStopped(bool value)
|
|
{
|
|
Remove();
|
|
}
|
|
|
|
public void Remove()
|
|
{
|
|
m_NetworkManager.NetworkTickSystem.Tick -= m_NetworkTickUpdate;
|
|
m_NetworkTickUpdate = null;
|
|
NetworkTransforms.Clear();
|
|
RemoveTickUpdate(m_NetworkManager);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked once per network tick, this will update any registered
|
|
/// authority instances.
|
|
/// </summary>
|
|
private void TickUpdate()
|
|
{
|
|
// TODO FIX: The local NetworkTickSystem can invoke with the same network tick as before
|
|
if (m_NetworkManager.ServerTime.Tick <= m_LastTick)
|
|
{
|
|
return;
|
|
}
|
|
foreach (var networkTransform in NetworkTransforms)
|
|
{
|
|
if (networkTransform.IsSpawned)
|
|
{
|
|
networkTransform.NetworkTickSystem_Tick();
|
|
}
|
|
}
|
|
m_LastTick = m_NetworkManager.ServerTime.Tick;
|
|
}
|
|
|
|
public NetworkTransformTickRegistration(NetworkManager networkManager)
|
|
{
|
|
m_NetworkManager = networkManager;
|
|
m_NetworkTickUpdate = new Action(TickUpdate);
|
|
networkManager.NetworkTickSystem.Tick += m_NetworkTickUpdate;
|
|
if (networkManager.IsServer)
|
|
{
|
|
networkManager.OnServerStopped += OnNetworkManagerStopped;
|
|
}
|
|
else
|
|
{
|
|
networkManager.OnClientStopped += OnNetworkManagerStopped;
|
|
}
|
|
}
|
|
}
|
|
private static int s_TickSynchPosition;
|
|
private int m_NextTickSync;
|
|
|
|
internal void RegisterForTickSynchronization()
|
|
{
|
|
s_TickSynchPosition++;
|
|
m_NextTickSync = NetworkManager.ServerTime.Tick + (s_TickSynchPosition % (int)NetworkManager.NetworkConfig.TickRate);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Will register the NetworkTransform instance for the single tick update entry point.
|
|
/// If a NetworkTransformTickRegistration has not yet been registered for the NetworkManager
|
|
/// instance, then create an entry.
|
|
/// </summary>
|
|
/// <param name="networkTransform"></param>
|
|
private static void RegisterForTickUpdate(NetworkTransform networkTransform)
|
|
{
|
|
if (!s_NetworkTickRegistration.ContainsKey(networkTransform.NetworkManager))
|
|
{
|
|
s_NetworkTickRegistration.Add(networkTransform.NetworkManager, new NetworkTransformTickRegistration(networkTransform.NetworkManager));
|
|
}
|
|
networkTransform.RegisterForTickSynchronization();
|
|
s_NetworkTickRegistration[networkTransform.NetworkManager].NetworkTransforms.Add(networkTransform);
|
|
}
|
|
|
|
/// <summary>
|
|
/// If a NetworkTransformTickRegistration exists for the NetworkManager instance, then this will
|
|
/// remove the NetworkTransform instance from the single tick update entry point.
|
|
/// </summary>
|
|
/// <param name="networkTransform"></param>
|
|
private static void DeregisterForTickUpdate(NetworkTransform networkTransform)
|
|
{
|
|
if (s_NetworkTickRegistration.ContainsKey(networkTransform.NetworkManager))
|
|
{
|
|
s_NetworkTickRegistration[networkTransform.NetworkManager].NetworkTransforms.Remove(networkTransform);
|
|
if (s_NetworkTickRegistration[networkTransform.NetworkManager].NetworkTransforms.Count == 0)
|
|
{
|
|
var registrationEntry = s_NetworkTickRegistration[networkTransform.NetworkManager];
|
|
registrationEntry.Remove();
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
internal interface INetworkTransformLogStateEntry
|
|
{
|
|
void AddLogEntry(NetworkTransform.NetworkTransformState networkTransformState, ulong targetClient, bool preUpdate = false);
|
|
}
|
|
}
|