com.unity.netcode.gameobjects@1.0.0-pre.6
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.0-pre.6] - 2022-03-02 ### Added - NetworkAnimator now properly synchrhonizes all animation layers as well as runtime-adjusted weighting between them (#1765) - Added first set of tests for NetworkAnimator - parameter syncing, trigger set / reset, override network animator (#1735) ### Changed ### Fixed - Fixed an issue where sometimes the first client to connect to the server could see messages from the server as coming from itself. (#1683) - Fixed an issue where clients seemed to be able to send messages to ClientId 1, but these messages would actually still go to the server (id 0) instead of that client. (#1683) - Improved clarity of error messaging when a client attempts to send a message to a destination other than the server, which isn't allowed. (#1683) - Disallowed async keyword in RPCs (#1681) - Fixed an issue where Alpha release versions of Unity (version 2022.2.0a5 and later) will not compile due to the UNet Transport no longer existing (#1678) - Fixed messages larger than 64k being written with incorrectly truncated message size in header (#1686) (credit: @kaen) - Fixed overloading RPC methods causing collisions and failing on IL2CPP targets. (#1694) - Fixed spawn flow to propagate `IsSceneObject` down to children NetworkObjects, decouple implicit relationship between object spawning & `IsSceneObject` flag (#1685) - Fixed error when serializing ConnectionApprovalMessage with scene management disabled when one or more objects is hidden via the CheckObjectVisibility delegate (#1720) - Fixed CheckObjectVisibility delegate not being properly invoked for connecting clients when Scene Management is enabled. (#1680) - Fixed NetworkList to properly call INetworkSerializable's NetworkSerialize() method (#1682) - Fixed NetworkVariables containing more than 1300 bytes of data (such as large NetworkLists) no longer cause an OverflowException (the limit on data size is now whatever limit the chosen transport imposes on fragmented NetworkDelivery mechanisms) (#1725) - Fixed ServerRpcParams and ClientRpcParams must be the last parameter of an RPC in order to function properly. Added a compile-time check to ensure this is the case and trigger an error if they're placed elsewhere (#1721) - Fixed FastBufferReader being created with a length of 1 if provided an input of length 0 (#1724) - Fixed The NetworkConfig's checksum hash includes the NetworkTick so that clients with a different tickrate than the server are identified and not allowed to connect (#1728) - Fixed OwnedObjects not being properly modified when using ChangeOwnership (#1731) - Improved performance in NetworkAnimator (#1735) - Removed the "always sync" network animator (aka "autosend") parameters (#1746)
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
using Unity.Collections;
|
||||
using Unity.Collections.LowLevel.Unsafe;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Netcode.Components
|
||||
@@ -14,24 +13,19 @@ namespace Unity.Netcode.Components
|
||||
{
|
||||
internal struct AnimationMessage : INetworkSerializable
|
||||
{
|
||||
public int StateHash; // if non-zero, then Play() this animation, skipping transitions
|
||||
// state hash per layer. if non-zero, then Play() this animation, skipping transitions
|
||||
public int StateHash;
|
||||
public float NormalizedTime;
|
||||
public int Layer;
|
||||
public float Weight;
|
||||
public byte[] Parameters;
|
||||
|
||||
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
|
||||
{
|
||||
serializer.SerializeValue(ref StateHash);
|
||||
serializer.SerializeValue(ref NormalizedTime);
|
||||
serializer.SerializeValue(ref Parameters);
|
||||
}
|
||||
}
|
||||
|
||||
internal struct AnimationParametersMessage : INetworkSerializable
|
||||
{
|
||||
public byte[] Parameters;
|
||||
|
||||
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
|
||||
{
|
||||
serializer.SerializeValue(ref Layer);
|
||||
serializer.SerializeValue(ref Weight);
|
||||
serializer.SerializeValue(ref Parameters);
|
||||
}
|
||||
}
|
||||
@@ -49,8 +43,6 @@ namespace Unity.Netcode.Components
|
||||
}
|
||||
|
||||
[SerializeField] private Animator m_Animator;
|
||||
[SerializeField] private uint m_ParameterSendBits;
|
||||
[SerializeField] private float m_SendRate = 0.1f;
|
||||
|
||||
public Animator Animator
|
||||
{
|
||||
@@ -58,43 +50,17 @@ namespace Unity.Netcode.Components
|
||||
set
|
||||
{
|
||||
m_Animator = value;
|
||||
ResetParameterOptions();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* AutoSend is the ability to select which parameters linked to this animator
|
||||
* get replicated on a regular basis regardless of a state change. The thinking
|
||||
* behind this is that many of the parameters people use are usually booleans
|
||||
* which result in a state change and thus would cause a full sync of state.
|
||||
* Thus if you really care about a parameter syncing then you need to be explict
|
||||
* by selecting it in the inspector when an NetworkAnimator is selected.
|
||||
*/
|
||||
public void SetParameterAutoSend(int index, bool value)
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
m_ParameterSendBits |= (uint)(1 << index);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_ParameterSendBits &= (uint)(~(1 << index));
|
||||
}
|
||||
}
|
||||
|
||||
public bool GetParameterAutoSend(int index)
|
||||
{
|
||||
return (m_ParameterSendBits & (uint)(1 << index)) != 0;
|
||||
}
|
||||
private bool m_SendMessagesAllowed = false;
|
||||
|
||||
// Animators only support up to 32 params
|
||||
public static int K_MaxAnimationParams = 32;
|
||||
|
||||
private int m_TransitionHash;
|
||||
private double m_NextSendTime = 0.0f;
|
||||
|
||||
private int m_AnimationHash;
|
||||
public int AnimationHash { get => m_AnimationHash; }
|
||||
private int[] m_TransitionHash;
|
||||
private int[] m_AnimationHash;
|
||||
private float[] m_LayerWeights;
|
||||
|
||||
private unsafe struct AnimatorParamCache
|
||||
{
|
||||
@@ -103,11 +69,11 @@ namespace Unity.Netcode.Components
|
||||
public fixed byte Value[4]; // this is a max size of 4 bytes
|
||||
}
|
||||
|
||||
// 128bytes per Animator
|
||||
// 128 bytes per Animator
|
||||
private FastBufferWriter m_ParameterWriter = new FastBufferWriter(K_MaxAnimationParams * sizeof(float), Allocator.Persistent);
|
||||
private NativeArray<AnimatorParamCache> m_CachedAnimatorParameters;
|
||||
|
||||
// We cache these values because UnsafeUtility.EnumToInt use direct IL that allows a nonboxing conversion
|
||||
// We cache these values because UnsafeUtility.EnumToInt uses direct IL that allows a non-boxing conversion
|
||||
private struct AnimationParamEnumWrapper
|
||||
{
|
||||
public static readonly int AnimatorControllerParameterInt;
|
||||
@@ -122,25 +88,6 @@ namespace Unity.Netcode.Components
|
||||
}
|
||||
}
|
||||
|
||||
internal void ResetParameterOptions()
|
||||
{
|
||||
|
||||
if (NetworkLog.CurrentLogLevel <= LogLevel.Developer)
|
||||
{
|
||||
NetworkLog.LogInfoServer("ResetParameterOptions");
|
||||
}
|
||||
|
||||
m_ParameterSendBits = 0;
|
||||
}
|
||||
|
||||
private bool sendMessagesAllowed
|
||||
{
|
||||
get
|
||||
{
|
||||
return IsServer && NetworkObject.IsSpawned;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnDestroy()
|
||||
{
|
||||
if (m_CachedAnimatorParameters.IsCreated)
|
||||
@@ -153,25 +100,36 @@ namespace Unity.Netcode.Components
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
if (IsServer)
|
||||
{
|
||||
m_SendMessagesAllowed = true;
|
||||
int layers = m_Animator.layerCount;
|
||||
|
||||
m_TransitionHash = new int[layers];
|
||||
m_AnimationHash = new int[layers];
|
||||
m_LayerWeights = new float[layers];
|
||||
}
|
||||
|
||||
var parameters = m_Animator.parameters;
|
||||
m_CachedAnimatorParameters = new NativeArray<AnimatorParamCache>(parameters.Length, Allocator.Persistent);
|
||||
|
||||
m_AnimationHash = -1;
|
||||
|
||||
for (var i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
var parameter = parameters[i];
|
||||
|
||||
if (m_Animator.IsParameterControlledByCurve(parameter.nameHash))
|
||||
{
|
||||
//we are ignoring parameters that are controlled by animation curves - syncing the layer states indirectly syncs the values that are driven by the animation curves
|
||||
// we are ignoring parameters that are controlled by animation curves - syncing the layer
|
||||
// states indirectly syncs the values that are driven by the animation curves
|
||||
continue;
|
||||
}
|
||||
|
||||
var cacheParam = new AnimatorParamCache();
|
||||
var cacheParam = new AnimatorParamCache
|
||||
{
|
||||
Type = UnsafeUtility.EnumToInt(parameter.type),
|
||||
Hash = parameter.nameHash
|
||||
};
|
||||
|
||||
cacheParam.Type = UnsafeUtility.EnumToInt(parameter.type);
|
||||
cacheParam.Hash = parameter.nameHash;
|
||||
unsafe
|
||||
{
|
||||
switch (parameter.type)
|
||||
@@ -199,115 +157,100 @@ namespace Unity.Netcode.Components
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
m_SendMessagesAllowed = false;
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if (!sendMessagesAllowed)
|
||||
if (!m_SendMessagesAllowed || !m_Animator || !m_Animator.enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int stateHash;
|
||||
float normalizedTime;
|
||||
if (!CheckAnimStateChanged(out stateHash, out normalizedTime))
|
||||
for (int layer = 0; layer < m_Animator.layerCount; layer++)
|
||||
{
|
||||
// We only want to check and send if we don't have any other state to since
|
||||
// as we will sync all params as part of the state sync
|
||||
CheckAndSend();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var animMsg = new AnimationMessage();
|
||||
animMsg.StateHash = stateHash;
|
||||
animMsg.NormalizedTime = normalizedTime;
|
||||
|
||||
m_ParameterWriter.Seek(0);
|
||||
m_ParameterWriter.Truncate();
|
||||
|
||||
WriteParameters(m_ParameterWriter, false);
|
||||
animMsg.Parameters = m_ParameterWriter.ToArray();
|
||||
|
||||
SendAnimStateClientRpc(animMsg);
|
||||
}
|
||||
|
||||
private void CheckAndSend()
|
||||
{
|
||||
var networkTime = NetworkManager.ServerTime.Time;
|
||||
if (sendMessagesAllowed && m_SendRate != 0 && m_NextSendTime < networkTime)
|
||||
{
|
||||
m_NextSendTime = networkTime + m_SendRate;
|
||||
|
||||
m_ParameterWriter.Seek(0);
|
||||
m_ParameterWriter.Truncate();
|
||||
|
||||
if (WriteParameters(m_ParameterWriter, true))
|
||||
{
|
||||
// we then sync the params we care about
|
||||
var animMsg = new AnimationParametersMessage()
|
||||
{
|
||||
Parameters = m_ParameterWriter.ToArray()
|
||||
};
|
||||
|
||||
SendParamsClientRpc(animMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool CheckAnimStateChanged(out int stateHash, out float normalizedTime)
|
||||
{
|
||||
stateHash = 0;
|
||||
normalizedTime = 0;
|
||||
|
||||
if (m_Animator.IsInTransition(0))
|
||||
{
|
||||
AnimatorTransitionInfo tt = m_Animator.GetAnimatorTransitionInfo(0);
|
||||
if (tt.fullPathHash != m_TransitionHash)
|
||||
{
|
||||
// first time in this transition
|
||||
m_TransitionHash = tt.fullPathHash;
|
||||
m_AnimationHash = 0;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
AnimatorStateInfo st = m_Animator.GetCurrentAnimatorStateInfo(0);
|
||||
if (st.fullPathHash != m_AnimationHash)
|
||||
{
|
||||
// first time in this animation state
|
||||
if (m_AnimationHash != 0)
|
||||
{
|
||||
// came from another animation directly - from Play()
|
||||
stateHash = st.fullPathHash;
|
||||
normalizedTime = st.normalizedTime;
|
||||
}
|
||||
m_TransitionHash = 0;
|
||||
m_AnimationHash = st.fullPathHash;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* $AS TODO: Right now we are not checking for changed values this is because
|
||||
the read side of this function doesn't have similar logic which would cause
|
||||
an overflow read because it doesn't know if the value is there or not. So
|
||||
there needs to be logic to track which indexes changed in order for there
|
||||
to be proper value change checking. Will revist in 1.1.0.
|
||||
*/
|
||||
private unsafe bool WriteParameters(FastBufferWriter writer, bool autoSend)
|
||||
{
|
||||
if (m_CachedAnimatorParameters == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < m_CachedAnimatorParameters.Length; i++)
|
||||
{
|
||||
if (autoSend && !GetParameterAutoSend(i))
|
||||
int stateHash;
|
||||
float normalizedTime;
|
||||
if (!CheckAnimStateChanged(out stateHash, out normalizedTime, layer))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var animMsg = new AnimationMessage
|
||||
{
|
||||
StateHash = stateHash,
|
||||
NormalizedTime = normalizedTime,
|
||||
Layer = layer,
|
||||
Weight = m_LayerWeights[layer]
|
||||
};
|
||||
|
||||
m_ParameterWriter.Seek(0);
|
||||
m_ParameterWriter.Truncate();
|
||||
|
||||
WriteParameters(m_ParameterWriter);
|
||||
animMsg.Parameters = m_ParameterWriter.ToArray();
|
||||
|
||||
SendAnimStateClientRpc(animMsg);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CheckAnimStateChanged(out int stateHash, out float normalizedTime, int layer)
|
||||
{
|
||||
bool shouldUpdate = false;
|
||||
stateHash = 0;
|
||||
normalizedTime = 0;
|
||||
|
||||
float layerWeightNow = m_Animator.GetLayerWeight(layer);
|
||||
|
||||
if (!Mathf.Approximately(layerWeightNow, m_LayerWeights[layer]))
|
||||
{
|
||||
m_LayerWeights[layer] = layerWeightNow;
|
||||
shouldUpdate = true;
|
||||
}
|
||||
if (m_Animator.IsInTransition(layer))
|
||||
{
|
||||
AnimatorTransitionInfo tt = m_Animator.GetAnimatorTransitionInfo(layer);
|
||||
if (tt.fullPathHash != m_TransitionHash[layer])
|
||||
{
|
||||
// first time in this transition for this layer
|
||||
m_TransitionHash[layer] = tt.fullPathHash;
|
||||
m_AnimationHash[layer] = 0;
|
||||
shouldUpdate = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AnimatorStateInfo st = m_Animator.GetCurrentAnimatorStateInfo(layer);
|
||||
if (st.fullPathHash != m_AnimationHash[layer])
|
||||
{
|
||||
// first time in this animation state
|
||||
if (m_AnimationHash[layer] != 0)
|
||||
{
|
||||
// came from another animation directly - from Play()
|
||||
stateHash = st.fullPathHash;
|
||||
normalizedTime = st.normalizedTime;
|
||||
}
|
||||
m_TransitionHash[layer] = 0;
|
||||
m_AnimationHash[layer] = st.fullPathHash;
|
||||
shouldUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
return shouldUpdate;
|
||||
}
|
||||
|
||||
/* $AS TODO: Right now we are not checking for changed values this is because
|
||||
the read side of this function doesn't have similar logic which would cause
|
||||
an overflow read because it doesn't know if the value is there or not. So
|
||||
there needs to be logic to track which indexes changed in order for there
|
||||
to be proper value change checking. Will revist in 1.1.0.
|
||||
*/
|
||||
private unsafe void WriteParameters(FastBufferWriter writer)
|
||||
{
|
||||
for (int i = 0; i < m_CachedAnimatorParameters.Length; i++)
|
||||
{
|
||||
ref var cacheValue = ref UnsafeUtility.ArrayElementAsRef<AnimatorParamCache>(m_CachedAnimatorParameters.GetUnsafePtr(), i);
|
||||
var hash = cacheValue.Hash;
|
||||
|
||||
@@ -340,24 +283,12 @@ namespace Unity.Netcode.Components
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we do not write any values to the writer then we should not send any data
|
||||
return writer.Length > 0;
|
||||
}
|
||||
|
||||
private unsafe void ReadParameters(FastBufferReader reader, bool autoSend)
|
||||
private unsafe void ReadParameters(FastBufferReader reader)
|
||||
{
|
||||
if (m_CachedAnimatorParameters == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < m_CachedAnimatorParameters.Length; i++)
|
||||
{
|
||||
if (autoSend && !GetParameterAutoSend(i))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
ref var cacheValue = ref UnsafeUtility.ArrayElementAsRef<AnimatorParamCache>(m_CachedAnimatorParameters.GetUnsafePtr(), i);
|
||||
var hash = cacheValue.Hash;
|
||||
|
||||
@@ -391,28 +322,20 @@ namespace Unity.Netcode.Components
|
||||
}
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
private unsafe void SendParamsClientRpc(AnimationParametersMessage animSnapshot, ClientRpcParams clientRpcParams = default)
|
||||
{
|
||||
if (animSnapshot.Parameters != null)
|
||||
{
|
||||
// We use a fixed value here to avoid the copy of data from the byte buffer since we own the data
|
||||
fixed (byte* parameters = animSnapshot.Parameters)
|
||||
{
|
||||
var reader = new FastBufferReader(parameters, Allocator.None, animSnapshot.Parameters.Length);
|
||||
ReadParameters(reader, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internally-called RPC client receiving function to update some animation parameters on a client when
|
||||
/// the server wants to update them
|
||||
/// </summary>
|
||||
/// <param name="animSnapshot">the payload containing the parameters to apply</param>
|
||||
/// <param name="clientRpcParams">unused</param>
|
||||
[ClientRpc]
|
||||
private unsafe void SendAnimStateClientRpc(AnimationMessage animSnapshot, ClientRpcParams clientRpcParams = default)
|
||||
{
|
||||
if (animSnapshot.StateHash != 0)
|
||||
{
|
||||
m_AnimationHash = animSnapshot.StateHash;
|
||||
m_Animator.Play(animSnapshot.StateHash, 0, animSnapshot.NormalizedTime);
|
||||
m_Animator.Play(animSnapshot.StateHash, animSnapshot.Layer, animSnapshot.NormalizedTime);
|
||||
}
|
||||
m_Animator.SetLayerWeight(animSnapshot.Layer, animSnapshot.Weight);
|
||||
|
||||
if (animSnapshot.Parameters != null && animSnapshot.Parameters.Length != 0)
|
||||
{
|
||||
@@ -420,11 +343,17 @@ namespace Unity.Netcode.Components
|
||||
fixed (byte* parameters = animSnapshot.Parameters)
|
||||
{
|
||||
var reader = new FastBufferReader(parameters, Allocator.None, animSnapshot.Parameters.Length);
|
||||
ReadParameters(reader, false);
|
||||
ReadParameters(reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internally-called RPC client receiving function to update a trigger when the server wants to forward
|
||||
/// a trigger for a client to play / reset
|
||||
/// </summary>
|
||||
/// <param name="animSnapshot">the payload containing the trigger data to apply</param>
|
||||
/// <param name="clientRpcParams">unused</param>
|
||||
[ClientRpc]
|
||||
private void SendAnimTriggerClientRpc(AnimationTriggerMessage animSnapshot, ClientRpcParams clientRpcParams = default)
|
||||
{
|
||||
@@ -438,11 +367,24 @@ namespace Unity.Netcode.Components
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the trigger for the associated animation
|
||||
/// Note, triggers are special vs other kinds of parameters. For all the other parameters we watch for changes
|
||||
/// in FixedUpdate and users can just set them normally off of Animator. But because triggers are transitory
|
||||
/// and likely to come and go between FixedUpdate calls, we require users to set them here to guarantee us to
|
||||
/// catch it...then we forward it to the Animator component
|
||||
/// </summary>
|
||||
/// <param name="triggerName">The string name of the trigger to activate</param>
|
||||
public void SetTrigger(string triggerName)
|
||||
{
|
||||
SetTrigger(Animator.StringToHash(triggerName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the trigger for the associated animation. See note for SetTrigger(string)
|
||||
/// </summary>
|
||||
/// <param name="hash">The hash for the trigger to activate</param>
|
||||
/// <param name="reset">If true, resets the trigger</param>
|
||||
public void SetTrigger(int hash, bool reset = false)
|
||||
{
|
||||
var animMsg = new AnimationTriggerMessage();
|
||||
@@ -451,15 +393,38 @@ namespace Unity.Netcode.Components
|
||||
|
||||
if (IsServer)
|
||||
{
|
||||
// trigger the animation locally on the server...
|
||||
if (reset)
|
||||
{
|
||||
m_Animator.ResetTrigger(hash);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Animator.SetTrigger(hash);
|
||||
}
|
||||
|
||||
// ...then tell all the clients to do the same
|
||||
SendAnimTriggerClientRpc(animMsg);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("Trying to call NetworkAnimator.SetTrigger on a client...ignoring");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the trigger for the associated animation. See note for SetTrigger(string)
|
||||
/// </summary>
|
||||
/// <param name="triggerName">The string name of the trigger to reset</param>
|
||||
public void ResetTrigger(string triggerName)
|
||||
{
|
||||
ResetTrigger(Animator.StringToHash(triggerName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the trigger for the associated animation. See note for SetTrigger(string)
|
||||
/// </summary>
|
||||
/// <param name="hash">The hash for the trigger to reset</param>
|
||||
public void ResetTrigger(int hash)
|
||||
{
|
||||
SetTrigger(hash, true);
|
||||
|
||||
Reference in New Issue
Block a user