先上效果圖:
紅色就是連線的效果,可以用在狀態機之間的連線,也可以通過簡單修改,改為在圖片上塗鴉。
注:如果使用的是LineRenderer實現的話雖然也能達到這個效果,但是不能與原生UI一致,導致不能使用遮罩,層級只能在其它UI的最上層或者最下層。
實現方法:
通過繼承MaskableGraphic類,重寫OnPopulateMesh(VertexHelper vh)方法,這樣就可以在UGUI上自定義各種各樣的自定義mesh了。
實現繪制mesh的方法:
①可以使用 VertexHelper.AddVert(UIVertex v) 這個方法增加好點位后還需要設置Triangle,調用VertexHelper.SetTriangle(int idx0, int idx1, int idx2)方法設置。
②也可以直接調用VertexHelper.AddUIVertexQuad(UIVertex[] verts) 此方法可以直接生成一個四邊形,參數傳入四邊形的四個頂點,傳入順序是順時針。
示例圖:
v0-------v1
| |
v3-------v2
↑
lineWidth
這里使用的是第二種方法。參數傳入順序為V0 V1 V2 V3,先設置一個線條的粗細值lineWidth=2,很容易的就知道四邊形四個點的坐標信息,這樣就能繪制出一個四邊形了。
然而這個線並不是直線,是彎曲的線,這樣才自然。既然實現了一個四邊形的繪制,那么如果想要組成線段,就需要對整條線段進行切分,
這條線右若干個四邊形組成,如果要彎曲的話,這里引用的是三階貝塞爾曲線。
貝塞爾曲線詳情點擊:https://www.cnblogs.com/yzxhz/p/13802952.html
注意的問題:
1.曲線有了,這里還有一點問題,在轉彎折角處如果不進行處理的話,折角處外邊會出現缺口,
解決辦法:
將每個四邊形的第一條邊與上一個四邊形的最后一條邊(也就是兩個四邊形的接縫處的兩條邊)斜率保持相同即可。
2.當兩個點左右換位置的時候,線段會出現繪制錯亂問題。
解決辦法:
注意兩個點位的左右順序,通過判斷x的大小來判斷哪個點在左邊,哪個點在右邊。
左右互換的話,兩個貝塞爾的中間點位也要進行對稱處理,還有初始第一條邊的斜率向上改為向下。
增加射線檢測:
線段繪制好后需要對線段部分和透明部分區分開可點擊區域,通過繼承ICanvasRaycastFilter接口,
實現IsRaycastLocationValid(Vector2 sp, Camera eventCamera)方法,可以達到此目的。
只要判斷屏幕上的點在每一小段的四邊形內部即可,如果存在一個四邊形內部,就說明當前鼠標包含在線段內部了,就可以觸發射線檢測了。
具體實現的方式看下面源碼即可。
完整代碼如下:
using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class DrawLine : MaskableGraphic, ICanvasRaycastFilter { List<List<UIVertex>> vertexQuadList = new List<List<UIVertex>>(); public float lineWidth = 2; public Vector3 startPos; public Vector3 endPos; protected override void OnPopulateMesh(VertexHelper vh) { vh.Clear(); for (int i = 0; i < vertexQuadList.Count; i++) { vh.AddUIVertexQuad(vertexQuadList[i].ToArray()); } } void Update() { //設定rect大小正好能圈住線 rectTransform.position = endPos + (startPos - endPos) / 2f; rectTransform.sizeDelta = new Vector2(Mathf.Abs(endPos.x - startPos.x), Mathf.Abs(endPos.y - startPos.y)); AddVert(); } /// <summary> /// 增加點位 /// </summary> public void AddVert() { vertexQuadList.Clear(); //貝塞爾的兩個中間點位,Start點位在左邊和右邊的情況區分 Vector3 tempMiddle1Pos = startPos + Vector3.Distance(startPos, endPos) / 5f * ((startPos.x < endPos.x) ? Vector3.right : Vector3.left); Vector3 tempMiddle2Pos = endPos + Vector3.Distance(startPos, endPos) / 5f * ((startPos.x < endPos.x) ? Vector3.left : Vector3.right); //增加貝塞爾點位 List<Vector3> bPoints = CreatThreeBezierCurve(startPos, endPos, tempMiddle1Pos, tempMiddle2Pos); //初始的方向設置為向上垂直方向 Vector3 lastVerticalDir = (startPos.x < endPos.x) ? Vector3.up : Vector3.down; for (int i = 0; i < bPoints.Count - 1; i++) { //當前的線段方向 Vector3 curDir = bPoints[i] - bPoints[i + 1]; //通過當前線段方向計算垂直方向 Vector3 curVerticalDir = Vector3.Cross(curDir.normalized, Vector3.forward).normalized; //已知兩個相鄰點位,並且已知線段的粗細,可以計算出四邊形 List<UIVertex> vertexQuad = GetTwoPointMesh(bPoints[i], bPoints[i + 1], lastVerticalDir, curVerticalDir); vertexQuadList.Add(vertexQuad); lastVerticalDir = curVerticalDir; } SetVerticesDirty(); } /// <summary> /// 獲得兩個點之間的面片 /// </summary> /// <param name="vertexQuad"></param> private List<UIVertex> GetTwoPointMesh(Vector3 _startPos, Vector3 _endPos, Vector3 beforeTwoNodeVerticalDir, Vector3 nextTwoNodeVerticalDir) { List<UIVertex> vertexQuad = new List<UIVertex>(); // v0-------v1 // | | // v3-------v2 // ↑ // width Vector3 v0 = _startPos - beforeTwoNodeVerticalDir * lineWidth - rectTransform.position; Vector3 v1 = _startPos + beforeTwoNodeVerticalDir * lineWidth - rectTransform.position; Vector3 v3 = _endPos - nextTwoNodeVerticalDir * lineWidth - rectTransform.position; Vector3 v2 = _endPos + nextTwoNodeVerticalDir * lineWidth - rectTransform.position; UIVertex uIVertex = new UIVertex(); uIVertex.position = v0; uIVertex.color = color; vertexQuad.Add(uIVertex); UIVertex uIVertex1 = new UIVertex(); uIVertex1.position = v1; uIVertex1.color = color; vertexQuad.Add(uIVertex1); UIVertex uIVertex2 = new UIVertex(); uIVertex2.position = v2; uIVertex2.color = color; vertexQuad.Add(uIVertex2); UIVertex uIVertex3 = new UIVertex(); uIVertex3.position = v3; uIVertex3.color = color; vertexQuad.Add(uIVertex3); return vertexQuad; } /// <summary> /// 設置線段的碰撞 /// </summary> /// <param name="sp"></param> /// <param name="eventCamera"></param> /// <returns></returns> public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera) { bool isEnterMesh = false; for (int i = 0; i < vertexQuadList.Count; i++) { if (IsContainInQuad(sp, transform.position + vertexQuadList[i][0].position, transform.position + vertexQuadList[i][1].position, transform.position + vertexQuadList[i][2].position, transform.position + vertexQuadList[i][3].position)) { isEnterMesh = true; break; } } return isEnterMesh; } bool IsContainInQuad(Vector3 point, Vector3 p1, Vector3 p2, Vector3 p3, Vector3 p4) { // p1-----p2 // | | // | point | // | | // p4-----p3 Vector2 p1p2 = p1 - p2; Vector2 p1p = p1 - point; Vector2 p3p4 = p3 - p4; Vector2 p3p = p3 - point; Vector2 p4p1 = p4 - p1; Vector2 p4p = p4 - point; Vector2 p2p3 = p2 - p3; Vector2 p2p = p2 - point; bool isBetweenP1P2_P3P4 = CrossAB(p1p2, p1p) * CrossAB(p3p4, p3p) > 0; bool isBetweenP4P1_P2P3 = CrossAB(p4p1, p4p) * CrossAB(p2p3, p2p) > 0; return isBetweenP1P2_P3P4 && isBetweenP4P1_P2P3; } float CrossAB(Vector2 a, Vector2 b) { return a.x * b.y - b.x * a.y; } public float nultiple = 8; /// <summary> /// 三階貝塞爾 /// </summary> /// <param name="startPoint"></param> /// <param name="endPoint"></param> /// <param name="middlePoint1"></param> public List<Vector3> CreatThreeBezierCurve(Vector3 startPoint, Vector3 endPoint, Vector3 middlePoint1, Vector3 middlePoint2) { List<Vector3> allPoints = new List<Vector3>(); for (int i = 0; i < nultiple; i++) { float tempPercent = (float)i / (float)nultiple; float dis1 = Vector3.Distance(startPoint, middlePoint1); Vector3 pointL1 = startPoint + Vector3.Normalize(middlePoint1 - startPoint) * dis1 * tempPercent; float dis2 = Vector3.Distance(middlePoint1, middlePoint2); Vector3 pointL2 = middlePoint1 + Vector3.Normalize(middlePoint2 - middlePoint1) * dis2 * tempPercent; float dis3 = Vector3.Distance(pointL1, pointL2); Vector3 pointLeft = pointL1 + Vector3.Normalize(pointL2 - pointL1) * dis3 * tempPercent; float dis4 = Vector3.Distance(middlePoint2, endPoint); Vector3 pointR1 = middlePoint2 + Vector3.Normalize(endPoint - middlePoint2) * dis4 * tempPercent; float dis5 = Vector3.Distance(pointL2, pointR1); Vector3 pointRight = pointL2 + Vector3.Normalize(pointR1 - pointL2) * dis5 * tempPercent; float disLeftAndRight = Vector3.Distance(pointLeft, pointRight); Vector3 linePoint = pointLeft + Vector3.Normalize(pointRight - pointLeft) * disLeftAndRight * tempPercent; allPoints.Add(linePoint); } allPoints.Add(endPoint); return allPoints; } }
場景中新建一個image,刪掉image組件,掛上上面的代碼即可。
調用的代碼如下:
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; public class DragNode : MonoBehaviour, IDragHandler, IPointerDownHandler, IPointerUpHandler { DrawLine drawLine; GameObject lineGo; public void OnPointerDown(PointerEventData eventData) { lineGo = new GameObject("Line"); lineGo.transform.SetParent(transform); drawLine = lineGo.AddComponent<DrawLine>(); drawLine.startPos = Input.mousePosition; drawLine.endPos = Input.mousePosition; drawLine.color = Color.red; } public void OnDrag(PointerEventData eventData) { drawLine.endPos = Input.mousePosition; } public void OnPointerUp(PointerEventData eventData) { DestroyImmediate(lineGo); } }
補充:如果需要遮罩,加一句代碼即可:
[RequireComponent(typeof(CanvasRenderer))]
public class DrawLine : MaskableGraphic, ICanvasRaycastFilter
就這樣。拜拜~