menu_bookDocumentation

Network Visibility Culling: AlwaysTransmit, INetworkVisible, CullDelay, and PVS-Based Object Culling

calendar_today May 4, 2026 schedule ~2 min read person patrickjr verified 50

Network Visibility Culling: How s&box Decides What to Send to Each Client

s&box has a built-in network visibility culling system that avoids sending updates for objects that are not visible to a client. Understanding this prevents unexpected "objects not appearing" bugs.

How Culling Works

Each NetworkObject tracks a _culledConnections set. Every network tick, UpdateTransmitState is called for each networked object:

  1. If AlwaysTransmit = true (the default), culling is skipped entirely — updates are always sent.
  2. Otherwise, the object's world bounds are checked against each connection's visibility origins using PVS (Potentially Visible Set) data from the map.
  3. If an object has been invisible to a connection for more than 2 seconds (CullDelay = 2f), it is added to _culledConnections and the client is notified via SetCullState.
  4. When an object becomes visible again, it is removed from _culledConnections and the client receives a full state update.

Visibility Origins

Each client sends its camera position as a "visibility origin" to the host every tick. The host uses these positions to determine PVS visibility. If a client has no camera, the origin defaults to Vector3.Zero.

Custom Visibility: INetworkVisible

You can override the default visibility logic by adding a component that implements Component.INetworkVisible:

CSHARP
public class MyVisibilityComponent : Component, Component.INetworkVisible
{
    public bool IsVisibleToConnection( Connection target, BBox worldBounds )
    {
        // Custom logic — e.g., always visible to the owner
        if ( target == GameObject.Network.Owner )
            return true;

        // Fall back to distance check
        return target.VisibilityOrigins.Any( o => o.Distance( WorldPosition ) < 1000f );
    }
}

When a INetworkVisible component is present on a GameObject, it takes priority over the default PVS check.

AlwaysTransmit

CSHARP
// Disable culling for this object (default behavior)
go.Network.AlwaysTransmit = true;

// Enable culling (object only sends updates when visible)
go.Network.AlwaysTransmit = false;

Changing AlwaysTransmit after spawn uses an internal [Rpc.Broadcast( NetFlags.OwnerOnly )] to propagate the change to all clients.

Dirty vs Clean Objects

The network update loop partitions objects into "dirty" (pending changes) and "clean" (fully ACK'd) lists. Dirty objects are processed first to ensure they end up in earlier network clusters, which are less likely to be delayed under congestion.

Was this helpful?