Struct AbilityObjectHitBroadcast
Tells observers which body an ability object hit, so they can draw the impact the server resolved instead of guessing at one.
public struct AbilityObjectHitBroadcast : IBroadcast
- Implements
-
IBroadcast
- Inherited Members
Remarks
Why an observer is told rather than left to work it out. An ability object's trajectory is deterministic and every peer agrees on it; its HIT SET is not. The server resolves hits inside a rewind to the CASTER'S view, and a third-party observer holds every character interpolated against its own latency — so an observer running the same query answered a question nobody asked. It could miss a hit the server landed (corrected, by AbilityObjectDestroyedBroadcast) and it could resolve one the server did not, which nothing corrected: its copy ended early, played its impact effect where nothing had happened, and — with a fork — carried on down a heading the server never took.
What it costs, and why that is less than it looks. An observer's copy is deliberately
run SpectatorInterpolationTicks behind the server's, so it stays consistent with the
interpolated peers it is drawn against — see ComputeObserverFastForwardTicks. That is
66 ms at the shipped tick rate, and it is a head start this message already holds: for
an observer inside roughly 133 ms round trip the authoritative answer arrives BEFORE
the local guess would have fired. Only observers past that see the impact late, and by
less than their one-way latency rather than by a round trip.
Sent to every observer of the caster, the owner included, exactly like AbilityObjectDestroyedBroadcast. The owner predicts its own hits and normally has this one already, in which case the receiver's per-object hit set makes the message a no-op — but an owner that MISPREDICTED A MISS had no correction at all before this, and its impact effect simply never played.
Reliable, and for the same reason its sibling is: there is no repeat behind it. A lost hit message is an impact nobody outside the server ever sees.
Fields
AbilityID
Ability the object belonged to.
public long AbilityID
Field Value
Blocked
True when the victim's raised shield ATE this object rather than being struck by it.
public bool Blocked
Field Value
Remarks
The third outcome of a collision, and until it travelled the receiver could not tell it
from the first. A block is decided by the peer that resolves hits
(AbilityObject.ResolvesHitsLocally), so the server ran NO OnHit chain for it — it
destroyed the object and played its destroy events and nothing else. The message it sent
was an ordinary impact, and every observer, the blocker's own client included, ran the
ability's entire OnHit chain off the back of it: impact effects for a shot that never
landed, AbilityForkHitAction redirecting a copy off a hit the server rejected, and
RNG draws the server never made (AbilityObject's parity contract).
It is the block counterpart of Deflected, and the two are exclusive: a deflect gives the projectile BACK on a new heading, a block consumes it. So this carries no heading — the object ended — and rides the ordinary impact shape, because the point and the normal are the shield's and the destroy chain draws the shield impact from them.
CasterObjectID
NetworkObject id of the casting character.
public int CasterObjectID
Field Value
ContainerID
Deterministic container id the object lived in (identical on every peer).
public int ContainerID
Field Value
Deflected
True when the victim TURNED THIS OBJECT AWAY rather than being struck by it.
public bool Deflected
Field Value
Remarks
A deflect is a rejected hit: the server ran no OnHit events, spent no hit count and dealt
no damage, but the projectile changed direction — and a trajectory change is the one
thing an observer cannot reproduce on its own, because the buff that caused it is the
DEFENDER's and the observer never resolves hits. One bit is all this flag needs to be:
it says a deflection happened, and nothing more. The new heading does NOT ride on it —
re-deriving the reflection from Normal is not safe, because reflecting
twice about the same normal returns the original vector and the caster's own client
receives this message after predicting the deflection itself. The absolute heading
therefore travels separately in PackedDeflectHeading, whose remarks carry
the full argument. See DeflectBuffTemplate.ResolveDeflectedHeading.
It is also what lets a receiver act on a hit whose VICTIM it cannot resolve. A character outside this client's streaming budget is not spawned here, so the hit itself must be dropped — running the OnHit events target-less would fire impact effects for a body that is really there — but the redirect must not be, or the observer's copy flies on down a heading the server never took until the destroy message catches up with it.
DirectImpact
True when an ECA ACTION resolved this impact rather than the object's own sweep.
public bool DirectImpact
Field Value
Remarks
A hitscan shot and an area blast run their own lag-compensated query and execute the OnHit chain directly, so they never touch the object's per-body hit set or its hit count. Their impacts were published by nobody at all until this flag existed, and a third party watching a gunfight saw beams appear and nothing be hit by them.
The flag is what keeps the two kinds apart on the receiver. A swept hit is applied through the hit set, which is what makes the message a free no-op for an owner that predicted it; an action impact is applied straight to the OnHit chain, because an area effect wired to OnTick pulses the same victims repeatedly and the hit set would silence every pulse after the first. These are sent to observers EXCEPT the owner for the same reason — with no hit set to absorb it, the owner would play each impact twice.
Ended
True when this hit ENDED the object on the server, so no separate destroy follows.
public bool Ended
Field Value
Remarks
One collision used to produce two reliable messages describing it: this one, and an
AbilityObjectDestroyedBroadcast naming the same caster, ability, container
and object. Every shipped ability carries HitCount 1, so the second was in
practice always implied by the first — ~15-20 bytes and a second reliable slot per
collision-ended object per observer, for one bit of information.
Stated, never inferred. An observer must not spend HitCount itself — its
copy is told about the hits the server resolved and never about the ones the server
declined, so counting locally ends the copy early. That argument is against INFERRING
the end, not against the server saying so inline, which is what this is.
Set only where the end is certain at the moment the message is WRITTEN, which is before
the OnHit chain runs (see AbilityObject.ApplyHit: a destroyed object cannot
publish, because destruction nulls its ability and caster). A chain that can PIERCE —
one carrying an AbilityHitCountAction — may cancel the decrement this hit would
otherwise apply, so for those the standalone destroy still travels. See
AbilityObject.HitEndsObject.
The standalone AbilityObjectDestroyedBroadcast remains for every end that
no hit message precedes: a shield sweeping nearby projectiles
(ShieldInterceptAction) destroys with observer notification and publishes no hit
at all.
Normal
Surface normal at Point. Not carried on a deflection.
public Vector3 Normal
Field Value
- Vector3
Remarks
Travels through AimDirectionCompression — four bytes rather than twelve. It is a unit vector and the only thing a receiver does with it is orient an impact effect; the packer's 0.0055° of yaw and 0.0027° of pitch are three orders of magnitude finer than that needs. The one caller that reasoned FROM a normal rather than drawing with it — the deflection — does not read this field at all: the heading it produced is carried absolute in PackedDeflectHeading, resolved on the server before the message was built. See AbilityObjectObserverBroadcastSerializers.
ObjectID
Object id within the container (identical on every peer).
public int ObjectID
Field Value
PackedDeflectHeading
The heading the object left on after a deflection, packed by AimDirectionCompression. Meaningless unless Deflected is set.
public uint PackedDeflectHeading
Field Value
Remarks
An ABSOLUTE heading, not an instruction to mirror. A receiver that recomputed the reflection from Normal would be correct exactly once: reflecting twice about the same normal returns the original vector, so the caster's own client — which predicts its deflections and then receives this message like everybody else — would turn the object straight back at the defender it had just been turned away from. Applying an absolute heading is idempotent, which is what makes the message safe to send to the peer that already worked it out.
Four bytes, on a message sent once per body per object, and it also removes the receiver's need to know anything about WHY the object turned — a future deflection that is not a simple mirror needs no wire change.
Point
World point of impact, measured on the server inside the rewind scope.
public Vector3 Point
Field Value
- Vector3
VictimObjectID
NetworkObject id of the character that was hit, or 0 when the hit resolved to scenery.
public int VictimObjectID
Field Value
Remarks
Zero is a real outcome rather than a failure: a projectile is free to end on a wall, and the OnHit events still run for it with no target character — which is what an authored impact decal or sound needs.