unity中Image组件的形状为矩形,要显示为圆形的图片可以使用遮罩、使用形状为矩形但是实际所有像素点的形状为圆形的图片等方法,这些方法各有优劣,这里使用脚本继承Image组件并重写其中的OnPopulateMesh方法的方式将组件形状改为圆形
一.简单实现
public class CircleImage : Image { //圆形由多少扇形(实际上是近似扇形的三角形)拼成 private int segements = 100; protected override void OnPopulateMesh(VertexHelper vertexHelper) { //清除继承的image的原有的图片绘制信息 vertexHelper.Clear(); //获取uv信息,四维向量uv的四个坐标分别代表图片矩形框左下角和右上角顶点的横纵坐标 Vector4 uv = overrideSprite == null ? Vector4.zero : DataUtility.GetOuterUV(overrideSprite); //获取图片宽高和uv的宽高 float imageWidth = rectTransform.rect.width; float imageHeigh = rectTransform.rect.height; float uvWidth = uv.z - uv.x; float uvHeigh = uv.w - uv.y; //获取uv的中心点位置 Vector2 uvCenter = new Vector2(uvWidth * 0.5f, uvHeigh * 0.5f); //求得图片像素和uv方框的换算比例 Vector2 converRatio = new Vector2(uvWidth / imageWidth, uvHeigh / imageHeigh); //求得每个扇形弧度 float radian = (2 * Mathf.PI) / segements; //规定半径 float radius = imageWidth * 0.5f; //生成初始顶点(原点),并给初始顶点赋值 UIVertex origin = new UIVertex(); origin.color = color; origin.position = Vector2.zero; //使用前面计算的图片像素和uv方框的换算比例计算顶点uv对应的图片点位置 origin.uv0 = new Vector2(origin.position.x * converRatio.x + uvCenter.x, origin.position.y * converRatio.y + uvCenter.y); //添加顶点 vertexHelper.AddVert(origin); //循环遍历生成并添加其他顶点信息 float curRadian = 0; for(int i = 0;i <= segements; i++) { //计算顶点坐标 float x = Mathf.Cos(curRadian) * radius; float y = Mathf.Sin(curRadian) * radius; curRadian += radian; //生成并添加顶点信息 UIVertex vertexTemp = new UIVertex(); vertexTemp.color = color; vertexTemp.position = new Vector2(x,y); vertexTemp.uv0 = new Vector2(vertexTemp.position.x * converRatio.x + uvCenter.x, vertexTemp.position.y * converRatio.y + uvCenter.y); vertexHelper.AddVert(vertexTemp); } //遍历生成所有的三角形面片 int id = 1; for (int i = 0; i < segements; i++) { //三个参数代表三角形的三个顶点,注意顶点顺序,面片的前后遵循左手定则(即叉乘结果的方向为前方) vertexHelper.AddTriangle(id, 0, id + 1); id++; } } }
二.一些优化和效果添加
1.添加技能CD效果
//圆形由多少扇形(实际上是近似扇形的三角形)拼成 [SerializeField] private int segements = 100; //显示部分占圆形的百分比 [SerializeField] private float showPercent = 1;
添加CD效果的显示百分比变量,并将扇形个数变量和百分比变量添加到序列化空间
[CustomEditor(typeof(CircleImage),true)] [CanEditMultipleObjects] public class CircleImageEditor : UnityEditor.UI.ImageEditor { SerializedProperty _fillPercent; SerializedProperty _segements; protected override void OnEnable() { base.OnEnable(); _fillPercent = serializedObject.FindProperty("showPercent"); _segements = serializedObject.FindProperty("segements"); } public override void OnInspectorGUI() { base.OnInspectorGUI(); serializedObject.Update(); EditorGUILayout.Slider(_fillPercent, 0, 1, new GUIContent("showPercent")); EditorGUILayout.PropertyField(_segements); serializedObject.ApplyModifiedProperties(); if (GUI.changed) { EditorUtility.SetDirty(target); } } }
添加Editor类,将刚才的两个变量添加到inspector面板中,方便修改参数和观看效果
//计算实际显示的扇形块数 int realSegements = (int)(showPercent * segements);
计算出实际的扇形块数
//使技能CD效果的圆心周围颜色改变,增强技能CD的效果 origin.color = Color32.Lerp(new Color32(0, 0, 0, 255), new Color32(255,255,255,255), showPercent);
原点处的颜色修改
if(i <= realSegements) vertexTemp.color = color; else vertexTemp.color = new Color32(60,60,60,255);
每一个像素点的颜色设置修改,如果是在CD效果的扇形区块内,则取用图片的像素点颜色,否则修改颜色
2.pivot的位置改变时,整个组件会跟随移动,需要修正这个问题
//圆心点坐标 Vector2 originPos = new Vector2((0.5f - rectTransform.pivot.x) * imageWidth, (0.5f - rectTransform.pivot.y) * imageHeigh);
计算圆心点坐标
//赋值圆心点位置 origin.position = originPos; //使用前面计算的图片像素和uv方框的换算比例计算顶点uv对应的图片点位置 origin.uv0 = new Vector2(uvCenter.x, uvCenter.y);
圆心点实际位置赋值为刚才计算好的圆心点坐标,圆心点的点位置对应图片的中心点
vertexTemp.position = new Vector2(x,y) + originPos;
vertexTemp.uv0 = new Vector2(x * converRatio.x + uvCenter.x, y * converRatio.y + uvCenter.y);
其他点的实际位置赋值和与图片上点的对应赋值
3.图片的可点击区域
unity中默认的可点击区域是矩形,需要修改时可以重写Image组件中的IsRaycastLocationValid方法,这是一个虚拟方法。
public override bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera) { Vector2 localPoint; RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, screenPoint, eventCamera, out localPoint); return IsValid(localPoint); }
首先将鼠标在屏幕上的位置转化为本地坐标,然后调用IsValid方法检测这个位置是否在图形区域内。
/// <summary> /// 判断点击是否有效 /// </summary> /// <param name="localPoint"></param>点击的点坐标 /// <returns></returns> private bool IsValid(Vector2 localPoint) { return GetCrossPointNum(localPoint,_vertexList) % 2 == 1; }
IsValid方法是自己定义的方法,用于检测点击是否有效。检测的方法是从当前点向右发射一条射线,检测这条射线和图形边界的交点个数,如果交点为偶数,则点在图形外,反之交点为奇数个,则点在图形内。通过GetCrossPointNum方法获得射线和图形边界的交点数。
/// <summary> /// 根据点坐标获取点所在水平直线和多边形的交点数 /// </summary> /// <param name="localPoint"></param>点坐标 /// <param name="vertexList"></param>多边形的顶点坐标集合 /// <returns></returns> private int GetCrossPointNum(Vector2 localPoint,List<Vector3> vertexList) { Vector3 vert1 = Vector3.zero; Vector3 vert2 = Vector3.zero; int count = vertexList.Count; int vertCount = 0; for(int i = 0;i < count;i++) { vert1 = vertexList[i]; vert2 = vertexList[(i + 1) % count]; if (IsYInRange(localPoint, vert1, vert2)) { //从当前点向右发射射线,找和图形边界的交点,因此需要当前点击点的横坐标小于求得的点坐标 if(localPoint.x < GetX(vert1, vert2, localPoint.y)) { vertCount++; } } } return vertCount; }
GetCrossPointNum方法通过遍历图形的边界上的顶点的方法检测交点数,实际上是判断这条向右的射线是否和图形的边相交。首先遍历图形的所有边(取出边的两个端点存储为vert1和vert2),每一条边都首先调用IsYInRange方法判断点的纵坐标是否在两个端点的纵坐标之间,是的话这个点所在水平直线一定和这条边(线段)相交,然后再判断交点在这个点的左边还是右边,通过调用GetX方法求得交点的横坐标,判断这个横坐标和当前点的横坐标的大小就可以知道交点在左边还是右边。
/// <summary> /// 判断已知点的纵坐标是否在给定两点的纵坐标之间 /// </summary> /// <param name="localPoint"></param>已知点 /// <param name="vert1"></param>给定点1 /// <param name="vert2"></param>给定点2 /// <returns></returns> private bool IsYInRange(Vector2 localPoint,Vector3 vert1,Vector3 vert2) { if(vert1.y > vert2.y) { return localPoint.y < vert1.y && localPoint.y > vert2.y; } else { return localPoint.y > vert1.y && localPoint.y < vert2.y; } }
IsYInRange方法判断点所在水平直线是否和线段相交。
/// <summary> /// 根据纵坐标计算纵坐标所在水平直线和已知两点连接直线的交点纵坐标 /// 运用直线的方程变形求得横坐标的计算公式 /// 不需要考虑纵坐标相等的情况,这个函数是在满足IsYInRange函数之后调用的,不会出现纵坐标相等的情况 /// </summary> /// <param name="vert1"></param>点1 /// <param name="vert2"></param>点2 /// <param name="y"></param>纵坐标 /// <returns></returns> private float GetX(Vector3 vert1,Vector3 vert2,float y) { return (y * vert2.x - y * vert1.x - vert2.x * vert1.y + vert1.x * vert2.y) / (vert2.y - vert1.y); }
GetX方法获得交点的横坐标,求解公式可以自己通过高中学习的直线方程变形得到。