86 lines
4.3 KiB
C#
86 lines
4.3 KiB
C#
using ProjectM.Simulation;
|
|
using Unity.Mathematics;
|
|
using Unity.Physics;
|
|
|
|
namespace ProjectM.Server
|
|
{
|
|
/// <summary>
|
|
/// Shared server-side collide-and-slide mover for OWNERLESS enemy ghosts (extracted from EnemyAISystem so the
|
|
/// boss brain reuses ONE copy — a fix to the tunnelling sweep or wall-glance reaches every enemy at once).
|
|
/// Both EnemyAISystem's three passes and BossAISystem call <see cref="SweptMove"/>. Pure given the CollisionWorld;
|
|
/// Burst-safe (non-generic closest-hit SphereCast, per the CLAUDE.md generic-collector hazard).
|
|
/// </summary>
|
|
public static class EnemyMoveUtil
|
|
{
|
|
/// <summary>Collide-and-slide sphere-cast for server-authoritative enemy movement: sweep the intended step
|
|
/// against the static environment (boundary ring + landmarks + cover rocks + player-built walls) and stop at /
|
|
/// glance along wall hits. Runs up to TWO slide iterations so a concave pocket (a cover rock meeting the
|
|
/// boundary rim) glances along BOTH surfaces instead of dead-stopping at the first (a single iteration left
|
|
/// enemies parked in pockets). Y is held flat (top-down movement plane).</summary>
|
|
public static float3 SweptMove(in PhysicsWorldSingleton physics, float3 from, float3 to, float radius, CollisionFilter filter)
|
|
{
|
|
float3 delta = to - from;
|
|
delta.y = 0f;
|
|
float dist = math.length(delta);
|
|
if (dist < 1e-5f)
|
|
return to;
|
|
const float skin = 0.05f;
|
|
var cw = physics.CollisionWorld;
|
|
float3 dir = delta / dist;
|
|
if (!cw.SphereCast(from, radius, dir, dist, out var hit, filter))
|
|
return to;
|
|
|
|
float allowed = math.max(0f, hit.Fraction * dist - skin);
|
|
float3 pos = from + dir * allowed;
|
|
pos.y = from.y;
|
|
float3 remaining = to - pos;
|
|
|
|
for (int i = 0; i < 2; i++)
|
|
{
|
|
float3 slide = EnemyAIMath.SlideVelocity(remaining, hit.SurfaceNormal);
|
|
float slideDist = math.length(slide);
|
|
if (slideDist < 1e-5f)
|
|
break;
|
|
float3 sdir = slide / slideDist;
|
|
if (cw.SphereCast(pos, radius, sdir, slideDist, out var hit2, filter))
|
|
{
|
|
float sAllowed = math.max(0f, hit2.Fraction * slideDist - skin);
|
|
pos += sdir * sAllowed;
|
|
remaining = slide - sdir * sAllowed; // carry the still-unused motion into the next glance
|
|
hit = hit2;
|
|
}
|
|
else
|
|
{
|
|
pos += slide;
|
|
break;
|
|
}
|
|
}
|
|
pos.y = from.y;
|
|
return pos;
|
|
}
|
|
|
|
/// <summary>Push a sphere at <paramref name="pos"/> out of any static env/structure it overlaps, along the
|
|
/// nearest surface normal (flattened to XZ; a near-vertical floor/ceiling normal is ignored so movers aren't
|
|
/// shoved off their plane). Fixes the "spawned or shoved INTO a rock -> SphereCast fraction ~0 -> frozen
|
|
/// forever" class: the collide-and-slide sweep can never move a mover that STARTS already penetrating, so we
|
|
/// un-embed it first. The correction is capped at one radius/tick so it eases out over a couple of ticks
|
|
/// rather than teleporting. Stateless; called once per enemy per tick before seeking.</summary>
|
|
public static float3 Depenetrate(in PhysicsWorldSingleton physics, float3 pos, float radius, CollisionFilter filter)
|
|
{
|
|
var cw = physics.CollisionWorld;
|
|
var input = new PointDistanceInput { Position = pos, MaxDistance = radius + 0.5f, Filter = filter };
|
|
if (!cw.CalculateDistance(input, out var hit) || hit.Distance >= radius)
|
|
return pos;
|
|
float3 n = hit.SurfaceNormal;
|
|
n.y = 0f;
|
|
float len = math.length(n);
|
|
if (len < 1e-5f)
|
|
return pos; // floor/ceiling normal -> don't shove the mover off its movement plane
|
|
float push = math.min(radius - hit.Distance, radius); // cap so a deep embed eases out over ticks, no pop
|
|
float3 outp = pos + (n / len) * push;
|
|
outp.y = pos.y;
|
|
return outp;
|
|
}
|
|
}
|
|
}
|