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:
Родитель
adc6432c87
Коммит
f98e7a2d80
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче