Last active
March 9, 2026 11:55
-
-
Save runevision/970445fa11280def3e8be241fd3dc720 to your computer and use it in GitHub Desktop.
Demo code for calculating the touch point between a "ballooning" circle and a rectangle
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using UnityEngine; | |
| [ExecuteAlways] | |
| public class BalloonTouch : MonoBehaviour { | |
| public Transform start; | |
| public Transform other; | |
| void Update() { | |
| Vector2 startPoint = start.position; | |
| Vector2 dir = start.up; | |
| // Animate rect for demonstration | |
| other.position = new Vector3(2f * Mathf.Sin(Time.time * 2.3f), 2.5f + Mathf.Cos(Time.time)); | |
| other.rotation = Quaternion.Euler(0f, 0f, Time.time * 90f); | |
| // Calculate score for rect | |
| float score = BalloonScoreRect(startPoint, dir, other, out Vector2 touch); | |
| // Draw debug visualizations | |
| float radius = 0.5f / score; | |
| DrawCircle(startPoint + dir * radius, radius, 48, Color.cyan); | |
| DrawCircle(touch, 0.05f, 12, Color.cyan); | |
| DrawCircle(startPoint, 0.05f, 12, Color.green); | |
| } | |
| static float BalloonScorePoint(Vector2 start, Vector2 dir, Vector2 point) { | |
| Vector2 vec = point - start; | |
| return Vector2.Dot(dir, vec) / vec.sqrMagnitude; | |
| } | |
| static float BalloonScoreLineSegment(Vector2 start, Vector2 dir, Vector2 p1, Vector2 p2, out Vector2 touch) { | |
| Vector2 lineVec = p2 - p1; | |
| Vector2 normal = new Vector2(-lineVec.y, lineVec.x).normalized; | |
| // This is what makes the ballon touch logic work. | |
| // By using a direction which is halfway between the input direction | |
| // and the (reverse) normal of the line, we get an intersection | |
| // which is exactly where the ballon would first touch the line. | |
| Vector2 intersectDir = (dir + normal); | |
| touch = Intersect(start, start + intersectDir, p1, p2); | |
| // Constrain touch point to be inside line segment. | |
| if (Vector2.Dot(touch - p2, p1 - p2) < 0) | |
| touch = p2; | |
| if (Vector2.Dot(touch - p1, p2 - p1) < 0) | |
| touch = p1; | |
| return BalloonScorePoint(start, dir, touch); | |
| } | |
| static float BalloonScoreRect(Vector2 start, Vector2 dir, Transform rect, out Vector2 touch) { | |
| void UseIfBetter(Vector2 p1, Vector2 p2, ref float score, ref Vector2 touch) { | |
| float newScore = BalloonScoreLineSegment(start, dir, p1, p2, out Vector2 newTouch); | |
| if (newScore > score) { | |
| score = newScore; | |
| touch = newTouch; | |
| } | |
| Debug.DrawLine(p1, p2); | |
| } | |
| Vector2 PA = rect.TransformPoint(new Vector2(1.0f, 1.0f)); | |
| Vector2 PB = rect.TransformPoint(new Vector2(-1.0f, 1.0f)); | |
| Vector2 PC = rect.TransformPoint(new Vector2(-1.0f, -1.0f)); | |
| Vector2 PD = rect.TransformPoint(new Vector2(1.0f, -1.0f)); | |
| float score = float.NegativeInfinity; | |
| touch = Vector2.zero; | |
| UseIfBetter(PA, PB, ref score, ref touch); | |
| UseIfBetter(PB, PC, ref score, ref touch); | |
| UseIfBetter(PC, PD, ref score, ref touch); | |
| UseIfBetter(PD, PA, ref score, ref touch); | |
| return score; | |
| } | |
| void DrawCircle(Vector2 center, float radius, int segments, Color color) { | |
| Vector2 p1 = center + new Vector2(radius, 0f); | |
| for (int i = 0; i < segments; i++) { | |
| int j = i + 1; | |
| float rad = j * 2f * Mathf.PI / segments; | |
| Vector2 p2 = new Vector2(Mathf.Cos(rad), Mathf.Sin(rad)) * radius + center; | |
| Debug.DrawLine(p1, p2, color); | |
| p1 = p2; | |
| } | |
| } | |
| static Vector2 Intersect(Vector2 line1A, Vector2 line1B, Vector2 line2A, Vector2 line2B) { | |
| // Line 1 | |
| float A1 = line1B.y - line1A.y; | |
| float B1 = line1A.x - line1B.x; | |
| float C1 = A1 * line1A.x + B1 * line1A.y; | |
| // Line 2 | |
| float A2 = line2B.y - line2A.y; | |
| float B2 = line2A.x - line2B.x; | |
| float C2 = A2 * line2A.x + B2 * line2A.y; | |
| float det = A1 * B2 - A2 * B1; | |
| if (det == 0) { | |
| // Parallel lines | |
| return Vector2.zero; | |
| } | |
| else { | |
| float x = (B2 * C1 - B1 * C2) / det; | |
| float y = (A1 * C2 - A2 * C1) / det; | |
| return new Vector2(x, y); | |
| } | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This demonstration is in the context of discussing automatic UI control navigation behavior based on a balloon approach. See the full discussion here:
godotengine/godot#103895
And my reply where I show a video of the code above here:
godotengine/godot#103895 (comment)