93 lines
4.5 KiB
C#
93 lines
4.5 KiB
C#
using Unity.Collections;
|
|
using Unity.Mathematics;
|
|
|
|
namespace ProjectM.Simulation
|
|
{
|
|
/// <summary>
|
|
/// Pure, deterministic auto-target assist helper for player abilities (M2 combat). Given a shooter
|
|
/// position, a raw aim direction, and a set of candidate target positions, picks the best target
|
|
/// inside an assist cone and snaps the shot toward it; otherwise returns the raw aim unchanged.
|
|
/// <para>
|
|
/// Authored as a <see langword="static"/> class (no state) so it is Burst-safe and allocation-free,
|
|
/// callable from inside predicted/jobified systems. It is intended to run server-side only (see
|
|
/// <c>AbilityFireSystem</c>) — the server's authoritative <c>Projectile.Direction</c> GhostField then
|
|
/// reconciles the client's raw-aim predicted projectile. Determinism: no wall-clock, no randomness,
|
|
/// pure math; ties are broken by smallest candidate index so identical inputs always yield identical
|
|
/// output across the prediction loop and across machines.
|
|
/// </para>
|
|
/// </summary>
|
|
public static class AutoTarget
|
|
{
|
|
/// <summary>
|
|
/// Resolves the planar (XZ) direction a shot should take. Returns the normalized direction toward
|
|
/// the nearest candidate within <paramref name="autoTargetRange"/> whose bearing from
|
|
/// <paramref name="from"/> lies within <paramref name="coneHalfAngleRadians"/> of
|
|
/// <paramref name="rawAimDir"/>; if no candidate qualifies, returns <paramref name="rawAimDir"/>
|
|
/// unchanged.
|
|
/// </summary>
|
|
/// <param name="from">Shooter world position; only the XZ plane is considered.</param>
|
|
/// <param name="rawAimDir">
|
|
/// Caller-normalized planar aim direction (world XZ mapped to <c>float2(x, y)</c>). If it is
|
|
/// effectively zero-length, it is returned unchanged (no valid heading to assist).
|
|
/// </param>
|
|
/// <param name="autoTargetRange">Max planar distance to consider a candidate; <c>0</c> (or less) disables assist.</param>
|
|
/// <param name="coneHalfAngleRadians">Half-angle of the assist cone, measured from <paramref name="rawAimDir"/>.</param>
|
|
/// <param name="candidatePositions">Candidate target world positions (XZ used). Read-only; not modified.</param>
|
|
/// <returns>
|
|
/// The normalized direction toward the chosen candidate, or <paramref name="rawAimDir"/> when no
|
|
/// candidate qualifies. Ties on distance are broken by the smallest candidate index for determinism.
|
|
/// </returns>
|
|
public static float2 Resolve(float3 from, float2 rawAimDir, float autoTargetRange, float coneHalfAngleRadians,
|
|
in NativeArray<float3> candidatePositions)
|
|
{
|
|
// No valid heading to assist along — caller guarantees normalization, but guard zero-length.
|
|
if (math.lengthsq(rawAimDir) < 1e-6f)
|
|
return rawAimDir;
|
|
|
|
// Disabled / nothing to consider.
|
|
if (autoTargetRange <= 0f || candidatePositions.Length == 0)
|
|
return rawAimDir;
|
|
|
|
float rangeSq = autoTargetRange * autoTargetRange;
|
|
float cosCone = math.cos(coneHalfAngleRadians);
|
|
|
|
int bestIndex = -1;
|
|
float bestDistSq = float.MaxValue;
|
|
float2 bestDir = rawAimDir;
|
|
|
|
for (int i = 0; i < candidatePositions.Length; i++)
|
|
{
|
|
// Planar (XZ) offset from shooter to candidate.
|
|
float3 offset = candidatePositions[i] - from;
|
|
float2 planar = new float2(offset.x, offset.z);
|
|
|
|
float distSq = math.lengthsq(planar);
|
|
|
|
// Skip self / coincident candidates (effectively zero distance → undefined bearing).
|
|
if (distSq < 1e-6f)
|
|
continue;
|
|
|
|
// Out of range.
|
|
if (distSq > rangeSq)
|
|
continue;
|
|
|
|
// Bearing test: dot of unit bearing with the (unit) raw aim vs cos(half-angle).
|
|
float2 dir = planar * math.rsqrt(distSq); // normalized planar bearing
|
|
float dot = math.dot(dir, rawAimDir);
|
|
if (dot < cosCone)
|
|
continue; // outside the assist cone
|
|
|
|
// Nearest wins; strict less-than keeps the first (smallest-index) candidate on ties.
|
|
if (distSq < bestDistSq)
|
|
{
|
|
bestDistSq = distSq;
|
|
bestIndex = i;
|
|
bestDir = dir;
|
|
}
|
|
}
|
|
|
|
return bestIndex >= 0 ? bestDir : rawAimDir;
|
|
}
|
|
}
|
|
}
|