Mass-Aware Interest Management for Browser Fat Games with Fish-Net and URP
Browser fat games live or die on how well they scale observation. As players absorb mass, their effective reach grows—and so does the set of entities they must see, simulate, and receive over the wire. Naïve fixed radii either starve large players of awareness or flood small ones (and the network) with irrelevant state. On WebGL that cost shows up fast: bandwidth spikes, GC pressure, and URP draw calls that miss frame budget.
Fish-Net’s interest management and URP’s cull distances are the two levers that matter most. Used independently they drift apart; used together and driven by the same mass curve they keep what a client receives aligned with what it renders. This article walks through a mass-aware interest model for browser IO-style titles: how to map mass to observer radius, how to mirror that scale in URP culling and LOD, and how to validate the pairing so denser worlds stay playable without blowing WebGL budgets.
Why a Fixed AOI Radius Breaks as Players Get Fat
Before wiring that shared mass curve, it helps to see exactly where a fixed area-of-interest radius fails in a fat game. In classic shooters or MMOs, relevance is mostly spatial: if another player is inside N metres, stream them. Fat and IO titles break that assumption. What matters is not a constant meter value but a moving function of mass, speed, and eat range. A tiny spawn cares about nearby pellets and anything that can swallow it in one bite. A heavy player cares about other large bodies across a much wider band, ignores most pellets, and must see threats long before they enter contact range. Relevance scales with the body, not the map grid.
A static AOI cannot serve both ends of that spectrum. Set the radius for newborns and large players starve: legitimate threats sit outside the bubble until they are already on top of them, which feels unfair and breaks chase-and-escape play. Set it for late-game mass and every fresh spawn is flooded with far-field transforms it will never interact with. Bandwidth, deserialization, and interest rebuilds all rise for entities that contribute nothing to the local fight. The same number that is “safe” at one mass is wasteful or dangerous at another.
One radius, many object classes
Growth also changes which object classes matter. Early on, pellets and small pickups dominate the interest set. Mid-game, other players and modest hazards take over. Late-game, only peer-scale bodies, high-value objectives, and long-range hazards justify the cost of observation. A single observer radius applied uniformly to every NetworkObject ignores that shift. You either over-subscribe cheap clutter for whales or under-subscribe the only entities a small client actually needs. Class-aware rules without a mass driver still leave you with a constant that does not track how the local player’s world of concern is expanding.
Why browsers make the failure obvious
Browser sessions amplify every mistake. Each extra observed transform is not an abstract server cost—it is CPU time, garbage-collector pressure, and WebSocket traffic on a thin client that already shares a tab with the DOM, the audio graph, and URP. Deserializing positions you will never render still allocates; rebuilding interest sets still spikes main-thread work; shipping state for far pellets still competes with input and prediction. Static AOI that “worked on a desktop build” starts hitching and draining battery the moment the world densifies and a few players snowball. The fix is not a larger constant radius. It is tying interest—and later, what URP bothers to cull and LOD—to the same mass signal that already defines how the player moves and eats.
Fish-Net Observer Primitives for Dense IO Worlds
That mass signal is exactly what Fish-Net’s observer stack is built to consume. You do not need a hand-rolled interest manager; you need ObserverManager and its condition pipeline wired so relevance tracks the same mass value that already drives movement and eating. Get the primitives right and mass-aware radius becomes a thin custom condition on top of a stack that already bounds work for dense arenas.
Match ObserverManager cadence to the IO tick
Fat games almost never need interest re-evaluated every rendered frame. Pellets spawn and despawn on the simulation tick, mass jumps on eat events, and bots repath on their own interval. Point ObserverManager’s update rate at that reality—once per network tick, or every N ticks—so you stop rescanning the whole world graph on every Update. When the cadence matches how fast relevance actually changes, observer CPU stays proportional to gameplay instead of frame rate, which matters on browser hosts that already share a thread with the renderer.
Bound candidates with HashGrid before any mass logic
When pellet and bot counts spike, a naive sweep across every NetworkObject becomes the cost centre long before bandwidth does. Fish-Net’s HashGrid and grid-based conditions partition space so each observer only considers entities in nearby cells. That spatial bound is what keeps evaluation viable as the arena densifies. Without it, even a perfectly mass-scaled radius still walks far too many objects every check.
Layer cheap conditions first; put mass on the shortlist
Order is not cosmetic. Apply the built-in filters before anything custom so expensive logic only ever sees survivors:
- Distance or grid condition — hard spatial bound from HashGrid
- Owner condition — drop objects the connection already owns
- Scene condition — ignore objects outside the relevant scene
- Custom condition last — mass, team, and object-class filters on the shortlist only
Custom observer conditions are the clean extension point. They receive the already-pruned set and decide visibility using the same mass, team, and class data the server uses for physics and scoring. That is where you will later attach the mass-scaled radius without re-implementing spatial queries or ownership rules.
Observers drive client URP load, not only bandwidth
Do not treat the observer set as a pure send-rate valve. Every enter or leave triggers a spawn or despawn of the corresponding NetworkObject on that client. Those side effects decide what URP must instantiate, cull, skin, and LOD. A loose condition does not merely waste WebSocket bytes; it forces the browser to create and destroy renderers, colliders, and materials the player never needed to see. Mass-aware interest therefore protects frame time on the client as directly as it protects server egress—the same shortlist that keeps the wire quiet also keeps the URP camera and cullers honest.
Clamping the Observation Radius to Mass and Role
That protection only holds if the radius itself is honest about what mass actually means. Treat observation distance as a clamped curve of mass (or the visual scale you already derive from it), not a linear multiplier that races away the moment someone becomes an apex blob. Early on, a modest base radius plus a gentle rise—square-root or similar—gives small players enough awareness to hunt and flee. Past a chosen mass band you hard-clamp: more mass still widens the view a little, but never enough to subscribe half the map. Without that ceiling, late-game interest sets grow faster than either the server or a browser client can absorb.
Different object classes, different effective ranges
A single radius for every NetworkObject is the next trap. Edible clutter—pellets, crumbs, low-value pickups—should use a shorter effective range than rival players or meaningful hazards. Apex blobs do not need every pellet in a wide annulus; they need the competitors and threats that can still interact with them. Implement this as class- or tag-weighted distances inside the custom observer condition (or as separate conditions layered in the stack), so the same mass curve produces a tight clutter shell and a wider peer shell. The HashGrid already limited the candidate set; role-aware ranges keep the final observed set lean.
Hysteresis, cooldowns, and event-driven updates
Boundary jitter is expensive. When an entity rides the edge of someone else’s radius, frame-to-frame enter/exit thrash forces constant observer spawn and despawn—exactly the renderer and collider churn the previous section warned against. Give the condition enter and exit thresholds that differ by a small margin, and optionally a short cooldown before a despawned object can re-enter. That hysteresis turns a noisy boundary into a stable shortlist.
Recompute the mass (and any derived radius or class weights) on mass-change events, not every network tick. Eating, splitting, and merging already raise authoritative state updates; hook the condition inputs there so you pay the curve evaluation only when mass actually moved. Idle titans and stationary pellets should not force fresh interest math each tick.
Finally, document and enforce a single rule: server-authoritative mass is the only source of truth for interest math. Client-predicted or visually smoothed scale must never feed the observer condition. If the server and a browser disagree on how fat someone is, you get asymmetric visibility, desyncs, and exploit surface. Keep the curve, clamps, class weights, and hysteresis tables on the server; clients consume the resulting spawn/despawn stream and point their URP cameras at whatever the observers already approved.
URP Culling in Lockstep with Mass-Scaled Interest
That client-side handoff only works if the renderer obeys the same contract the observers already enforced. Once Fish-Net has decided an entity is in or out of interest, URP should not keep drawing, lighting, or simulating it as if it still mattered. The cleanest way to get there is to treat network interest and camera culling as one policy: the mass-scaled distances, class weights, and hysteresis bands you already defined on the server become the numbers the browser uses for far-plane, layer culling, and LOD tier cuts.
Share one distance table across observers and the camera
Bake the clamped mass curve into a small shared table the client can read when it configures the URP camera and per-layer cull distances. Far-plane should sit just beyond the largest observation radius the local player can currently hold—usually the rival-player band—so you are not paying for geometry the network will never deliver. Layer cull distances follow the same class split: edible clutter drops out earlier than player silhouettes and hazards. LOD tiers map onto those bands too; a pellet at the edge of interest can collapse to a cheap billboard or a single-quad impostor long before a competing blob does. When mass changes and the server recomputes radius, the client updates the same table and pushes the new far-plane and layer distances in one place instead of maintaining a second, drifting set of magic numbers.
Cheap paths for high-count clutter, ruthless cuts as you grow
Pellets and other edible noise dominate entity count in fat games. Prefer GPU instancing and simple URP Lit or Unlit paths with a shared material over unique materials or per-orb property blocks. Instancing keeps draw-call and set-pass cost flat while the interest system still throttles how many instances ever exist in the scene. Reserve richer shading and unique variants for player bodies and interactive hazards—the objects whose observation radius stays wide as mass climbs.
As the local player’s scale grows, the same curve that widened rival observation should tighten non-essential cost first. Drop soft shadow casters on debris, trim trail and particle budgets, and collapse secondary VFX before you ever shorten gameplay-critical silhouettes. Players need to read who can eat whom; they do not need every sparkle at maximum range. When an object leaves interest, despawn must tear it out of render and simulation paths immediately—disable renderers, release rigidbodies or simple movers, and return pooled instances on the same frame the network callback fires. Leaving a “sleeping” mesh in the hierarchy after despawn is how browser frame time leaks even when bandwidth looks healthy.
Done this way, the mass-aware observer stack and URP culling stay in lockstep: every meter the server grants or revokes is a meter the camera, LOD group, and GPU stop paying for. That single policy is what keeps both WebSocket payload and frame time coherent as the world densifies and bodies get fat.
Protecting WebGL Bandwidth and Main-Thread Time as Interest Sets Shift
That coherence only holds if the browser can absorb those grants and revokes without melting its WebSocket and main-thread budgets. WebGL clients are uniquely sensitive to message frequency and to the spikes that arrive when an interest set reshuffles. A mass pickup that suddenly widens the observer radius does not just add bytes—it queues a wave of NetworkObject spawns, first-state messages, Awake/OnStartClient work, and URP registration, all on the same thread that also runs the render loop and the JavaScript garbage collector. Desktop hosts can shrug this off; browser peers cannot.
Batch the spawn wave, do not thrash the condition stack
Treat observer-driven spawn traffic as something you batch, not something you drip every tick. Prefer Fish-Net’s spawn batching and timed observer rebuilds over re-evaluating the full condition stack on every network tick the instant mass changes. After a pickup, queue a single recomputation rather than letting radius, distance, and custom mass checks thrash frame-by-frame while the client is still absorbing the previous wave. The enter/exit hysteresis and cooldowns already discussed on the radius curve are essential here: pair them with a short expansion cooldown so the interest set grows in deliberate steps instead of a cascade of Instantiates that stall the main thread mid-growth.
Differentiate fidelity: rivals stay fresh, clutter rides cheaper
Not every observed object deserves the same transport cost. Pellets, food crumbs, and other non-combat clutter can use heavier compression, lower send rates, and coarser interpolation once they are inside the radius. Rival players and anything that can actually eat you keep fresher state and tighter tick alignment. The mass-scaled observer already decides whether an object is relevant; the send pipeline then decides how often and how precisely. That split stops the WebSocket payload from exploding when a large body surveys a dense food field while still giving combat-critical peers the update cadence they need.
Profile the triad after growth, not only in steady state
After mass milestones, profile three numbers together: interest-set size (how many NetworkObjects the client currently has spawned), messages per second on the connection, and frame time or main-thread occupancy. A radius curve that looks fine in isolation can still tank the client if spawn bursts coincide with physics steps or GC. Capture those moments right after growth events rather than only during steady-state cruising; that is where the mass-aware policy either proves itself or reveals a missing batch boundary.
Finally, plan the host or dedicated-server view separately from any single WebGL peer. The server may legitimately observe more of the world—full simulation authority, AI, anti-cheat—while each browser client only ever receives the mass-clamped slice. Do not treat the server’s observer configuration as a template you can copy onto clients. The budgets diverge the moment WebGL is in the picture, and the interest stack must be tuned with that split in mind.
From Mass Event to Live Tuning: The Reference Pipeline
That split is exactly why the stack needs a single, repeatable path from mass change to what the browser actually draws—not a one-off editor setup that drifts as the economy shifts. Treat interest and culling as one pipeline you can re-run whenever a player grows, shrinks, or changes role.
The end-to-end path
Keep the sequence deterministic so every mass event produces the same observer and render outcome on every build:
- Mass event — server-authoritative mass (or scale) changes on eat, decay, or merge; nothing else feeds interest math.
- Radius table — look up clamped radii per object class (rivals, pellets, hazards) from the mass curve, with hysteresis bands already applied.
- Custom condition — Fish-Net condition stack evaluates distance against those radii, after HashGrid and cheaper owner/scene filters.
- Observer refresh — rebuild the interest set on the network tick, not every frame; queue enter/exit with cooldowns so boundary thrash never hits the wire.
- Client spawn — batched NetworkObject spawn/despawn on the WebGL client; strip despawned entities from local simulation immediately.
- URP LOD apply — set layer cull distances, LOD tiers, and VFX/shadow cuts from the same mass-scaled distances so visibility never outruns what the observer already allowed.
What to measure every session
Instrument the live path, not only the editor host. Track p95 observed-object count by mass tier, outbound bandwidth per client, spawn and despawn rate after growth spikes, and URP batch and draw counts on the browser build. When any of those climb together after a mass jump, the radius table or send-rate split is the first place to look—not a generic “optimize networking” pass.
Validate like production, tune from the apex
Script growth curves that force players through early nibble, mid-game pack fights, and late apex mass, then run multi-client browser sessions against a real server. Editor host play hides WebGL main-thread cost and WebSocket batching behaviour; browser tests do not. Tune clamps on the late-game apex cases first—those are where fixed AOI and generous culling explode—then walk the same tables backward so early-game still feels informative: enough pellets and nearby rivals to read the space, without flooding a tiny client. When the curves hold under scripted densification, ship the radius tables behind remote config. Live IO economies change pellet value, soft caps, and map density without warning; swapping table entries without a full client rebuild keeps bandwidth and frame time under control while the world keeps growing.
Mass-aware interest is not a one-time observer setup. It is this loop—event, table, condition, refresh, spawn, LOD—measured on real browsers and adjustable in production. Run it deliberately, and Fish-Net plus URP stay inside budget even as fat-game worlds densify and the largest players keep getting larger.