Skip to content

fix: NetworkAnimator conditional exit transitions - #4238

Open
NoelStephensUnity wants to merge 5 commits into
develop-3.x.xfrom
fix/networkanimator-conditional-exit-port
Open

NoelStephensUnity wants to merge 5 commits into
develop-3.x.xfrom
fix/networkanimator-conditional-exit-port

Conversation

@NoelStephensUnity

@NoelStephensUnity NoelStephensUnity commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Purpose of this PR

User-PR: #4181

This PR resolves conditional transitions targeting an Exit node by following transitions through the parent state-machine hierarchy using GetStateMachineTransitions().

This supports both direct and nested Exit transitions, including transitions targeting another state machine. It prevents incorrect validation errors and allows late-joining clients to correctly synchronize the resulting animator state.

Jira ticket

Tracked by GitHub issue #3912.
Fix: #3912.

Changelog

  • Fixed: NetworkAnimator now resolves conditional transitions through sub-state-machine Exit nodes.

Documentation

  • No documentation changes or additions were necessary.

Testing & QA (How your changes can be verified during release Playtest)

Functional Testing

Manual testing:

  • Manual testing done

Verified that a client joining during a State A -> Exit -> State B transition correctly synchronizes with the transition and reaches State B without validation or destination-state errors.

Automated tests:

  • Covered by existing automated tests
  • Covered by new automated tests

Added EditMode coverage for direct and nested Exit transitions, including transitions targeting another state machine.

Does the change require QA team to:

  • Review automated tests?
  • Execute manual tests?
  • Provide feedback about the PR?

Up-port

Not needed; this PR targets develop-3.x.x.

Backports

#4240

…p by originating state

- Exit from the root state machine, or from a sub-state machine with no outgoing transition, resolves through the Entry node.
- Trigger transitions into a sub-state machine resolve to its default and entry states.
- The late-join lookup keeps every transition into a destination and picks the one from the current originating state.
- Editor tests are guarded by COM_UNITY_MODULES_ANIMATION, with the define added to the test asmdef.
- CHANGELOG entry.
@NoelStephensUnity
NoelStephensUnity marked this pull request as ready for review November 3, 2026 16:35
@NoelStephensUnity
NoelStephensUnity requested review from a team and EmandM as code owners November 3, 2026 16:35

@u-pr u-pr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Harness Review

Needs changes

The change expands transition resolution and adds editor coverage, but the runtime integration still has a late-join synchronization gap.

Reviewed commit 96e3a00

🤖 Helpful? 👍/👎

A late-joining client starts in its default state, so requiring the originating state left it stuck there. Adds a runtime test that joins mid-transition from a non-default state.
@NoelStephensUnity

Copy link
Copy Markdown
Member Author

@u-pr please review again.

@u-pr

u-pr Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

The update in fc9f03227 cleanly addresses the issue of late-joining clients starting in a default state during a non-default transition, and the new NetworkAnimatorLateJoinTransitionTests integration test covers this scenario well.

A few items to consider for this revision:

1. TryGetTransitionInfo fallback to transitions[0]

In TryGetTransitionInfo:

transitionInfo = transitions[0];
foreach (var entry in transitions)
{
    if (entry.OriginatingState == originatingState)
    {
        transitionInfo = entry;
        break;
    }
}
return true;

If the authority is transitioning into destinationState from a state that did not transition via a tracked trigger (e.g., an untracked state, or a transition conditioned on floats/bools/exit time), originatingState will not match any entry in transitions. Because it falls back to transitions[0] and returns true:

  • WriteSynchronizationData will set stateHash to transitions[0].OriginatingState and destinationStateHash to destinationState.
  • In UpdateAnimationState, because !m_Animator.IsInTransition(...) is now true on late joiners, the client will cross-fade into destinationState using transitions[0]'s duration and starting from transitions[0].OriginatingState, rather than relying on parameter synchronization or direct state playback.

Have you considered returning true only when entry.OriginatingState == originatingState matches, and false otherwise?

2. Defensive null check for childStateMachine.stateMachine

In ParseStateMachineStates:

foreach (var childStateMachine in stateMachine.stateMachines)
{
    var nestedStateMachine = childStateMachine.stateMachine;
    ParseStateMachineStates(layerIndex, ref animatorController, ref nestedStateMachine, childStateMachineParents);
}

If an AnimatorController asset contains a broken or missing sub-state machine reference, childStateMachine.stateMachine can be null, resulting in a NullReferenceException inside ParseStateMachineStates. Adding a null check before recursing guards against this.

3. Redundant ContainsKey(layer) lookups

At lines 1107 and 1571, m_DestinationStateToTransitionInfo.ContainsKey(...) is checked immediately before calling TryGetTransitionInfo(...). Since TryGetTransitionInfo already checks m_DestinationStateToTransitionInfo.TryGetValue(layer, out var layerTransitions), calling TryGetTransitionInfo directly avoids an extra dictionary lookup.

🤖 Helpful? 👍/👎

Transitions not in the table no longer fall back to the first entry for the destination, and a missing sub-state machine reference no longer throws while the table is built.
@NoelStephensUnity

Copy link
Copy Markdown
Member Author

🤖 All three addressed in 739def1: strict origin match in TryGetTransitionInfo, null check before recursing, and the redundant ContainsKey removed on the authority side (kept on the receiver, where a missing layer table is valid and should not log).

@NoelStephensUnity

Copy link
Copy Markdown
Member Author

@u-pr please review again

@u-pr

u-pr Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

The latest changes in 739def1 cleanly resolve all previous review items:

  • Strict origin state matching: TryGetTransitionInfo now strictly requires entry.OriginatingState == originatingState rather than falling back to transitions[0].
  • Defensive null check: Skipping null childStateMachine.stateMachine references prevents potential NullReferenceExceptions when controller assets contain broken sub-state machine links.
  • Lookup simplification: Redundant ContainsKey(layer) was removed on the authority side while keeping the check on the receiver to prevent spurious developer error logs on layers without transition tables.
  • Test coverage: The negative assertion in TransitionInfoLookupMatchesOriginatingState verifies that unmatched originating states return false, complementing the existing integration and editor tests.

The implementation and accompanying tests look solid and ready to merge.

🤖 Helpful? 👍/👎

@NoelStephensUnity
NoelStephensUnity enabled auto-merge (squash) November 3, 2026 17:46
CrossFade normalizes the transition duration to the current state's length, so cross-fading from the default state blended at the wrong speed when the clip lengths differ.
@codecov-github-com

codecov-github-com Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Codecov Report

Attention: Patch coverage is 92.30769% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
....gameobjects/Runtime/Components/NetworkAnimator.cs 92.30% 8 Missing ⚠️
@@                Coverage Diff                @@
##           develop-3.x.x    #4238      +/-   ##
=================================================
+ Coverage          76.90%   77.27%   +0.36%     
=================================================
  Files                159      159              
  Lines              27663    27749      +86     
=================================================
+ Hits               21274    21442     +168     
+ Misses              6389     6307      -82     
Flag Coverage Δ
NGOv2_project_testproject_ubuntu 76.36% <92.30%> (+0.37%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
....gameobjects/Runtime/Components/NetworkAnimator.cs 85.50% <92.30%> (+9.47%) ⬆️

... and 5 files with indirect coverage changes

Components Coverage Δ
com.unity.netcode.gameobjects 77.27% <92.38%> (+0.36%) ⬆️

ℹ️ Need help interpreting these results?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NetworkAnimator: Conditional triggered transitions to Exit node log false errors

3 participants