Generally you'd check which side of the line your circle is on, and flip the normal if it's pointing away from the circle:
Vector2 outward = circle.center - line.startPoint;
if(Vector2.Dot(outward, normal) < 0f)
return -1f * normal;
return normal;
This is susceptible to an error called "tunneling," where a small, fast-moving circle might move into and juuuuust past the line in a single update. Then, even if it came from the top side, our check above will see that the circle is currently on the bottom side and produce a downward-pointing normal instead.
For extreme cases of tunneling (where the circle hops all the way over the line and we never detect the collision at all), there's not much we can do except reduce the timestep, give the colliders some "thickness," or switch to more expensive continuous collision detection.
But for slight tunneling we can sometimes do better by checking against the incoming velocity instead of position - assuming that if we've encountered a collision, then whichever way the circle was moving should be opposite the surface normal. That would look like this:
// Compute relative velocity of the circle from the perspective of the line.
Vector2 inward = circle.velocity - line.velocity;
if(Vector2.Dot(inward, normal) > 0f)
return -1f * normal;
return normal;
This can run into errors if you have multiple collisions being resolved in sequence, if an earlier collision resolution modified the velocity so it's not pointing in the direction that originally caused the collision. So watch out for that, and make sure you're looking at the snapshot of the velocity from the impact, not from a subsequent rebound.