69 lines
3.9 KiB
C#
69 lines
3.9 KiB
C#
using Unity.Mathematics;
|
|
|
|
namespace ProjectM.Simulation
|
|
{
|
|
/// <summary>
|
|
/// Pure, deterministic, Burst-safe helpers for turning a screen-cursor camera ray into a planar (XZ)
|
|
/// aim direction (and the ground point it targets) for the top-down player. Isolated from the managed
|
|
/// <c>Camera</c> so the projection is unit-testable without a render context (mirrors
|
|
/// <see cref="AutoTarget"/> / <c>CharacterControlMath</c> / <c>BaseGridMath</c>). Used client-side by the
|
|
/// input gather (facing direction) and the aim-reticle presentation (the world ground point), but kept
|
|
/// here in Simulation alongside the other pure combat/movement math.
|
|
/// </summary>
|
|
public static class AimMath
|
|
{
|
|
/// <summary>
|
|
/// Intersects a camera ray with the horizontal plane at <paramref name="planeY"/>. Returns false (and
|
|
/// <paramref name="hit"/> = default) when the ray is parallel to / points away from the plane (cursor
|
|
/// above the horizon); otherwise <paramref name="hit"/> is the world-space intersection point.
|
|
/// </summary>
|
|
/// <param name="rayOrigin">Camera ray origin (world space).</param>
|
|
/// <param name="rayDir">Camera ray direction (need not be normalized).</param>
|
|
/// <param name="planeY">World Y of the aim plane (the player's movement plane).</param>
|
|
/// <param name="hit">The world-space ground intersection point, when this returns true.</param>
|
|
public static bool TryGroundHit(float3 rayOrigin, float3 rayDir, float planeY, out float3 hit)
|
|
{
|
|
hit = default;
|
|
// Solve rayOrigin.y + t * rayDir.y == planeY. A near-zero rayDir.y means the ray runs parallel.
|
|
float denom = rayDir.y;
|
|
if (math.abs(denom) < 1e-6f)
|
|
return false;
|
|
float t = (planeY - rayOrigin.y) / denom;
|
|
if (t < 0f)
|
|
return false; // intersection behind the ray origin (cursor above the horizon line)
|
|
hit = rayOrigin + rayDir * t;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Normalized planar aim direction (world XZ mapped to <c>float2(x, y)</c>) from
|
|
/// <paramref name="playerPos"/> toward the cursor's ground-projection point. Returns
|
|
/// <paramref name="fallback"/> when the ray misses the plane, OR the hit lies within
|
|
/// <paramref name="deadZoneRadius"/> of the player (too close to define a stable heading), so the
|
|
/// caller holds its previous aim instead of snapping or spinning when the cursor is over the character.
|
|
/// </summary>
|
|
/// <param name="rayOrigin">Camera ray origin (world space).</param>
|
|
/// <param name="rayDir">Camera ray direction (need not be normalized).</param>
|
|
/// <param name="playerPos">Player world position; only XZ is used for the heading.</param>
|
|
/// <param name="planeY">World Y of the aim plane.</param>
|
|
/// <param name="fallback">Direction returned when there is no valid hit (e.g. the previous aim).</param>
|
|
/// <param name="deadZoneRadius">
|
|
/// World radius around the player inside which facing is held (no update). 0 keeps only the tiny
|
|
/// epsilon guard against an exactly-coincident hit (the original behaviour).
|
|
/// </param>
|
|
public static float2 PlanarAimFromRay(float3 rayOrigin, float3 rayDir, float3 playerPos, float planeY,
|
|
float2 fallback, float deadZoneRadius = 0f)
|
|
{
|
|
if (!TryGroundHit(rayOrigin, rayDir, planeY, out var hit))
|
|
return fallback;
|
|
|
|
float2 planar = new float2(hit.x - playerPos.x, hit.z - playerPos.z);
|
|
float dz = math.max(1e-3f, deadZoneRadius); // never below the original coincident-hit epsilon
|
|
if (math.lengthsq(planar) < dz * dz)
|
|
return fallback;
|
|
|
|
return math.normalize(planar);
|
|
}
|
|
}
|
|
}
|