|
在Unity UI中实现UILineRenderer组件绘制线条
背景介绍
在Unity的UI系统中,绘制线条并不像在3D世界中那样直观(使用Unity自带的LineRender组件在UI中连线并不方便,它在三维中更合适)。没有内置的工具来处理这种需求(当然可是使用canvas的第二种模式渲染3维物体到ui,然后使用LineRender,这增加了一点复杂性,但也不失为一种好办法)。如果你只希望在常规UI元素之间绘制连接线(例如在UI上连接不同的图标或控件),需要自己编写逻辑。
为了满足这种需求,我封装了一个名为UILineRenderer的组件。该组件能够处理UI中的线条绘制,并且可以适应UI元素复杂的父子关系,提供线条宽度、颜色设置等功能,非常适合在UI界面中使用。
功能简介
UILineRenderer的主要功能包括:
连接两个UI元素(当然你也可以根据我的思路拓展更多).
支持自定义线条宽度与颜色
能够根据UI元素的坐标变化实时更新线条
通过鼠标位置动态调整线条的终点位置
处理UI元素复杂的父子层级关系
目前仅支持连接两个Ui元素(有一个中间状态就是只添加了一个UI元素,你可以设置鼠标作为第二个临时点,然后在合适的时候设置第二个UI元素)
但是我重写的OnPopulateMesh逻辑实际上是支持连接多个点的,你可以修改这个组件实现你的需求.
先看效果
可以连接两个UI元素
在设置了第一个UI元素的时候,可以设置鼠标位置,从而实现始终连接鼠标(这里截图导致鼠标没了)
gitee示例
连线模块小功能: 这是一个连线功能的小插件
绘制线段核心源码:
using UnityEngine.UI;
using UnityEngine;
using System.Collections.Generic;
[RequireComponent(typeof(CanvasRenderer))]//需要该组件才能生效
public class NSZXTPoint1_script : 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(); // 清空当前顶点数据
// 如果没有足够的点,则不绘制任何东西
//points.Count可以修改为自定义的点
if (points == null || points.Count < 3)
{
print("points.Count: " + points.Count);
return;
}
// 遍历每个点,创建线段
for (int i = 0; i < points.Count - 1; i++)
{
print("绘制循环中");
Vector2 start = points;
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 // 输出的局部坐标
);
// 如果已经有三个点,则移除第是三个点,以保持绘制最新线条
//points.Count可以修改为自定义的点
if (points.Count == 3)
{
//自定义的点修改后,这里的RemoveAt记得也要修改
points.RemoveAt(2);
}
// 添加转换后的局部坐标到点列表中
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 ResetSelf()
{
points.Clear();
lineColor = Color.white;
lineWidth = 5f;
SetVerticesDirty();
}
}
使用示例-尿酸折线图(测试项目使用的是,在其他界面按钮触发本方法,一开始给定几个点的坐标,直接绘制,不需要鼠标拖拽方法了)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System;
public class NSManager_script : MonoBehaviour
{
//折线图界面Panel
public GameObject NSZXTPanel;
//折线图本体Panel
public GameObject NSZXTMainPanel;
public NSZXTPoint1_script t;
private Vector2 NSZXTMainPanelPoint1, NSZXTMainPanelPoint2, NSZXTMainPanelPoint1Zone, NSZXTMainPanelPoint2Zone;
public void NSZXTMainPanelOpenFF()
{
NSZXTPanel.SetActive(true);
//折线图点设置位置
NSZXTMainPanelPoint1 = new Vector2(-450, 30);
NSZXTMainPanelPoint2 = new Vector2(-300, -270);
NSZXTMainPanel.GetComponentsInChildren<RectTransform>()[1].anchoredPosition = NSZXTMainPanelPoint1;
NSZXTMainPanel.GetComponentsInChildren<RectTransform>()[2].anchoredPosition = NSZXTMainPanelPoint2;
//折线图点的设置
t.AppendUIElement(NSZXTMainPanel.GetComponentsInChildren<RectTransform>()[1]);
t.AppendUIElement(NSZXTMainPanel.GetComponentsInChildren<RectTransform>()[2]);
t.AppendUIElement(NSZXTMainPanel.GetComponentsInChildren<RectTransform>()[3]);
print("折线图点1位置:" + NSZXTMainPanel.GetComponentsInChildren<RectTransform>()[1].name + " : " + NSZXTMainPanel.GetComponentsInChildren<RectTransform>()[1].anchoredPosition.x + " : " + NSZXTMainPanel.GetComponentsInChildren<RectTransform>()[1].anchoredPosition.y);
print("折线图点2位置:" + NSZXTMainPanel.GetComponentsInChildren<RectTransform>()[2].name + " : " + NSZXTMainPanel.GetComponentsInChildren<RectTransform>()[2].anchoredPosition.x + " : " + NSZXTMainPanel.GetComponentsInChildren<RectTransform>()[2].anchoredPosition.y);
}
}
示例结构图
挂载了UILineRender的组件和其他UI元素一样,所以要放在下面,如果没有放在本层级最下面,可能会出现线段显示异常!
我这里示例将绘制线段脚本组件放到了一个叫Line的空物体上,然后将此空物体拉到本层级的最下面,显示成功!
|