Repository navigation
Use the real partial tick for ClientTickCounter - #1333
AlexProGear wants to merge 1 commit into
Conversation
RenderFrameEvent.Pre handed ClientTickCounter.getGameTimeDeltaTicks(), which is the frame-to-frame delta (~0.333 at 60 fps, i.e. nearly constant), not the 0..1 residual inside the current tick. ClientTickCounter.getTotal() is ticksInGame + partialTicks, so the value only changed once per game tick: everything derived from it (the zappy-seed driven pattern wobble, sentinel rotation) advanced at 20 Hz no matter the framerate. Pass getGameTimeDeltaPartialTick(false) instead - the same partial tick vanilla uses in LevelRenderer and GameRenderer.
|
Oh my god. This was one line change. Did you have to use claude??? |
|
Sorry, I REALLY didn't want to use my brain for this so I made it do PR for me :) |
|
Can you please:
|
|
Also, you mention that you didn't change the code tied to the Why not just remove the unused parameter entirely? Broken code shouldn't be left untouched just because its output isn't being used - that just adds confusion for people looking at the codebase in the future. |
|
@AlexProGear Could you rewrite the PR description to bring it in-line with the Contribution Policy/AI Policy and address comments raised? |
ClientTickCounterends up holding a per-frame delta instead of the partial tick, so every animation that reads it advances at the game tick rate instead of the framerate.The bug
RenderFrameEvent.Predoes:DeltaTracker.getGameTimeDeltaTicks()returnsTimer.deltaTicks, which is(now - lastFrame) / msPerTick: the game time that elapsed during this frame. It is roughly0.333at 60 fps, and it tracks the framerate rather than the position inside the tick.ClientTickCounteris built around the other value:clientTickEnd()resetspartialTicksto0at the end of every tick andrenderTickStart()fills it back in for the frames that follow, so the value is clearly meant to be the0..1position inside the current tick. Feeding it a per-frame delta keepsticksInGame + partialTickson a single value for the whole tick (at 60 fps it stays atN + 0.33for all three frames of tickN), so it only moves once per tick.Everything that derives its animation time from
getTotal()therefore runs at 20 Hz:RenderLib.zSeed = ClientTickCounter.getTotal() * speed- the pattern wobble,HexAdditionalRenderers.renderSentinel-getTotal() / 2, the sentinel bob and spin,ClientRenderHelper- scrolling patterns,GuiSpellcasting- pattern previews in the GUI,MsgCastParticleS2C- cast particles,IotaHolderItem- the hue of iota tints.Visually, the pattern wobble and the sentinel step instead of moving smoothly.
The fix
Use the member vanilla itself uses as the render partial tick:
DeltaTracker.getGameTimeDeltaPartialTick(boolean)returnsdeltaTickResidual, the0..1fraction of the current tick - exactly whatrenderTickStart()/clientTickEnd()are written for. For comparison, vanilla passesgetGameTimeDeltaPartialTick(false)where it needs a render partial tick (LevelRenderer,GameRenderer) and only callsgetGameTimeDeltaTicks()where it wants the elapsed frame time instead (post-effect, entity-effect and overlay processing).getTotal()now grows smoothly within each tick, so all of the consumers above animate at the framerate. The value at a tick boundary is unchanged, and thepaused/frozenhandling ofDeltaTrackercomes along for free.Testing
Tested in a NeoForge 1.21.1 dev client on the
0.12.0-develline: before the change the pattern wobble visibly stepped at 20 Hz, after it is smooth at the same framerate, with no other behavioural difference. The compiled bytecode was checked to confirm which method the listener actually invokes.The same call in the Fabric module
FabricHexClientInitializer.kt:39(WorldRenderEvents.START) passesit.tickCounter().gameTimeDeltaTicksto the samerenderTickStart, and this repository uses official Mojang mappings for both loaders, so it is the sameDeltaTracker.getGameTimeDeltaTicks()frame delta. I left it out of this PR because I could not build the Fabric module here, but the equivalent change would be:Note
The
RenderLevelStageEventlistener above is left alone on purpose: it forwards itspartialTicktoHexAdditionalRenderers.overlayLeveland on torenderSentinel, where that parameter is never read (the renderer positions itself fromClientTickCounter.getTotal()instead).