mojira.dev
MC-297196

Knockback is synchronized to client with inconsistent delay

  • To reproduce: Punch a mob repeatedly with random frequency.

  • Expected result: Every knock backward happens immediately after each punch.

  • Actual result: The time between punch and knockback varies.

Further info:

During the game’s tick cycle, first, entity positions are sent to clients when chunks are ticked, then entities positions are stepped when entities are ticked.

Knockback seems to apply its impulse after the entity tick has finished - at the end of the game’s tick cycle - leaving the entity with an updated impulse velocity but its position has not yet changed.

Crucially, during the following tick, when chunks are ticked, a check is done for a hasImpulse flag on an entity, with what seems to be the intent of bypassing the normal update rate for mobs and to provide immediate feedback. This check passes, sends the position immediately, and unsets the hasImpulse flag, meaning we get an immediate update, but it’s of whatever movement happened before the knockback was applied.

Then, following that, entities are ticked. The position updates to reflect the impulse, but the entity now has a hasImpulse flag of false so the next chunk tick will not bypass the update rate.

The result is knockback is not only delayed by a tick, but also knockback obeys the 3 tick refresh rate of mobs, causing different response time to a player's attack depending on which of those 3 ticks the hit landed.

To test this, I’ve created a mod which adds an additional call to LivingEntity::aiStep at the very end of LivingEntity::knockback to update its position straight away, and the result is very responsive, consistent knockback.

Linked issues

Comments 11

I can confirm that attack knockback feels delayed, but the inconsistent timing of the knockback is somewhat hard to notice. If I understood the report correctly, then:
- At the start of the tick, the server sends the target entity’s position to the client immediately if it was attacked the previous tick, but because the entity’s position on the server has not been updated yet, this information is essentially wasted.
- If the server were to recalculate the entity’s position immediately after the knockback is applied, the client would always receive this updated position either at the beginning of a tick (before chunk processing) with the result that (assuming constant 20 TPS):
- Knockback applies roughly 0.05s seconds sooner
- There is still a delay of up to 0.05s for the client, depending on when in the tick the entity was attacked

Is the above accurate? If so I will confirm this report so that Mojang can look at it, but only if its title is changed to something clearer, such as “Attack knockback is delayed by a tick”. The current title makes it sound like mobs take 3 game ticks to fully update.

Hi, thank you for that thorough message.
I apologize for not being clearer. I'm finding this quite a complicated thing to communicate so I appreciate your input.

Yes, you have all of that correct, but I want to stress that it can take between 1 and 3 ticks to react. The inconsistency is why I want this looked into.

Every entity has an update rate set when it is registered. When the game ticks the chunks and considers sending the entity positions to the clients, it checks something like:

worldTicks % mobUpdateRate == 0 || hasImpulse == true

so if mobUpdateRate is 3 then the positions will only be sent every 3 ticks - or always if the hasImpulse flag is true.

I'm unsure if handling the attack is asynchronous on the server, but it's which tick in the 3 tick update cycle the attack takes place on which causes the most inconsistency.

Given the fact we’ve established that the entity’s ‘knockbacked’ position doesn’t manage to bypass the once-every-3-tick update cycle of entities, the following is true;
If you attack on the tick that the entity is updated, your client won't be sent the update until three ticks after, which is where I got the title from. This is very visible if you’re looking for it. It feels like lag in singleplayer. I’d like to get a video to demonstrate but I’m about to go to work.
I personally think this is the important part here, because there is actual code in the game to prevent this kind of issue, like with entering a bed, for example, but due to the order of things, with knockback, it hasn’t worked as intended.

Please let me know if you’d like some more info and I’ll get it as soon as I can.

I cannot reproduce the behavior of the entity taking up to 3 game ticks to register its knockback. When I do tick freeze with a mob coming at me, punch it, and then tick step, the mob remains on the ground. When I tick step again, the mob becomes airborne. This happens every time. I notice only two issues:

  • The mob’s motion should have changed on the tick it flashed red, not the tick afterwards

  • Once it does change, the mob’s velocity does not seem to have been updated instantly, but rather it sometimes takes a couple of ticks for it to go flying back at maximum speed. This is not really an issue with vertical velocity, only horizontal velocity. This may not be a bug but I find it undesirable and would expect attack knockback to be strongest immediately after the knockback is taken.

If the issue you notice is not either of these then I’m afraid I cannot reproduce it. A video and/or detailed reproduction steps would make it clearer what you’re referring to.

I confirm this bug. The issue is caused in ChunkMap.tick() where ServerEntity.sendChanges() is called for all tracked entities in the chunk before the entity ticks, this is an issue because in the case of a player attack, it, with other packets, is handled at the end of a tick, thus gets processed right before on the next tick it is sent to clients, before the entity is moved on the server. I have also confirmed the possibility of fixing this, reducing the delay in movement to 0 ticks, by moving the logic from ChunkMap.tick() to after ServerLevel.guardEntityTick(Consumer<Entity> consumer, Entity entity) is called in ServerLevel.tick(BooleanSupplier booleanSupplier) (Using the entity that just got ticked, rather than doing all tracked entities at once).

With this fix, if you were to do tick freeze, hit a mob, then tick step, it would, as expected, immediately be seen on the client. In addition, it is sometimes possible, in this bug, to have 1-3 ticks of delay, as, for most mobs, they update every 3 ticks unless hasImpulse is set to true, so if you were a tick before an update, it would be delayed by one tick, but if an update would have happened on that tick regardless of hasImpulse, it becomes a whole 3 ticks of delay.

1 more comments

Thank you, Rachel.

Much better title. A big improvement on mine.

I had suspected tick freeze would hide the bug, as it seems to force synchronisation on each step.

Your proposed fix is excellent. Could moving logic around have unintended side effects with movement caused by the environment, or would the fix only effect the synchronisation itself?

The order is in fact how it was done before 1.14, when this bug was introduced. Therefore, any other logical changes are how they were intended and byproducts of them moving it as well.

I should note, easier fix: just move the call for ChunkMap.tick() to the end of ServerLevel.tick(BooleanSupplier booleanSupplier). This is what I did in my PR for Paper, and it works flawlessly.

Revvilo

(Unassigned)

Community Consensus

Platform

Normal

Networking

1.21.5, 25w17a, 1.21.10

Retrieved