Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Additional documentation and release notes are available at [Multiplayer Documen

### Fixed

- Issue where `NetworkAnimator` logged an error for a trigger-conditioned transition into an Exit node, and a client that joined during that transition, or during one into a sub-state machine, did not synchronize with it. Transitions that pass through Exit, Entry or sub-state machine nodes now resolve to the state they reach, and a late-joining client cross-fades from the matching originating state when several states transition into the same destination. (#4238)
- Issue where `NetworkTime` reported a `TickOffset` of almost a whole tick, instead of zero, for some times that land on a tick (for example 0.3 at a tick rate of 10), which put `TickWithPartial` one tick ahead or, in long sessions, `Tick` one tick behind. Times between minus one tick and zero now report tick -1 with a positive `TickOffset`, as other negative times do. (#4228)
- Issue where bits written with `BitWriter` were lost when a later `BitWriter.TryBeginWriteBits` call in the same bitwise context grew the `FastBufferWriter`. The bytes written earlier in that context were replaced with zeros or uninitialized memory. (#4226)
- Issue where a growable `FastBufferWriter` created with a size of 0 never returned from its first write or `TryBeginWrite` call that needed the buffer to grow. (#4224)
Expand Down
175 changes: 145 additions & 30 deletions com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,8 @@
internal List<TransitionStateInfo> TransitionStateInfoList;

// Used to get the associated transition information required to synchronize late joining clients with transitions
// [Layer][DestinationState][TransitionStateInfo]
private readonly Dictionary<int, Dictionary<int, TransitionStateInfo>> m_DestinationStateToTransitionInfo = new Dictionary<int, Dictionary<int, TransitionStateInfo>>();
// [Layer][DestinationState][TransitionStateInfo per originating state]
private readonly Dictionary<int, Dictionary<int, List<TransitionStateInfo>>> m_DestinationStateToTransitionInfo = new Dictionary<int, Dictionary<int, List<TransitionStateInfo>>>();

// Named differently to avoid serialization conflicts with NetworkBehaviour
internal NetworkManager LocalNetworkManager;
Expand All @@ -285,14 +285,43 @@
/// </summary>
private void BuildDestinationToTransitionInfoTable()
{
m_DestinationStateToTransitionInfo.Clear();
foreach (var entry in TransitionStateInfoList)
{
m_DestinationStateToTransitionInfo.TryAdd(entry.Layer, new Dictionary<int, TransitionStateInfo>());
m_DestinationStateToTransitionInfo.TryAdd(entry.Layer, new Dictionary<int, List<TransitionStateInfo>>());
var destinationStateTransitionInfo = m_DestinationStateToTransitionInfo[entry.Layer];
destinationStateTransitionInfo.TryAdd(entry.DestinationState, entry);
if (!destinationStateTransitionInfo.TryGetValue(entry.DestinationState, out var transitions))
{
transitions = new List<TransitionStateInfo>();
destinationStateTransitionInfo.Add(entry.DestinationState, transitions);
}
transitions.Add(entry);
}
}

/// <summary>
/// Gets the transition info for a destination state, matched by originating state since several states can
/// transition into the same destination. Transitions not in the table, such as non-trigger ones, return false.
/// </summary>
internal bool TryGetTransitionInfo(int layer, int destinationState, int originatingState, out TransitionStateInfo transitionInfo)
{
transitionInfo = null;
if (!m_DestinationStateToTransitionInfo.TryGetValue(layer, out var layerTransitions) || !layerTransitions.TryGetValue(destinationState, out var transitions))
{
return false;

Check warning on line 311 in com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs#L310-L311

Added lines #L310 - L311 were not covered by tests
}

foreach (var entry in transitions)
{
if (entry.OriginatingState == originatingState)
{
Comment thread
NoelStephensUnity marked this conversation as resolved.
Resolved
transitionInfo = entry;
return true;
}
}
return false;
}

[Serializable]
internal class AnimatorParameterEntry
{
Expand Down Expand Up @@ -323,7 +352,83 @@

private readonly Dictionary<int, AnimatorControllerParameter> m_ParameterToNameLookup = new Dictionary<int, AnimatorControllerParameter>();

private void ParseStateMachineStates(int layerIndex, ref AnimatorController animatorController, ref AnimatorStateMachine stateMachine)
private void AddTransitionStateInfo(int layerIndex, AnimatorState originatingState, AnimatorState destinationState, AnimatorStateTransition transition, AnimatorControllerParameter parameter, int transitionIndex)
{
var transitionInfo = new TransitionStateInfo()
{
Layer = layerIndex,
OriginatingState = originatingState.nameHash,
DestinationState = destinationState.nameHash,
TransitionDuration = transition.duration,
TriggerNameHash = parameter.nameHash,
TransitionIndex = transitionIndex
};
TransitionStateInfoList.Add(transitionInfo);
}

private void AddStateMachineDestinations(int layerIndex, AnimatorState originatingState, AnimatorStateMachine destinationStateMachine, AnimatorStateTransition transition,
AnimatorControllerParameter parameter, int transitionIndex, HashSet<AnimatorStateMachine> visitedStateMachines)
{
if (!visitedStateMachines.Add(destinationStateMachine))
{
return;

Check warning on line 374 in com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs#L373-L374

Added lines #L373 - L374 were not covered by tests
}

if (destinationStateMachine.defaultState != null)
{
AddTransitionStateInfo(layerIndex, originatingState, destinationStateMachine.defaultState, transition, parameter, transitionIndex);
}

foreach (var entryTransition in destinationStateMachine.entryTransitions)
{
if (entryTransition.destinationState != null)
{
AddTransitionStateInfo(layerIndex, originatingState, entryTransition.destinationState, transition, parameter, transitionIndex);
}
else if (entryTransition.destinationStateMachine != null)
{
AddStateMachineDestinations(layerIndex, originatingState, entryTransition.destinationStateMachine, transition, parameter, transitionIndex, visitedStateMachines);
}

Check warning on line 391 in com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs#L388-L391

Added lines #L388 - L391 were not covered by tests
}
}

private void AddExitTransitionStateInfo(int layerIndex, AnimatorState originatingState, AnimatorStateMachine exitingStateMachine, AnimatorStateTransition transition,
AnimatorControllerParameter parameter, int transitionIndex, List<AnimatorStateMachine> parentStateMachines, int parentIndex)
{
// Exiting the layer's root state machine re-enters it through its Entry node
if (parentIndex < 0)
{
AddStateMachineDestinations(layerIndex, originatingState, exitingStateMachine, transition, parameter, transitionIndex, new HashSet<AnimatorStateMachine>());
return;
}

var parentStateMachine = parentStateMachines[parentIndex];
var stateMachineTransitions = parentStateMachine.GetStateMachineTransitions(exitingStateMachine);
// With no transition out of the sub-state machine, its Exit goes through the parent's Entry node
if (stateMachineTransitions.Length == 0)
{
AddStateMachineDestinations(layerIndex, originatingState, parentStateMachine, transition, parameter, transitionIndex, new HashSet<AnimatorStateMachine>());
return;
}

foreach (var stateMachineTransition in stateMachineTransitions)
{
if (stateMachineTransition.destinationState != null)
{
AddTransitionStateInfo(layerIndex, originatingState, stateMachineTransition.destinationState, transition, parameter, transitionIndex);
}
else if (stateMachineTransition.destinationStateMachine != null)
{
AddStateMachineDestinations(layerIndex, originatingState, stateMachineTransition.destinationStateMachine, transition, parameter, transitionIndex, new HashSet<AnimatorStateMachine>());
}
else if (stateMachineTransition.isExit)
{
AddExitTransitionStateInfo(layerIndex, originatingState, parentStateMachine, transition, parameter, transitionIndex, parentStateMachines, parentIndex - 1);
}
}
}

private void ParseStateMachineStates(int layerIndex, ref AnimatorController animatorController, ref AnimatorStateMachine stateMachine, List<AnimatorStateMachine> parentStateMachines = null)
{
for (int y = 0; y < stateMachine.states.Length; y++)
{
Expand Down Expand Up @@ -355,23 +460,17 @@
{
case AnimatorControllerParameterType.Trigger:
{
if (transition.destinationStateMachine != null)
if (transition.destinationState != null)
{
AddTransitionStateInfo(layerIndex, animatorState, transition.destinationState, transition, parameter, z);
}
else if (transition.isExit)
{
var destinationStateMachine = transition.destinationStateMachine;
ParseStateMachineStates(layerIndex, ref animatorController, ref destinationStateMachine);
AddExitTransitionStateInfo(layerIndex, animatorState, stateMachine, transition, parameter, z, parentStateMachines, parentStateMachines != null ? parentStateMachines.Count - 1 : -1);
}
else if (transition.destinationState != null)
else if (transition.destinationStateMachine != null)
{
var transitionInfo = new TransitionStateInfo()
{
Layer = layerIndex,
OriginatingState = animatorState.nameHash,
DestinationState = transition.destinationState.nameHash,
TransitionDuration = transition.duration,
TriggerNameHash = parameter.nameHash,
TransitionIndex = z
};
TransitionStateInfoList.Add(transitionInfo);
AddStateMachineDestinations(layerIndex, animatorState, transition.destinationStateMachine, transition, parameter, z, new HashSet<AnimatorStateMachine>());
}
else
{
Expand All @@ -387,6 +486,18 @@
}
}
}

var childStateMachineParents = parentStateMachines != null ? new List<AnimatorStateMachine>(parentStateMachines) : new List<AnimatorStateMachine>();
childStateMachineParents.Add(stateMachine);
foreach (var childStateMachine in stateMachine.stateMachines)
{
var nestedStateMachine = childStateMachine.stateMachine;
Comment thread
NoelStephensUnity marked this conversation as resolved.
Resolved
// A missing sub-state machine asset reference leaves this null
if (nestedStateMachine != null)
{
ParseStateMachineStates(layerIndex, ref animatorController, ref nestedStateMachine, childStateMachineParents);
}
}
}

/// <summary>
Expand Down Expand Up @@ -996,14 +1107,11 @@
stateHash = nextState.fullPathHash;

// Check if this transition can be synchronized using cross-fading
if (m_DestinationStateToTransitionInfo.TryGetValue(layer, out var layerTransitions))
if (TryGetTransitionInfo(layer, nextState.shortNameHash, synchronizationStateInfo.shortNameHash, out var transitionInfo))
{
if (layerTransitions.TryGetValue(nextState.shortNameHash, out var transitionInfo))
{
stateHash = transitionInfo.OriginatingState;
// Set the destination state to cross-fade to from the originating state
animationState.DestinationStateHash = transitionInfo.DestinationState;
}
stateHash = transitionInfo.OriginatingState;
// Set the destination state to cross-fade to from the originating state
animationState.DestinationStateHash = transitionInfo.DestinationState;
}
}

Expand Down Expand Up @@ -1460,13 +1568,20 @@
{
// At this point all entries in the lookup table should be valid.
// Look up the transition info for this layer and destination state.
if (m_DestinationStateToTransitionInfo.TryGetValue(animationState.Layer, out var layerTransitions))
if (m_DestinationStateToTransitionInfo.ContainsKey(animationState.Layer))
{
if (layerTransitions.TryGetValue(animationState.DestinationStateHash, out var transitionInfo))
if (TryGetTransitionInfo(animationState.Layer, animationState.DestinationStateHash, animationState.StateHash, out var transitionInfo))
{
// Make sure we are on the originating/starting state we are going to cross-fade into
if (currentState.shortNameHash == animationState.StateHash)
var onOriginatingState = currentState.shortNameHash == animationState.StateHash;
if (onOriginatingState || !m_Animator.IsInTransition(animationState.Layer))
{
// A late-joining client starts in its default state. CrossFade normalizes the duration to the
// current state's length, so enter the originating state first and apply it before cross-fading.
if (!onOriginatingState)
{
m_Animator.Play(animationState.StateHash, animationState.Layer, 0.0f);
m_Animator.Update(0.0f);
}
// 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(transitionInfo.DestinationState, transitionInfo.TransitionDuration, transitionInfo.Layer, 0.0f, animationState.NormalizedTime);
}
Expand Down
Loading