在Unity UI中实现UILineRenderer组件绘制线条
背景介绍
在Unity的UI系统中,绘制线条并不像在3D世界中那样直观(使用Unity自带的LineRender组件在UI中连线并不方便,它在三维中更合适)。没有内置的工具来处理这种需求。如果你希望在UI元素之间绘制连接线(例如在UI上连接不同的图标或控件),需要自己编写逻辑。
目前只适配常规ui情况
为了满足这种需求,我封装了一个名为UILineRenderer的组件。该组件能够处理UI中的线条绘制,并且可以适应UI元素复杂的父子关系,提供线条宽度、颜色设置等功能,非常适合在UI界面中使用。
功能简介
UILineRenderer的主要功能包括:
连接两个UI元素(当然你也可以根据我的思路拓展更多).
支持自定义线条宽度与颜色
能够根据UI元素的坐标变化实时更新线条
通过鼠标位置动态调整线条的终点位置
处理UI元素复杂的父子层级关系
目前仅支持连接两个Ui元素(有一个中间状态就是只添加了一个UI元素,你可以设置鼠标作为第二个临时点,然后在合适的时候设置第二个UI元素)
但是我重写的OnPopulateMesh逻辑实际上是支持连接多个点的,你可以修改这个组件实现你的需求.
先看效果
可以连接两个UI元素
在设置了第一个UI元素的时候,可以设置鼠标位置,从而实现始终连接鼠标(这里截图导致鼠标没了)
源码
using UnityEngine.UI;
using UnityEngine;
using System.Collections.Generic;
[RequireComponent(typeof(CanvasRenderer))]//需要该组件才能生效
public class UILineRenderer : Graphic
{
private List<Vector2> points = new List<Vector2>(); // 用于存储线条的点
[SerializeField] private float lineWidth = 5f; // 线条宽度
[SerializeField] private Color lineColor = Color.white; // 默认线条颜色
// 每次需要重新绘制UI时调用
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear(); // 清空当前顶点数据
// 如果没有足够的点,则不绘制任何东西
if (points == null || points.Count < 2)
return;
// 遍历每个点,创建线段
for (int i = 0; i < points.Count - 1; i++)
{
Vector2 start = points[i];
Vector2 end = points[i + 1];
// 计算垂直方向的法线,使线条有宽度
Vector2 direction = (end - start).normalized;
Vector2 perpendicular = new Vector2(-direction.y, direction.x) * lineWidth / 2f;
// 四个顶点(左下、左上、右上、右下)
UIVertex vertex = UIVertex.simpleVert;
vertex.color = lineColor; // 定义颜色
// 左下
vertex.position = new Vector3(start.x - perpendicular.x, start.y - perpendicular.y);
vh.AddVert(vertex);
// 左上
vertex.position = new Vector3(start.x + perpendicular.x, start.y + perpendicular.y);
vh.AddVert(vertex);
// 右上
vertex.position = new Vector3(end.x + perpendicular.x, end.y + perpendicular.y);
vh.AddVert(vertex);
// 右下
vertex.position = new Vector3(end.x - perpendicular.x, end.y - perpendicular.y);
vh.AddVert(vertex);
// 添加两个三角形来组成矩形线条
int index = vh.currentVertCount;
vh.AddTriangle(index - 4, index - 3, index - 2);
vh.AddTriangle(index - 4, index - 2, index - 1);
}
}
/// <summary>
/// 设置一个Ui元素
/// 为什么要转换坐标?因为UI元素极可能不在同一个父物体下,存在错综复杂的父子关系
/// 先获取UiElement世界坐标系转屏幕坐标系再转到此脚本所在的Ui坐标系
/// </summary>
/// <param name="uiElement"></param>
public void AppendUIElement(RectTransform uiElement)
{
Vector2 localPoint;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
rectTransform, // 当前 UILineRenderer 的 RectTransform
RectTransformUtility.WorldToScreenPoint(null, uiElement.position), // UI 元素的世界坐标转换为屏幕坐标
null,
out localPoint // 输出的局部坐标
);
// 如果已经有两个点,则移除第二个点,以保持绘制最新线条
if (points.Count == 2)
{
points.RemoveAt(1);
}
// 添加转换后的局部坐标到点列表中
points.Add(localPoint);
// 标记为需要重新绘制
SetVerticesDirty();
}
/// <summary>
/// 设置鼠标位置为第二个点,此时鼠标和第一个UiElement可以构成一条线
/// </summary>
/// <param name="point"></param>
public void SetMouse()
{
if (points.Count==2)
{
points.RemoveAt(1);
}
var mousePostion = Input.mousePosition;
RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, mousePostion, null, out Vector2 point);
points.Add(point);
SetVerticesDirty();
}
/// <summary>
/// 设置线的颜色
/// </summary>
/// <param name="newColor"></param>
public void SetLineColor(Color newColor)
{
lineColor = newColor;
SetVerticesDirty();
}
/// <summary>
/// 设置线的宽带
/// </summary>
/// <param name="width"></param>
public void SetWidth(float width)
{
lineWidth = width;
SetVerticesDirty();
}
/// <summary>
/// 重置组件
/// </summary>
public void Reset()
{
points.Clear();
lineColor = Color.white;
lineWidth = 5f;
SetVerticesDirty();
}
}
示例
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Test : MonoBehaviour
{
public UILineRenderer t;
public Image img1;//拖拽
public Image img2;
private bool keyA = false;
// Start is called before the first frame update
void Start()
{
t.AppendUIElement(img1.rectTransform);
}
// Update is called once per frame
void Update()
{
if (keyA==false)
{
t.SetMouse();
}
if (Input.GetKeyDown(KeyCode.A))
{
keyA = true;
t.AppendUIElement(img2.rectTransform);
}
}
}
示例结构图
挂载了UILineRender的组件和其他UI元素一样,所以要放在下面