网络知识 娱乐 【Unity实战技巧】教你4招计算UI物体的包围盒(Bounds)

【Unity实战技巧】教你4招计算UI物体的包围盒(Bounds)

UI包围盒计算教程

      • 👉一、前言
      • 👉二、四种UI包围盒的获取方法
        • 1、法一:通过Unity自带API(UI有无旋转均适用)
        • 2、法二:通过UI宽高和中心坐标计算(适用于无旋转UI)
        • 3、法三:通过UI的中心坐标、大小和锚点计算(适用于无旋转UI)
        • 4、法四:通过UI的四个边角顶点坐标计算(适用于旋转一定角度后的UI)
          • (1)、计算UI旋转后的顶点坐标(自身坐标系下)
          • (2)、计算UI旋转后的包围盒
      • 👉三、实战应用demo
        • 1、搭建测试demo场景
        • 2、核心代码
        • 3、测试结果
          • (1)UI无旋转情况下的包围盒
          • (2)UI有旋转情况下的包围盒
          • (3)结论
        • 4、demo源工程
      • 👉四、包围盒(Bouns)知识点
        • 1、变量
        • 2、函数

👉一、前言

使用Unity做UI的框选、对齐等功能时常用到包围盒(Bounds)的计算。如果UI在无旋转的情况下,如图:(紫色框是其bounds)
在这里插入图片描述
只要知道UI的中心坐标和其大小(UI矩形宽高)就很容易计算出它的包围盒;
;一旦UI经过旋转之后,如图:(红色框是其bounds)
在这里插入图片描述
此时的包围盒的size并不等于该UI矩形的宽高,所以是需要经过一定的数学运算才能求出此时包围盒的size。本人在实际开发过程中,总结了4种Unity中如何计算UI物体的包围盒的方法,详见后文博客内容。

👉二、四种UI包围盒的获取方法

(注:以下方法的UI的中心坐标均指的是局部坐标系下,锚点为(0.5,0.5)时的中心原点坐标)

1、法一:通过Unity自带API(UI有无旋转均适用)

RecTransformUtility.CalculateRelativeRectTransformBounds(Transfrom root,Transform child)

使用此方法通过计算子物体在父物体的局部坐标下的bounds并返回。

2、法二:通过UI宽高和中心坐标计算(适用于无旋转UI)

  1. 实例化Bounds后设置center和size
RectTransform rect;//UI矩形对象
Bounds bounds=new Bounds();
bounds.center=rect.transform.localPosition;
bounds.size=rect.rect.size;
  1. 将center和size在构造Bounds时作为实参传入
RectTransform rect;
Bounds bounds=new bounds(rect.transform.localPosition,rect.rect.size);

以上两个方法都是通过设置包围盒bounds的中心坐标center和大小size来得到rect对象的包围盒。

3、法三:通过UI的中心坐标、大小和锚点计算(适用于无旋转UI)

RectTransform rect;
Bounds bounds=new Bounds();
bounds.min=new Vector3(rect.transform.localPostion.x-rect.pivot.x*rect.rect.size.x,rect.transform.localPostion.y-rect.pivot.y*rect.rect.size.y,0);
bounds.max=new Vector3(rect.transform.localPostion.x+rect.pivot.x*rect.rect.size.x,rect.transform.localPostion.y+rect.pivot.y*rect.rect.size.y,0);

此方法通过设置包围盒bounds的最小点min和最大点max来得到rect对象的包围盒

4、法四:通过UI的四个边角顶点坐标计算(适用于旋转一定角度后的UI)

(1)、计算UI旋转后的顶点坐标(自身坐标系下)
  1. 已知条件
    已知中心点(x0,y0)、某点坐标(x,y)、旋转角度θ;求旋转后(x’,y’)的坐标
  2. 计算原理

x’=cos(θ)(x-x0)-sin(θ)(y-y0)+x0;
y’=sin(θ)(x-x0)+cos(θ)(y-y0)+y0
;

详细推导过程可看:在平面中,一个点绕任一点旋转θ角度后的点的坐标

  1. 计算UI旋转θ角度后的四个顶点坐标
RectTransform rect;
Vector3[] corners=new Vector3[4];
rect.GetLocalCoreners(corners);//获取原局部坐标系下的四个边角坐标

float radian=θ*Mathf.PI / 180;//角度转弧度
//corners[0]'坐标
float a = Mathf.Cos(radian) * corners[0].x - Mathf.Sin(radian) * corners[0].y;
float b = Mathf.Sin(radian) * corners[0].x + Mathf.Cos(radian) * corners[0].y;
//corners[2]'坐标
float c = Mathf.Cos(radian) * corners[2].x - Mathf.Sin(radian) * corners[2].y;
float d = Mathf.Sin(radian) * corners[2].x + Mathf.Cos(radian) * corners[2].y;
//corners[1]'坐标
float e = Mathf.Cos(radian) * corners[1].x - Mathf.Sin(radian) * corners[1].y;
float f = Mathf.Sin(radian) * corners[1].x + Mathf.Cos(radian) * corners[1].y;
//corners[3]'坐标
float g = Mathf.Cos(radian) * corners[3].x - Mathf.Sin(radian) * corners[3].y;
float h = Mathf.Sin(radian) * corners[3].x + Mathf.Cos(radian) * corners[3].y;
(2)、计算UI旋转后的包围盒

如图:
在这里插入图片描述
我们要计算旋转后的UI的包围盒,只需要知道紫色框的size即可。从图中可看出,size.x等于corners[0]’ 与 corners[2]’在X轴上的绝对距离;size.y=corners[1]’ 与 corners[3]’在Y轴上的绝对距离。即:

Vector2 size=new Vector2(Mathf.Abs(a-c),Mathf.Abs(f-h));
Bounds bounds=new Bounds(rect.transform.localPosition,size);

bounds即为UI旋转θ角度后的包围盒。

👉三、实战应用demo

1、搭建测试demo场景

如图:
在这里插入图片描述
添加一张Image作为测试包围盒的对象,分别添加三个按钮测试有无旋转情况下的UI包围盒。新建脚本CalculateUIBounds写测试代码,挂载到Canvas上,拖拽对象如上图所示,代码如下。

2、核心代码

using UnityEngine;
using UnityEngine.UI;

public class CalculateUIBounds : MonoBehaviour
{

    public GameObject  imgObj;
    private RectTransform imgRect;
    public Button clockWiseBtn;
    public Button antiClockWiseBtn;
    public Button notRotateBtn;
    Vector3[] corners = new Vector3[4];

    void Start()
    {
        imgRect = imgObj.GetComponent<RectTransform>();
        clockWiseBtn.onClick.AddListener(() =>
        {
            CalculateRotateUIBounds(-45);
        });
        antiClockWiseBtn.onClick.AddListener(() =>
        {
            CalculateRotateUIBounds(120);
        });
        notRotateBtn.onClick.AddListener(CalculateNotRotateUIBounds);
        
        imgRect.GetLocalCorners(corners);//获取局部坐标系下的UI边角坐标
        foreach (var item in corners)
        {
            Debug.Log("旋转前UI矩形四个顶点坐标:" + item);
        }
    }
    /// 
    /// 计算旋转的一定角度后的UI包围盒
    /// 
    /// 
    private void CalculateRotateUIBounds(float angle)
    {
        imgObj.transform.rotation = Quaternion.Euler(new Vector3(0, 0, angle));

        float radian = angle * Mathf.PI / 180;//角度转弧度
        //计算corner[0]'坐标的xy值
        float _corner0X = Mathf.Cos(radian) * corners[0].x - Mathf.Sin(radian) * corners[0].y;
        float _corner0Y = Mathf.Sin(radian) * corners[0].x + Mathf.Cos(radian) * corners[0].y;
        Vector2 _corner0 = new Vector2(_corner0X, _corner0Y);
        //计算corner[1]'坐标的xy值
        float _corner1X = Mathf.Cos(radian) * corners[1].x - Mathf.Sin(radian) * corners[1].y;
        float _corner1Y = Mathf.Sin(radian) * corners[1].x + Mathf.Cos(radian) * corners[1].y;
        Vector2 _corner1 = new Vector2(_corner1X, _corner1Y);
        //计算corner[2]'坐标的xy值
        float _corner2X = Mathf.Cos(radian) * corners[2].x - Mathf.Sin(radian) * corners[2].y;
        float _corner2Y = Mathf.Sin(radian) * corners[2].x + Mathf.Cos(radian) * corners[2].y;
        Vector2 _corner2 = new Vector2(_corner2X, _corner2Y);
        //计算corner[3]'坐标的xy值
        float _corner3X = Mathf.Cos(radian) * corners[3].x - Mathf.Sin(radian) * corners[3].y;
        float _corner3Y = Mathf.Sin(radian) * corners[3].x + Mathf.Cos(radian) * corners[3].y;
        Vector2 _corner3 = new Vector2(_corner3X, _corner3Y);

        Debug.Log("旋转后UI矩形四个顶点坐标:左下角:" + _corner0+" 左上角:"+ _corner0+" 右上角:"+_corner0+" 右下角:"+_corner0);

        Bounds modelBounds = new Bounds();
        modelBounds.center = imgRect.localPosition;
        modelBounds.size = new Vector2(Mathf.Abs(_corner0X- _corner2X), Mathf.Abs(_corner1Y- _corner3Y));
        Debug.Log("方法四:通过UI顶点坐标计算的UI矩形包围盒:" + modelBounds);

        Bounds viewBounds = RectTransformUtility.CalculateRelativeRectTransformBounds(imgRect.parent, imgRect);
        Debug.Log("方法一:通过Unity自带API计算的UI矩形包围盒:" + viewBounds);

    }
    /// 
    /// 计算无旋转UI包围盒
    /// 
    private void CalculateNotRotateUIBounds()
    {
        Bounds notRotate1 = new Bounds(imgRect.transform.localPosition,imgRect.rect.size);
        Debug.Log("方法二:通过计算的无旋转UI矩形包围盒:" + notRotate1);

        Bounds notRotate2 = new Bounds();
        notRotate2.center = imgRect.transform.localPosition;
        notRotate2.min = new Vector3(imgRect.transform.localPosition.x - imgRect.pivot.x * imgRect.rect.size.x, imgRect.transform.localPosition.y - imgRect.pivot.y * imgRect.rect.size.y);
        notRotate2.max= new Vector3(imgRect.transform.localPosition.x + imgRect.pivot.x * imgRect.rect.size.x, imgRect.transform.localPosition.y + imgRect.pivot.y * imgRect.rect.size.y);
        Debug.Log("方法三:通过计算的无旋转UI矩形包围盒:" + notRotate2);

        Bounds viewBounds = RectTransformUtility.CalculateRelativeRectTransformBounds(imgRect.parent, imgRect);
        Debug.Log("方法一:通过Unity自带API计算的无旋转UI矩形包围盒:" + viewBounds);

    }
}

3、测试结果

(1)UI无旋转情况下的包围盒

在这里插入图片描述

通过控制台输出,我们可以看到通过方法二和三得出的包围盒数据与方法一Unity自带API算出来的结果是一致。

(2)UI有旋转情况下的包围盒
  1. 顺时针旋转45度
    在这里插入图片描述
    可以看出,通过我自写算法实现的方法四计算出来的顺时针旋转45度后的UI包围盒数据与方法一Unity自带API算出来的结果一致。

  2. 逆时针旋转120度
    在这里插入图片描述
    同理,逆时针旋转120度后的UI包围盒数据与方法一Unity自带API算出来的结果一致。

(3)结论

1.在UI无旋转的情况下用方法一、二、三获取的包围盒是一样的。
2.在UI有旋转的情况下用方法一、四获取的包围盒是一样的。
3.以上demo测试结果证明了我的四种方法均可获取到正确的UI包围盒(分有无旋转情况),具体用那种方法看你项目需求,最简单的还是方法一,一句代码就完事了。但是注意只是能获取到包围盒,并不是设置;设置UI包围盒的话,是需要设置其中心坐标center和大小size,或min(最小点)和max(最大点)才行。

4、demo源工程

四种计算UI包围盒的方法demo包
基于Unity 2018.4.36

👉四、包围盒(Bouns)知识点

Bounds:表示一个轴对齐的包围盒(简称AABB),是与坐标轴对齐并且完全包围某个对象的盒体。

1、变量

属性说明
center包围盒的中心
extents包围盒的范围,始终是包围盒的size的一半
max包围盒的最大点,始终等于center+extents
min包围盒的最小点,始终等于center-extents
size包围盒的总大小,始终是extents的两倍

2、函数

方法说明
ClosestPoint该包围盒上最近的点
Containspoint是否包含在该包围盒中
Encapsulate合并增大包围盒,以便可以包裹 某个点point或多个对象
Expand通过沿每一侧将边界的size增加amount,扩展这些边界
IntersectRayray是否与该包围盒交叠
Intersects另一个包围盒是否与该包围盒交叠
SetMinMax将这些边界设置为该盒体的min和max值
SqrDistance该点与该包围盒之间的最小平方距离
ToString返回边界的格式化字符串