Table of Contents

Class TargetSelector

Namespace
FishMMO.Shared
Assembly
FishMMO.Shared.dll

Abstract base class for selecting targets in an ability or event context. Implementations consume the current EventData (its Target or Initiator serve as the spatial / contextual reference) and yield one or more UnityEngine.GameObjects for triggers, conditions or actions to operate on.

Asset safety: selectors are serialized inline on Trigger ScriptableObjects via [SerializeReference]. Unity cannot serialize references to scene GameObjects from asset files, so selectors intentionally hold no direct scene references. To "pick a specific scene object" from an asset-based Trigger, use NamedSceneObjectTargetSelector or TaggedSceneObjectTargetSelector — they resolve scene objects at runtime by name or tag. For inline (MonoBehaviour-hosted) triggers in a scene, prefer setting Target at the invocation site so the trigger receives the picked GameObject through standard event flow.

[Serializable]
public abstract class TargetSelector
Inheritance
TargetSelector
Derived
Inherited Members

Fields

Conditions

Conditions that must be met for a target to be valid. Evaluated per-candidate inside AreConditionsMet(GameObject, EventData). Honors the ambient ConditionFilter via AreConditionsMet(List<BaseCondition>, EventData, Func<BaseCondition, bool>).

[Tooltip("Conditions that must be met for a target to be valid.")]
[SerializeReference]
public List<BaseCondition> Conditions

Field Value

List<BaseCondition>

Methods

AreConditionsMet(GameObject, EventData)

Evaluates this selector's per-target Conditions against the candidate target. Builds a forked EventData scoped to the candidate so conditions see the right Target / TargetCharacter, then delegates to AreConditionsMet(List<BaseCondition>, EventData, Func<BaseCondition, bool>) so nested conditions' own TargetSelector and Combine settings are honored uniformly with top-level Trigger conditions.

protected bool AreConditionsMet(GameObject target, EventData eventData)

Parameters

target GameObject

The candidate target GameObject.

eventData EventData

The parent event data, or null.

Returns

bool

True when no conditions exist, or all conditions pass.

GatherRewound(EventData, GameObject, List<GameObject>, GatherTargets)

Collects candidates inside a single rewind scope and returns them once it has closed.

protected static void GatherRewound(EventData eventData, GameObject context, List<GameObject> results, TargetSelector.GatherTargets gather)

Parameters

eventData EventData

The event driving the selection.

context GameObject

The spatial origin object; its scene is the one rewound.

results List<GameObject>

Receives the selected objects, in the gather's order.

gather TargetSelector.GatherTargets

The query and ranking to run under the scope.

Remarks

This is ChainTargetSelector's pattern, generalised, and it exists for two reasons the chain learned first.

Ranking must read the same world the query did. LagCompensatedQuery closes its rewind before returning, so a selector that queried through it and then measured distances was selecting from the rewound world and ranking by live positions — the nearest candidate by one measure and a different one by the other, differing by the peer's speed times its latency. Conditions are evaluated inside the scope for the same reason: a range condition that disagrees with the query that produced the candidate is not a filter, it is a coin toss.

Nothing is yielded while the world is displaced. Selectors are iterators and the consumer between two yields is the damage and ECA pipeline. Materialising the whole result first means the scope has closed before any of that runs.

GetContext(EventData)

protected static GameObject GetContext(EventData eventData)

Parameters

eventData EventData

Returns

GameObject

GetTooltipContribution()

Returns a short, designer-facing tooltip line describing this selector's targeting (e.g. "Nearest enemy within 10m"), or null when the selector has nothing to contribute. Override on selectors that have player-visible targeting semantics.

public virtual string GetTooltipContribution()

Returns

string

IsAuthoritativePeer(EventData)

Returns the spatial origin for this selector — preferring Target, falling back to Initiator's GameObject.

protected static bool IsAuthoritativePeer(EventData eventData)

Parameters

eventData EventData

The current event data.

Returns

bool

A GameObject to use as a spatial reference, or null.

Remarks

No current callers, and that is not an oversight. Every spatial selector now gates on ResolvesTargetsLocally(EventData) instead, so the caster's own client predicts its selections. This is kept for a future selector whose consumer is genuinely server-authoritative and has nothing to predict — one that resolves loot rights, or picks a persistence target. Reach for it only when you can say why a client must see NOTHING; if the answer is "the effect is authoritative", that is already handled a level down by the action's own IsServer(ICharacter, EventData) gate, and this one would merely also delete the caster's feedback.

Server only, and not "not a replicate tick". Every physics selector used to open with if (tickData.IsReplicateTick) yield break;, on the theory that a replicate tick means a client replay. It does not. AbilityObject's self-target dispatch and its OnPreSpawn/OnSpawn dispatches, and AbilityController's activation triggers, all attach a TickEventData built from the replicate PredictionTick — on the server as well. So the guard fired on the server too, and a point-blank area ability selected nothing and dealt no damage on any peer. AbilityApplyAreaAction was corrected for exactly this and gates on the peer instead; this is the same rule, applied at the selector.

The tick payload is left on the event untouched. It still carries real information for the tick-domain consumers downstream (buff stamping, cooldowns) — it just never meant what the old guard read it as.

QueryBufferSize(int)

Query buffer size for a selector whose authored cap is maxHits.

protected static int QueryBufferSize(int maxHits)

Parameters

maxHits int

Returns

int

Remarks

Deliberately larger than the cap. Sizing the buffer at exactly MaxHits makes the physics broadphase perform the truncation, in its own order, before the selector ever sees the candidates — so a cap of 5 in a crowd of 20 picked five arbitrary characters and the deterministic sort that follows had nothing to work with. Querying wide and capping after the sort is what makes the cap mean "the first five in a defined order".

A starting size, not a limit. It only moves the truncation point from MaxHits up to MaxHits * 4; every caller must still grow the buffer through TryGrowQueryBuffer<T>(ref T[], int) when a query comes back full.

The rule itself lives on TargetOrdering now, because the ability actions resolve hits without a selector and need the identical arithmetic — it was duplicated inline in AbilityApplyAreaAction for exactly as long as it lived somewhere only selectors could reach. This forwarder stays so the selectors read naturally.

ResolvesSelectionsLocally(EventData)

ResolvesTargetsLocally(EventData), reachable from outside the selector hierarchy.

public static bool ResolvesSelectionsLocally(EventData eventData)

Parameters

eventData EventData

The event driving the selection.

Returns

bool

True when this peer may resolve a spatial selection for itself.

Remarks

The fan-out in TriggerExecution has to ask the same question the selectors ask, for the opposite reason: a selector asks it to decide whether to resolve, and the fan-out asks it to tell an EMPTY SELECTION apart from a DECLINED one. Those two had the same shape and very different meanings — "nobody was standing there" must run nothing, while "this peer is not allowed to look" must still let the authored presentation play, or a chain, area, cone or line ability is silent on every screen but the caster's. One implementation, because two copies of this predicate drifting apart is how an observer starts resolving somebody else's hits.

Public rather than internal: the answer is a peer fact, not a selector implementation detail, and the fan-out lives in a different namespace.

ResolvesTargetsLocally(EventData)

True when this peer may resolve a spatial selection for ITSELF — the server, or the client that owns the initiator.

protected static bool ResolvesTargetsLocally(EventData eventData)

Parameters

eventData EventData

Returns

bool

Remarks

This is the gate every spatial selector uses, and it is deliberately wider than IsAuthoritativePeer(EventData). Selection used to be server-only, which made the cap trivially agreed — the client computed nothing, so it could not compute something different. The cost was that an area, cone, line or chain ability produced NO client-side prediction at all: the selector yielded an empty set on the caster's own machine, so no action downstream ever ran, and the entire "hit what you see" path (predicted numbers, immediate resource movement, impact FX at the predicted tick) applied only to projectiles, whose hits are resolved by AbilityObject's sweep rather than by a selector.

Why widening it is safe: selection is not authority. A selector answers "which bodies are in this volume", never "what may be done to them". Every action re-asks that question for itself one level down — ApplyDamageAction, ApplyHealAction and ApplyBuffAction gate on MayPredict(ICharacter, EventData) and predict feedback, while ApplyThreatAction, ApplyTauntAction, ApplyReviveAction, ApplyDispelAction, the equip actions and the achievement action gate on IsServer(ICharacter, EventData) and stay server-only. So opening the selector grants a client exactly the predictions it was already trusted to make for a projectile hit, and nothing else: no kill, no combat report, no loot rights, no threat.

What makes the two peers agree. The same thing that already makes the projectile sweep agree, and it is not reproducible float arithmetic. The server runs its gather inside GatherRewound(EventData, GameObject, List<GameObject>, GatherTargets), rewound to the caster's view; the client runs the identical gather against its live world, which IS that view — this is precisely why LagCompensationTick.TryResolve answers false off-server rather than trying to rewind a second time. Both therefore query the same world, rank it with the same distance-then-identity order, and cap the same list. The residual disagreement is interpolation error at the volume boundary, exactly the bound a swept projectile hit has always carried.

And when they do disagree, it self-corrects. A body the client picked and the server did not gets its predicted damage undone by the next authoritative resource push, and its predicted number greys out unconfirmed (PredictedCombatEvents); a predicted buff the server never applies is dropped by the confirmation deadline in BuffController. A body the server picked and the client did not simply arrives as an ordinary combat report. Both are bounded to about a round trip and neither can accumulate.

An OBSERVER still answers false, and that part has not moved. It holds every character interpolated against its own latency, has no input stream for the caster, and is going to be told what happened — a third party resolving somebody else's area ability for itself is the failure this whole gate exists to prevent.

The DETACHED-object case is why this is not simply MayPredict(ICharacter, EventData). When a caster despawns — including merely leaving an observer's streaming range — its in-flight ability objects are detached and their caster replaced by a SnapshotCharacter phantom, which has no NetworkObject. Both MayPredict and IsServer treat "nothing networked to ask" as permission (the undecidable case that keeps scene-authored triggers and edit-mode tests working), so a phantom's OnTick trigger would pass the peer gate on EVERY client and let a third party resolve selections — and, one level down, run the server-only actions too. Deferring to the ability object closes it: ResolvesHitsLocally reads the caster's NetworkObject and so answers server-only for a phantom, which is exactly the treatment AbilityApplyAreaAction, AbilityApplyHitscanAction and AbilityApplyTargetAction already take. Selections with no ability object in the event (a buff tick, a region trigger) keep the plain MayPredict answer.

SelectTargets(EventData)

Selects targets based on the supplied event context.

public abstract IEnumerable<GameObject> SelectTargets(EventData eventData)

Parameters

eventData EventData

The event data driving the selection. Target (when set) or Initiator typically serves as the spatial origin.

Returns

IEnumerable<GameObject>

The selected GameObjects.