What you can do is have your ball fire out whisker rays around it to detect nearby colliders, using 2D raycast methods:
RaycastHit2D hit = Physics2D.Raycast(ballPosition, rayDirection, maxDistance);
(You can add other parameters to limit the ray to only the pipes' physics layer, or specific z-values)
Then for each ray that hits a pipe wall, you can get its normal - a unit vector sticking perpendicularly outward from the edge.
We can do a bit of math on our current velocity to divide it into two parts: one that runs parallel to the wall and one that runs perpendicular to the wall.
float awayDistance = Vector2.dot(hit.normal, rayDirection * hit.distance);
float awaySpeed = Vector2.Dot(hit.normal, body.velocity);
Vector2 perpendicularVelocity = hit.normal * awaySpeed;
Vector2 parallelVelocity = body.velocity - perpendicularVelocity;
To keep the ball in roughly the middle of the pipe, you can apply a repulsive spring force based on the distance the ball is from each wall (awayDistance
), and add some damping to the perpendicular velocity. This will help nudge the ball around 90-degree corners like the one in the diagram: it gets repulsed from the floor and the ceiling drops away, so there's no counter-force holding it down. (Just be careful not to double-count walls that get hit by more than one whisker ray)
Then you can apply another force/impulse to keep the length of the parallel component of the velocity close to your target speed, so the ball moves along at a roughly steady rate.