Table of Contents

Class AbilityApplyHitscanAction

Namespace
FishMMO.Shared
Assembly
FishMMO.Shared.dll

Action that resolves an instantaneous ray from the ability object and runs the ability's OnHit events for everything it passes through — the hitscan half of the ability system.

[Serializable]
public class AbilityApplyHitscanAction : BaseAction, IAction
Inheritance
AbilityApplyHitscanAction
Implements
Inherited Members

Remarks

This is what makes Bullet and Beam abilities possible, and they are the same action wired to different events. Nothing else about the two differs: both resolve a ray from the ability object's pose, in ray order, capped at MaxHitsValue.

Bullet An instant ability with this action on its OnSpawn event. The object exists for one tick, fires once, and its lifetime expires. Nothing travels, so there is no projectile to dodge and no lead to aim — the shot lands on the tick it was fired.
Beam A channelled ability (one carrying AbilityController.ChanneledTemplate) with this action on its OnSpawn event. SpawnChanneledAbility already spawns one object per tick for the life of the channel, each at the caster's CURRENT aim, so the beam tracks where the player is looking and re-resolves its hits every tick. Damage per tick is the ability's damage; hold duration is the channel's.

Why a per-tick respawn rather than one long-lived object with this on OnTick. An ability object's pose is fixed at spawn and advanced by a closed form (AbilityMoveTransformAction), so a single object re-raycasting each tick would keep firing along the heading it was born with — a beam welded to the direction you were facing when you pressed the button. The channel path re-resolves the aim per tick because it genuinely re-spawns, which is also why every peer can reproduce it: each spawn is broadcast with its own pose. Putting this on OnTick is still legal and is the right choice for a fixed emplacement — a turret, a trap, a laser fence — where not tracking is the point.

Server only, like every other hit-resolving action. A physics query is not reproducible across peers, so it must run exactly once, where hits are authoritative; clients learn the outcome through the usual paths (damage via the resource broadcast, the visual via AbilityActivatedBroadcast reproducing the spawn). The owner still sees its own damage immediately, because ApplyDamageAction predicts on the caster's client and PredictedCombatEvents greys out anything the server does not confirm.

Lag compensated, and it is the shape that needs it most. A ray is infinitely thin, so unlike a blast radius it has no tolerance to absorb the gap between where the shooter saw a target and where the server holds it — at 300 ms that gap is metres and the shot is simply a miss. RaycastNearest(EventData, GameObject, Vector3, Vector3, float, LayerMask, int, bool, List<CompensatedHit>, Transform) rewinds every character in the scene to the tick the caster's client was rendering, runs the ray there, and orders and caps the result inside the same scope.

Fields

BlockedByScenery

Whether scenery stops the shot.

[Tooltip("When true the ray is blocked by scenery, so cover works. When false it passes through the world and only characters count.")]
public bool BlockedByScenery

Field Value

bool

Remarks

The one authored decision this action has that the area version does not, and it is a real gameplay choice rather than an implementation detail. True — the default — means the ray is blocked by the first thing it meets that is not a character, so a target behind a wall is safe and cover works. False means only characters are considered at all and the ray passes through the world, which is what a debug weapon or a deliberately wall-piercing ability wants.

It is expressed as "does scenery count" rather than left to the layer mask because the mask defaults to every layer: an author who never touches it would otherwise get a shot that spends its entire pierce count on terrain and hits nobody.

MaxHitsValue

The value provider that determines how many distinct characters the ray may hit.

[Tooltip("How many distinct characters the ray may hit. 1 stops at the first; 0 or less pierces everything.")]
[SerializeReference]
public IIntValueProvider MaxHitsValue

Field Value

IIntValueProvider

Remarks

The pierce count. One is a shot that stops at the first target; zero or less is a beam that passes through everything on its line, matching CappedCount(int, int) and every selector.

RangeValue

The value provider that determines how far the ray reaches, in metres.

[Tooltip("The value provider that determines how far the ray reaches, in metres.")]
[SerializeReference]
public IFloatValueProvider RangeValue

Field Value

IFloatValueProvider

Remarks

Authored rather than taken from Ability.Range, which is Speed * LifeTime and therefore zero for anything that does not travel — every hitscan ability by definition.

TargetLayerMask

Layers the ray tests against.

[Tooltip("Layers the ray tests against.")]
public LayerMask TargetLayerMask

Field Value

LayerMask

Methods

Execute(ICharacter, EventData)

Resolves the ray and runs the ability's OnHit events for each character it found.

public override void Execute(ICharacter initiator, EventData eventData)

Parameters

initiator ICharacter

The character that cast the ability.

eventData EventData

Event data carrying the ability object.