Unity中,實現物體的2D頂牌始終位於物體包圍盒中間下方邊緣,並自動計算頂牌中心點,避免頂牌遮擋物體

zerozabuu發表於2024-08-16

  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 }

相關文章