Table of Contents

Class ObserverStreamingPolicy

Namespace
FishMMO.Shared
Assembly
FishMMO.Shared.dll

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

float

DensityRadius

Radius, in metres, within which other characters count towards local density.

public static float DensityRadius { get; set; }

Property Value

float

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

float

EngagedFullRateBudget

How many characters inside the engagement radius receive every tick.

public static int EngagedFullRateBudget { get; set; }

Property Value

int

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

byte

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

float

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

float

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

float

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

int

GuildWeight

Relevance weight for a character in the viewer's guild.

public static float GuildWeight { get; set; }

Property Value

float

HighDensity

Neighbour count at or above which a character's range is fully scaled down.

public static int HighDensity { get; set; }

Property Value

int

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

IReadOnlyList<ObserverStreamingPolicy.LodBand>

LowDensity

Neighbour count at or below which a character keeps its full configured range.

public static int LowDensity { get; set; }

Property Value

int

MinimumRange

Absolute floor on any scaled range, in metres, so combat never happens out of sight.

public static float MinimumRange { get; set; }

Property Value

float

PartyWeight

Relevance weight for a character in the viewer's party.

public static float PartyWeight { get; set; }

Property Value

float

RangeChangeThreshold

Range changes smaller than this, in metres, are not applied — avoids churning the observer rebuild.

public static float RangeChangeThreshold { get; set; }

Property Value

float

RangeScaleAtHighDensity

Fraction of the configured range applied at HighDensity.

public static float RangeScaleAtHighDensity { get; set; }

Property Value

float

RescheduleIntervalTicks

Ticks between scheduling passes. 15 is half a second at 30 Hz.

public static uint RescheduleIntervalTicks { get; set; }

Property Value

uint

VisibilityBudget

Hard cap on how many CHARACTERS one viewer may observe at once.

public static int VisibilityBudget { get; set; }

Property Value

int

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

float

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

key string
value string

Returns

bool

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

distance float

Returns

byte

ResolveEngagementRange(float)

The full-rate radius for an observer whose longest usable ability reaches longestAbilityRange.

public static float ResolveEngagementRange(float longestAbilityRange)

Parameters

longestAbilityRange float

Longest 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

baseRange float
neighbourCount int

Returns

float

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

inCombat bool

Observed character is in combat.

sameParty bool

Observed character shares the viewer's party.

sameGuild bool

Observed character shares the viewer's guild.

distance float

Distance from viewer to observed, in metres.

maxRange float

Distance at which proximity contributes nothing.

Returns

float

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

bands IEnumerable<ObserverStreamingPolicy.LodBand>

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)

Parameters

tick uint
interval byte
phase int

Returns

bool