Skip to content

NetworkObject prediction update subscription is not removed on client despawn (reversed asServer guard) #1075

Description

@alexchernyakov

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:

if (!asServer)
    return;

Minimal lifecycle reproduction

This can be isolated in an EditMode test without a transport or gameplay scripts:

  1. Create an inactive GameObject with a TimeManager and NetworkObject.
  2. Assign the NetworkObject's TimeManager property through its non-public setter using reflection. Leave PredictionSmoother null.
  3. Read the TimeManager.OnUpdate event's backing delegate and record its invocation-list length (zero when null).
  4. Invoke the private InvokeStartCallbacks_Prediction(false) using reflection. The count increases by one.
  5. Invoke InvokeStopCallbacks_Prediction(false). Expected: baseline count. Actual with the existing guard: the count remains baseline + 1.
  6. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions