Skip to content

Commit c884fed

Browse files
fix: batched mode never cleared a transform state's change flags
ClearForNextTick had one production call site, inside OnUpdateAuthoritativeState, which a registered instance never reaches because the tick loop skips anything the batched delta check already handled. So in batched mode the change flags were never reset: any axial group that changed once kept its Has*Change bit for the life of the instance and was serialized on every state update from then on. Measured at 200 instances over 60 ticks, half float position and a compressed quaternion, with scale registered and never touched: 100% of entries carried HasScaleChange and paid 6 bytes for a scale that had not moved since spawn. The per instance path, which does clear, costs the same with scale registered or not. This is the third defect of the same shape, so rather than a second copy of the block the clear is now ClearStateForNextTick, called from OnUpdateAuthoritativeState and from PrepareBatchedDeltaEntry. The per instance side is a pure extraction: same condition, same body, same order relative to the axis check and the delta check.
1 parent 5894d94 commit c884fed

2 files changed

Lines changed: 39 additions & 18 deletions

File tree

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
2929
- Fixed issue where scenes additively loaded before a session started were tracked as loaded on the server but had no scene handle entries, which caused `NetworkSceneManager.UnloadScene` to log an error and leave the scene registered as loaded even though it unloaded on all peers. (#4146)
3030
- Issue where objects using `NetworkTransform.UseHalfFloatPrecision` appeared to jitter on non-authority instances while they were stationary or coming to rest, even though the authority was not moving them. (#4129)
3131
- Issue where re-enabling a `NetworkTransform` position axis that had drifted out of half float delta range while it was disabled would not teleport, because the per axis check assigned its result rather than accumulating it and an in-range axis discarded what an out-of-range one had found. (#4123)
32+
- Issue where `TransformSyncModes.Batched` never cleared a `NetworkTransform` state update's change flags between ticks, because the only thing that cleared them ran from the per instance tick path. Any axial group that changed once kept its change flag for the life of the instance and was serialized on every state update from then on, so a registered but never changing scale cost 6 bytes per update indefinitely. (#4123)
3233
- Issue where lerp smoothing was applied per frame instead of over time, which caused the `Lerp` and `SmoothDampening` interpolation types to smooth by different amounts at different frame rates. Results at 60fps are unchanged. (#4132)
3334
- Issue where setting a maximum interpolation time of 1.0 would stop a `NetworkTransform` from interpolating at all when using the `Lerp` or `SmoothDampening` interpolation types. (#4132)
3435
- Issue where `NetworkTransform` interpolated towards a point in time taken from the local clock rather than the server clock that state updates are stamped on, which starved the interpolator on clients and reduced interpolation to snapping between state updates. (#4135)

com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2179,9 +2179,10 @@ internal void PrepareInterpolationEntry(ref InterpolationEntry entry)
21792179
/// </summary>
21802180
internal void PrepareBatchedDeltaEntry(ref TransformDeltaEntry entry)
21812181
{
2182-
// The per instance path runs this from OnUpdateAuthoritativeState, which a registered instance
2183-
// never reaches, so it runs here instead. Without it a batched instance silently skips the
2184-
// teleport that re-enabling an axis can require.
2182+
// Both of these run from OnUpdateAuthoritativeState on the per instance path, which a registered
2183+
// instance never reaches. Without the clear the change flags only accumulate, and without the
2184+
// axis check a batched instance silently skips the teleport that re-enabling an axis requires.
2185+
ClearStateForNextTick();
21852186
AxisChangedDeltaPositionCheck();
21862187

21872188
// The entry is what the job reads, and anything the main thread set between ticks (a Teleport or
@@ -3608,27 +3609,46 @@ private void AxisChangedDeltaPositionCheck()
36083609
}
36093610
}
36103611

3612+
/// <summary>
3613+
/// Clears the previous tick's change flags so the next delta check starts from a clean bitset.
3614+
/// </summary>
3615+
/// <remarks>
3616+
/// Skipped while an explicit set or a teleport is pending, since both carry state the next update
3617+
/// still has to send.<br /><br />
3618+
/// Both synchronization modes have to run this and neither can borrow the other's call: the per
3619+
/// instance path runs it from <see cref="OnUpdateAuthoritativeState"/>, which a registered instance
3620+
/// never reaches, and the batched path from <see cref="PrepareBatchedDeltaEntry"/>. Without it the
3621+
/// bitset only ever accumulates, so an axial group that changed once keeps its change flag for the
3622+
/// life of the instance and is serialized on every state update from then on.
3623+
/// </remarks>
3624+
private void ClearStateForNextTick()
3625+
{
3626+
if (m_LocalAuthoritativeNetworkState.ExplicitSet
3627+
|| !m_LocalAuthoritativeNetworkState.FlagStates.IsDirty
3628+
|| m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame)
3629+
{
3630+
return;
3631+
}
3632+
3633+
m_LocalAuthoritativeNetworkState.FlagStates.ClearForNextTick();
3634+
if (TrackStateUpdateId)
3635+
{
3636+
m_LocalAuthoritativeNetworkState.FlagStates.TrackByStateId = true;
3637+
m_LocalAuthoritativeNetworkState.StateId++;
3638+
}
3639+
else
3640+
{
3641+
m_LocalAuthoritativeNetworkState.FlagStates.TrackByStateId = false;
3642+
}
3643+
}
3644+
36113645
/// <summary>
36123646
/// Called by authority to check for deltas and update non-authoritative instances
36133647
/// if any are found.
36143648
/// </summary>
36153649
internal void OnUpdateAuthoritativeState(bool settingState = false)
36163650
{
3617-
// If our replicated state is not dirty and our local authority state is dirty, clear it.
3618-
if (!m_LocalAuthoritativeNetworkState.ExplicitSet && m_LocalAuthoritativeNetworkState.FlagStates.IsDirty && !m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame)
3619-
{
3620-
// Now clear our bitset and prepare for next network tick state update
3621-
m_LocalAuthoritativeNetworkState.FlagStates.ClearForNextTick();
3622-
if (TrackStateUpdateId)
3623-
{
3624-
m_LocalAuthoritativeNetworkState.FlagStates.TrackByStateId = true;
3625-
m_LocalAuthoritativeNetworkState.StateId++;
3626-
}
3627-
else
3628-
{
3629-
m_LocalAuthoritativeNetworkState.FlagStates.TrackByStateId = false;
3630-
}
3631-
}
3651+
ClearStateForNextTick();
36323652

36333653
AxisChangedDeltaPositionCheck();
36343654

0 commit comments

Comments
 (0)