fix: client authoritative NetworkAnimator skips sending to 2nd player when in server mode (#2127)

co-authored-by: Vadim Tsvetkov
co-authored-by: Andrei Soprachev
* fix: animator syncronization for >2 clients
* fix: remove redundant rpc call when host
* test
This includes coverage for running in server mode only as well as validates the fix where a second client will receive updates.

co-authored-by: Noel Stephens - Unity
* fix
Fixed some additional issues with trigger synchronization.
Removed the bool from ProcessAnimationMessageQueue as it was no longer needed.

* fix
This is a new approach to synchronizing transitions with late joining players without having to actually set the associated conditional trigger.  By building a small list of all states and then building a quick lookup table, we can just synchronize the "transition state" which for late joining clients is a cross fade between the start and destination state.  We synchronize the normalized time (where it was in the transition on the server side when the client connected) of the transition this way as well. Now, we just synchronize states (which some can be transition states).

* test
increased number of clients
Fixed issue where the late joining client was not being shutdown at the end of the LateJoinSynchronizationTest.

* update
NetworkAnimator uses ISerializationCallbackReceiver to build its transition to states table for late joining client synchronization when a transition is ocurring.

Co-authored-by: Vadim Tsvetkov <florius0@ninsar.pro>
Co-authored-by: Andrei Soprachev <soprachev@mail.ru>
Co-authored-by: Unity Netcode CI <74025435+netcode-ci-service@users.noreply.github.com>
This commit is contained in:
Noel Stephens 2022-09-28 13:18:53 -05:00 коммит произвёл GitHub
Родитель adc6432c87
Коммит f98e7a2d80
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 581 добавлений и 114 удалений

Просмотреть файл

@ -29,8 +29,10 @@ Additional documentation and release notes are available at [Multiplayer Documen
- Fixed Connection Approval Timeout not working client side. (#2164)
- Fixed ClientRpcs always reporting in the profiler view as going to all clients, even when limited to a subset of clients by `ClientRpcParams`. (#2144)
- Fixed RPC codegen failing to choose the correct extension methods for `FastBufferReader` and `FastBufferWriter` when the parameters were a generic type (i.e., List<int>) and extensions for multiple instantiations of that type have been defined (i.e., List<int> and List<string>) (#2142)
- Fixed the issue where running a server (i.e. not host) the second player would not receive updates (unless a third player joined). (#2127)
- Fixed issue where late-joining client transition synchronization could fail when more than one transition was occurring.(#2127)
- Fixed throwing an exception in `OnNetworkUpdate` causing other `OnNetworkUpdate` calls to not be executed. (#1739)
- Fixed synchronisation when Time.timeScale is set to 0. This changes timing update to use unscaled deltatime. Now network updates rate are independant from the local time scale. (#2171)
- Fixed synchronization when Time.timeScale is set to 0. This changes timing update to use unscaled deltatime. Now network updates rate are independent from the local time scale. (#2171)
- Fixed not sending all NetworkVariables to all clients when a client connects to a server. (#1987)
## [1.0.2] - 2022-09-12

Просмотреть файл

@ -1,14 +1,19 @@
#if COM_UNITY_MODULES_ANIMATION
using System;
using System.Collections.Generic;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor.Animations;
#endif
namespace Unity.Netcode.Components
{
internal class NetworkAnimatorStateChangeHandler : INetworkUpdateSystem
{
private NetworkAnimator m_NetworkAnimator;
private bool m_IsServer;
/// <summary>
/// This removes sending RPCs from within RPCs when the
@ -32,7 +37,14 @@ namespace Unity.Netcode.Components
foreach (var sendEntry in m_SendTriggerUpdates)
{
m_NetworkAnimator.SendAnimTriggerClientRpc(sendEntry.AnimationTriggerMessage, sendEntry.ClientRpcParams);
if (!sendEntry.SendToServer)
{
m_NetworkAnimator.SendAnimTriggerClientRpc(sendEntry.AnimationTriggerMessage, sendEntry.ClientRpcParams);
}
else
{
m_NetworkAnimator.SendAnimTriggerServerRpc(sendEntry.AnimationTriggerMessage);
}
}
m_SendTriggerUpdates.Clear();
}
@ -44,8 +56,8 @@ namespace Unity.Netcode.Components
{
case NetworkUpdateStage.PreUpdate:
{
// Only the server forwards messages and synchronizes players
if (m_NetworkAnimator.NetworkManager.IsServer)
// Only the owner or the server send messages
if (m_NetworkAnimator.IsOwner || m_IsServer)
{
// Flush any pending messages
FlushMessages();
@ -125,6 +137,7 @@ namespace Unity.Netcode.Components
private struct TriggerUpdate
{
public bool SendToServer;
public ClientRpcParams ClientRpcParams;
public NetworkAnimator.AnimationTriggerMessage AnimationTriggerMessage;
}
@ -134,11 +147,23 @@ namespace Unity.Netcode.Components
/// <summary>
/// Invoked when a server needs to forward an update to a Trigger state
/// </summary>
internal void SendTriggerUpdate(NetworkAnimator.AnimationTriggerMessage animationTriggerMessage, ClientRpcParams clientRpcParams = default)
internal void QueueTriggerUpdateToClient(NetworkAnimator.AnimationTriggerMessage animationTriggerMessage, ClientRpcParams clientRpcParams = default)
{
m_SendTriggerUpdates.Add(new TriggerUpdate() { ClientRpcParams = clientRpcParams, AnimationTriggerMessage = animationTriggerMessage });
}
internal void QueueTriggerUpdateToServer(NetworkAnimator.AnimationTriggerMessage animationTriggerMessage)
{
m_SendTriggerUpdates.Add(new TriggerUpdate() { AnimationTriggerMessage = animationTriggerMessage, SendToServer = true });
}
private Queue<NetworkAnimator.AnimationMessage> m_AnimationMessageQueue = new Queue<NetworkAnimator.AnimationMessage>();
internal void AddAnimationMessageToProcessQueue(NetworkAnimator.AnimationMessage message)
{
m_AnimationMessageQueue.Enqueue(message);
}
internal void DeregisterUpdate()
{
NetworkUpdateLoop.UnregisterNetworkUpdate(this, NetworkUpdateStage.PreUpdate);
@ -147,34 +172,278 @@ namespace Unity.Netcode.Components
internal NetworkAnimatorStateChangeHandler(NetworkAnimator networkAnimator)
{
m_NetworkAnimator = networkAnimator;
m_IsServer = networkAnimator.NetworkManager.IsServer;
NetworkUpdateLoop.RegisterNetworkUpdate(this, NetworkUpdateStage.PreUpdate);
}
}
/// <summary>
/// NetworkAnimator enables remote synchronization of <see cref="UnityEngine.Animator"/> state for on network objects.
/// </summary>
[AddComponentMenu("Netcode/Network Animator")]
[RequireComponent(typeof(Animator))]
public class NetworkAnimator : NetworkBehaviour
public class NetworkAnimator : NetworkBehaviour, ISerializationCallbackReceiver
{
internal struct AnimationMessage : INetworkSerializable
[Serializable]
internal class TransitionStateinfo
{
// state hash per layer. if non-zero, then Play() this animation, skipping transitions
internal bool Transition;
public int Layer;
public int OriginatingState;
public int DestinationState;
public float TransitionDuration;
public int TriggerNameHash;
public int TransitionIndex;
}
/// <summary>
/// Used to build the destination state to transition info table
/// </summary>
[HideInInspector]
[SerializeField]
internal List<TransitionStateinfo> TransitionStateInfoList;
// Used to get the associated transition information required to synchronize late joining clients with transitions
// [Layer][DestinationState][TransitionStateInfo]
private Dictionary<int, Dictionary<int, TransitionStateinfo>> m_DestinationStateToTransitioninfo = new Dictionary<int, Dictionary<int, TransitionStateinfo>>();
/// <summary>
/// Builds the m_DestinationStateToTransitioninfo lookup table
/// </summary>
private void BuildDestinationToTransitionInfoTable()
{
foreach (var entry in TransitionStateInfoList)
{
if (!m_DestinationStateToTransitioninfo.ContainsKey(entry.Layer))
{
m_DestinationStateToTransitioninfo.Add(entry.Layer, new Dictionary<int, TransitionStateinfo>());
}
var destinationStateTransitionInfo = m_DestinationStateToTransitioninfo[entry.Layer];
if (!destinationStateTransitionInfo.ContainsKey(entry.DestinationState))
{
destinationStateTransitionInfo.Add(entry.DestinationState, entry);
}
}
}
/// <summary>
/// Creates the
/// </summary>
private void BuildTransitionStateInfoList()
{
#if UNITY_EDITOR
TransitionStateInfoList = new List<TransitionStateinfo>();
var animatorController = m_Animator.runtimeAnimatorController as AnimatorController;
if (animatorController == null)
{
return;
}
for (int x = 0; x < animatorController.layers.Length; x++)
{
var layer = animatorController.layers[x];
for (int y = 0; y < layer.stateMachine.states.Length; y++)
{
var animatorState = layer.stateMachine.states[y].state;
var transitions = layer.stateMachine.GetStateMachineTransitions(layer.stateMachine);
for (int z = 0; z < animatorState.transitions.Length; z++)
{
var transition = animatorState.transitions[z];
if (transition.conditions.Length == 0 && transition.isExit)
{
// We don't need to worry about exit transitions with no conditions
continue;
}
foreach (var condition in transition.conditions)
{
var parameterName = condition.parameter;
var parameters = animatorController.parameters;
foreach (var parameter in parameters)
{
switch (parameter.type)
{
case AnimatorControllerParameterType.Trigger:
{
// Match the condition with an existing trigger
if (parameterName == parameter.name)
{
var transitionInfo = new TransitionStateinfo()
{
Layer = x,
OriginatingState = animatorState.nameHash,
DestinationState = transition.destinationState.nameHash,
TransitionDuration = transition.duration,
TriggerNameHash = parameter.nameHash,
TransitionIndex = z
};
TransitionStateInfoList.Add(transitionInfo);
}
break;
}
default:
break;
}
}
}
}
}
}
#endif
}
public void OnAfterDeserialize()
{
BuildDestinationToTransitionInfoTable();
}
public void OnBeforeSerialize()
{
BuildTransitionStateInfoList();
}
internal struct AnimationState : INetworkSerializable
{
internal bool IsDirty;
// Not to be serialized, used for processing the animation state
internal bool HasBeenProcessed;
internal int StateHash;
internal float NormalizedTime;
internal int Layer;
internal float Weight;
// For synchronizing transitions
internal bool Transition;
// The StateHash is where the transition starts
// and the DestinationStateHash is the destination state
internal int DestinationStateHash;
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref StateHash);
serializer.SerializeValue(ref NormalizedTime);
serializer.SerializeValue(ref Layer);
serializer.SerializeValue(ref Weight);
if (serializer.IsWriter)
{
var writer = serializer.GetFastBufferWriter();
var writeSize = FastBufferWriter.GetWriteSize(Transition);
writeSize += FastBufferWriter.GetWriteSize(StateHash);
writeSize += FastBufferWriter.GetWriteSize(NormalizedTime);
writeSize += FastBufferWriter.GetWriteSize(Layer);
writeSize += FastBufferWriter.GetWriteSize(Weight);
if (Transition)
{
writeSize += FastBufferWriter.GetWriteSize(DestinationStateHash);
}
if (!writer.TryBeginWrite(writeSize))
{
throw new OverflowException($"[{GetType().Name}] Could not serialize: Out of buffer space.");
}
writer.WriteValue(Transition);
writer.WriteValue(StateHash);
writer.WriteValue(NormalizedTime);
writer.WriteValue(Layer);
writer.WriteValue(Weight);
if (Transition)
{
writer.WriteValue(DestinationStateHash);
}
}
else
{
var reader = serializer.GetFastBufferReader();
// Begin reading the Transition flag
if (!reader.TryBeginRead(FastBufferWriter.GetWriteSize(Transition)))
{
throw new OverflowException($"[{GetType().Name}] Could not deserialize: Out of buffer space.");
}
reader.ReadValue(out Transition);
// Now determine what remains to be read
var readSize = FastBufferWriter.GetWriteSize(StateHash);
readSize += FastBufferWriter.GetWriteSize(NormalizedTime);
readSize += FastBufferWriter.GetWriteSize(Layer);
readSize += FastBufferWriter.GetWriteSize(Weight);
if (Transition)
{
readSize += FastBufferWriter.GetWriteSize(DestinationStateHash);
}
// Now read the remaining information about this AnimationState
if (!reader.TryBeginRead(readSize))
{
throw new OverflowException($"[{GetType().Name}] Could not deserialize: Out of buffer space.");
}
reader.ReadValue(out StateHash);
reader.ReadValue(out NormalizedTime);
reader.ReadValue(out Layer);
reader.ReadValue(out Weight);
if (Transition)
{
reader.ReadValue(out DestinationStateHash);
}
}
}
}
internal struct AnimationMessage : INetworkSerializable
{
// Not to be serialized, used for processing the animation message
internal bool HasBeenProcessed;
// state hash per layer. if non-zero, then Play() this animation, skipping transitions
internal List<AnimationState> AnimationStates;
/// <summary>
/// Resets all AnimationStates' IsDirty flag
/// </summary>
internal void ClearDirty()
{
if (AnimationStates == null)
{
return;
}
for (int i = 0; i < AnimationStates.Count; i++)
{
var animationState = AnimationStates[i];
animationState.IsDirty = false;
AnimationStates[i] = animationState;
}
}
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
if (serializer.IsReader)
{
if (AnimationStates == null)
{
AnimationStates = new List<AnimationState>();
}
else if (AnimationStates.Count > 0)
{
AnimationStates.Clear();
}
}
var count = AnimationStates.Count;
serializer.SerializeValue(ref count);
var animationState = new AnimationState();
for (int i = 0; i < count; i++)
{
if (serializer.IsWriter)
{
if (AnimationStates[i].IsDirty)
{
animationState = AnimationStates[i];
}
}
serializer.SerializeNetworkSerializable(ref animationState);
if (serializer.IsReader)
{
AnimationStates.Add(animationState);
}
}
}
}
@ -223,7 +492,8 @@ namespace Unity.Netcode.Components
return true;
}
// Animators only support up to 32 params
// Animators only support up to 32 parameters
// TODO: Look into making this a range limited property
private const int k_MaxAnimationParams = 32;
private int[] m_TransitionHash;
@ -269,9 +539,9 @@ namespace Unity.Netcode.Components
m_NetworkAnimatorStateChangeHandler = null;
}
if (IsServer)
if (m_CachedNetworkManager != null)
{
NetworkManager.OnClientConnectedCallback -= OnClientConnectedCallback;
m_CachedNetworkManager.OnClientConnectedCallback -= OnClientConnectedCallback;
}
if (m_CachedAnimatorParameters != null && m_CachedAnimatorParameters.IsCreated)
@ -293,40 +563,52 @@ namespace Unity.Netcode.Components
private List<int> m_ParametersToUpdate;
private List<ulong> m_ClientSendList;
private ClientRpcParams m_ClientRpcParams;
private List<AnimationState> m_AnimationMessageStates;
// Only used in Cleanup
private NetworkManager m_CachedNetworkManager;
/// <inheritdoc/>
public override void OnNetworkSpawn()
{
if (IsOwner || IsServer)
int layers = m_Animator.layerCount;
// Initializing the below arrays for everyone handles an issue
// when running in owner authoritative mode and the owner changes.
m_TransitionHash = new int[layers];
m_AnimationHash = new int[layers];
m_LayerWeights = new float[layers];
if (IsServer)
{
int layers = m_Animator.layerCount;
m_TransitionHash = new int[layers];
m_AnimationHash = new int[layers];
m_LayerWeights = new float[layers];
m_ClientSendList = new List<ulong>(128);
m_ClientRpcParams = new ClientRpcParams();
m_ClientRpcParams.Send = new ClientRpcSendParams();
m_ClientRpcParams.Send.TargetClientIds = m_ClientSendList;
if (IsServer)
{
NetworkManager.OnClientConnectedCallback += OnClientConnectedCallback;
}
// Cache the NetworkManager instance to remove the OnClientConnectedCallback subscription
m_CachedNetworkManager = NetworkManager;
NetworkManager.OnClientConnectedCallback += OnClientConnectedCallback;
}
// Store off our current layer weights
for (int layer = 0; layer < m_Animator.layerCount; layer++)
{
float layerWeightNow = m_Animator.GetLayerWeight(layer);
if (layerWeightNow != m_LayerWeights[layer])
{
m_LayerWeights[layer] = layerWeightNow;
}
}
// !! Note !!
// Do not clear this list. We re-use the AnimationState entries
// initialized below
m_AnimationMessageStates = new List<AnimationState>();
if (IsServer)
// Store off our current layer weights and create our animation
// state entries per layer.
for (int layer = 0; layer < m_Animator.layerCount; layer++)
{
m_AnimationMessageStates.Add(new AnimationState());
float layerWeightNow = m_Animator.GetLayerWeight(layer);
if (layerWeightNow != m_LayerWeights[layer])
{
m_ClientSendList = new List<ulong>(128);
m_ClientRpcParams = new ClientRpcParams();
m_ClientRpcParams.Send = new ClientRpcSendParams();
m_ClientRpcParams.Send.TargetClientIds = m_ClientSendList;
m_LayerWeights[layer] = layerWeightNow;
}
}
// Build our reference parameter values to detect when they change
var parameters = m_Animator.parameters;
m_CachedAnimatorParameters = new NativeArray<AnimatorParamCache>(parameters.Length, Allocator.Persistent);
m_ParametersToUpdate = new List<int>(parameters.Length);
@ -373,6 +655,7 @@ namespace Unity.Netcode.Components
m_NetworkAnimatorStateChangeHandler = new NetworkAnimatorStateChangeHandler(this);
}
/// <inheritdoc/>
public override void OnNetworkDespawn()
{
Cleanup();
@ -393,18 +676,25 @@ namespace Unity.Netcode.Components
m_ParametersToUpdate.Add(i);
}
SendParametersUpdate(m_ClientRpcParams);
var animationMessage = new AnimationMessage
{
// Assign the existing m_AnimationMessageStates list
AnimationStates = m_AnimationMessageStates
};
for (int layer = 0; layer < m_Animator.layerCount; layer++)
{
AnimatorStateInfo st = m_Animator.GetCurrentAnimatorStateInfo(layer);
var stateHash = st.fullPathHash;
var normalizedTime = st.normalizedTime;
var totalSpeed = st.speed * st.speedMultiplier;
var adjustedNormalizedMaxTime = totalSpeed > 0.0f ? 1.0f / totalSpeed : 0.0f;
// NOTE:
// When synchronizing, for now we will just complete the transition and
// synchronize the player to the next state being transitioned into
if (m_Animator.IsInTransition(layer))
var isInTransition = m_Animator.IsInTransition(layer);
var animMsg = m_AnimationMessageStates[layer];
// Synchronizing transitions with trigger conditions for late joining clients is now
// handled by cross fading between the late joining client's current layer's AnimationState
// and the transition's destination AnimationState.
if (isInTransition)
{
var tt = m_Animator.GetAnimatorTransitionInfo(layer);
var nextState = m_Animator.GetNextAnimatorStateInfo(layer);
@ -422,23 +712,41 @@ namespace Unity.Netcode.Components
{
normalizedTime = 0.0f;
}
stateHash = nextState.fullPathHash;
// Use the destination state to transition info lookup table to see if this is a transition we can
// synchronize using cross fading
if (m_DestinationStateToTransitioninfo.ContainsKey(layer))
{
if (m_DestinationStateToTransitioninfo[layer].ContainsKey(nextState.shortNameHash))
{
var destinationInfo = m_DestinationStateToTransitioninfo[layer][nextState.shortNameHash];
stateHash = destinationInfo.OriginatingState;
// Set the destination state to cross fade to from the originating state
animMsg.DestinationStateHash = destinationInfo.DestinationState;
}
}
}
var animMsg = new AnimationMessage
{
Transition = m_Animator.IsInTransition(layer),
StateHash = stateHash,
NormalizedTime = normalizedTime,
Layer = layer,
Weight = m_LayerWeights[layer]
};
animMsg.Transition = isInTransition; // The only time this could be set to true
animMsg.StateHash = stateHash; // When a transition, this is the originating/starting state
animMsg.NormalizedTime = normalizedTime;
animMsg.Layer = layer;
animMsg.Weight = m_LayerWeights[layer];
animMsg.IsDirty = true;
m_AnimationMessageStates[layer] = animMsg;
}
if (animationMessage.AnimationStates.Count > 0)
{
// Server always send via client RPC
SendAnimStateClientRpc(animMsg, m_ClientRpcParams);
SendAnimStateClientRpc(animationMessage, m_ClientRpcParams);
animationMessage.ClearDirty();
}
}
/// <summary>
/// Required for the server to synchronize newly joining players
/// </summary>
private void OnClientConnectedCallback(ulong playerId)
{
m_NetworkAnimatorStateChangeHandler.SynchronizeClient(playerId);
@ -461,47 +769,59 @@ namespace Unity.Netcode.Components
if (m_Animator.runtimeAnimatorController == null)
{
if (NetworkManager.LogLevel == LogLevel.Developer)
{
Debug.LogError($"[{GetType().Name}] Could not find an assigned {nameof(RuntimeAnimatorController)}! Cannot check {nameof(Animator)} for changes in state!");
}
return;
}
int stateHash;
float normalizedTime;
// This sends updates only if a layer change or transition is happening
var animationMessage = new AnimationMessage
{
// Assign the existing m_AnimationMessageStates list
AnimationStates = m_AnimationMessageStates
};
// This sends updates only if a layer's AnimationState changes
for (int layer = 0; layer < m_Animator.layerCount; layer++)
{
AnimatorStateInfo st = m_Animator.GetCurrentAnimatorStateInfo(layer);
var totalSpeed = st.speed * st.speedMultiplier;
var adjustedNormalizedMaxTime = totalSpeed > 0.0f ? 1.0f / totalSpeed : 0.0f;
// determine if we have reached the end of our state time, if so we can skip
if (st.normalizedTime >= adjustedNormalizedMaxTime)
{
continue;
}
if (!CheckAnimStateChanged(out stateHash, out normalizedTime, layer))
{
continue;
}
var animMsg = new AnimationMessage
var animationState = new AnimationState
{
Transition = m_Animator.IsInTransition(layer),
IsDirty = true,
Transition = false, // Only used during synchronization
StateHash = stateHash,
NormalizedTime = normalizedTime,
Layer = layer,
Weight = m_LayerWeights[layer]
};
animationMessage.AnimationStates.Add(animationState);
}
// Make sure there is something to send
if (animationMessage.AnimationStates.Count > 0)
{
if (!IsServer && IsOwner)
{
SendAnimStateServerRpc(animMsg);
SendAnimStateServerRpc(animationMessage);
}
else
{
SendAnimStateClientRpc(animMsg);
SendAnimStateClientRpc(animationMessage);
}
animationMessage.ClearDirty();
}
}
@ -596,7 +916,7 @@ namespace Unity.Netcode.Components
/// <summary>
/// Checks if any of the Animator's states have changed
/// </summary>
private unsafe bool CheckAnimStateChanged(out int stateHash, out float normalizedTime, int layer)
private bool CheckAnimStateChanged(out int stateHash, out float normalizedTime, int layer)
{
stateHash = 0;
normalizedTime = 0;
@ -746,9 +1066,9 @@ namespace Unity.Netcode.Components
}
/// <summary>
/// Applies the AnimationMessage state to the Animator
/// Applies the AnimationState state to the Animator
/// </summary>
private unsafe void UpdateAnimationState(AnimationMessage animationState)
internal void UpdateAnimationState(AnimationState animationState)
{
if (animationState.StateHash == 0)
{
@ -756,9 +1076,46 @@ namespace Unity.Netcode.Components
}
var currentState = m_Animator.GetCurrentAnimatorStateInfo(animationState.Layer);
if (currentState.fullPathHash != animationState.StateHash || m_Animator.IsInTransition(animationState.Layer) != animationState.Transition)
// If it is a transition, then we are synchronizing transitions in progress when a client late joins
if (animationState.Transition)
{
m_Animator.Play(animationState.StateHash, animationState.Layer, animationState.NormalizedTime);
// We should have all valid entries for any animation state transition update
// Verify the AnimationState's assigned Layer exists
if (m_DestinationStateToTransitioninfo.ContainsKey(animationState.Layer))
{
// Verify the inner-table has the destination AnimationState name hash
if (m_DestinationStateToTransitioninfo[animationState.Layer].ContainsKey(animationState.DestinationStateHash))
{
// Make sure we are on the originating/starting state we are going to cross fade into
if (currentState.shortNameHash == animationState.StateHash)
{
// Get the transition state information
var transitionStateInfo = m_DestinationStateToTransitioninfo[animationState.Layer][animationState.DestinationStateHash];
// Cross fade from the current to the destination state for the transitions duration while starting at the server's current normalized time of the transition
m_Animator.CrossFade(transitionStateInfo.DestinationState, transitionStateInfo.TransitionDuration, transitionStateInfo.Layer, 0.0f, animationState.NormalizedTime);
}
else if (NetworkManager.LogLevel == LogLevel.Developer)
{
NetworkLog.LogWarning($"Current State Hash ({currentState.fullPathHash}) != AnimationState.StateHash ({animationState.StateHash})");
}
}
else if (NetworkManager.LogLevel == LogLevel.Developer)
{
NetworkLog.LogError($"[DestinationState To Transition Info] Layer ({animationState.Layer}) sub-table does not contain destination state ({animationState.DestinationStateHash})!");
}
}
else if (NetworkManager.LogLevel == LogLevel.Developer)
{
NetworkLog.LogError($"[DestinationState To Transition Info] Layer ({animationState.Layer}) does not exist!");
}
}
else
{
if (currentState.fullPathHash != animationState.StateHash)
{
m_Animator.Play(animationState.StateHash, animationState.Layer, animationState.NormalizedTime);
}
}
m_Animator.SetLayerWeight(animationState.Layer, animationState.Weight);
}
@ -781,7 +1138,7 @@ namespace Unity.Netcode.Components
return;
}
UpdateParameters(parametersUpdate);
if (NetworkManager.ConnectedClientsIds.Count - 2 > 0)
if (NetworkManager.ConnectedClientsIds.Count > (IsHost ? 2 : 1))
{
m_ClientSendList.Clear();
m_ClientSendList.AddRange(NetworkManager.ConnectedClientsIds);
@ -811,11 +1168,11 @@ namespace Unity.Netcode.Components
/// The server sets its local state and then forwards the message to the remaining clients
/// </summary>
[ServerRpc]
private unsafe void SendAnimStateServerRpc(AnimationMessage animSnapshot, ServerRpcParams serverRpcParams = default)
private unsafe void SendAnimStateServerRpc(AnimationMessage animationMessage, ServerRpcParams serverRpcParams = default)
{
if (IsServerAuthoritative())
{
m_NetworkAnimatorStateChangeHandler.SendAnimationUpdate(animSnapshot);
m_NetworkAnimatorStateChangeHandler.SendAnimationUpdate(animationMessage);
}
else
{
@ -823,15 +1180,21 @@ namespace Unity.Netcode.Components
{
return;
}
UpdateAnimationState(animSnapshot);
if (NetworkManager.ConnectedClientsIds.Count - 2 > 0)
foreach (var animationState in animationMessage.AnimationStates)
{
UpdateAnimationState(animationState);
}
m_NetworkAnimatorStateChangeHandler.AddAnimationMessageToProcessQueue(animationMessage);
if (NetworkManager.ConnectedClientsIds.Count > (IsHost ? 2 : 1))
{
m_ClientSendList.Clear();
m_ClientSendList.AddRange(NetworkManager.ConnectedClientsIds);
m_ClientSendList.Remove(serverRpcParams.Receive.SenderClientId);
m_ClientSendList.Remove(NetworkManager.ServerClientId);
m_ClientRpcParams.Send.TargetClientIds = m_ClientSendList;
m_NetworkAnimatorStateChangeHandler.SendAnimationUpdate(animSnapshot, m_ClientRpcParams);
m_NetworkAnimatorStateChangeHandler.SendAnimationUpdate(animationMessage, m_ClientRpcParams);
}
}
}
@ -840,16 +1203,25 @@ namespace Unity.Netcode.Components
/// Internally-called RPC client receiving function to update some animation state on a client
/// </summary>
[ClientRpc]
private unsafe void SendAnimStateClientRpc(AnimationMessage animSnapshot, ClientRpcParams clientRpcParams = default)
private unsafe void SendAnimStateClientRpc(AnimationMessage animationMessage, ClientRpcParams clientRpcParams = default)
{
if (IsServer)
// This should never happen
if (IsHost)
{
if (NetworkManager.LogLevel == LogLevel.Developer)
{
NetworkLog.LogWarning("Detected the Host is sending itself animation updates! Please report this issue.");
}
return;
}
var isServerAuthoritative = IsServerAuthoritative();
if (!isServerAuthoritative && !IsOwner || isServerAuthoritative)
{
UpdateAnimationState(animSnapshot);
foreach (var animationState in animationMessage.AnimationStates)
{
UpdateAnimationState(animationState);
}
}
}
@ -858,44 +1230,67 @@ namespace Unity.Netcode.Components
/// The server sets its local state and then forwards the message to the remaining clients
/// </summary>
[ServerRpc]
private void SendAnimTriggerServerRpc(AnimationTriggerMessage animationTriggerMessage, ServerRpcParams serverRpcParams = default)
internal void SendAnimTriggerServerRpc(AnimationTriggerMessage animationTriggerMessage, ServerRpcParams serverRpcParams = default)
{
// If it is server authoritative
if (IsServerAuthoritative())
{
m_NetworkAnimatorStateChangeHandler.SendTriggerUpdate(animationTriggerMessage);
// The only condition where this should (be allowed to) happen is when the owner sends the server a trigger message
if (OwnerClientId == serverRpcParams.Receive.SenderClientId)
{
m_NetworkAnimatorStateChangeHandler.QueueTriggerUpdateToClient(animationTriggerMessage);
}
else if (NetworkManager.LogLevel == LogLevel.Developer)
{
NetworkLog.LogWarning($"[Server Authoritative] Detected the a non-authoritative client is sending the server animation trigger updates. If you recently changed ownership of the {name} object, then this could be the reason.");
}
}
else
{
// Ignore if a non-owner sent this.
if (serverRpcParams.Receive.SenderClientId != OwnerClientId)
{
if (NetworkManager.LogLevel == LogLevel.Developer)
{
NetworkLog.LogWarning($"[Owner Authoritative] Detected the a non-authoritative client is sending the server animation trigger updates. If you recently changed ownership of the {name} object, then this could be the reason.");
}
return;
}
// trigger the animation locally on the server...
m_Animator.SetBool(animationTriggerMessage.Hash, animationTriggerMessage.IsTriggerSet);
if (NetworkManager.ConnectedClientsIds.Count - 2 > 0)
// set the trigger locally on the server
InternalSetTrigger(animationTriggerMessage.Hash, animationTriggerMessage.IsTriggerSet);
// send the message to all non-authority clients excluding the server and the owner
if (NetworkManager.ConnectedClientsIds.Count > (IsHost ? 2 : 1))
{
m_ClientSendList.Clear();
m_ClientSendList.AddRange(NetworkManager.ConnectedClientsIds);
m_ClientSendList.Remove(serverRpcParams.Receive.SenderClientId);
m_ClientSendList.Remove(NetworkManager.ServerClientId);
m_ClientRpcParams.Send.TargetClientIds = m_ClientSendList;
m_NetworkAnimatorStateChangeHandler.SendTriggerUpdate(animationTriggerMessage, m_ClientRpcParams);
m_NetworkAnimatorStateChangeHandler.QueueTriggerUpdateToClient(animationTriggerMessage, m_ClientRpcParams);
}
}
}
/// <summary>
/// See above <see cref="m_LastTriggerHash"/>
/// </summary>
private void InternalSetTrigger(int hash, bool isSet = true)
{
m_Animator.SetBool(hash, isSet);
}
/// <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="animationTriggerMessage">the payload containing the trigger data to apply</param>
/// <param name="clientRpcParams">unused</param>
[ClientRpc]
internal void SendAnimTriggerClientRpc(AnimationTriggerMessage animationTriggerMessage, ClientRpcParams clientRpcParams = default)
{
m_Animator.SetBool(animationTriggerMessage.Hash, animationTriggerMessage.IsTriggerSet);
InternalSetTrigger(animationTriggerMessage.Hash, animationTriggerMessage.IsTriggerSet);
}
/// <summary>
@ -923,14 +1318,20 @@ namespace Unity.Netcode.Components
var animTriggerMessage = new AnimationTriggerMessage() { Hash = hash, IsTriggerSet = setTrigger };
if (IsServer)
{
SendAnimTriggerClientRpc(animTriggerMessage);
/// <see cref="UpdatePendingTriggerStates"/> as to why we queue
m_NetworkAnimatorStateChangeHandler.QueueTriggerUpdateToClient(animTriggerMessage);
if (!IsHost)
{
InternalSetTrigger(hash);
}
}
else
{
SendAnimTriggerServerRpc(animTriggerMessage);
/// <see cref="UpdatePendingTriggerStates"/> as to why we queue
m_NetworkAnimatorStateChangeHandler.QueueTriggerUpdateToServer(animTriggerMessage);
if (!IsServerAuthoritative())
{
m_Animator.SetTrigger(hash);
InternalSetTrigger(hash);
}
}
}

Просмотреть файл

@ -9,6 +9,7 @@ namespace TestProject.RuntimeTests
public static Dictionary<ulong, Dictionary<int, List<AnimatorStateInfo>>> OnStateEnterCounter = new Dictionary<ulong, Dictionary<int, List<AnimatorStateInfo>>>();
public static bool IsIntegrationTest;
public static bool IsManualTestEnabled = true;
public static bool IsVerboseDebug = false;
public static void ResetTest(bool isIntegrationTest = true)
{
@ -17,6 +18,14 @@ namespace TestProject.RuntimeTests
OnStateEnterCounter.Clear();
}
public static void LogMessage(string message)
{
if (IsVerboseDebug)
{
Debug.Log(message);
}
}
public static bool AllStatesEnteredMatch(List<ulong> clientIdsToCheck)
{
if (clientIdsToCheck.Contains(NetworkManager.ServerClientId))
@ -26,7 +35,7 @@ namespace TestProject.RuntimeTests
if (!OnStateEnterCounter.ContainsKey(NetworkManager.ServerClientId))
{
Debug.Log($"Server has not entered into any states! OnStateEntered Entry Count ({OnStateEnterCounter.Count})");
LogMessage($"Server has not entered into any states! OnStateEntered Entry Count ({OnStateEnterCounter.Count})");
return false;
}
@ -38,7 +47,11 @@ namespace TestProject.RuntimeTests
var layerStates = layerEntries.Value;
if (layerStates.Count > 1)
{
Debug.Log($"Server layer ({layerIndex}) state was entered ({layerStates.Count}) times!");
if (IsVerboseDebug)
{
}
LogMessage($"Server layer ({layerIndex}) state was entered ({layerStates.Count}) times!");
return false;
}
@ -46,7 +59,7 @@ namespace TestProject.RuntimeTests
{
if (!OnStateEnterCounter.ContainsKey(clientId))
{
Debug.Log($"Client-{clientId} never entered into any state for layer index ({layerIndex})!");
LogMessage($"Client-{clientId} never entered into any state for layer index ({layerIndex})!");
return false;
}
var clientStates = OnStateEnterCounter[clientId];
@ -58,7 +71,7 @@ namespace TestProject.RuntimeTests
var clientLayerStateEntries = clientStates[layerIndex];
if (clientLayerStateEntries.Count > 1)
{
Debug.Log($"Client-{clientId} layer ({layerIndex}) state was entered ({layerStates.Count}) times!");
LogMessage($"Client-{clientId} layer ({layerIndex}) state was entered ({layerStates.Count}) times!");
return false;
}
// We should have only entered into the state once on the server
@ -68,7 +81,7 @@ namespace TestProject.RuntimeTests
// We just need to make sure we are looking at the same state
if (clientAnimStateInfo.fullPathHash != serverAnimStateInfo.fullPathHash)
{
Debug.Log($"Client-{clientId} full path hash ({clientAnimStateInfo.fullPathHash}) for layer ({layerIndex}) was not the same as the Server full path hash ({serverAnimStateInfo.fullPathHash})!");
LogMessage($"Client-{clientId} full path hash ({clientAnimStateInfo.fullPathHash}) for layer ({layerIndex}) was not the same as the Server full path hash ({serverAnimStateInfo.fullPathHash})!");
return false;
}
}
@ -91,11 +104,36 @@ namespace TestProject.RuntimeTests
OnStateEnterCounter[localClientId].Add(layerIndex, new List<AnimatorStateInfo>());
}
OnStateEnterCounter[localClientId][layerIndex].Add(stateInfo);
LogMessage($"[{layerIndex}][{stateInfo.shortNameHash}][{stateInfo.normalizedTime}][{animator.IsInTransition(layerIndex)}]");
}
else if (IsManualTestEnabled)
{
Debug.Log($"[{layerIndex}][{stateInfo.shortNameHash}][{stateInfo.normalizedTime}][{animator.IsInTransition(layerIndex)}]");
}
}
//public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
//{
// if (IsIntegrationTest)
// {
// var networkObject = animator.GetComponent<NetworkObject>();
// var localClientId = networkObject.NetworkManager.IsServer ? NetworkManager.ServerClientId : networkObject.NetworkManager.LocalClientId;
// if (!OnStateEnterCounter.ContainsKey(localClientId))
// {
// OnStateEnterCounter.Add(localClientId, new Dictionary<int, List<AnimatorStateInfo>>());
// if (!OnStateEnterCounter[localClientId].ContainsKey(layerIndex))
// {
// OnStateEnterCounter[localClientId].Add(layerIndex, new List<AnimatorStateInfo>());
// }
// OnStateEnterCounter[localClientId][layerIndex].Add(stateInfo);
// LogMessage($"[{layerIndex}][{stateInfo.shortNameHash}][{stateInfo.normalizedTime}][{animator.IsInTransition(layerIndex)}]");
// }
// }
// else if (IsManualTestEnabled)
// {
// Debug.Log($"[{layerIndex}][{stateInfo.shortNameHash}][{stateInfo.normalizedTime}][{animator.IsInTransition(layerIndex)}]");
// }
// base.OnStateUpdate(animator, stateInfo, layerIndex);
//}
}
}

Просмотреть файл

@ -108,9 +108,25 @@ namespace TestProject.RuntimeTests
return m_Animator.GetBool("TestTrigger");
}
public void SetTrigger(string name = "TestTrigger")
public void SetTrigger(string name = "TestTrigger", bool monitorTrigger = false)
{
m_NetworkAnimator.SetTrigger(name);
if (monitorTrigger && IsServer)
{
StartCoroutine(TriggerMonitor(name));
}
}
private System.Collections.IEnumerator TriggerMonitor(string triggerName)
{
var triggerStatus = m_Animator.GetBool(triggerName);
var waitTime = new WaitForSeconds(2 * (1.0f / NetworkManager.NetworkConfig.TickRate));
while (triggerStatus)
{
Debug.Log($"[{triggerName}] is still triggered.");
yield return waitTime;
}
Debug.Log($"[{triggerName}] is no longer triggered.");
}
public void SetLateJoinParam(bool isEnabled)

Просмотреть файл

@ -19,12 +19,14 @@ namespace TestProject.RuntimeTests
/// Possibly we could build this at runtime, but for now it uses the same animator controller as the manual
/// test does.
/// </summary>
[TestFixture(HostOrServer.Host)]
[TestFixture(HostOrServer.Server)]
public class NetworkAnimatorTests : NetcodeIntegrationTest
{
private const string k_AnimatorObjectName = "AnimatorObject";
private const string k_OwnerAnimatorObjectName = "OwnerAnimatorObject";
protected override int NumberOfClients => 1;
protected override int NumberOfClients => 3;
private GameObject m_AnimationTestPrefab => m_AnimatorObjectPrefab ? m_AnimatorObjectPrefab as GameObject : null;
private GameObject m_AnimationOwnerTestPrefab => m_OwnerAnimatorObjectPrefab ? m_OwnerAnimatorObjectPrefab as GameObject : null;
@ -32,6 +34,11 @@ namespace TestProject.RuntimeTests
private Object m_AnimatorObjectPrefab;
private Object m_OwnerAnimatorObjectPrefab;
public NetworkAnimatorTests(HostOrServer hostOrServer)
{
m_UseHost = hostOrServer == HostOrServer.Host;
}
protected override void OnOneTimeSetup()
{
m_AnimatorObjectPrefab = Resources.Load(k_AnimatorObjectName);
@ -315,6 +322,7 @@ namespace TestProject.RuntimeTests
{
VerboseDebug($" ++++++++++++++++++ Late Join Trigger Test [{TriggerTest.Iteration}][{ownerShipMode}] Starting ++++++++++++++++++ ");
TriggerTest.IsVerboseDebug = m_EnableVerboseDebug;
CheckStateEnterCount.IsVerboseDebug = m_EnableVerboseDebug;
AnimatorTestHelper.IsTriggerTest = m_EnableVerboseDebug;
bool isClientOwner = ownerShipMode == OwnerShipMode.ClientOwner;
@ -339,7 +347,7 @@ namespace TestProject.RuntimeTests
else
{
// Set the animation trigger via the server
AnimatorTestHelper.ServerSideInstance.SetTrigger();
AnimatorTestHelper.ServerSideInstance.SetTrigger("TestTrigger", m_EnableVerboseDebug);
}
// Wait for all triggers to fire
@ -367,14 +375,15 @@ namespace TestProject.RuntimeTests
yield return CreateAndStartNewClient();
Assert.IsTrue(m_ClientNetworkManagers.Length == 2, $"Newly created and connected client was not added to {nameof(m_ClientNetworkManagers)}!");
Assert.IsTrue(m_ClientNetworkManagers.Length == NumberOfClients + 1, $"Newly created and connected client was not added to {nameof(m_ClientNetworkManagers)}!");
// Wait for it to spawn client-side
yield return WaitForConditionOrTimeOut(WaitForClientsToInitialize);
AssertOnTimeout($"Timed out waiting for the late joining client-side instance of {GetNetworkAnimatorName(authoritativeMode)} to be spawned!");
// Make sure the AnimatorTestHelper client side instances (plus host) is the same as the TotalClients
Assert.True((AnimatorTestHelper.ClientSideInstances.Count + 1) == TotalClients);
// Make sure the AnimatorTestHelper client side instances is the same as the TotalClients
var calculatedClients = (AnimatorTestHelper.ClientSideInstances.Count + (m_UseHost ? 1 : 0));
Assert.True(calculatedClients == TotalClients, $"Number of client");
// Now check that the late joining client and all other clients are synchronized to the trigger
yield return WaitForConditionOrTimeOut(() => AllTriggersDetected(ownerShipMode));
@ -393,7 +402,7 @@ namespace TestProject.RuntimeTests
yield return WaitForConditionOrTimeOut(() => ParameterValuesMatch(ownerShipMode, authoritativeMode, m_EnableVerboseDebug));
AssertOnTimeout($"Timed out waiting for the client-side parameters to match {m_ParameterValues.ValuesToString()}!");
var newlyJoinedClient = m_ClientNetworkManagers[1];
var newlyJoinedClient = m_ClientNetworkManagers[NumberOfClients];
yield return StopOneClient(newlyJoinedClient);
VerboseDebug($" ------------------ Late Join Trigger Test [{TriggerTest.Iteration}][{ownerShipMode}] Stopping ------------------ ");
}
@ -448,16 +457,17 @@ namespace TestProject.RuntimeTests
// Create and join a new client (late joining client)
yield return CreateAndStartNewClient();
Assert.IsTrue(m_ClientNetworkManagers.Length == 2, $"Newly created and connected client was not added to {nameof(m_ClientNetworkManagers)}!");
Assert.IsTrue(m_ClientNetworkManagers.Length == NumberOfClients + 1, $"Newly created and connected client was not added to {nameof(m_ClientNetworkManagers)}!");
// Wait for the client to have spawned and the spawned prefab to be instantiated
yield return WaitForConditionOrTimeOut(WaitForClientsToInitialize);
AssertOnTimeout($"Timed out waiting for the late joining client-side instance of {GetNetworkAnimatorName(authoritativeMode)} to be spawned!");
// Make sure the AnimatorTestHelper client side instances (plus host) is the same as the TotalClients
Assert.True((AnimatorTestHelper.ClientSideInstances.Count + 1) == TotalClients);
// Make sure the AnimatorTestHelper client side instances is the same as the TotalClients
var calculatedClients = (AnimatorTestHelper.ClientSideInstances.Count + (m_UseHost ? 1 : 0));
Assert.True(calculatedClients == TotalClients, $"Number of client");
var lateJoinObjectInstance = AnimatorTestHelper.ClientSideInstances[m_ClientNetworkManagers[1].LocalClientId];
var lateJoinObjectInstance = AnimatorTestHelper.ClientSideInstances[m_ClientNetworkManagers[NumberOfClients].LocalClientId];
yield return WaitForConditionOrTimeOut(() => Mathf.Approximately(lateJoinObjectInstance.transform.rotation.eulerAngles.y, 180.0f));
AssertOnTimeout($"[Late Join] Timed out waiting for cube to reach 180.0f!");
@ -465,7 +475,7 @@ namespace TestProject.RuntimeTests
yield return WaitForConditionOrTimeOut(LateJoinClientSynchronized);
AssertOnTimeout("[Late Join] Timed out waiting for newly joined client to have expected state synchronized!");
var newlyJoinedClient = m_ClientNetworkManagers[1];
var newlyJoinedClient = m_ClientNetworkManagers[NumberOfClients];
yield return StopOneClient(newlyJoinedClient);
VerboseDebug($" ------------------ Late Join Synchronization Test [{TriggerTest.Iteration}][{ownerShipMode}] Stopping ------------------ ");
}
@ -496,18 +506,18 @@ namespace TestProject.RuntimeTests
/// </summary>
private bool LateJoinClientSynchronized()
{
if (!StateSyncTest.StatesEntered.ContainsKey(m_ClientNetworkManagers[1].LocalClientId))
if (!StateSyncTest.StatesEntered.ContainsKey(m_ClientNetworkManagers[NumberOfClients].LocalClientId))
{
VerboseDebug($"Late join client has not had any states synchronized yet!");
return false;
}
var serverStates = StateSyncTest.StatesEntered[m_ServerNetworkManager.LocalClientId];
var clientStates = StateSyncTest.StatesEntered[m_ClientNetworkManagers[1].LocalClientId];
var clientStates = StateSyncTest.StatesEntered[m_ClientNetworkManagers[NumberOfClients].LocalClientId];
if (serverStates.Count() != clientStates.Count())
{
VerboseDebug($"[Count][Server] {serverStates.Count} | [Client-{m_ClientNetworkManagers[1].LocalClientId}]{clientStates.Count}");
VerboseDebug($"[Count][Server] {serverStates.Count} | [Client-{m_ClientNetworkManagers[NumberOfClients].LocalClientId}]{clientStates.Count}");
return false;
}