han wrote on Jan 6
th, 2019 at 12:48pm:
So ClientUpdatePosition should be called the very same Tick after all RPCs are received.
According to my logs, Level.TimeSeconds has different values in ClientAdjustPosition and ClientUpdatePosition, so I presumed that these functions are called in two different ticks. Maybe my guess is wrong and the given functions are technically called in the same tick, but ClientAdjustPosition is called before updating Level.TimeSeconds and ClientUpdatePosition is called after updating Level.TimeSeconds.
What really matters here is that ClientAdjustPosition and ClientUpdatePosition do not constitute a single transaction; there is at least one event (AnimEnd) that can be called between these functions, albeit it's not a part of the synchronization process.
function AnimEnd()
{
local name MyAnimGroup;
bAnimTransition = false;
if ( Physics == PHYS_Spider )
{
if ( VSize(Velocity)<10 )
PlayDuck();
else PlayCrawling();
}
else if (Physics == PHYS_Walking)
{
if (bIsCrouching)
{
if ( !bIsTurning && ((Velocity.X * Velocity.X + Velocity.Y * Velocity.Y) < 1000) )
PlayDuck();
else
PlayCrawling();
}
else
{
MyAnimGroup = GetAnimGroup(AnimSequence);
if ((Velocity.X * Velocity.X + Velocity.Y * Velocity.Y) < 1000)
{
if ( MyAnimGroup == 'Waiting' )
PlayWaiting();
else
{
bAnimTransition = true;
TweenToWaiting(0.2);
}
}
else if (bIsWalking)
{
if ( (MyAnimGroup == 'Waiting') || (MyAnimGroup == 'Landing') || (MyAnimGroup == 'Gesture') || (MyAnimGroup == 'TakeHit') )
{
TweenToWalking(0.1);
bAnimTransition = true;
}
else
PlayWalking();
}
else
{
if ( (MyAnimGroup == 'Waiting') || (MyAnimGroup == 'Landing') || (MyAnimGroup == 'Gesture') || (MyAnimGroup == 'TakeHit') )
{
bAnimTransition = true;
TweenToRunning(0.1);
}
else
PlayRunning();
}
}
}
else if (bIsReducedCrouch)
PlayDuck();
else
PlayInAir();
}
Right after returning from ClientAdjustPosition, the value of Physics corresponds to some situation in the past, but ClientUpdatePosition cannot properly reproduce all animations since CurrentTimeStamp (the initial timestamp for synchronization in the past) up to the current moment, because AutonomousPhysics doesn't update animations. Any call to PlayAnim/LoopAnim within ClientUpdatePosition will run an animation only after returning from ClientUpdatePosition. Consequently, such calls generally lead to badly looking interrupted/doubled animations:
https://www.youtube.com/watch?v=H4k1UV-8_I0The doubled landing and the doubled standing up showed in this video happen due to calls to ClientUpdatePosition which tries to replay animations as a part of the synchronization process. The first person view is also affected, because some animation-related functions alter BaseEyeHeight which determines the target camera height.
I see only one viable way to reduce the appearance of such glitches to the minimum: calls to PlayDuck, PlayLanded, TweenToWaiting, etc should be allowed only when bUpdatePosition is false or when such calls take place inside BeginState of any PlayerPawn's state.
Although playing animations in BeginState leads to similar glitches when the state is first entered due to normal client-side evaluations and then reentered by means of ClientAdjustPosition, I don't see any way to set proper player animations (relevant for the given state) in case if the state was entered only by means of ClientAdjustPosition. So, here I have to choose between the two evils: doubled animations or mismatching between the animation and the new state.
After adding the restrictions for PlayDuck, PlayLanded, TweenToWaiting, etc as described above, I still have an issue with AnimEnd operating on outdated Physics; this is why I want to perform the synchronization ClientAdjustPosition + ClientUpdatePosition as a single transaction.