"Trajectory preview dots bunch at the apex — sample by arc length, not time"
▸ SYMPTOM
A throw/aim trajectory preview renders as a dotted arc, but the dots bunch together near the top of the arc and spread apart near the launch and landing points. As the player adjusts their aim, the uneven spacing "swings" visually and looks rough.
▸ CAUSE
The preview is sampling the trajectory at even time intervals. In a ballistic arc, the projectile moves slowly near the apex and quickly near the endpoints. Even-time sampling produces:
- Clustered dots at the apex (low velocity → small position changes per time step)
- Sparse dots at the start and end (high velocity → large position changes per time step)
This creates a non-uniform visual density that shifts as the aim angle changes.
▸ FIX
Sample by arc length, not by time. Two-pass approach:
-
Build the polyline — integrate the trajectory at a fine time substep using the projectile's own step function (same gravity, same trace radius/tags), accumulating the total polyline length. Clip the polyline at the first world-hit trace.
-
Walk the polyline at even distance intervals — divide the total arc length by the desired number of dots, then emit markers at each evenly-spaced distance along the polyline.
// Pass 1: build fine polyline
var points = new List<Vector3>();
var lengths = new List<float>(); // cumulative arc length
float totalLength = 0f;
var pos = launchPos;
var vel = launchVel;
for (int i = 0; i < maxSteps; i++)
{
var prev = pos;
Step(ref pos, ref vel, fineDt); // reuse projectile's own Step()
if (TraceHit(prev, pos, out var hitPos))
{
points.Add(hitPos);
totalLength += Vector3.DistanceBetween(prev, hitPos);
lengths.Add(totalLength);
break;
}
totalLength += Vector3.DistanceBetween(prev, pos);
points.Add(pos);
lengths.Add(totalLength);
}
// Pass 2: emit dots at even arc-length intervals
float spacing = totalLength / dotCount;
int ptIdx = 0;
for (int d = 1; d <= dotCount; d++)
{
float target = d * spacing;
while (ptIdx < lengths.Count - 1 && lengths[ptIdx + 1] < target)
ptIdx++;
float t = (target - lengths[ptIdx])
/ (lengths[ptIdx + 1] - lengths[ptIdx]);
var dotPos = Vector3.Lerp(points[ptIdx], points[ptIdx + 1], t);
DrawDot(dotPos);
}▸ WHY IT WORKS
Arc-length parameterization decouples the visual spacing from the projectile's velocity profile. Every dot is the same distance along the curve from its neighbors regardless of how fast the projectile is moving at that point. Reusing the projectile's exact Step function (with the same gravity source, trace radius, and collision tags) also guarantees the preview cannot disagree with the real trajectory — a finer preview substep is strictly more accurate, never in conflict.