Table of Contents

Class LagCompensatedQuery

Namespace
FishMMO.Shared
Assembly
FishMMO.Shared.dll

Spatial queries resolved against where characters were when the caster's client saw them.

public static class LagCompensatedQuery
Inheritance
LagCompensatedQuery
Inherited Members

Remarks

Every hit-resolving query in the ability system should route through here rather than calling UnityEngine.PhysicsScene directly. Both entry points execute the query eagerly inside the rewind scope and return a count, so no caller can accidentally hold characters displaced while it enumerates results — the failure that would apply damage and run ECA actions against a world several hundred milliseconds stale.

Anything that decides WHICH hits survive belongs inside the scope too, which is why OverlapSphereNearest(EventData, GameObject, Vector3, float, LayerMask, int, bool, List<CompensatedHit>) ranks, deduplicates and caps in there rather than handing back a raw buffer. A caller that selects its candidates from the rewound world and then ranks them by live positions is reading two different worlds; at 300 ms that is metres, and it picks a different victim than the one the caster was looking at. The selectors solve the same problem with TargetSelector.GatherRewound.

When there is nothing to compensate — a server-driven caster, a client whose tick bookkeeping is not yet established, or a scene with no recorded history — the query runs uncompensated. That is the behaviour these call sites had before, so an unregistered character degrades accuracy instead of dropping the hit.

Methods

OverlapSphereNearest(EventData, GameObject, Vector3, float, LayerMask, int, bool, List<CompensatedHit>)

The characters nearest center, resolved against the caster's view of the world, deduplicated per character and capped at maxHits.

public static int OverlapSphereNearest(EventData eventData, GameObject context, Vector3 center, float radius, LayerMask mask, int maxHits, bool charactersOnly, List<LagCompensatedQuery.CompensatedHit> results)

Parameters

eventData EventData

Event whose Initiator is the caster whose view is reconstructed.

context GameObject

Object whose scene is queried and rewound.

center Vector3

Centre of the query, and the point distances are measured from.

radius float

Query radius.

mask LayerMask

Layers to query.

maxHits int

Maximum distinct bodies to keep. Zero or less means no cap.

charactersOnly bool

Drop hits that resolve to no ICharacter before they consume a slot of the cap. True for anything that resolves damage: TargetLayerMask defaults to every layer, so a blast let off beside terrain would otherwise spend its whole cap on scenery and hit nobody.

results List<LagCompensatedQuery.CompensatedHit>

Receives the hits, nearest first. Cleared first.

Returns

int

The number of hits written to results.

Remarks

Everything that decides the answer happens inside one rewind scope. The query, the distance measurement and the ranking all read the same displaced world. Splitting them was the trap this method exists to close: the previous shape returned a raw buffer with the scope already shut, so the caller selected its candidates from the world the caster saw and then ranked them by where those characters are NOW — two different worlds, differing by the target's speed times the caster's latency, which at 300 ms is metres. It is the same failure TargetSelector.GatherRewound was built for, and the reason RewoundOverlapSphere was deleted rather than left available.

Ordered by distance, not by identity. A cap is only meaningful over an ordered set, and the order a blast radius means is "nearest first". Identity order is equally reproducible and gameplay nonsense: truncating it keeps the lowest ObjectIds, so a three-target AoE in a crowd hit the same three earliest-spawned characters every time and never the ones standing on the impact point. Ties fall through to CompareStable(TargetRank, TargetRank), which every peer computes identically.

One entry per character. Keyed through ResolveHitKey(Collider, out ICharacter), so a body with two hitboxes costs one hit and one slot of the cap — otherwise the same ability hits a different NUMBER of characters depending on how its targets happen to be rigged, and a character rigged with its collider on a child is dropped entirely. The nearest collider on a body is the one kept, which falls out of walking the distance order.

The cap is applied by early exit, not by truncation. Walking the sorted ranks and stopping at maxHits distinct characters means the per-candidate component resolution runs for roughly the cap rather than for the whole crowd. That is where the saving in an area query is — not in narrowing the physics query, which is one broadphase traversal either way.

RaycastNearest(EventData, GameObject, Vector3, Vector3, float, LayerMask, int, bool, List<CompensatedHit>, Transform)

The bodies a ray passes through, in order along it, deduplicated per character and capped at maxHits.

public static int RaycastNearest(EventData eventData, GameObject context, Vector3 origin, Vector3 direction, float distance, LayerMask mask, int maxHits, bool charactersOnly, List<LagCompensatedQuery.CompensatedHit> results, Transform ignoreRoot = null)

Parameters

eventData EventData

Event whose Initiator is the caster whose view is reconstructed.

context GameObject

Object whose scene is queried and rewound.

origin Vector3

Where the ray starts.

direction Vector3

Ray direction. Normalised here; a degenerate vector returns nothing.

distance float

Ray length.

mask LayerMask

Layers to query.

maxHits int

Maximum distinct bodies to keep. Zero or less means no cap.

charactersOnly bool

Drop hits that resolve to no ICharacter before they consume a slot of the cap. See the note on OverlapSphereNearest(EventData, GameObject, Vector3, float, LayerMask, int, bool, List<CompensatedHit>); for a ray this also means scenery does not stop a shot, so set it false when a wall should block one.

results List<LagCompensatedQuery.CompensatedHit>

Receives the hits, nearest first. Cleared first.

ignoreRoot Transform

Optional transform whose own colliders and children are skipped BEFORE the cap is charged — for a projectile excluding itself, whose collider sits on the ray origin.

Returns

int

The number of hits written to results.

Remarks

The hitscan path, and the one that needs compensation most. A ray is infinitely thin, so unlike a volume it has no tolerance to absorb the staleness of a live-position query: the difference between a hit and a miss is the target's own width. At 300 ms a running peer is metres from where the shooter saw it, which is simply a miss.

Ordered along the ray. Distance leads and identity only breaks exact ties — a line is a sequence and every effect authored on one (pierce, falloff, "the first thing you hit") reads it that way. Unity's non-allocating overload promises no order at all, so without this the pierce cap chose its victims arbitrarily. This is the one query shape where the boundary CAN pick the order, because a ray admits only one reading; the overlap above deliberately does not, which is why its identity sort was removed.

One entry per character, so a body with two hitboxes costs one pierce rather than two, and the nearest of its colliders is the one reported — which falls out of walking the ray order. A beam re-queried every tick therefore hits each target once per tick, which is what a beam means; the per-lifetime hit set that stops a projectile draining its hit count into one victim belongs to AbilityObject and is deliberately not duplicated here.

TryResolveRewind(EventData, out ICharacter, out RewindTarget)

Resolves the caster and the tick its client was rendering peers at.

public static bool TryResolveRewind(EventData eventData, out ICharacter caster, out RewindTarget target)

Parameters

eventData EventData
caster ICharacter
target RewindTarget

Returns

bool