Unity中,实现物体的2D顶牌始终位于物体包围盒中间下方边缘,并自动计算顶牌中心点,避免顶牌遮挡物体
1 /* 2 * 3 * 1.2D顶牌跟随物体 4 * 2.顶牌始终位于物体包围盒中间下方边缘位置 5 * 3.自动计算顶牌中心点,避免顶牌遮挡物体 6 * 7 */ 8 using System.Collections.Generic; 9 using UnityEngine; 10 using UnityEngine.UI; 11 12 public class TransformCard : MonoBehaviour 13 { 14 public Canvas canvas; 15 public RectTransform uiRectTrans; 16 17 public void Update() 18 { 19 UpdateUIRectTrans(); 20 } 21 22 public void UpdateUIRectTrans() 23 { 24 var bounds = GetBounds(this.transform);//得到物体包围盒 25 var screenPoints = GetScreenPoints(bounds);//把包围盒的顶点转化成屏幕坐标 26 List<Vector2> convexHull = GetConvexHull(screenPoints);//得到最大凸多边形 27 var minX = float.MaxValue; 28 var minY = float.MaxValue; 29 var maxX = float.MinValue; 30 var maxY = float.MinValue; 31 32 foreach (var point in convexHull) 33 { 34 if (point.x > maxX) maxX = point.x; 35 if (point.x < minX) minX = point.x; 36 if (point.y > maxY) maxY = point.y; 37 if (point.y < minY) minY = point.y; 38 } 39 40 var rayStart = new Vector2((minX + maxX) *0.5f, minY);//计算起点,中间最底部 41 42 Vector2 p0, p1, interPoint; 43 var hasIntersection = GetIntersection(rayStart, Vector2.up, convexHull, out interPoint, out p0, out p1);//得到与凸包的交点 44 45 if (hasIntersection) 46 { 47 //根据相交的边的方向, 计算UI中心点,始终保持UI边缘贴着凸包边缘,而不是重叠 48 var interLineDirection = p0.y > p1.y ? p0 - p1 : p1 - p0; 49 var angle = Vector2.SignedAngle(Vector2.up, interLineDirection); 50 if (angle > 0f) 51 { 52 uiRectTrans.pivot = new Vector2(1f - angle / 90f * 0.5f, 1f); 53 } 54 else if (angle < 0f) 55 { 56 uiRectTrans.pivot = new Vector2(-angle / 90f * 0.5f, 1f); 57 } 58 else 59 { 60 uiRectTrans.pivot = new Vector2(0.5f, 1f); 61 } 62 63 //锚点在屏幕左下角 64 uiRectTrans.anchorMin = Vector2.zero; 65 uiRectTrans.anchorMax = Vector2.zero; 66 67 uiRectTrans.anchoredPosition = interPoint; 68 } 69 ////Debug.Log("Convex Hull Points:"); 70 //for (int i = 0; i < convexHull.Count; i++) 71 //{ 72 // //Debug.LogError(convexHull[i]); 73 // Test2DImage("convex_" + i, convexHull[i]); 74 //} 75 } 76 77 //把包围盒的顶点转化成屏幕坐标 78 public List<Vector2> GetScreenPoints(Bounds bounds) 79 { 80 List<Vector3> boundsVertices = new List<Vector3>(); 81 List<Vector2> screenPoints = new List<Vector2>(); 82 var halfForward = this.transform.forward.normalized * bounds.size.z * 0.5f; 83 var halfRight = this.transform.right.normalized * bounds.size.x * 0.5f; 84 var center = bounds.center; 85 var height = new Vector3(0f, bounds.size.y, 0f); 86 boundsVertices.Add(center + halfForward + halfRight); 87 boundsVertices.Add(center - halfForward + halfRight); 88 boundsVertices.Add(center - halfForward - halfRight); 89 boundsVertices.Add(center + halfForward - halfRight); 90 boundsVertices.Add(boundsVertices[0] + height); 91 boundsVertices.Add(boundsVertices[1] + height); 92 boundsVertices.Add(boundsVertices[2] + height); 93 boundsVertices.Add(boundsVertices[3] + height); 94 for (int i = 0; i < boundsVertices.Count; i++) 95 { 96 var screenPoint = Camera.main.WorldToScreenPoint(boundsVertices[i]); 97 screenPoints.Add(screenPoint); 98 //Test3DSphere("Test3DSphere"+i, boundsVertices[i]); 99 } 100 return screenPoints; 101 } 102 103 //测试用,生成凸多边形的顶点 104 public void Test2DImage(string name, Vector3 pos) 105 { 106 var testObj = GameObject.Find(name); 107 if (testObj == null) 108 { 109 testObj = new GameObject(name); 110 testObj.name = name; 111 testObj.AddComponent<Image>().color = Color.red; 112 } 113 testObj.transform.parent = canvas.transform; 114 var rectTrans = testObj.transform as RectTransform; 115 rectTrans.sizeDelta = new Vector2(10f, 10f); 116 rectTrans.position = pos; 117 } 118 119 //测试用,生成包围盒顶点 120 static public void Test3DSphere(string name, Vector3 pos) 121 { 122 var testObj = GameObject.Find(name); 123 if (testObj == null) 124 { 125 testObj = GameObject.CreatePrimitive(PrimitiveType.Sphere); 126 testObj.name = name; 127 } 128 testObj.transform.localScale = Vector3.one * 0.1f; 129 testObj.transform.position = pos; 130 } 131 132 //获取包围盒 133 public static Bounds GetBounds(Transform trans) 134 { 135 var b = trans.GetComponent<MeshFilter>().mesh.bounds; 136 b.size = Vector3.Scale(b.size, trans.localScale);//mesh.bounds是本地坐标,所以要同步大小 137 var center = b.center + trans.position;//mesh.bounds是本地坐标,所以要加上position 138 center.y -= b.size.y / 2f;//把中心放在底部 139 b.center = center; 140 return b; 141 } 142 143 //计算凸包 144 public static List<Vector2> GetConvexHull(List<Vector2> points) 145 { 146 // 步骤1:找出最低且最左的点 147 Vector2 startPoint = points[0]; 148 foreach (Vector2 p in points) 149 { 150 if (p.y < startPoint.y || (p.y == startPoint.y && p.x < startPoint.x)) 151 { 152 startPoint = p; 153 } 154 } 155 156 // 步骤2:按照极角排序 157 points.Sort((a, b) => 158 { 159 float angleA = Mathf.Atan2(a.y - startPoint.y, a.x - startPoint.x); 160 float angleB = Mathf.Atan2(b.y - startPoint.y, b.x - startPoint.x); 161 if (angleA < angleB) return -1; 162 if (angleA > angleB) return 1; 163 return Vector2.Distance(startPoint, a).CompareTo(Vector2.Distance(startPoint, b)); 164 }); 165 166 // 步骤3:构建凸包 167 List<Vector2> hull = new List<Vector2>(); 168 hull.Add(startPoint); 169 170 for (int i = 1; i < points.Count; i++) 171 { 172 while (hull.Count > 1 && Cross(hull[hull.Count - 2], hull[hull.Count - 1], points[i]) <= 0) 173 { 174 hull.RemoveAt(hull.Count - 1); 175 } 176 hull.Add(points[i]); 177 } 178 179 return hull; 180 } 181 182 // 用于计算向量叉乘的帮助函数 183 public static float Cross(Vector2 O, Vector2 A, Vector2 B) 184 { 185 return (A.x - O.x) * (B.y - O.y) - (A.y - O.y) * (B.x - O.x); 186 } 187 188 // 返回从点A出发,沿着方向B,与凸多边形C的最近交点。 189 public static bool GetIntersection(Vector2 A, Vector2 B, List<Vector2> convexPolygon, out Vector2 closestIntersection, out Vector2 C1, out Vector2 C2) 190 { 191 bool hasIntersection = false; 192 float closestDistance = float.MaxValue; 193 closestIntersection = C1 = C2 = Vector2.zero; 194 // 遍历凸多边形的每条边 195 for (int i = 0; i < convexPolygon.Count; i++) 196 { 197 Vector2 T1 = convexPolygon[i]; 198 Vector2 T2 = convexPolygon[(i + 1) % convexPolygon.Count]; 199 200 // 计算与当前边的交点 201 Vector2? intersection = GetLineSegmentIntersection(A, B, T1, T2); 202 203 if (intersection != null) 204 { 205 float distance = Vector2.Distance(A, intersection.Value); 206 if (distance < closestDistance) 207 { 208 closestDistance = distance; 209 closestIntersection = intersection.Value; 210 hasIntersection = true; 211 C1 = T1; 212 C2 = T2; 213 } 214 } 215 } 216 return hasIntersection; 217 } 218 219 // 计算从点A出发,沿着方向B,与线段C1-C2的交点 220 public static Vector2? GetLineSegmentIntersection(Vector2 A, Vector2 B, Vector2 C1, Vector2 C2) 221 { 222 Vector2 dirAB = B.normalized; 223 Vector2 dirC = C2 - C1; 224 Vector2 normalC = new Vector2(-dirC.y, dirC.x); 225 226 float denominator = Vector2.Dot(dirAB, normalC); 227 if (Mathf.Abs(denominator) < 1e-6) 228 { 229 return null; // 平行或共线,无交点 230 } 231 232 float t = Vector2.Dot(C1 - A, normalC) / denominator; 233 if (t < 0) 234 { 235 return null; // 交点在A的反方向 236 } 237 238 Vector2 P = A + t * dirAB; 239 240 // 检查P是否在线段C1-C2上 241 float crossProduct = (P.x - C1.x) * (C2.y - C1.y) - (P.y - C1.y) * (C2.x - C1.x); 242 if (Mathf.Abs(crossProduct) > 0.05f) 243 { 244 return null; // 不在线段上 245 } 246 247 float dotProduct = Vector2.Dot(P - C1, C2 - C1); 248 if (dotProduct < 0 || dotProduct > Vector2.Dot(C2 - C1, C2 - C1)) 249 { 250 return null; // 在延长线但不在线段上 251 } 252 253 return P; 254 } 255 }