General
- FishNet: observed in 4.7.2; the same guard is present in upstream 4.7.3R source and the current main branch.
- Unity: 6000.5.8f1.
- Discord troubleshooting: not discussed on Discord; investigated locally with source inspection and lifecycle regression tests.
Description
NetworkObject.InvokeStartCallbacks_Prediction(false) subscribes TimeManager_Update to TimeManager.OnUpdate, but the matching InvokeStopCallbacks_Prediction(false) returns before unsubscribing. The stop method currently returns when !asServer, whereas the subscription belongs to the client lifecycle.
Consequently, a client-only despawn leaves the callback registered. Reusing the same pooled NetworkObject and starting its client lifecycle again adds another subscription. This can retain despawned objects, invoke duplicate callbacks, and increase the cost and allocations of modifying the shared multicast delegate. The same guard also skips PredictionSmoother.OnStopClient() for client shutdown.
Source
Upstream source, pinned to 4.7.3R.
Start subscribes inside if (!asServer). Stop currently begins with:
Minimal lifecycle reproduction
This can be isolated in an EditMode test without a transport or gameplay scripts:
- Create an inactive GameObject with a TimeManager and NetworkObject.
- Assign the NetworkObject's TimeManager property through its non-public setter using reflection. Leave PredictionSmoother null.
- Read the TimeManager.OnUpdate event's backing delegate and record its invocation-list length (zero when null).
- Invoke the private
InvokeStartCallbacks_Prediction(false) using reflection. The count increases by one.
- Invoke
InvokeStopCallbacks_Prediction(false). Expected: baseline count. Actual with the existing guard: the count remains baseline + 1.
- Repeat the start/stop pair. Each cycle retains another subscription.
The corresponding integration scenario is a dedicated server with a separate client, repeatedly spawning and despawning a pooled NetworkObject while that client observes it.
Proposed fix
private void InvokeStopCallbacks_Prediction(bool asServer)
{
- if (!asServer)
+ if (asServer)
return;
Keep the existing null check, unsubscribe, and smoother cleanup unchanged. This makes cleanup match the client-only subscription.
Validation
With this correction, local isolated lifecycle tests pass for:
- 100 consecutive client start/stop cycles, returning the event subscriber count to baseline after every cycle.
- Server-only start/stop, which must not add a client update subscriber.
- Host lifecycle with server shutdown first: the client subscription remains until client shutdown.
- Host lifecycle with client shutdown first: client shutdown removes the subscription; subsequent server shutdown does not affect the count.
These are direct lifecycle-method regression tests, not a claim of complete end-to-end networking coverage. No performance improvement percentage is claimed.
Related reports
I did not find an exact duplicate of this specific client-unsubscribe issue.
General
Description
NetworkObject.InvokeStartCallbacks_Prediction(false)subscribesTimeManager_UpdatetoTimeManager.OnUpdate, but the matchingInvokeStopCallbacks_Prediction(false)returns before unsubscribing. The stop method currently returns when!asServer, whereas the subscription belongs to the client lifecycle.Consequently, a client-only despawn leaves the callback registered. Reusing the same pooled NetworkObject and starting its client lifecycle again adds another subscription. This can retain despawned objects, invoke duplicate callbacks, and increase the cost and allocations of modifying the shared multicast delegate. The same guard also skips
PredictionSmoother.OnStopClient()for client shutdown.Source
Upstream source, pinned to 4.7.3R.
Start subscribes inside
if (!asServer). Stop currently begins with:Minimal lifecycle reproduction
This can be isolated in an EditMode test without a transport or gameplay scripts:
InvokeStartCallbacks_Prediction(false)using reflection. The count increases by one.InvokeStopCallbacks_Prediction(false). Expected: baseline count. Actual with the existing guard: the count remains baseline + 1.The corresponding integration scenario is a dedicated server with a separate client, repeatedly spawning and despawning a pooled NetworkObject while that client observes it.
Proposed fix
private void InvokeStopCallbacks_Prediction(bool asServer) { - if (!asServer) + if (asServer) return;Keep the existing null check, unsubscribe, and smoother cleanup unchanged. This makes cleanup match the client-only subscription.
Validation
With this correction, local isolated lifecycle tests pass for:
These are direct lifecycle-method regression tests, not a claim of complete end-to-end networking coverage. No performance improvement percentage is claimed.
Related reports
I did not find an exact duplicate of this specific client-unsubscribe issue.