com.unity.netcode.gameobjects@1.0.2
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.0.2] - 2022-09-12 - Fixed issue where `NetworkTransform` was not honoring the InLocalSpace property on the authority side during OnNetworkSpawn. (#2170) - Fixed issue where `NetworkTransform` was not ending extrapolation for the previous state causing non-authoritative instances to become out of synch. (#2170) - Fixed issue where `NetworkTransform` was not continuing to interpolate for the remainder of the associated tick period. (#2170) - Fixed issue during `NetworkTransform.OnNetworkSpawn` for non-authoritative instances where it was initializing interpolators with the replicated network state which now only contains the transform deltas that occurred during a network tick and not the entire transform state. (#2170)
This commit is contained in:
10
CHANGELOG.md
10
CHANGELOG.md
@@ -7,6 +7,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
|||||||
|
|
||||||
Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com).
|
Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com).
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.2] - 2022-09-12
|
||||||
|
|
||||||
|
- Fixed issue where `NetworkTransform` was not honoring the InLocalSpace property on the authority side during OnNetworkSpawn. (#2170)
|
||||||
|
- Fixed issue where `NetworkTransform` was not ending extrapolation for the previous state causing non-authoritative instances to become out of synch. (#2170)
|
||||||
|
- Fixed issue where `NetworkTransform` was not continuing to interpolate for the remainder of the associated tick period. (#2170)
|
||||||
|
- Fixed issue during `NetworkTransform.OnNetworkSpawn` for non-authoritative instances where it was initializing interpolators with the replicated network state which now only contains the transform deltas that occurred during a network tick and not the entire transform state. (#2170)
|
||||||
|
|
||||||
## [1.0.1] - 2022-08-23
|
## [1.0.1] - 2022-08-23
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@@ -14,7 +22,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
|
|||||||
- Changed version to 1.0.1. (#2131)
|
- Changed version to 1.0.1. (#2131)
|
||||||
- Updated dependency on `com.unity.transport` to 1.2.0. (#2129)
|
- Updated dependency on `com.unity.transport` to 1.2.0. (#2129)
|
||||||
- When using `UnityTransport`, _reliable_ payloads are now allowed to exceed the configured 'Max Payload Size'. Unreliable payloads remain bounded by this setting. (#2081)
|
- When using `UnityTransport`, _reliable_ payloads are now allowed to exceed the configured 'Max Payload Size'. Unreliable payloads remain bounded by this setting. (#2081)
|
||||||
- Preformance improvements for cases with large number of NetworkObjects, by not iterating over all unchanged NetworkObjects
|
- Performance improvements for cases with large number of NetworkObjects, by not iterating over all unchanged NetworkObjects
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -248,6 +248,8 @@ namespace Unity.Netcode
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Part the of reason for disabling extrapolation is how we add and use measurements over time.
|
||||||
|
// TODO: Add detailed description of this area in Jira ticket
|
||||||
if (sentTime > m_EndTimeConsumed || m_LifetimeConsumedCount == 0) // treat only if value is newer than the one being interpolated to right now
|
if (sentTime > m_EndTimeConsumed || m_LifetimeConsumedCount == 0) // treat only if value is newer than the one being interpolated to right now
|
||||||
{
|
{
|
||||||
m_LastBufferedItemReceived = new BufferedItem(newMeasurement, sentTime);
|
m_LastBufferedItemReceived = new BufferedItem(newMeasurement, sentTime);
|
||||||
@@ -292,7 +294,9 @@ namespace Unity.Netcode
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override float InterpolateUnclamped(float start, float end, float time)
|
protected override float InterpolateUnclamped(float start, float end, float time)
|
||||||
{
|
{
|
||||||
return Mathf.LerpUnclamped(start, end, time);
|
// Disabling Extrapolation:
|
||||||
|
// TODO: Add Jira Ticket
|
||||||
|
return Mathf.Lerp(start, end, time);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -311,13 +315,17 @@ namespace Unity.Netcode
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Quaternion InterpolateUnclamped(Quaternion start, Quaternion end, float time)
|
protected override Quaternion InterpolateUnclamped(Quaternion start, Quaternion end, float time)
|
||||||
{
|
{
|
||||||
return Quaternion.SlerpUnclamped(start, end, time);
|
// Disabling Extrapolation:
|
||||||
|
// TODO: Add Jira Ticket
|
||||||
|
return Quaternion.Slerp(start, end, time);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Quaternion Interpolate(Quaternion start, Quaternion end, float time)
|
protected override Quaternion Interpolate(Quaternion start, Quaternion end, float time)
|
||||||
{
|
{
|
||||||
return Quaternion.SlerpUnclamped(start, end, time);
|
// Disabling Extrapolation:
|
||||||
|
// TODO: Add Jira Ticket
|
||||||
|
return Quaternion.Slerp(start, end, time);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,8 +207,13 @@ namespace Unity.Netcode.Components
|
|||||||
internal float ScaleX, ScaleY, ScaleZ;
|
internal float ScaleX, ScaleY, ScaleZ;
|
||||||
internal double SentTime;
|
internal double SentTime;
|
||||||
|
|
||||||
|
// Authoritative and non-authoritative sides use this to determine if a NetworkTransformState is
|
||||||
|
// dirty or not.
|
||||||
internal bool IsDirty;
|
internal bool IsDirty;
|
||||||
|
|
||||||
|
// Non-Authoritative side uses this for ending extrapolation of the last applied state
|
||||||
|
internal int EndExtrapolationTick;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This will reset the NetworkTransform BitSet
|
/// This will reset the NetworkTransform BitSet
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -301,6 +306,15 @@ namespace Unity.Netcode.Components
|
|||||||
/// Whether or not z component of position will be replicated
|
/// Whether or not z component of position will be replicated
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool SyncPositionZ = true;
|
public bool SyncPositionZ = true;
|
||||||
|
|
||||||
|
private bool SynchronizePosition
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return SyncPositionX || SyncPositionY || SyncPositionZ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether or not x component of rotation will be replicated
|
/// Whether or not x component of rotation will be replicated
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -313,6 +327,15 @@ namespace Unity.Netcode.Components
|
|||||||
/// Whether or not z component of rotation will be replicated
|
/// Whether or not z component of rotation will be replicated
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool SyncRotAngleZ = true;
|
public bool SyncRotAngleZ = true;
|
||||||
|
|
||||||
|
private bool SynchronizeRotation
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return SyncRotAngleX || SyncRotAngleY || SyncRotAngleZ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether or not x component of scale will be replicated
|
/// Whether or not x component of scale will be replicated
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -326,6 +349,15 @@ namespace Unity.Netcode.Components
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool SyncScaleZ = true;
|
public bool SyncScaleZ = true;
|
||||||
|
|
||||||
|
|
||||||
|
private bool SynchronizeScale
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return SyncScaleX || SyncScaleY || SyncScaleZ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The current position threshold value
|
/// The current position threshold value
|
||||||
/// Any changes to the position that exceeds the current threshold value will be replicated
|
/// Any changes to the position that exceeds the current threshold value will be replicated
|
||||||
@@ -347,7 +379,6 @@ namespace Unity.Netcode.Components
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public float ScaleThreshold = ScaleThresholdDefault;
|
public float ScaleThreshold = ScaleThresholdDefault;
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets whether the transform should be treated as local (true) or world (false) space.
|
/// Sets whether the transform should be treated as local (true) or world (false) space.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -390,7 +421,6 @@ namespace Unity.Netcode.Components
|
|||||||
private readonly NetworkVariable<NetworkTransformState> m_ReplicatedNetworkStateServer = new NetworkVariable<NetworkTransformState>(new NetworkTransformState(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
|
private readonly NetworkVariable<NetworkTransformState> m_ReplicatedNetworkStateServer = new NetworkVariable<NetworkTransformState>(new NetworkTransformState(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
|
||||||
private readonly NetworkVariable<NetworkTransformState> m_ReplicatedNetworkStateOwner = new NetworkVariable<NetworkTransformState>(new NetworkTransformState(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
|
private readonly NetworkVariable<NetworkTransformState> m_ReplicatedNetworkStateOwner = new NetworkVariable<NetworkTransformState>(new NetworkTransformState(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
|
||||||
|
|
||||||
|
|
||||||
internal NetworkVariable<NetworkTransformState> ReplicatedNetworkState
|
internal NetworkVariable<NetworkTransformState> ReplicatedNetworkState
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -404,10 +434,10 @@ namespace Unity.Netcode.Components
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used by both authoritative and non-authoritative instances.
|
||||||
|
// This represents the most recent local authoritative state.
|
||||||
private NetworkTransformState m_LocalAuthoritativeNetworkState;
|
private NetworkTransformState m_LocalAuthoritativeNetworkState;
|
||||||
|
|
||||||
private bool m_HasSentLastValue = false; // used to send one last value, so clients can make the difference between lost replication data (clients extrapolate) and no more data to send.
|
|
||||||
|
|
||||||
private ClientRpcParams m_ClientRpcParams = new ClientRpcParams() { Send = new ClientRpcSendParams() };
|
private ClientRpcParams m_ClientRpcParams = new ClientRpcParams() { Send = new ClientRpcSendParams() };
|
||||||
private List<ulong> m_ClientIds = new List<ulong>() { 0 };
|
private List<ulong> m_ClientIds = new List<ulong>() { 0 };
|
||||||
|
|
||||||
@@ -420,7 +450,7 @@ namespace Unity.Netcode.Components
|
|||||||
private BufferedLinearInterpolator<float> m_ScaleZInterpolator;
|
private BufferedLinearInterpolator<float> m_ScaleZInterpolator;
|
||||||
private readonly List<BufferedLinearInterpolator<float>> m_AllFloatInterpolators = new List<BufferedLinearInterpolator<float>>(6);
|
private readonly List<BufferedLinearInterpolator<float>> m_AllFloatInterpolators = new List<BufferedLinearInterpolator<float>>(6);
|
||||||
|
|
||||||
private int m_LastSentTick;
|
// Used by integration test
|
||||||
private NetworkTransformState m_LastSentState;
|
private NetworkTransformState m_LastSentState;
|
||||||
|
|
||||||
internal NetworkTransformState GetLastSentState()
|
internal NetworkTransformState GetLastSentState()
|
||||||
@@ -428,6 +458,19 @@ namespace Unity.Netcode.Components
|
|||||||
return m_LastSentState;
|
return m_LastSentState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculated when spawned, this is used to offset a newly received non-authority side state by 1 tick duration
|
||||||
|
/// in order to end the extrapolation for that state's values.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Example:
|
||||||
|
/// NetworkState-A is received, processed, and measurements added
|
||||||
|
/// NetworkState-A is duplicated (NetworkState-A-Post) and its sent time is offset by the tick frequency
|
||||||
|
/// One tick later, NetworkState-A-Post is applied to end that delta's extrapolation.
|
||||||
|
/// <see cref="OnNetworkStateChanged"/> to see how NetworkState-A-Post doesn't get excluded/missed
|
||||||
|
/// </remarks>
|
||||||
|
private double m_TickFrequency;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This will try to send/commit the current transform delta states (if any)
|
/// This will try to send/commit the current transform delta states (if any)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -445,19 +488,12 @@ namespace Unity.Netcode.Components
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If authority is invoking this, then treat it like we do with <see cref="Update"/>
|
// If we are authority, update the authoritative state
|
||||||
if (CanCommitToTransform)
|
if (CanCommitToTransform)
|
||||||
{
|
{
|
||||||
// If our replicated state is not dirty and our local authority state is dirty, clear it.
|
UpdateAuthoritativeState(transform);
|
||||||
if (!ReplicatedNetworkState.IsDirty() && m_LocalAuthoritativeNetworkState.IsDirty)
|
|
||||||
{
|
|
||||||
// Now clear our bitset and prepare for next network tick state update
|
|
||||||
m_LocalAuthoritativeNetworkState.ClearBitSetForNextTick();
|
|
||||||
}
|
}
|
||||||
|
else // Non-Authority
|
||||||
TryCommitTransform(transformToCommit, m_CachedNetworkManager.LocalTime.Time);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
// We are an owner requesting to update our state
|
// We are an owner requesting to update our state
|
||||||
if (!m_CachedIsServer)
|
if (!m_CachedIsServer)
|
||||||
@@ -483,47 +519,46 @@ namespace Unity.Netcode.Components
|
|||||||
NetworkLog.LogError($"[{name}] is trying to commit the transform without authority!");
|
NetworkLog.LogError($"[{name}] is trying to commit the transform without authority!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var isDirty = ApplyTransformToNetworkState(ref m_LocalAuthoritativeNetworkState, dirtyTime, transformToCommit);
|
|
||||||
|
|
||||||
// if dirty, send
|
// If the transform has deltas (returns dirty) then...
|
||||||
// if not dirty anymore, but hasn't sent last value for limiting extrapolation, still set isDirty
|
if (ApplyTransformToNetworkState(ref m_LocalAuthoritativeNetworkState, dirtyTime, transformToCommit))
|
||||||
// if not dirty and has already sent last value, don't do anything
|
|
||||||
// extrapolation works by using last two values. if it doesn't receive anything anymore, it'll continue to extrapolate.
|
|
||||||
// This is great in case there's message loss, not so great if we just don't have new values to send.
|
|
||||||
// the following will send one last "copied" value so unclamped interpolation tries to extrapolate between two identical values, effectively
|
|
||||||
// making it immobile.
|
|
||||||
if (isDirty)
|
|
||||||
{
|
{
|
||||||
// Commit the state
|
// ...commit the state
|
||||||
ReplicatedNetworkState.Value = m_LocalAuthoritativeNetworkState;
|
ReplicatedNetworkState.Value = m_LocalAuthoritativeNetworkState;
|
||||||
m_HasSentLastValue = false;
|
|
||||||
m_LastSentTick = m_CachedNetworkManager.LocalTime.Tick;
|
|
||||||
m_LastSentState = m_LocalAuthoritativeNetworkState;
|
|
||||||
}
|
|
||||||
else if (!m_HasSentLastValue && m_CachedNetworkManager.LocalTime.Tick >= m_LastSentTick + 1) // check for state.IsDirty since update can happen more than once per tick. No need for client, RPCs will just queue up
|
|
||||||
{
|
|
||||||
// Since the last m_LocalAuthoritativeNetworkState could have included a IsTeleportingNextFrame
|
|
||||||
// we need to reset this here so only the deltas are applied and interpolation is not reset again.
|
|
||||||
m_LastSentState.IsTeleportingNextFrame = false;
|
|
||||||
m_LastSentState.SentTime = m_CachedNetworkManager.LocalTime.Time; // time 1+ tick later
|
|
||||||
// Commit the state
|
|
||||||
ReplicatedNetworkState.Value = m_LastSentState;
|
|
||||||
m_HasSentLastValue = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ResetInterpolatedStateToCurrentAuthoritativeState()
|
private void ResetInterpolatedStateToCurrentAuthoritativeState()
|
||||||
{
|
{
|
||||||
var serverTime = NetworkManager.ServerTime.Time;
|
var serverTime = NetworkManager.ServerTime.Time;
|
||||||
m_PositionXInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.PositionX, serverTime);
|
|
||||||
m_PositionYInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.PositionY, serverTime);
|
|
||||||
m_PositionZInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.PositionZ, serverTime);
|
|
||||||
|
|
||||||
m_RotationInterpolator.ResetTo(Quaternion.Euler(m_LocalAuthoritativeNetworkState.RotAngleX, m_LocalAuthoritativeNetworkState.RotAngleY, m_LocalAuthoritativeNetworkState.RotAngleZ), serverTime);
|
// TODO: Look into a better way to communicate the entire state for late joining clients.
|
||||||
|
// Since the replicated network state will just be the most recent deltas and not the entire state.
|
||||||
|
//m_PositionXInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.PositionX, serverTime);
|
||||||
|
//m_PositionYInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.PositionY, serverTime);
|
||||||
|
//m_PositionZInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.PositionZ, serverTime);
|
||||||
|
|
||||||
m_ScaleXInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleX, serverTime);
|
//m_RotationInterpolator.ResetTo(Quaternion.Euler(m_LocalAuthoritativeNetworkState.RotAngleX, m_LocalAuthoritativeNetworkState.RotAngleY, m_LocalAuthoritativeNetworkState.RotAngleZ), serverTime);
|
||||||
m_ScaleYInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleY, serverTime);
|
|
||||||
m_ScaleZInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleZ, serverTime);
|
//m_ScaleXInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleX, serverTime);
|
||||||
|
//m_ScaleYInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleY, serverTime);
|
||||||
|
//m_ScaleZInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleZ, serverTime);
|
||||||
|
|
||||||
|
// NOTE ABOUT THIS CHANGE:
|
||||||
|
// !!! This will exclude any scale changes because we currently do not spawn network objects with scale !!!
|
||||||
|
// Regarding Scale: It will be the same scale as the default scale for the object being spawned.
|
||||||
|
var position = InLocalSpace ? transform.localPosition : transform.position;
|
||||||
|
m_PositionXInterpolator.ResetTo(position.x, serverTime);
|
||||||
|
m_PositionYInterpolator.ResetTo(position.y, serverTime);
|
||||||
|
m_PositionZInterpolator.ResetTo(position.z, serverTime);
|
||||||
|
var rotation = InLocalSpace ? transform.localRotation : transform.rotation;
|
||||||
|
m_RotationInterpolator.ResetTo(rotation, serverTime);
|
||||||
|
|
||||||
|
// TODO: (Create Jira Ticket) Synchronize local scale during NetworkObject synchronization
|
||||||
|
// (We will probably want to byte pack TransformData to offset the 3 float addition)
|
||||||
|
m_ScaleXInterpolator.ResetTo(transform.localScale.x, serverTime);
|
||||||
|
m_ScaleYInterpolator.ResetTo(transform.localScale.y, serverTime);
|
||||||
|
m_ScaleZInterpolator.ResetTo(transform.localScale.z, serverTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -656,104 +691,92 @@ namespace Unity.Netcode.Components
|
|||||||
private void ApplyAuthoritativeState()
|
private void ApplyAuthoritativeState()
|
||||||
{
|
{
|
||||||
var networkState = ReplicatedNetworkState.Value;
|
var networkState = ReplicatedNetworkState.Value;
|
||||||
var interpolatedPosition = networkState.InLocalSpace ? transform.localPosition : transform.position;
|
var adjustedPosition = networkState.InLocalSpace ? transform.localPosition : transform.position;
|
||||||
|
|
||||||
// todo: we should store network state w/ quats vs. euler angles
|
// TODO: We should store network state w/ quats vs. euler angles
|
||||||
var interpolatedRotAngles = networkState.InLocalSpace ? transform.localEulerAngles : transform.eulerAngles;
|
var adjustedRotAngles = networkState.InLocalSpace ? transform.localEulerAngles : transform.eulerAngles;
|
||||||
var interpolatedScale = transform.localScale;
|
var adjustedScale = transform.localScale;
|
||||||
var isTeleporting = networkState.IsTeleportingNextFrame;
|
|
||||||
|
|
||||||
// InLocalSpace Read:
|
// InLocalSpace Read:
|
||||||
InLocalSpace = networkState.InLocalSpace;
|
InLocalSpace = networkState.InLocalSpace;
|
||||||
|
|
||||||
// Update the position values that were changed in this state update
|
// NOTE ABOUT INTERPOLATING AND THE CODE BELOW:
|
||||||
if (networkState.HasPositionX)
|
// 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.
|
||||||
|
var useInterpolatedValue = !networkState.IsTeleportingNextFrame && Interpolate;
|
||||||
|
if (useInterpolatedValue)
|
||||||
{
|
{
|
||||||
interpolatedPosition.x = isTeleporting || !Interpolate ? networkState.PositionX : m_PositionXInterpolator.GetInterpolatedValue();
|
if (SyncPositionX) { adjustedPosition.x = m_PositionXInterpolator.GetInterpolatedValue(); }
|
||||||
}
|
if (SyncPositionY) { adjustedPosition.y = m_PositionYInterpolator.GetInterpolatedValue(); }
|
||||||
|
if (SyncPositionZ) { adjustedPosition.z = m_PositionZInterpolator.GetInterpolatedValue(); }
|
||||||
|
|
||||||
if (networkState.HasPositionY)
|
if (SyncScaleX) { adjustedScale.x = m_ScaleXInterpolator.GetInterpolatedValue(); }
|
||||||
{
|
if (SyncScaleY) { adjustedScale.y = m_ScaleYInterpolator.GetInterpolatedValue(); }
|
||||||
interpolatedPosition.y = isTeleporting || !Interpolate ? networkState.PositionY : m_PositionYInterpolator.GetInterpolatedValue();
|
if (SyncScaleZ) { adjustedScale.z = m_ScaleZInterpolator.GetInterpolatedValue(); }
|
||||||
}
|
|
||||||
|
|
||||||
if (networkState.HasPositionZ)
|
if (SynchronizeRotation)
|
||||||
{
|
{
|
||||||
interpolatedPosition.z = isTeleporting || !Interpolate ? networkState.PositionZ : m_PositionZInterpolator.GetInterpolatedValue();
|
var interpolatedEulerAngles = m_RotationInterpolator.GetInterpolatedValue().eulerAngles;
|
||||||
|
if (SyncRotAngleX) { adjustedRotAngles.x = interpolatedEulerAngles.x; }
|
||||||
|
if (SyncRotAngleY) { adjustedRotAngles.y = interpolatedEulerAngles.y; }
|
||||||
|
if (SyncRotAngleZ) { adjustedRotAngles.z = interpolatedEulerAngles.z; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the rotation values that were changed in this state update
|
|
||||||
if (networkState.HasRotAngleChange)
|
|
||||||
{
|
|
||||||
var eulerAngles = new Vector3();
|
|
||||||
if (Interpolate)
|
|
||||||
{
|
|
||||||
eulerAngles = m_RotationInterpolator.GetInterpolatedValue().eulerAngles;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (networkState.HasRotAngleX)
|
|
||||||
{
|
|
||||||
interpolatedRotAngles.x = isTeleporting || !Interpolate ? networkState.RotAngleX : eulerAngles.x;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (networkState.HasRotAngleY)
|
|
||||||
{
|
|
||||||
interpolatedRotAngles.y = isTeleporting || !Interpolate ? networkState.RotAngleY : eulerAngles.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (networkState.HasRotAngleZ)
|
|
||||||
{
|
|
||||||
interpolatedRotAngles.z = isTeleporting || !Interpolate ? networkState.RotAngleZ : eulerAngles.z;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update all scale axis that were changed in this state update
|
|
||||||
if (networkState.HasScaleX)
|
|
||||||
{
|
|
||||||
interpolatedScale.x = isTeleporting || !Interpolate ? networkState.ScaleX : m_ScaleXInterpolator.GetInterpolatedValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (networkState.HasScaleY)
|
|
||||||
{
|
|
||||||
interpolatedScale.y = isTeleporting || !Interpolate ? networkState.ScaleY : m_ScaleYInterpolator.GetInterpolatedValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (networkState.HasScaleZ)
|
|
||||||
{
|
|
||||||
interpolatedScale.z = isTeleporting || !Interpolate ? networkState.ScaleZ : m_ScaleZInterpolator.GetInterpolatedValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply the new position
|
|
||||||
if (networkState.HasPositionChange)
|
|
||||||
{
|
|
||||||
if (InLocalSpace)
|
|
||||||
{
|
|
||||||
|
|
||||||
transform.localPosition = interpolatedPosition;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
transform.position = interpolatedPosition;
|
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; }
|
||||||
|
|
||||||
|
if (networkState.HasRotAngleX) { adjustedRotAngles.x = networkState.RotAngleX; }
|
||||||
|
if (networkState.HasRotAngleY) { adjustedRotAngles.y = networkState.RotAngleY; }
|
||||||
|
if (networkState.HasRotAngleZ) { adjustedRotAngles.z = networkState.RotAngleZ; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply the new rotation
|
// NOTE: The below conditional checks for applying axial values are required in order to
|
||||||
if (networkState.HasRotAngleChange)
|
// prevent the non-authoritative side from making adjustments when interpolation is off.
|
||||||
|
|
||||||
|
// TODO: Determine if we want to enforce, frame by frame, the non-authoritative transform values.
|
||||||
|
// We would want save the position, rotation, and scale (each individually) after applying each
|
||||||
|
// authoritative transform state received. Otherwise, the non-authoritative side could make
|
||||||
|
// changes to an axial value (if interpolation is turned off) until authority sends an update for
|
||||||
|
// that same axial value. When interpolation is on, the state's values being synchronized are
|
||||||
|
// always applied each frame.
|
||||||
|
|
||||||
|
// Apply the new position if it has changed or we are interpolating and synchronizing position
|
||||||
|
if (networkState.HasPositionChange || (useInterpolatedValue && SynchronizePosition))
|
||||||
{
|
{
|
||||||
if (InLocalSpace)
|
if (InLocalSpace)
|
||||||
{
|
{
|
||||||
transform.localRotation = Quaternion.Euler(interpolatedRotAngles);
|
transform.localPosition = adjustedPosition;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
transform.rotation = Quaternion.Euler(interpolatedRotAngles);
|
transform.position = adjustedPosition;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply the new scale
|
// Apply the new rotation if it has changed or we are interpolating and synchronizing rotation
|
||||||
if (networkState.HasScaleChange)
|
if (networkState.HasRotAngleChange || (useInterpolatedValue && SynchronizeRotation))
|
||||||
{
|
{
|
||||||
transform.localScale = interpolatedScale;
|
if (InLocalSpace)
|
||||||
|
{
|
||||||
|
transform.localRotation = Quaternion.Euler(adjustedRotAngles);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
transform.rotation = Quaternion.Euler(adjustedRotAngles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the new scale if it has changed or we are interpolating and synchronizing scale
|
||||||
|
if (networkState.HasScaleChange || (useInterpolatedValue && SynchronizeScale))
|
||||||
|
{
|
||||||
|
transform.localScale = adjustedScale;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -910,6 +933,7 @@ namespace Unity.Netcode.Components
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentRotation.eulerAngles = currentEulerAngles;
|
currentRotation.eulerAngles = currentEulerAngles;
|
||||||
|
|
||||||
m_RotationInterpolator.AddMeasurement(currentRotation, sentTime);
|
m_RotationInterpolator.AddMeasurement(currentRotation, sentTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -932,6 +956,7 @@ namespace Unity.Netcode.Components
|
|||||||
|
|
||||||
if (Interpolate)
|
if (Interpolate)
|
||||||
{
|
{
|
||||||
|
// Add measurements for the new state's deltas
|
||||||
AddInterpolatedState(newState);
|
AddInterpolatedState(newState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -954,18 +979,25 @@ namespace Unity.Netcode.Components
|
|||||||
m_ScaleZInterpolator.MaxInterpolationBound = maxInterpolationBound;
|
m_ScaleZInterpolator.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>
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
// we only want to create our interpolators during Awake so that, when pooled, we do not create tons
|
// Rotation is a single Quaternion since each Euler axis will affect the quaternion's final value
|
||||||
// of gc thrash each time objects wink out and are re-used
|
m_RotationInterpolator = new BufferedLinearInterpolatorQuaternion();
|
||||||
|
|
||||||
|
// All other interpolators are BufferedLinearInterpolatorFloats
|
||||||
m_PositionXInterpolator = new BufferedLinearInterpolatorFloat();
|
m_PositionXInterpolator = new BufferedLinearInterpolatorFloat();
|
||||||
m_PositionYInterpolator = new BufferedLinearInterpolatorFloat();
|
m_PositionYInterpolator = new BufferedLinearInterpolatorFloat();
|
||||||
m_PositionZInterpolator = new BufferedLinearInterpolatorFloat();
|
m_PositionZInterpolator = new BufferedLinearInterpolatorFloat();
|
||||||
m_RotationInterpolator = new BufferedLinearInterpolatorQuaternion(); // rotation is a single Quaternion since each euler axis will affect the quaternion's final value
|
|
||||||
m_ScaleXInterpolator = new BufferedLinearInterpolatorFloat();
|
m_ScaleXInterpolator = new BufferedLinearInterpolatorFloat();
|
||||||
m_ScaleYInterpolator = new BufferedLinearInterpolatorFloat();
|
m_ScaleYInterpolator = new BufferedLinearInterpolatorFloat();
|
||||||
m_ScaleZInterpolator = new BufferedLinearInterpolatorFloat();
|
m_ScaleZInterpolator = new BufferedLinearInterpolatorFloat();
|
||||||
|
|
||||||
|
// Used to quickly iteration over the BufferedLinearInterpolatorFloat
|
||||||
|
// instances
|
||||||
if (m_AllFloatInterpolators.Count == 0)
|
if (m_AllFloatInterpolators.Count == 0)
|
||||||
{
|
{
|
||||||
m_AllFloatInterpolators.Add(m_PositionXInterpolator);
|
m_AllFloatInterpolators.Add(m_PositionXInterpolator);
|
||||||
@@ -982,6 +1014,7 @@ namespace Unity.Netcode.Components
|
|||||||
{
|
{
|
||||||
m_CachedIsServer = IsServer;
|
m_CachedIsServer = IsServer;
|
||||||
m_CachedNetworkManager = NetworkManager;
|
m_CachedNetworkManager = NetworkManager;
|
||||||
|
m_TickFrequency = 1.0 / NetworkManager.NetworkConfig.TickRate;
|
||||||
|
|
||||||
Initialize();
|
Initialize();
|
||||||
|
|
||||||
@@ -990,8 +1023,10 @@ namespace Unity.Netcode.Components
|
|||||||
// that can be invoked when ownership changes.
|
// that can be invoked when ownership changes.
|
||||||
if (CanCommitToTransform)
|
if (CanCommitToTransform)
|
||||||
{
|
{
|
||||||
|
var currentPosition = InLocalSpace ? transform.localPosition : transform.position;
|
||||||
|
var currentRotation = InLocalSpace ? transform.localRotation : transform.rotation;
|
||||||
// Teleport to current position
|
// Teleport to current position
|
||||||
SetStateInternal(transform.position, transform.rotation, transform.localScale, true);
|
SetStateInternal(currentPosition, currentRotation, transform.localScale, true);
|
||||||
|
|
||||||
// Force the state update to be sent
|
// Force the state update to be sent
|
||||||
TryCommitTransform(transform, m_CachedNetworkManager.LocalTime.Time);
|
TryCommitTransform(transform, m_CachedNetworkManager.LocalTime.Time);
|
||||||
@@ -1163,8 +1198,25 @@ namespace Unity.Netcode.Components
|
|||||||
TryCommitTransform(transform, m_CachedNetworkManager.LocalTime.Time);
|
TryCommitTransform(transform, m_CachedNetworkManager.LocalTime.Time);
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: this is currently in update, to be able to catch any transform changes. A FixedUpdate mode could be added to be less intense, but it'd be
|
/// <summary>
|
||||||
// conditional to users only making transform update changes in FixedUpdate.
|
/// Will update the authoritative transform state if any deltas are detected.
|
||||||
|
/// This will also reset the m_LocalAuthoritativeNetworkState if it is still dirty
|
||||||
|
/// but the replicated network state is not.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="transformSource">transform to be updated</param>
|
||||||
|
private void UpdateAuthoritativeState(Transform transformSource)
|
||||||
|
{
|
||||||
|
// If our replicated state is not dirty and our local authority state is dirty, clear it.
|
||||||
|
if (!ReplicatedNetworkState.IsDirty() && m_LocalAuthoritativeNetworkState.IsDirty)
|
||||||
|
{
|
||||||
|
m_LastSentState = m_LocalAuthoritativeNetworkState;
|
||||||
|
// Now clear our bitset and prepare for next network tick state update
|
||||||
|
m_LocalAuthoritativeNetworkState.ClearBitSetForNextTick();
|
||||||
|
}
|
||||||
|
|
||||||
|
TryCommitTransform(transformSource, m_CachedNetworkManager.LocalTime.Time);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// If you override this method, be sure that:
|
/// If you override this method, be sure that:
|
||||||
@@ -1179,21 +1231,15 @@ namespace Unity.Netcode.Components
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we are authority, update the authoritative state
|
||||||
if (CanCommitToTransform)
|
if (CanCommitToTransform)
|
||||||
{
|
{
|
||||||
// If our replicated state is not dirty and our local authority state is dirty, clear it.
|
UpdateAuthoritativeState(transform);
|
||||||
if (!ReplicatedNetworkState.IsDirty() && m_LocalAuthoritativeNetworkState.IsDirty)
|
|
||||||
{
|
|
||||||
// Now clear our bitset and prepare for next network tick state update
|
|
||||||
m_LocalAuthoritativeNetworkState.ClearBitSetForNextTick();
|
|
||||||
}
|
}
|
||||||
TryCommitTransform(transform, m_CachedNetworkManager.LocalTime.Time);
|
else // Non-Authority
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
if (Interpolate)
|
if (Interpolate)
|
||||||
{
|
{
|
||||||
// eventually, we could hoist this calculation so that it happens once for all objects, not once per object
|
|
||||||
var serverTime = NetworkManager.ServerTime;
|
var serverTime = NetworkManager.ServerTime;
|
||||||
var cachedDeltaTime = Time.deltaTime;
|
var cachedDeltaTime = Time.deltaTime;
|
||||||
var cachedServerTime = serverTime.Time;
|
var cachedServerTime = serverTime.Time;
|
||||||
@@ -1205,7 +1251,8 @@ namespace Unity.Netcode.Components
|
|||||||
|
|
||||||
m_RotationInterpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime);
|
m_RotationInterpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime);
|
||||||
}
|
}
|
||||||
// Now apply the current authoritative state
|
|
||||||
|
// Apply the current authoritative state
|
||||||
ApplyAuthoritativeState();
|
ApplyAuthoritativeState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ namespace Unity.Netcode.EditorTests
|
|||||||
Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(2f).Within(k_Precision));
|
Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(2f).Within(k_Precision));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Ignore("TODO: Fix this test to still handle testing message loss without extrapolation")]
|
||||||
[Test]
|
[Test]
|
||||||
public void MessageLoss()
|
public void MessageLoss()
|
||||||
{
|
{
|
||||||
@@ -305,6 +306,7 @@ namespace Unity.Netcode.EditorTests
|
|||||||
Assert.Throws<InvalidOperationException>(() => interpolator.Update(1f, serverTime));
|
Assert.Throws<InvalidOperationException>(() => interpolator.Update(1f, serverTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Ignore("TODO: Fix this test to still test duplicated values without extrapolation")]
|
||||||
[Test]
|
[Test]
|
||||||
public void TestDuplicatedValues()
|
public void TestDuplicatedValues()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
using Unity.Netcode.Components;
|
using Unity.Netcode.Components;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
@@ -7,6 +8,9 @@ using Unity.Netcode.TestHelpers.Runtime;
|
|||||||
|
|
||||||
namespace Unity.Netcode.RuntimeTests
|
namespace Unity.Netcode.RuntimeTests
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Helper component for all NetworkTransformTests
|
||||||
|
/// </summary>
|
||||||
public class NetworkTransformTestComponent : NetworkTransform
|
public class NetworkTransformTestComponent : NetworkTransform
|
||||||
{
|
{
|
||||||
public bool ServerAuthority;
|
public bool ServerAuthority;
|
||||||
@@ -37,15 +41,52 @@ namespace Unity.Netcode.RuntimeTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestFixture(HostOrServer.Host, Authority.Server)]
|
/// <summary>
|
||||||
[TestFixture(HostOrServer.Host, Authority.Owner)]
|
/// Helper component for NetworkTransform parenting tests
|
||||||
[TestFixture(HostOrServer.Server, Authority.Server)]
|
/// </summary>
|
||||||
[TestFixture(HostOrServer.Server, Authority.Owner)]
|
public class ChildObjectComponent : NetworkBehaviour
|
||||||
|
{
|
||||||
|
public readonly static List<ChildObjectComponent> Instances = new List<ChildObjectComponent>();
|
||||||
|
public static ChildObjectComponent ServerInstance { get; internal set; }
|
||||||
|
public readonly static Dictionary<ulong, NetworkObject> ClientInstances = new Dictionary<ulong, NetworkObject>();
|
||||||
|
|
||||||
|
public static void Reset()
|
||||||
|
{
|
||||||
|
ServerInstance = null;
|
||||||
|
ClientInstances.Clear();
|
||||||
|
Instances.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnNetworkSpawn()
|
||||||
|
{
|
||||||
|
if (IsServer)
|
||||||
|
{
|
||||||
|
ServerInstance = this;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ClientInstances.Add(NetworkManager.LocalClientId, NetworkObject);
|
||||||
|
}
|
||||||
|
Instances.Add(this);
|
||||||
|
base.OnNetworkSpawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Integration tests for NetworkTransform that will test both
|
||||||
|
/// server and host operating modes and will test both authoritative
|
||||||
|
/// models for each operating mode.
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture(HostOrServer.Host, Authority.ServerAuthority)]
|
||||||
|
[TestFixture(HostOrServer.Host, Authority.OwnerAuthority)]
|
||||||
|
[TestFixture(HostOrServer.Server, Authority.ServerAuthority)]
|
||||||
|
[TestFixture(HostOrServer.Server, Authority.OwnerAuthority)]
|
||||||
|
|
||||||
public class NetworkTransformTests : NetcodeIntegrationTest
|
public class NetworkTransformTests : NetcodeIntegrationTest
|
||||||
{
|
{
|
||||||
private NetworkObject m_AuthoritativePlayer;
|
private NetworkObject m_AuthoritativePlayer;
|
||||||
private NetworkObject m_NonAuthoritativePlayer;
|
private NetworkObject m_NonAuthoritativePlayer;
|
||||||
|
private NetworkObject m_ChildObjectToBeParented;
|
||||||
|
|
||||||
private NetworkTransformTestComponent m_AuthoritativeTransform;
|
private NetworkTransformTestComponent m_AuthoritativeTransform;
|
||||||
private NetworkTransformTestComponent m_NonAuthoritativeTransform;
|
private NetworkTransformTestComponent m_NonAuthoritativeTransform;
|
||||||
@@ -55,8 +96,8 @@ namespace Unity.Netcode.RuntimeTests
|
|||||||
|
|
||||||
public enum Authority
|
public enum Authority
|
||||||
{
|
{
|
||||||
Server,
|
ServerAuthority,
|
||||||
Owner
|
OwnerAuthority
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Interpolation
|
public enum Interpolation
|
||||||
@@ -78,14 +119,32 @@ namespace Unity.Netcode.RuntimeTests
|
|||||||
|
|
||||||
protected override int NumberOfClients => 1;
|
protected override int NumberOfClients => 1;
|
||||||
|
|
||||||
|
protected override IEnumerator OnSetup()
|
||||||
|
{
|
||||||
|
ChildObjectComponent.Reset();
|
||||||
|
return base.OnSetup();
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnCreatePlayerPrefab()
|
protected override void OnCreatePlayerPrefab()
|
||||||
{
|
{
|
||||||
var networkTransformTestComponent = m_PlayerPrefab.AddComponent<NetworkTransformTestComponent>();
|
var networkTransformTestComponent = m_PlayerPrefab.AddComponent<NetworkTransformTestComponent>();
|
||||||
networkTransformTestComponent.ServerAuthority = m_Authority == Authority.Server;
|
networkTransformTestComponent.ServerAuthority = m_Authority == Authority.ServerAuthority;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnServerAndClientsCreated()
|
protected override void OnServerAndClientsCreated()
|
||||||
{
|
{
|
||||||
|
var childObject = CreateNetworkObjectPrefab("ChildObject");
|
||||||
|
childObject.AddComponent<ChildObjectComponent>();
|
||||||
|
var childNetworkTransform = childObject.AddComponent<NetworkTransform>();
|
||||||
|
childNetworkTransform.InLocalSpace = true;
|
||||||
|
m_ChildObjectToBeParented = childObject.GetComponent<NetworkObject>();
|
||||||
|
|
||||||
|
// Now apply local transform values
|
||||||
|
m_ChildObjectToBeParented.transform.position = m_ChildObjectLocalPosition;
|
||||||
|
var childRotation = m_ChildObjectToBeParented.transform.rotation;
|
||||||
|
childRotation.eulerAngles = m_ChildObjectLocalRotation;
|
||||||
|
m_ChildObjectToBeParented.transform.rotation = childRotation;
|
||||||
|
m_ChildObjectToBeParented.transform.localScale = m_ChildObjectLocalScale;
|
||||||
if (m_EnableVerboseDebug)
|
if (m_EnableVerboseDebug)
|
||||||
{
|
{
|
||||||
m_ServerNetworkManager.LogLevel = LogLevel.Developer;
|
m_ServerNetworkManager.LogLevel = LogLevel.Developer;
|
||||||
@@ -102,8 +161,8 @@ namespace Unity.Netcode.RuntimeTests
|
|||||||
var serverSideClientPlayer = m_ServerNetworkManager.ConnectedClients[m_ClientNetworkManagers[0].LocalClientId].PlayerObject;
|
var serverSideClientPlayer = m_ServerNetworkManager.ConnectedClients[m_ClientNetworkManagers[0].LocalClientId].PlayerObject;
|
||||||
var clientSideClientPlayer = m_ClientNetworkManagers[0].LocalClient.PlayerObject;
|
var clientSideClientPlayer = m_ClientNetworkManagers[0].LocalClient.PlayerObject;
|
||||||
|
|
||||||
m_AuthoritativePlayer = m_Authority == Authority.Server ? serverSideClientPlayer : clientSideClientPlayer;
|
m_AuthoritativePlayer = m_Authority == Authority.ServerAuthority ? serverSideClientPlayer : clientSideClientPlayer;
|
||||||
m_NonAuthoritativePlayer = m_Authority == Authority.Server ? clientSideClientPlayer : serverSideClientPlayer;
|
m_NonAuthoritativePlayer = m_Authority == Authority.ServerAuthority ? clientSideClientPlayer : serverSideClientPlayer;
|
||||||
|
|
||||||
// Get the NetworkTransformTestComponent to make sure the client side is ready before starting test
|
// Get the NetworkTransformTestComponent to make sure the client side is ready before starting test
|
||||||
m_AuthoritativeTransform = m_AuthoritativePlayer.GetComponent<NetworkTransformTestComponent>();
|
m_AuthoritativeTransform = m_AuthoritativePlayer.GetComponent<NetworkTransformTestComponent>();
|
||||||
@@ -118,7 +177,6 @@ namespace Unity.Netcode.RuntimeTests
|
|||||||
Assert.True(m_AuthoritativeTransform.CanCommitToTransform);
|
Assert.True(m_AuthoritativeTransform.CanCommitToTransform);
|
||||||
Assert.False(m_NonAuthoritativeTransform.CanCommitToTransform);
|
Assert.False(m_NonAuthoritativeTransform.CanCommitToTransform);
|
||||||
|
|
||||||
|
|
||||||
yield return base.OnServerAndClientsConnected();
|
yield return base.OnServerAndClientsConnected();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,6 +192,305 @@ namespace Unity.Netcode.RuntimeTests
|
|||||||
CommitToTransform
|
CommitToTransform
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true when the server-host and all clients have
|
||||||
|
/// instantiated the child object to be used in <see cref="NetworkTransformParentingLocalSpaceOffsetTests"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
private bool AllChildObjectInstancesAreSpawned()
|
||||||
|
{
|
||||||
|
if (ChildObjectComponent.ServerInstance == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var clientNetworkManager in m_ClientNetworkManagers)
|
||||||
|
{
|
||||||
|
if (!ChildObjectComponent.ClientInstances.ContainsKey(clientNetworkManager.LocalClientId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool AllChildObjectInstancesHaveChild()
|
||||||
|
{
|
||||||
|
foreach (var instance in ChildObjectComponent.ClientInstances.Values)
|
||||||
|
{
|
||||||
|
if (instance.transform.parent == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// To test that local position, rotation, and scale remain the same when parented.
|
||||||
|
private Vector3 m_ChildObjectLocalPosition = new Vector3(5.0f, 0.0f, -5.0f);
|
||||||
|
private Vector3 m_ChildObjectLocalRotation = new Vector3(-35.0f, 90.0f, 270.0f);
|
||||||
|
private Vector3 m_ChildObjectLocalScale = new Vector3(0.1f, 0.5f, 0.4f);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A wait condition specific method that assures the local space coordinates
|
||||||
|
/// are not impacted by NetworkTransform when parented.
|
||||||
|
/// </summary>
|
||||||
|
private bool AllInstancesKeptLocalTransformValues()
|
||||||
|
{
|
||||||
|
foreach (var childInstance in ChildObjectComponent.Instances)
|
||||||
|
{
|
||||||
|
var childLocalPosition = childInstance.transform.localPosition;
|
||||||
|
var childLocalRotation = childInstance.transform.localRotation.eulerAngles;
|
||||||
|
var childLocalScale = childInstance.transform.localScale;
|
||||||
|
|
||||||
|
if (!Aproximately(childLocalPosition, m_ChildObjectLocalPosition))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!AproximatelyEuler(childLocalRotation, m_ChildObjectLocalRotation))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Aproximately(childLocalScale, m_ChildObjectLocalScale))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles validating the local space values match the original local space values.
|
||||||
|
/// If not, it generates a message containing the axial values that did not match
|
||||||
|
/// the target/start local space values.
|
||||||
|
/// </summary>
|
||||||
|
private IEnumerator WaitForAllChildrenLocalTransformValuesToMatch()
|
||||||
|
{
|
||||||
|
yield return WaitForConditionOrTimeOut(AllInstancesKeptLocalTransformValues);
|
||||||
|
var infoMessage = string.Empty;
|
||||||
|
if (s_GlobalTimeoutHelper.TimedOut)
|
||||||
|
{
|
||||||
|
foreach (var childInstance in ChildObjectComponent.Instances)
|
||||||
|
{
|
||||||
|
var childLocalPosition = childInstance.transform.localPosition;
|
||||||
|
var childLocalRotation = childInstance.transform.localRotation.eulerAngles;
|
||||||
|
var childLocalScale = childInstance.transform.localScale;
|
||||||
|
|
||||||
|
if (!Aproximately(childLocalPosition, m_ChildObjectLocalPosition))
|
||||||
|
{
|
||||||
|
infoMessage += $"[{childInstance.name}] Child's Local Position ({childLocalPosition}) | Original Local Position ({m_ChildObjectLocalPosition})\n";
|
||||||
|
}
|
||||||
|
if (!AproximatelyEuler(childLocalRotation, m_ChildObjectLocalRotation))
|
||||||
|
{
|
||||||
|
infoMessage += $"[{childInstance.name}] Child's Local Rotation ({childLocalRotation}) | Original Local Rotation ({m_ChildObjectLocalRotation})\n";
|
||||||
|
}
|
||||||
|
if (!Aproximately(childLocalScale, m_ChildObjectLocalScale))
|
||||||
|
{
|
||||||
|
infoMessage += $"[{childInstance.name}] Child's Local Scale ({childLocalScale}) | Original Local Rotation ({m_ChildObjectLocalScale})\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AssertOnTimeout($"Timed out waiting for all children to have the correct local space values:\n {infoMessage}");
|
||||||
|
}
|
||||||
|
yield return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates that local space transform values remain the same when a NetworkTransform is
|
||||||
|
/// parented under another NetworkTransform
|
||||||
|
/// </summary>
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator NetworkTransformParentedLocalSpaceTest([Values] Interpolation interpolation, [Values] OverrideState overideState)
|
||||||
|
{
|
||||||
|
var overrideUpdate = overideState == OverrideState.CommitToTransform;
|
||||||
|
m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
|
||||||
|
m_NonAuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
|
||||||
|
var authoritativeChildObject = SpawnObject(m_ChildObjectToBeParented.gameObject, m_AuthoritativeTransform.NetworkManager);
|
||||||
|
|
||||||
|
// Assure all of the child object instances are spawned
|
||||||
|
yield return WaitForConditionOrTimeOut(AllChildObjectInstancesAreSpawned);
|
||||||
|
AssertOnTimeout("Timed out waiting for all child instances to be spawned!");
|
||||||
|
// Just a sanity check as it should have timed out before this check
|
||||||
|
Assert.IsNotNull(ChildObjectComponent.ServerInstance, $"The server-side {nameof(ChildObjectComponent)} instance is null!");
|
||||||
|
|
||||||
|
// This determines which parent on the server side should be the parent
|
||||||
|
if (m_AuthoritativeTransform.IsServerAuthoritative())
|
||||||
|
{
|
||||||
|
Assert.True(ChildObjectComponent.ServerInstance.NetworkObject.TrySetParent(m_AuthoritativeTransform.transform, false), "[Authoritative] Failed to parent the child object!");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.True(ChildObjectComponent.ServerInstance.NetworkObject.TrySetParent(m_NonAuthoritativeTransform.transform, false), "[Non-Authoritative] Failed to parent the child object!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// This waits for all child instances to be parented
|
||||||
|
yield return WaitForConditionOrTimeOut(AllChildObjectInstancesHaveChild);
|
||||||
|
AssertOnTimeout("Timed out waiting for all instances to have parented a child!");
|
||||||
|
|
||||||
|
// This validates each child instance has preserved their local space values
|
||||||
|
yield return WaitForAllChildrenLocalTransformValuesToMatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates that moving, rotating, and scaling the authority side with a single
|
||||||
|
/// tick will properly synchronize the non-authoritative side with the same values.
|
||||||
|
/// </summary>
|
||||||
|
private IEnumerator MoveRotateAndScaleAuthority(Vector3 position, Vector3 rotation, Vector3 scale)
|
||||||
|
{
|
||||||
|
m_AuthoritativeTransform.transform.position = position;
|
||||||
|
yield return null;
|
||||||
|
var authoritativeRotation = m_AuthoritativeTransform.transform.rotation;
|
||||||
|
authoritativeRotation.eulerAngles = rotation;
|
||||||
|
m_AuthoritativeTransform.transform.rotation = authoritativeRotation;
|
||||||
|
yield return null;
|
||||||
|
m_AuthoritativeTransform.transform.localScale = scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates we don't extrapolate beyond the target value
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This will first wait for any authoritative changes to have been synchronized
|
||||||
|
/// with the non-authoritative side. It will then wait for the specified number
|
||||||
|
/// of tick periods to assure the values don't change
|
||||||
|
/// </remarks>
|
||||||
|
private IEnumerator WaitForPositionRotationAndScaleToMatch(int ticksToWait)
|
||||||
|
{
|
||||||
|
// Validate we interpolate to the appropriate position and rotation
|
||||||
|
yield return WaitForConditionOrTimeOut(PositionRotationScaleMatches);
|
||||||
|
AssertOnTimeout("Timed out waiting for non-authority to match authority's position or rotation");
|
||||||
|
|
||||||
|
// Wait for the specified number of ticks
|
||||||
|
for (int i = 0; i < ticksToWait; i++)
|
||||||
|
{
|
||||||
|
yield return s_DefaultWaitForTick;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify both sides match (i.e. no drifting or over-extrapolating)
|
||||||
|
Assert.IsTrue(PositionsMatch(), $"Non-authority position did not match after waiting for {ticksToWait} ticks! " +
|
||||||
|
$"Authority ({m_AuthoritativeTransform.transform.position}) Non-Authority ({m_NonAuthoritativeTransform.transform.position})");
|
||||||
|
Assert.IsTrue(RotationsMatch(), $"Non-authority rotation did not match after waiting for {ticksToWait} ticks! " +
|
||||||
|
$"Authority ({m_AuthoritativeTransform.transform.rotation.eulerAngles}) Non-Authority ({m_NonAuthoritativeTransform.transform.rotation.eulerAngles})");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Waits until the next tick
|
||||||
|
/// </summary>
|
||||||
|
private IEnumerator WaitForNextTick()
|
||||||
|
{
|
||||||
|
var currentTick = m_AuthoritativeTransform.NetworkManager.LocalTime.Tick;
|
||||||
|
while (m_AuthoritativeTransform.NetworkManager.LocalTime.Tick == currentTick)
|
||||||
|
{
|
||||||
|
yield return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The number of iterations to change position, rotation, and scale for NetworkTransformMultipleChangesOverTime
|
||||||
|
private const int k_PositionRotationScaleIterations = 8;
|
||||||
|
|
||||||
|
protected override void OnNewClientCreated(NetworkManager networkManager)
|
||||||
|
{
|
||||||
|
networkManager.NetworkConfig.NetworkPrefabs = m_ServerNetworkManager.NetworkConfig.NetworkPrefabs;
|
||||||
|
base.OnNewClientCreated(networkManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This validates that multiple changes can occur within the same tick or over
|
||||||
|
/// several ticks while still keeping non-authoritative instances synchronized.
|
||||||
|
/// </summary>
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator NetworkTransformMultipleChangesOverTime([Values] TransformSpace testLocalTransform, [Values] OverrideState overideState)
|
||||||
|
{
|
||||||
|
var overrideUpdate = overideState == OverrideState.CommitToTransform;
|
||||||
|
m_AuthoritativeTransform.InLocalSpace = testLocalTransform == TransformSpace.Local;
|
||||||
|
|
||||||
|
var positionStart = new Vector3(1.0f, 0.5f, 2.0f);
|
||||||
|
var rotationStart = new Vector3(0.0f, 45.0f, 0.0f);
|
||||||
|
var scaleStart = new Vector3(1.0f, 1.0f, 1.0f);
|
||||||
|
var position = positionStart;
|
||||||
|
var rotation = rotationStart;
|
||||||
|
var scale = scaleStart;
|
||||||
|
|
||||||
|
// Move and rotate within the same tick, validate the non-authoritative instance updates
|
||||||
|
// to each set of changes. Repeat several times.
|
||||||
|
for (int i = 1; i < k_PositionRotationScaleIterations + 1; i++)
|
||||||
|
{
|
||||||
|
position = positionStart * i;
|
||||||
|
rotation = rotationStart * i;
|
||||||
|
scale = scaleStart * i;
|
||||||
|
// Wait for tick to change so we cam start close to the beginning the next tick in order
|
||||||
|
// to apply both deltas within the same tick period.
|
||||||
|
yield return WaitForNextTick();
|
||||||
|
|
||||||
|
// Apply deltas
|
||||||
|
MoveRotateAndScaleAuthority(position, rotation, scale);
|
||||||
|
|
||||||
|
// Wait for deltas to synchronize on non-authoritative side
|
||||||
|
yield return WaitForPositionRotationAndScaleToMatch(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check scale for all player instances when a client late joins
|
||||||
|
// NOTE: This validates the use of the spawned object's transform values as opposed to the replicated state (which now is only the last deltas)
|
||||||
|
yield return CreateAndStartNewClient();
|
||||||
|
var newClientNetworkManager = m_ClientNetworkManagers[NumberOfClients];
|
||||||
|
foreach (var playerRelativeEntry in m_PlayerNetworkObjects)
|
||||||
|
{
|
||||||
|
foreach (var playerInstanceEntry in playerRelativeEntry.Value)
|
||||||
|
{
|
||||||
|
var playerInstance = playerInstanceEntry.Value;
|
||||||
|
if (newClientNetworkManager.LocalClientId == playerInstance.OwnerClientId)
|
||||||
|
{
|
||||||
|
Assert.IsTrue(Aproximately(m_PlayerPrefab.transform.localScale, playerInstance.transform.localScale), $"{playerInstance.name}'s cloned instance's scale does not match original scale!\n" +
|
||||||
|
$"[ClientId-{playerRelativeEntry.Key} Relative] Player-{playerInstance.OwnerClientId}'s LocalScale ({playerInstance.transform.localScale}) vs Target Scale ({m_PlayerPrefab.transform.localScale})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repeat this in the opposite direction
|
||||||
|
for (int i = -1; i > -1 * (k_PositionRotationScaleIterations + 1); i--)
|
||||||
|
{
|
||||||
|
position = positionStart * i;
|
||||||
|
rotation = rotationStart * i;
|
||||||
|
scale = scaleStart * i;
|
||||||
|
// Wait for tick to change so we cam start close to the beginning the next tick in order
|
||||||
|
// to apply both deltas within the same tick period.
|
||||||
|
yield return WaitForNextTick();
|
||||||
|
|
||||||
|
MoveRotateAndScaleAuthority(position, rotation, scale);
|
||||||
|
yield return WaitForPositionRotationAndScaleToMatch(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for tick to change so we cam start close to the beginning the next tick in order
|
||||||
|
// to apply as many deltas within the same tick period as we can (if not all)
|
||||||
|
yield return WaitForNextTick();
|
||||||
|
|
||||||
|
// Move and rotate within the same tick several times, then validate the non-authoritative
|
||||||
|
// instance updates to the authoritative instance's final position and rotation.
|
||||||
|
for (int i = 1; i < k_PositionRotationScaleIterations + 1; i++)
|
||||||
|
{
|
||||||
|
position = positionStart * i;
|
||||||
|
rotation = rotationStart * i;
|
||||||
|
scale = scaleStart * i;
|
||||||
|
|
||||||
|
MoveRotateAndScaleAuthority(position, rotation, scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return WaitForPositionRotationAndScaleToMatch(1);
|
||||||
|
|
||||||
|
// Wait for tick to change so we cam start close to the beginning the next tick in order
|
||||||
|
// to apply as many deltas within the same tick period as we can (if not all)
|
||||||
|
yield return WaitForNextTick();
|
||||||
|
|
||||||
|
// Repeat this in the opposite direction and rotation
|
||||||
|
for (int i = -1; i > -1 * (k_PositionRotationScaleIterations + 1); i--)
|
||||||
|
{
|
||||||
|
position = positionStart * i;
|
||||||
|
rotation = rotationStart * i;
|
||||||
|
scale = scaleStart * i;
|
||||||
|
MoveRotateAndScaleAuthority(position, rotation, scale);
|
||||||
|
}
|
||||||
|
yield return WaitForPositionRotationAndScaleToMatch(1);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tests changing all axial values one at a time.
|
/// Tests changing all axial values one at a time.
|
||||||
/// These tests are performed:
|
/// These tests are performed:
|
||||||
@@ -419,6 +776,13 @@ namespace Unity.Netcode.RuntimeTests
|
|||||||
Mathf.Abs(a.z - b.z) <= k_AproximateDeltaVariance;
|
Mathf.Abs(a.z - b.z) <= k_AproximateDeltaVariance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool AproximatelyEuler(Vector3 a, Vector3 b)
|
||||||
|
{
|
||||||
|
return Mathf.DeltaAngle(a.x, b.x) <= k_AproximateDeltaVariance &&
|
||||||
|
Mathf.DeltaAngle(a.y, b.y) <= k_AproximateDeltaVariance &&
|
||||||
|
Mathf.DeltaAngle(a.z, b.z) <= k_AproximateDeltaVariance;
|
||||||
|
}
|
||||||
|
|
||||||
private const float k_AproximateDeltaVariance = 0.01f;
|
private const float k_AproximateDeltaVariance = 0.01f;
|
||||||
private bool PositionsMatchesValue(Vector3 positionToMatch)
|
private bool PositionsMatchesValue(Vector3 positionToMatch)
|
||||||
{
|
{
|
||||||
@@ -504,6 +868,11 @@ namespace Unity.Netcode.RuntimeTests
|
|||||||
return PositionsMatchesValue(position) && RotationMatchesValue(eulerRotation) && ScaleMatchesValue(scale);
|
return PositionsMatchesValue(position) && RotationMatchesValue(eulerRotation) && ScaleMatchesValue(scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool PositionRotationScaleMatches()
|
||||||
|
{
|
||||||
|
return RotationsMatch() && PositionsMatch() && ScaleValuesMatch();
|
||||||
|
}
|
||||||
|
|
||||||
private bool RotationsMatch()
|
private bool RotationsMatch()
|
||||||
{
|
{
|
||||||
var authorityEulerRotation = m_AuthoritativeTransform.transform.rotation.eulerAngles;
|
var authorityEulerRotation = m_AuthoritativeTransform.transform.rotation.eulerAngles;
|
||||||
|
|||||||
@@ -2,19 +2,19 @@
|
|||||||
"name": "com.unity.netcode.gameobjects",
|
"name": "com.unity.netcode.gameobjects",
|
||||||
"displayName": "Netcode for GameObjects",
|
"displayName": "Netcode for GameObjects",
|
||||||
"description": "Netcode for GameObjects is a high-level netcode SDK that provides networking capabilities to GameObject/MonoBehaviour workflows within Unity and sits on top of underlying transport layer.",
|
"description": "Netcode for GameObjects is a high-level netcode SDK that provides networking capabilities to GameObject/MonoBehaviour workflows within Unity and sits on top of underlying transport layer.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"unity": "2020.3",
|
"unity": "2020.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"com.unity.nuget.mono-cecil": "1.10.1",
|
"com.unity.nuget.mono-cecil": "1.10.1",
|
||||||
"com.unity.transport": "1.2.0"
|
"com.unity.transport": "1.2.0"
|
||||||
},
|
},
|
||||||
"upmCi": {
|
"upmCi": {
|
||||||
"footprint": "8824c99a21c438135052b8a8d42b6a8cb865bea3"
|
"footprint": "01764b7751e27d1e2af672c49cec3ed5691b53b7"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"url": "https://github.com/Unity-Technologies/com.unity.netcode.gameobjects.git",
|
"url": "https://github.com/Unity-Technologies/com.unity.netcode.gameobjects.git",
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"revision": "ce1ab3ca9495caf3f906d8ca5459677614214837"
|
"revision": "fe0c300aa691f31d2aec1d4b73e2971f28122d3b"
|
||||||
},
|
},
|
||||||
"samples": [
|
"samples": [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user