targeting

Targeting (Auto-Attack + Selected Target)

Two halves of "what am I aiming at". (1) Auto-attack driver: enrolled attackers acquire a target (ITargetSelector) and attack on a cadence (IAttackAction) over World — survivors/auto-shooter auto-fire, turrets, sentries, pet auto-attack. (2) The SELECTED target (TargetSelection:Enabled): SetTargetRequest in, TargetChanged out, validated against the actor's interest set + an ITargetPolicy SPI, readable server-side through ITargetService — tab-target MMOs, RTS selection, a sports pass target, a card game's targeting step. DriveAutoAttack makes your swings land on what you clicked; RevealTargets drives target-of-target frames (off by default — who you selected is information).

Category Combat Seams 5 Services 2 Options 2 Wire ids 2 Unity types 1
Server
services.AddCrossplayTargeting();
Unity package
com.crossplay.targeting
Depends on
world

Seams you implement 5

Crossplay calls these; your game supplies them. Each ships an inert or permissive default, so register yours before services.AddCrossplayTargeting(); and it wins.

IAttackAction game SPI

Effect seam: what does an attack DO? The piece decides WHEN to attack and WHO; the meaning — a projectile volley, a melee hit, a hitscan ray, a chain, a heal — is the game's. The default NullAttackAction is inert (targets are still acquired; nothing fires). A generic projectile-firing action ships as a Hosting bridge (over the Projectiles piece); games register their own for melee / hitscan / bespoke effects. Runs on the poll thread — keep it allocation-free.

  • void Attack(WorldEntity attacker, WorldEntity target)

    Perform one attack from attacker against target.

IAttackCadenceProvider game SPI

Per-attacker fire-cadence seam: how many seconds between THIS attacker's shots right now? Targeting asks it when scheduling each attacker's next shot; a game supplies per-weapon or upgraded fire rates — typically from a Stats-backed Hosting bridge — without Targeting ever knowing what a "weapon" is. Absent (no game registers one), every attacker uses the global CooldownSeconds — today's behavior exactly. The analogue of Movement's per-entity speed seam, for the fire-rate axis.

  • bool TryGetCooldown(long attackerEntityId, out double cooldownSeconds)

    The seconds between shots for this attacker, or false to use the options cooldown.

ITargetHostility game SPI

Faction seam: is a candidate a valid target for an attacker? The piece never decides who is an enemy — a game's teams / PvE / neutral rules do. The default TwoFactionHostility treats players and server entities as opposing sides (the common PvE shape); register your own BEFORE AddCrossplayTargeting for teams, friendly-fire, or PvP.

  • bool IsHostile(WorldEntity attacker, WorldEntity candidate)

    True if attacker may target candidate.

ITargetPolicy game SPI

Selection seam: may this actor SELECT this candidate? Deliberately not ITargetHostility. Hostility answers "may I auto-attack it", and selection is a different question with a different answer: you select a party member to heal them, a shopkeeper to talk to them, a chest to open it. Reusing the hostility rule here would make every friendly and neutral entity unclickable, so the two stay separate and the default here is permissive — anything you can see, you may select. Register your own BEFORE AddCrossplayTargetSelection to refuse corpses, stealthed players, out-of-phase entities, or non-interactive props.

  • bool MaySelect(WorldEntity actor, WorldEntity candidate)

    Whether actor may select candidate.

ITargetSelector game SPI

Acquisition seam: which entity (if any) should this attacker attack right now? The default NearestInConeSelector picks the nearest hostile within range (and cone, when the options narrow it) using the world's spatial queries. Register your own BEFORE AddCrossplayTargeting for lowest-health / highest-threat / priority-tag targeting.

  • bool TrySelectTarget(WorldEntity attacker, TargetingContext context, out WorldEntity target)

    Selects a target for attacker, or returns false if none.

Services you call 2

Crossplay implements these. Resolve them from DI and call them from your own systems.

ITargetingService

Enrolls entities as auto-attackers and, on a cadence, drives each to acquire a target and attack. Attach any entity explicitly (turret / pet / specific NPC), or set AutoAttachPlayers to auto-enroll players. Detach is automatic on entity removal. The mechanism is genre-neutral; WHO is hostile and WHAT an attack does are seams (ITargetHostility / IAttackAction).

  • bool Attach(long entityId)

    Enrolls an entity as an attacker (idempotent).

  • int Count { get; }

    Number of live attackers (diagnostics).

  • bool Detach(long entityId)

    Stops driving an entity.

ITargetService

WHO each entity has SELECTED — the server-authoritative answer that did not exist (GAP-09). The gap this closes. A game could only carry a target id inside AbilityRequest.TargetData's opaque bytes, which means the client asserts its own target and the server never knows it: range and visibility cannot be validated once in one place, a target-of-target frame is impossible, and no server-side consumer — the auto-attack driver, a threat table, an NPC brain — can read "who is this player fighting". This is the tab-target selection every MMO, RTS, sports game and card game needs, expressed generically: an id, validated, with events. Selection is NOT hostility. You select a party member to heal them and a shopkeeper to talk to them, so what may be selected is ITargetPolicy (permissive by default) and never ITargetHostility, which answers the different question of what may be auto-attacked.

  • bool Clear(long actorEntityId)

    Clears an actor's selection and announces it.

  • int Count { get; }

    Number of live selections (diagnostics).

  • bool Select(long actorEntityId, long targetEntityId)

    Validates and stores a selection, announcing it to the actor (and to observers when RevealTargets is on).

  • event Action<long, long> SelectionChanged

    Raised as (actor, target) whenever a selection changes, with 0 for cleared. The seam every server-side consumer reads: the auto-attack driver's SelectedTargetSelector, an NPC brain, a threat feed, an ability resolver.

  • bool TryGetTarget(long actorEntityId, out long targetEntityId)

    The entity an actor currently has selected.

Configuration 2

Every tunable lives in an options object — there are no magic numbers to hunt for.

TargetingOptions

Targeting tuning — defaults live here, never inline in the service (rule 4).

  • bool AutoAttachPlayers { get; set; }

    When true, every player entity is auto-enrolled as an attacker on world entry (the survivors-like / auto-shooter default). When false, the game enrolls attackers explicitly via Attach (turrets, sentries, specific NPCs, pets).

  • float ConeDegrees { get; set; }

    Firing arc, degrees. 360 = all directions (the attacker can hit any target around it — the auto-shooter default); < 360 = only targets within this cone of the attacker's facing (turrets, directional weapons).

  • double CooldownSeconds { get; set; }

    Seconds between attacks per attacker — the global fire rate.

  • ushort CooldownStatId { get; set; }

    Per-attacker fire-rate stat (0 = use the global CooldownSeconds). When set, the attacker's value of this stat is its cooldown in seconds — so per-weapon fire rates and fire-rate upgrades ride the ordinary Stats wire (a Hosting bridge reads it via IAttackCadenceProvider). Mirrors AbilityDefinition.CooldownStatId.

  • bool ExcludeNonCombatants { get; set; }

    Exclude the framework's own transient NON-COMBATANT entity classes — in-flight projectiles and loot pickups — from being valid targets, so stock auto-fire never shoots a bolt or a dropped coin. Enforced by the Hosting composition when Loot/Projectiles are present; a game's ITargetHostility then only expresses real faction/team rules. Default true (the sensible default); set false to restore the classic "any un-owned entity is a target" behavior.

  • int MaxUpdatesPerTick { get; set; }

    Cap on attacker evaluations per server tick — a crowd of attackers degrades re-acquisition rate instead of the tick (rule 11: unbounded work gets a budget).

  • float Range { get; set; }

    Acquisition range, world units.

TargetSelectionOptions

Tuning for the player-facing SELECTED TARGET (GAP-09) — separate from TargetingOptions because the two are unrelated: that one tunes the auto-attack driver's acquisition, this one tunes what a client is allowed to select. Defaults live here, never inline (rule 4).

  • bool AllowSelfTarget { get; set; }

    Whether selecting the actor itself is allowed. Default true — self-target is how a heal, a buff, or a "inspect me" verb is addressed in most games; a game where it is meaningless can refuse it here instead of writing a policy.

  • bool DriveAutoAttack { get; set; }

    Whether the auto-attack driver attacks the SELECTED target in preference to the one it would acquire on its own (SelectedTargetSelector). The tab-target shape: click a mob and your swings land on it. The selection still has to be hostile and in range, so it never turns into "attack your own healer", and automatic acquisition remains the fallback. Default false, because an auto-shooter wants nearest-first regardless of what the player has clicked.

  • bool Enabled { get; set; }

    Whether the selected-target wire is served at all. Off = the piece stays the zero-wire auto-attack driver it has always been, and nothing is registered.

  • float MaxRange { get; set; }

    Furthest a target may be selected, world units. 0 = no extra limit, i.e. anything the actor can SEE (its interest set) may be selected — which is the right default, because interest radius is already the game's "what exists for you" answer and a second radius that disagrees with it just makes nameplates unclickable.

  • int MaxRevalidationsPerTick { get; set; }

    Cap on stored selections revalidated per server tick. A target that dies or despawns is cleared immediately by the world event; this sweep catches the slower cases (walked out of range, changed zone), so degrading it under load costs latency, never correctness (rule 11).

  • bool RevealTargets { get; set; }

    Whether an actor's target is broadcast to the entities that can see it (target-of-target frames, "the boss is looking at the healer" tells). Default false: who you have selected is information, and leaking it is a competitive disadvantage a shooter or a card game must opt into rather than discover. The ACTOR is always told its own target regardless.

Wire messages 2

The protocol this piece speaks. Ids are allocated per piece so they can never collide.

6501 SetTargetRequest

Client → server: the entity this client has selected — a tab-target click, an RTS selection, a sports game's pass target, a card game's targeting step. The server VALIDATES it (the candidate must exist, be visible to the actor, be in range, and pass the game's ITargetPolicy) and answers with TargetChanged. A refused request leaves the previous target standing, so a mis-click cannot silently drop the one you were fighting.

6502 TargetChanged

Server → the actor: your selected target is now this (0 = none). Also sent to the actor's observers when TargetSelectionOptions.RevealTargets is on, which is what drives a target-of-target frame and "who is that boss looking at" tells. Sent on acceptance AND on server-side clearing — the target died, walked out of view, or changed zone — so a client never keeps a stale target frame. It is the only truth: a client that guessed wrong is corrected by the next one of these.

Unity services you inject 1

Crossplay binds these in the client context. Inject and call them from your own MonoBehaviours and presenters.

ITargetClient

The selected target, client side. Ask to select an entity; the SERVER decides and answers, and MyTarget only ever holds what it accepted — so a click on something out of view, out of range, or refused by the game's policy simply does not take, and a target that dies or walks away clears itself without the client having to notice. The piece carries no presentation: subscribe to MyTargetChanged and drive your own target frame, nameplate highlight, or selection ring. When the server reveals targets, TargetOf answers for any visible entity too — that is the target-of-target frame. Remove this package and selection simply stops; nothing else breaks.

  • long MyTarget

    The entity this client currently has selected, or 0 for none.

  • event Action<long> MyTargetChanged

    Raised with the newly selected entity (0 = cleared) whenever the SERVER's answer changes — including when it clears the target because it died, left view, or changed zone.

  • event Action<long, long> TargetRevealed

    Raised as (actor, target) for any entity whose selection the server revealed. Silent unless the game turned TargetSelection:RevealTargets on.

  • long TargetOf(long entityId)

    The last known target of any entity (0 when unknown or none) — target-of-target.

  • void Select(long entityId)

    Asks the server to select an entity. Optimistic UI is deliberately NOT applied: MyTarget changes when the answer arrives, so what you draw is always what the server agreed to.

  • void Clear()

    Asks the server to clear the selection (always accepted).