Class ObserverStreamingPolicy
Tunables and pure decision functions for per-observer streaming: how far a character is visible from (scaled by local density), which observed characters a client receives at full rate, and what reduced rate the rest get.
public static class ObserverStreamingPolicy
- Inheritance
-
ObserverStreamingPolicy
- Inherited Members
Remarks
Two levers, both server-side. The first is the observer range: FishNet's
DistanceCondition is cloned per object, so its distance can be changed at runtime
per character. In a crowd, every extra metre of range multiplies the number of characters
each client streams, so range is shrunk as local density rises and restored as it falls.
The second is the observer cap: of everything a client can see, only the
FullRateObserverCap most relevant characters send every unreliable update;
the rest are sent every Nth, with N chosen by distance. Relevance favours characters in
combat, then party and guild members, then proximity.
Everything here is pure and static so it can be unit-tested without a network, and so the
scene server can override the numbers from its configuration at startup
(ApplySetting(string, string)). Reliable sends are never rate limited — see
IObserverSendFilter.
Properties
CombatWeight
Relevance weight for a character currently in combat. Kept above PartyWeight + DistanceWeight so a fighter at the edge of range still outranks an idle party member standing next to the viewer.
public static float CombatWeight { get; set; }
Property Value
DensityRadius
Radius, in metres, within which other characters count towards local density.
public static float DensityRadius { get; set; }
Property Value
DistanceWeight
Relevance weight for proximity: a character at distance 0 scores this, one at the viewer's full observer range scores 0.
public static float DistanceWeight { get; set; }
Property Value
EngagedFullRateBudget
How many characters inside the engagement radius receive every tick.
public static int EngagedFullRateBudget { get; set; }
Property Value
Remarks
The engagement exemption exists so lag compensation can rewind to a real tick sample, and without a budget it is unbounded: thirty players inside 40 m is thirty full-rate streams. The top entries by relevance keep every tick; the rest inside the radius fall to every second tick, which costs them a compensation accuracy of one tick — around 3 cm at walking speed, against the six ticks the distance band would otherwise have given them.
EngagedOverflowInterval
Interval applied to engaged characters beyond EngagedFullRateBudget.
public static byte EngagedOverflowInterval { get; set; }
Property Value
EngagementRange
Radius, in metres, inside which a character's transform is sent to an observer at FULL rate regardless of any distance or cap throttling.
public static float EngagementRange { get; set; }
Property Value
Remarks
This is what makes lag compensation honest. A throttled transform reaches the observer every 3, 6 or 8 ticks, and the client interpolates across that gap — so what it renders is a position that existed on no server tick, and no rewind can reproduce it. Inside this radius the observer receives every tick, so the pose it saw IS a tick sample and the rewind lands exactly on it.
It is a floor, not the whole rule: ResolveEngagementRange(float) widens it to cover a character's own longest ability, up to EngagementRangeCeiling. Everything beyond keeps the bandwidth saving, which is where most of it lives — exempting a 40 m disc out of a 100 m observer range leaves 84% of the observed area still throttled.
EngagementRangeCeiling
Hard ceiling on the engagement radius: no attack or ability in this project reaches further, so nothing beyond it can ever need tick-exact compensation.
public static float EngagementRangeCeiling { get; set; }
Property Value
EngagementRangeMargin
Metres added to a character's longest ability range when resolving its engagement radius, covering the ground both parties can close during the compensation window.
public static float EngagementRangeMargin { get; set; }
Property Value
Remarks
The rewind reaches up to LagCompensationTick.MaximumCompensationTicks into the past;
at a closing speed of roughly 12 m/s that is a few metres, and a target that is about to
come into range needs to already be at full rate when it does.
FullRateObserverCap
How many observed characters a single client receives at full rate. Everyone else it can see is rate limited by LodBands.
public static int FullRateObserverCap { get; set; }
Property Value
GuildWeight
Relevance weight for a character in the viewer's guild.
public static float GuildWeight { get; set; }
Property Value
HighDensity
Neighbour count at or above which a character's range is fully scaled down.
public static int HighDensity { get; set; }
Property Value
LodBands
Distance bands applied to characters beyond the cap, ascending by distance. The last band should have an infinite distance so every character matches one.
public static IReadOnlyList<ObserverStreamingPolicy.LodBand> LodBands { get; }
Property Value
LowDensity
Neighbour count at or below which a character keeps its full configured range.
public static int LowDensity { get; set; }
Property Value
MinimumRange
Absolute floor on any scaled range, in metres, so combat never happens out of sight.
public static float MinimumRange { get; set; }
Property Value
PartyWeight
Relevance weight for a character in the viewer's party.
public static float PartyWeight { get; set; }
Property Value
RangeChangeThreshold
Range changes smaller than this, in metres, are not applied — avoids churning the observer rebuild.
public static float RangeChangeThreshold { get; set; }
Property Value
RangeScaleAtHighDensity
Fraction of the configured range applied at HighDensity.
public static float RangeScaleAtHighDensity { get; set; }
Property Value
RescheduleIntervalTicks
Ticks between scheduling passes. 15 is half a second at 30 Hz.
public static uint RescheduleIntervalTicks { get; set; }
Property Value
VisibilityBudget
Hard cap on how many CHARACTERS one viewer may observe at once.
public static int VisibilityBudget { get; set; }
Property Value
Remarks
The difference between this and FullRateObserverCap is existence versus fidelity. The full-rate cap slows the 25th character down; this one makes the 41st not exist on that client at all — no transform, no vitals, no buffs, no spawn. It is what bounds a client's cost in a town, where distance alone bounds nothing.
Applies only to registered characters. Interactables, world items and scene objects are governed by their own distance conditions and are not budgeted — they are cheap and their absence is far more confusing than a missing distant stranger.
VisibilityBudgetHysteresis
How far past VisibilityBudget an ALREADY VISIBLE character keeps its slot, as a fraction of the budget.
public static float VisibilityBudgetHysteresis { get; set; }
Property Value
Remarks
Rank hysteresis, not distance hysteresis. Two characters of near-identical score sitting either side of the boundary would otherwise swap every pass, and each swap is a spawn and a despawn — far more expensive than any rate change, and visible as flicker.
Methods
ApplySetting(string, string)
Applies one key=value server setting. Unknown keys are ignored; malformed
values are rejected. Returns true when a setting was applied.
public static bool ApplySetting(string key, string value)
Parameters
Returns
Remarks
Keys: ObserverFullRateCap, ObserverCombatWeight, ObserverPartyWeight,
ObserverGuildWeight, ObserverDistanceWeight, ObserverDensityRadius,
ObserverLowDensity, ObserverHighDensity, ObserverRangeScaleAtHighDensity,
ObserverMinimumRange, ObserverRescheduleTicks, and
ObserverLodBands as distance:interval,distance:interval,...
(e.g. 20:2,45:4,inf:8).
LodInterval(float)
Send interval, in ticks, for a character beyond the cap at the given distance. Returns 1 (full rate) when no band matches.
public static byte LodInterval(float distance)
Parameters
distancefloat
Returns
ResolveEngagementRange(float)
The full-rate radius for an observer whose longest usable ability reaches
longestAbilityRange.
public static float ResolveEngagementRange(float longestAbilityRange)
Parameters
longestAbilityRangefloatLongest range among the character's known abilities.
Returns
- float
The radius inside which throttling is suspended.
Remarks
Melee characters keep almost all of the LOD saving; a long-range caster pays for exactly
the reach it has. Note that every ability authored today resolves to a range of 0
(Ability.Range is Speed * LifeTime, and no template sets Speed), so the
floor is currently doing all the work — this widens automatically once ranges are authored.
ScaledRange(float, int)
Observer range for a character with neighbourCount other characters
within DensityRadius: the full baseRange at or below
LowDensity, scaled linearly to RangeScaleAtHighDensity at
HighDensity, never below MinimumRange (or the base range,
whichever is smaller).
public static float ScaledRange(float baseRange, int neighbourCount)
Parameters
Returns
Score(bool, bool, bool, float, float)
Relevance of an observed character to a viewer. Higher is more relevant.
public static float Score(bool inCombat, bool sameParty, bool sameGuild, float distance, float maxRange)
Parameters
inCombatboolObserved character is in combat.
samePartyboolObserved character shares the viewer's party.
sameGuildboolObserved character shares the viewer's guild.
distancefloatDistance from viewer to observed, in metres.
maxRangefloatDistance at which proximity contributes nothing.
Returns
SetLodBands(IEnumerable<LodBand>)
Replaces the LOD bands. Bands are sorted by distance; an empty list means "never limit".
public static void SetLodBands(IEnumerable<ObserverStreamingPolicy.LodBand> bands)
Parameters
ShouldSendThisTick(uint, byte, int)
True when an update should be sent on tick to an observer whose
interval is interval. phase (typically the
connection id) spreads different observers' send ticks so a cap of limited observers
does not all fire on the same tick.
public static bool ShouldSendThisTick(uint tick, byte interval, int phase)