I'm trying to focus on the question being about how Exit Time might work and WHY this specific behaviour happens.
I think what's likely happening, is that the way Animator States evaluate/tick their logic, including exit time and conditions, is that when a State is updated it's normalized time is incremented first and logic is run only after that. So a State that just starts to play almost never starts at normalized time zero, rather at zero plus whatever the delta time was for the animator that frame.
I tested this by creating a StateMachineBehaviour Script that prints out the StateInfo.normalizedTime in OnStateUpdate(...) method, but only once per State entry (so only on first frame of it updating) and the results we're like expected, never zero, but always just a tiny bit over.
You can repeat the test yourself, by making a StateMachineBehaviour with these contents and attaching that SMB to some State.
private bool FirstFrameConsumed;
/// <summary>
/// OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
/// </summary>
override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
FirstFrameConsumed = false;
}
/// <summary>
/// OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
/// </summary>
override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
if ( FirstFrameConsumed )
{
return;
}
FirstFrameConsumed = true;
Debug.Log(stateInfo.normalizedTime);
}
I think this also explains why exit times like 0.01 and lower occasionally seem to cause and immediate transition, but why that behaviour is irregular (the delta time on first frame of state is inconsistent, at least when Animator updates in Update Mode: Normal).
Secondly, this brings to light how States seems to handle their conditional checks for Exit Time. Since Exit Time = 0 won't transition if State starts at normalizedTime = 0.00056 (random, small increment of delta time at state entry), it's reason to suspect that a transition only occurs if it's conditions (Exit Time being a condition) are fulfilled before the normalizedTime passes its Exit Time.
This second assumption I tested by creating a simple AnimatorController setup with empty states (which have some default duration or around a second or so), then slowed down the states so I can manually toggle some AnimatorControllerParameters and observe how the transitions are evaluated.
Transitions:
- Reset (Trigger) parameter immediately transitions to state A, 0-length transition.
- A-to-B: TriggerB Condition, Exit Time 0.5, 0.5 normalized length transition.
Scenario 1: I set TriggerB right at start of A. At 0.5 normalizedTime the trigger is consumed and transition to B starts to proceed.
Scenario 2: I set TriggerB after Exit Time (normalizedTime > 0.50), TriggerB remains unconsumed until next loop when normalizedTime just passes the Exit Time.
While this behaviour is handy for guaranteeing you can setup transitions that always start at a fixed point and produce a consistent-looking blend between two states, this does sometimes feel unintuitive (I'd personally expect the conditions to trigger the transition anywhere between exit time and end of state/loop), often making it tricky to create dynamic transitions with the builtin tools.