0
\$\begingroup\$

I have a circle that I'd like to move around in semicircle with it's location following an external object.

I can get the direction the circle should be facing with something like this:

Vector3 targetDir = X.transform.position - circle.transform.position;

How can I stick the movement of the circle inside a semicircle?

enter image description here

\$\endgroup\$
1
  • \$\begingroup\$ I think you can also think of it like the centre point of your circle must stay within a smaller half-circle that's the size of the larger half-circle shrunk by the radius of the circle. So then you just need to figure out the center of the half-circle and draw a line between the target location and the center point, and where that crosses this smaller half-circle is where you should draw the circle. Hope that makes sense. \$\endgroup\$ May 17, 2021 at 20:34

1 Answer 1

1
\$\begingroup\$

A useful trick for collision detection and finding closest points is Minkowski addition. This says that if we want to know how close a disc can get to an edge, we can inflate that edge by the radius of the disc, and correspondingly deflate the disc to a single point at its center. The situation of collisions between the point and the inflated edge is equivalent to the original situation we wanted to solve, but usually easier to reason about.

For the case of the small disc inside the semi-circle, we could attack it like this:

Geometry diagram

Here the pale purple semi-circle is our container. It's defined by a center point \$C\$ in purple, an outer radius \$R\$ in the solid blue line, and a diameter line in red that cuts it to just a semi-circle instead of a full one. I've also labelled the vector \$\vec n \$ in red, which is the unit normal perpendicular to this diameter.

From those, we can define a shrunken circle and a shifted diameter, inset by the radius of the small disc \$r\$, and represented here with the dashed lines. These form the boundary of the region the center of our disc can occupy - the darker purple not-quite-semi-circle.

If the target point we want to approach is inside that darker part-circle, we're done. That's a position our disc can reach without leaving the outer semi-circle.

If not, then closest our disc can get to it is to have its center along either the red or blue dashed lines.

What's great is we can use the projection of the target point onto our normal vector \$\vec n\$ to very quickly determine which one. If it's less than one disc radius "below" the circle center, or anywhere higher, then the closest we can reach to it has to be along the red dashed line. We can just project it down onto that line, and then clamp its position, so it doesn't go further left or right than our "half-width" value \$h\$, which we can compute with Pythagorean theorem:

$$\begin{align} h^2 + r^2 &= (R - r)^2\\ h^2 + r^2 - r^2 &= R^2 - 2Rr + r^2 - r^2\\ h^2 &= R^2 - 2Rr\\ h &= \sqrt {R^2 - 2Rr} \end{align}$$

(Here I'm taking just the positive root because we've defined \$h\$ to be a positive length)

If the target point is on the other side of that dashed red line, then it's either within the radius \$(R - r)\$ and thus we can go there directly, or it's on the blue dashed line - in which case we can just scale the vector from the center to the target to the length \$(R - r)\$ and we're done.

Here's some C# code implementing these ideas. I called it "Googley Eye" and the disc "pupil" since that seemed more intuitive. 😉

public struct GoogleyEye {

    public Vector2 center;

    public readonly float outerRadius;
    public readonly float pupilRadius;
    readonly float _innerHalfWidth;

    Vector2 _unitNormal;
    public Vector2 normal {
        get { return _unitNormal; }
        set { _unitNormal = value.normalized; }
    }

    public GoogleyEye(Vector2 center, float outerRadius, float pupilRadius, Vector2 normal) {
        this.center = center;
        this.outerRadius = outerRadius;
        this.pupilRadius = pupilRadius;
        _unitNormal = normal.normalized;

        _innerHalfWidth = Mathf.Sqrt(outerRadius * (outerRadius - 2f * pupilRadius));
    }

    public Vector2 ClosestPupilCenter(Vector2 target) {
        // Vector from the center of the circle to the target.
        var fromCenter = target - center;

        // Perpendicular distance of the target "above" the semi-circle.
        var offDiameter = Vector2.Dot(_unitNormal, fromCenter);

        // If this is less than a pupil radius inside the semi-circle,
        // then it's above the red dashed line, and the closest we can
        // get is on the red dashed line from -h to h.
        if (offDiameter > -pupilRadius) {
            // Quick perpendicular pointing along the diameter.
            var diameterDirection = new Vector2(-_unitNormal.y, _unitNormal.x);
            // Horizontal distance from center to target along this line.
            var alongDiameter = Vector2.Dot(diameterDirection, fromCenter);

            // Clamp it so it doesn't go outside our range of motion.
            alongDiameter = Mathf.Clamp(alongDiameter, -_innerHalfWidth, _innerHalfWidth);

            // Add the horizontal and vertical offsets to our center
            // to get the final point.
            return center - pupilRadius * _unitNormal + alongDiameter * diameterDirection;
        }

        // Radius of the inner dashed blue circle.
        float innerRadius = outerRadius - pupilRadius;
        // If we're inside this circle, then we're inside both dashed
        // regions, and our pupil can go to the target point directly.
        if (fromCenter.sqrMagnitude < innerRadius * innerRadius) {
            return target;
        }

        // Otherwise, we just project it to the closest point on the circle.
        return center + fromCenter.normalized * innerRadius;
    }
}
\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .