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 }