大家好,我是阿赵
在制作游戏的时候,经常会遇到需要绘制多边形属性图的需求,比如这种效果:
可以根据需要的属性的数量变化多边形的边数,然后每一个顶点从中心点开始到多边形的顶点的长度代表了该属性的强度,然后多个属性的顶点连接起来形成一个多边形图形。
这里介绍一下我自己的做法。
一、 需求拆分:
需要做到上图的效果,可以拆分成几个部分
1、底板
这一个图片,当然也可以用程序计算生成出来,但我个人觉得,这个底图除了会出现边数不一样以外,其他的样式差不多是固定的,而程序生成的图片可能没有美术做的底图好看,所以我是比较建议直接出图使用。如果需要换边数,可以直接换图
2、属性多边形
这个形状是会根据属性的变化而变化的,所以是不能直接用美术出图的,必现要动态生成。既然是动态的绘制多边形,所以用到的知识点肯定是动态计算顶点和索引,构成Mesh网格来显示了。由于这个形状基本上都是用于UI的,所以不能直接用Mesh和MeshRenderer来渲染,我这里选择的是MaskableGraphic。
MaskableGraphic可以直接在UI上通过顶点和索引绘制多边形。所以我们只需要固定一个中心点坐标,然后通过设置一些参数,比如:
- 边数
- 最大半径
- 颜色
- 属性的最大最小值
- 每一个边的当前值
- 等等
然后根据参数计算几个顶点的坐标,再通过(中心点、当前顶点、下一个顶点)作为一个三角形的索引,就可以逐个三角形绘制出来了。最后指定颜色,半透明也是在颜色里面指定。
3、多边形描边
这个的原理和上一步一样,都是使用MaskableGraphic来自定义顶点和索引绘制。
可以设置的参数和上面绘制多边形基本一致,只需要加多:
1、 描边的厚度
2、 描边的颜色
绘制描边的算法比绘制多边形本身复杂一些,我一开始想得简单,直接把多边形的半径扩大,然后往回减去描边厚度,来得到描边的一个角上的两个顶点。不过那样做是不行的,将会导致描边的线段不是均匀的厚度,到了尖角的地方会变得很厚。
后来我还是老老实实的对多边形的每条边进行往内平移,并且求出每条平移后的线和下一条线的延伸线的交点,作为描边的第二层的顶点。然后如果有描边的情况下,多边形的顶点也是使用了描边的第二层的顶点,两者就完全接得上了。
由于这个原因,所以需要求线段平移和交点的方法。
4、在编辑器调整效果
最后,综合以上所述,在编辑器里面可以暴露这些参数,可以在做UI的时候就看到效果,并且用代码去设置。这里还有一个angle角度的变量,是用于旋转多边形的,因为有时候我们的需求多边形不一定是正上方有个顶点,可以是旋转一定角度。
有一个isChange的变量,下面的代码里面会说到,只有在修改的情况下才会重新计算顶点。所以如果在编辑器的非运行状态下,可以手点一下激活,这样在调整参数的时候,画面就会立刻刷新变化。
二、 代码实现
1、 MaskableGraphic的基础代码
使用MaskableGraphic之前介绍过,基础用法很简单,类继承MaskableGraphic,然后重写OnPopulateMesh方法,在里面输入顶点和索引列表。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;[RequireComponent(typeof(CanvasRenderer))][RequireComponent(typeof(RectTransform))]
public class UIPolyOutlineEx : MaskableGraphic
{private List<UIVertex> vertexList;private List<int> indexList;protected override void OnPopulateMesh(VertexHelper vh){vh.Clear(); if (vertexList != null && vertexList.Count > 0&&indexList!= null && indexList.Count > 0){vh.AddUIVertexStream(vertexList, indexList);}}
}
2、 设置变量
根据刚才的分析,设置了变量
public int sideCount = 0;
public float radius = 100;
public float minVal = 0.1f;
public float maxVal = 1;
public float edgeLen = 10;
public float[] sideValueArr;
public float angle = 0;
public Color insideColor = Color.green;
public Color edgeColor = Color.black;public bool isChange = false;
private List<UIVertex> outlinePoints;
private List<UIVertex> insidePoints;
private List<UIVertex> vertexList;
private List<int> indexList;
3、 计算顶点
需要根据参数,求出内部多边形和外部描边的顶点,中间还有一个平移线段的顶点信息,所以需要三个数组来计算。计算完之后,就把他们保存起来。这里我是直接保存成UIVertex的,由于描边和内部颜色不一样,所以UIVertex里面也记录了颜色,所以描边的顶点列表和内部的顶点列表是完全分开的,就算看着是同一个点,其实也是两边都保存。
private void CreatePoints(){List<Vector2> outsidePoints = new List<Vector2>();List<Vector2> outsidePoints2 = new List<Vector2>();List<Vector2> moveLinePoints = new List<Vector2>();outlinePoints = new List<UIVertex>();insidePoints = new List<UIVertex>();if (sideCount < 3){return;}float offsetRate = edgeLen / radius;Rect rect = gameObject.GetComponent<RectTransform>().rect;UIVertex vex0 = UIVertex.simpleVert;vex0.position = new Vector3(rect.center.x, rect.center.y, 0);Vector3 uv0 = new Vector2(0.5f, 0.5f);vex0.uv0 = uv0;vex0.color = insideColor;insidePoints.Add(vex0);for (int i = 0; i < sideCount; i++){float val = 0;if (sideValueArr != null && i < sideValueArr.Length){val = sideValueArr[i];}else{val = 0;}float ang = 360f / sideCount * i + angle;ang = ang * Mathf.Deg2Rad;val = Mathf.Clamp(val, minVal, maxVal);float x = val * Mathf.Sin(ang);float y = val * Mathf.Cos(ang);outsidePoints.Add(new Vector2(x, y));}if (edgeLen <= 0){for (int i = 0; i < outsidePoints.Count; i++){UIVertex vex = UIVertex.simpleVert;vex.position = new Vector3(rect.center.x + outsidePoints[i].x * radius, rect.center.y + outsidePoints[i].y * radius, 0);Vector2 uv = new Vector2(outsidePoints[i].x, outsidePoints[i].y);uv *= 0.5f;uv += new Vector2(0.5f, 0.5f);vex.uv0 = uv;vex.color = insideColor;insidePoints.Add(vex);}}else{for (int i = 0; i < outsidePoints.Count; i++){Vector2 p0 = Vector2.zero;Vector2 p1 = outsidePoints[i];Vector2 p2;if (i < outsidePoints.Count - 1){p2 = outsidePoints[i + 1];}else{p2 = outsidePoints[0];}Vector2 foot = GetPointToLine(p0, p1, p2);Vector2 dir = p0 - foot;dir.Normalize();Vector2 p3 = p1 + dir * offsetRate;Vector2 p4 = p2 + dir * offsetRate;moveLinePoints.Add(p3);moveLinePoints.Add(p4);}for (int i = 0; i < outsidePoints.Count; i++){int ind1 = i * 2;int ind2 = i * 2 + 1;int ind3 = i * 2 - 2;int ind4 = i * 2 - 1;if (i == 0){ind3 = moveLinePoints.Count - 2;ind4 = moveLinePoints.Count - 1;}Vector2 crossPoint = GetLineCrossPoint(moveLinePoints[ind1], moveLinePoints[ind2], moveLinePoints[ind3], moveLinePoints[ind4]);print(moveLinePoints[ind1] * radius + "," + moveLinePoints[ind2] * radius + "|" + moveLinePoints[ind3] * radius + "," + moveLinePoints[ind4] * radius + "||" + crossPoint * radius);outsidePoints2.Add(crossPoint);}for (int i = 0; i < outsidePoints.Count; i++){UIVertex vex = UIVertex.simpleVert;vex.position = new Vector3(rect.center.x + outsidePoints2[i].x * radius, rect.center.y + outsidePoints2[i].y * radius, 0);Vector2 uv = new Vector2(outsidePoints2[i].x, outsidePoints2[i].y);uv *= 0.5f;uv += new Vector2(0.5f, 0.5f);vex.uv0 = uv;vex.color = insideColor;insidePoints.Add(vex);vex = UIVertex.simpleVert;vex.position = new Vector3(rect.center.x + outsidePoints[i].x * radius, rect.center.y + outsidePoints[i].y * radius, 0);uv = new Vector2(outsidePoints[i].x, outsidePoints[i].y);uv *= 0.5f;uv += new Vector2(0.5f, 0.5f);vex.uv0 = uv;vex.color = edgeColor;outlinePoints.Add(vex);vex = UIVertex.simpleVert;vex.position = new Vector3(rect.center.x + outsidePoints2[i].x * radius, rect.center.y + outsidePoints2[i].y * radius, 0);uv = new Vector2(outsidePoints2[i].x, outsidePoints2[i].y);uv *= 0.5f;uv += new Vector2(0.5f, 0.5f);vex.uv0 = uv;vex.color = edgeColor;outlinePoints.Add(vex);}}}private Vector2 GetPointToLine(Vector2 p0, Vector2 p1, Vector2 p2){Vector2 lineDir = p1 - p2;lineDir.Normalize();float x0 = p0.x;float y0 = p0.y;float x1 = p1.x;// -lineDir.x*10;float y1 = p1.y;// - lineDir.y * 10;float x2 = p2.x;// + lineDir.x * 10;float y2 = p2.y;// + lineDir.x * 10;if ((x1 == x0 && y1 == y0) || (x2 == x0 && y2 == y0)){return new Vector2(x0, y0);//点和线段一边重合}float k = 1;if (x1 != x2){k = (y2 - y1) / (x2 - x1);}float a = k;float b = -1;float c = y1 - k * x1;float d = Mathf.Abs(a * x0 + b * y0 + c) / Mathf.Sqrt(a * a + b * b);float px = (b * b * x0 - a * b * y0 - a * c) / (a * a + b * b);float py = (a * a * y0 - a * b * x0 - b * c) / (a * a + b * b);return new Vector2(px, py);}private Vector2 GetLineCrossPoint(Vector2 p1, Vector2 p2, Vector2 p3, Vector2 p4){Vector2 dir1 = (p1 - p2).normalized;p1 += dir1;p2 -= dir1;Vector2 dir2 = (p3 - p4).normalized;p3 += dir2;p4 -= dir2;Vector2 bx = p4 - p3;float d1 = Mathf.Abs(CrossMulVec(bx, p1 - p3));float d2 = Mathf.Abs(CrossMulVec(bx, p2 - p3));float dx = d1 + d2;if (dx == 0){return p1;}float t = d1 / dx;Vector2 temp = (p2 - p1) * t;return p1 + temp;}private float CrossMulVec(Vector2 p1, Vector2 p2){return p1.x * p2.y - p2.x * p1.y;
}
4、 求构成多边形的点和索引
由于绘制的时候,需要把描边和内部的所有顶点和索引合并在一起,所以这里需要再计算一次,把两个列表合并。
private void CreateMeshParam(){vertexList = new List<UIVertex>();indexList = new List<int>();int insidePointCount = 0;if (insidePoints != null && insidePoints.Count > 0){insidePointCount = insidePoints.Count;for (int i = 0; i < insidePoints.Count; i++){vertexList.Add(insidePoints[i]);}List<int> tempList = new List<int>();if (sideCount > 2){int ind3 = 0;for (int i = 0; i < sideCount; i++){tempList.Add(0);tempList.Add(i + 1);ind3 = i + 2;if (ind3 > sideCount){ind3 = 1;}tempList.Add(ind3);}}indexList = tempList;}if (outlinePoints != null && outlinePoints.Count > 0){for (int i = 0; i < outlinePoints.Count; i++){vertexList.Add(outlinePoints[i]);}for (int i = 0; i < sideCount; i++){int ind1 = i * 2;int ind2 = i * 2 + 2;int ind3 = i * 2 + 1;int ind4 = i * 2 + 3;if (i == sideCount - 1){ind2 = 0;ind4 = 1;}indexList.Add(ind1 + insidePointCount);indexList.Add(ind2 + insidePointCount);indexList.Add(ind3 + insidePointCount);indexList.Add(ind3 + insidePointCount);indexList.Add(ind2 + insidePointCount);indexList.Add(ind4 + insidePointCount);}}}
5、 各种属性的GetSet
在运行的时候,我们不会希望每一帧都需要重复去计算多边形的顶点和索引,希望只有参数变化的时候才重新计算。所以那些可以改变的参数,应该都做成GetSet方法,然后在Set方法的时候,把一个isChange的标记设置为true。那么当isChange为true的时候,才重新计算。
public void InitData(int sideCount,float r,float edge)
{SetSideCount(sideCount);radius = r;edgeLen = edge;isChange = true;
}
public void SetIsChange()
{isChange = true;
}
public void SetSideCount(int val)
{sideCount = val;float[] newArr = new float[val];if(sideValueArr!=null&&sideValueArr.Length>0){for(int i = 0;i<val;i++){if(sideValueArr.Length>val){newArr[i] = sideValueArr[i];}}}sideValueArr = newArr;isChange = true;
}public void SetValArr(float[] vals)
{if (sideValueArr == null){return;}for (int i = 0; i < sideValueArr.Length; i++){if (i < vals.Length){sideValueArr[i] = vals[i];}}isChange = true;
}public void SetOneVal(int index,float val)
{if(index<0){return;}if(sideValueArr != null&&sideValueArr.Length> index){sideValueArr[index] = val;}isChange = true;
}public void SetOneValByLua(double index, double val)
{int ind = Mathf.FloorToInt((float)index);float value = (float)val;SetOneVal(ind - 1, value);
}public void SetAngle(float ang)
{angle = ang;isChange = true;
}public void SetRange(float min,float max)
{minVal = min;maxVal = max;isChange = true;
}public void SetInsideColor(Color col)
{insideColor = col;isChange = true;
}public void SetOutlineColor(Color col)
{edgeColor = col;isChange = true;
}public void SetRadius(float val)
{radius = val;isChange = true;
}
这是一个重新计算顶点和索引的方法,只有在isChange为true的时候,才会调用。
private void UpdatePoly()
{if (sideCount == 0){vertexList = null;return;}CreatePoints();CreateMeshParam();
}
6、 修改OnPopulateMesh方法
在OnPopulateMesh里面判断isChange,然后调用UpdatePoly方法。
protected override void OnPopulateMesh(VertexHelper vh){//base.OnPopulateMesh(vh);vh.Clear();if(isChange){UpdatePoly();if(Application.isPlaying)isChange = false;}if (vertexList != null && vertexList.Count > 0){vh.AddUIVertexStream(vertexList, indexList);}}
三、完整代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace azhao
{[RequireComponent(typeof(CanvasRenderer))][RequireComponent(typeof(RectTransform))]public class UIPolyOutlineEx : MaskableGraphic{public int sideCount = 0;public float radius = 100;public float minVal = 0.1f;public float maxVal = 1;public float edgeLen = 10;public float[] sideValueArr;public float angle = 0;public Color insideColor = Color.green;public Color edgeColor = Color.black;public bool isChange = false;private List<UIVertex> outlinePoints;private List<UIVertex> insidePoints;private List<UIVertex> vertexList;private List<int> indexList;// Start is called before the first frame updateprivate new IEnumerator Start(){UpdatePoly();yield return null;}public void InitData(int sideCount,float r,float edge){SetSideCount(sideCount);radius = r;edgeLen = edge;isChange = true;}public void SetIsChange(){isChange = true;}public void SetSideCount(int val){sideCount = val;float[] newArr = new float[val];if(sideValueArr!=null&&sideValueArr.Length>0){for(int i = 0;i<val;i++){if(sideValueArr.Length>val){newArr[i] = sideValueArr[i];}}}sideValueArr = newArr;isChange = true;}public void SetValArr(float[] vals){if (sideValueArr == null){return;}for (int i = 0; i < sideValueArr.Length; i++){if (i < vals.Length){sideValueArr[i] = vals[i];}}isChange = true;}public void SetOneVal(int index,float val){if(index<0){return;}if(sideValueArr != null&&sideValueArr.Length> index){sideValueArr[index] = val;}isChange = true;}public void SetOneValByLua(double index, double val){int ind = Mathf.FloorToInt((float)index);float value = (float)val;SetOneVal(ind - 1, value);}public void SetAngle(float ang){angle = ang;isChange = true;}public void SetRange(float min,float max){minVal = min;maxVal = max;isChange = true;}public void SetInsideColor(Color col){insideColor = col;isChange = true;}public void SetOutlineColor(Color col){edgeColor = col;isChange = true;}public void SetRadius(float val){radius = val;isChange = true;}private void UpdatePoly(){if (sideCount == 0){vertexList = null;return;}CreatePoints();CreateMeshParam();}private void CreatePoints(){List<Vector2> outsidePoints = new List<Vector2>();List<Vector2> outsidePoints2 = new List<Vector2>();List<Vector2> moveLinePoints = new List<Vector2>();outlinePoints = new List<UIVertex>();insidePoints = new List<UIVertex>();if (sideCount < 3){return;}float offsetRate = edgeLen / radius;Rect rect = gameObject.GetComponent<RectTransform>().rect;UIVertex vex0 = UIVertex.simpleVert;vex0.position = new Vector3(rect.center.x, rect.center.y, 0);Vector3 uv0 = new Vector2(0.5f, 0.5f);vex0.uv0 = uv0;vex0.color = insideColor;insidePoints.Add(vex0);for (int i = 0; i < sideCount; i++){float val = 0;if (sideValueArr != null && i < sideValueArr.Length){val = sideValueArr[i];}else{val = 0;}float ang = 360f / sideCount * i + angle;ang = ang * Mathf.Deg2Rad;val = Mathf.Clamp(val, minVal, maxVal);float x = val * Mathf.Sin(ang);float y = val * Mathf.Cos(ang);outsidePoints.Add(new Vector2(x, y));}if (edgeLen <= 0){for (int i = 0; i < outsidePoints.Count; i++){UIVertex vex = UIVertex.simpleVert;vex.position = new Vector3(rect.center.x + outsidePoints[i].x * radius, rect.center.y + outsidePoints[i].y * radius, 0);Vector2 uv = new Vector2(outsidePoints[i].x, outsidePoints[i].y);uv *= 0.5f;uv += new Vector2(0.5f, 0.5f);vex.uv0 = uv;vex.color = insideColor;insidePoints.Add(vex);}}else{for (int i = 0; i < outsidePoints.Count; i++){Vector2 p0 = Vector2.zero;Vector2 p1 = outsidePoints[i];Vector2 p2;if (i < outsidePoints.Count - 1){p2 = outsidePoints[i + 1];}else{p2 = outsidePoints[0];}Vector2 foot = GetPointToLine(p0, p1, p2);Vector2 dir = p0 - foot;dir.Normalize();Vector2 p3 = p1 + dir * offsetRate;Vector2 p4 = p2 + dir * offsetRate;moveLinePoints.Add(p3);moveLinePoints.Add(p4);}for (int i = 0; i < outsidePoints.Count; i++){int ind1 = i * 2;int ind2 = i * 2 + 1;int ind3 = i * 2 - 2;int ind4 = i * 2 - 1;if (i == 0){ind3 = moveLinePoints.Count - 2;ind4 = moveLinePoints.Count - 1;}Vector2 crossPoint = GetLineCrossPoint(moveLinePoints[ind1], moveLinePoints[ind2], moveLinePoints[ind3], moveLinePoints[ind4]);print(moveLinePoints[ind1] * radius + "," + moveLinePoints[ind2] * radius + "|" + moveLinePoints[ind3] * radius + "," + moveLinePoints[ind4] * radius + "||" + crossPoint * radius);outsidePoints2.Add(crossPoint);}for (int i = 0; i < outsidePoints.Count; i++){UIVertex vex = UIVertex.simpleVert;vex.position = new Vector3(rect.center.x + outsidePoints2[i].x * radius, rect.center.y + outsidePoints2[i].y * radius, 0);Vector2 uv = new Vector2(outsidePoints2[i].x, outsidePoints2[i].y);uv *= 0.5f;uv += new Vector2(0.5f, 0.5f);vex.uv0 = uv;vex.color = insideColor;insidePoints.Add(vex);vex = UIVertex.simpleVert;vex.position = new Vector3(rect.center.x + outsidePoints[i].x * radius, rect.center.y + outsidePoints[i].y * radius, 0);uv = new Vector2(outsidePoints[i].x, outsidePoints[i].y);uv *= 0.5f;uv += new Vector2(0.5f, 0.5f);vex.uv0 = uv;vex.color = edgeColor;outlinePoints.Add(vex);vex = UIVertex.simpleVert;vex.position = new Vector3(rect.center.x + outsidePoints2[i].x * radius, rect.center.y + outsidePoints2[i].y * radius, 0);uv = new Vector2(outsidePoints2[i].x, outsidePoints2[i].y);uv *= 0.5f;uv += new Vector2(0.5f, 0.5f);vex.uv0 = uv;vex.color = edgeColor;outlinePoints.Add(vex);}}}private Vector2 GetPointToLine(Vector2 p0, Vector2 p1, Vector2 p2){Vector2 lineDir = p1 - p2;lineDir.Normalize();float x0 = p0.x;float y0 = p0.y;float x1 = p1.x;// -lineDir.x*10;float y1 = p1.y;// - lineDir.y * 10;float x2 = p2.x;// + lineDir.x * 10;float y2 = p2.y;// + lineDir.x * 10;if ((x1 == x0 && y1 == y0) || (x2 == x0 && y2 == y0)){return new Vector2(x0, y0);//点和线段一边重合}float k = 1;if (x1 != x2){k = (y2 - y1) / (x2 - x1);}float a = k;float b = -1;float c = y1 - k * x1;float d = Mathf.Abs(a * x0 + b * y0 + c) / Mathf.Sqrt(a * a + b * b);float px = (b * b * x0 - a * b * y0 - a * c) / (a * a + b * b);float py = (a * a * y0 - a * b * x0 - b * c) / (a * a + b * b);return new Vector2(px, py);}private Vector2 GetLineCrossPoint(Vector2 p1, Vector2 p2, Vector2 p3, Vector2 p4){Vector2 dir1 = (p1 - p2).normalized;p1 += dir1;p2 -= dir1;Vector2 dir2 = (p3 - p4).normalized;p3 += dir2;p4 -= dir2;Vector2 bx = p4 - p3;float d1 = Mathf.Abs(CrossMulVec(bx, p1 - p3));float d2 = Mathf.Abs(CrossMulVec(bx, p2 - p3));float dx = d1 + d2;if (dx == 0){return p1;}float t = d1 / dx;Vector2 temp = (p2 - p1) * t;return p1 + temp;}private float CrossMulVec(Vector2 p1, Vector2 p2){return p1.x * p2.y - p2.x * p1.y;}private void CreateMeshParam(){vertexList = new List<UIVertex>();indexList = new List<int>();int insidePointCount = 0;if (insidePoints != null && insidePoints.Count > 0){insidePointCount = insidePoints.Count;for (int i = 0; i < insidePoints.Count; i++){vertexList.Add(insidePoints[i]);}List<int> tempList = new List<int>();if (sideCount > 2){int ind3 = 0;for (int i = 0; i < sideCount; i++){tempList.Add(0);tempList.Add(i + 1);ind3 = i + 2;if (ind3 > sideCount){ind3 = 1;}tempList.Add(ind3);}}indexList = tempList;}if (outlinePoints != null && outlinePoints.Count > 0){for (int i = 0; i < outlinePoints.Count; i++){vertexList.Add(outlinePoints[i]);}for (int i = 0; i < sideCount; i++){int ind1 = i * 2;int ind2 = i * 2 + 2;int ind3 = i * 2 + 1;int ind4 = i * 2 + 3;if (i == sideCount - 1){ind2 = 0;ind4 = 1;}indexList.Add(ind1 + insidePointCount);indexList.Add(ind2 + insidePointCount);indexList.Add(ind3 + insidePointCount);indexList.Add(ind3 + insidePointCount);indexList.Add(ind2 + insidePointCount);indexList.Add(ind4 + insidePointCount);}}}// Update is called once per framevoid Update(){}protected override void OnPopulateMesh(VertexHelper vh){//base.OnPopulateMesh(vh);vh.Clear();if(isChange){UpdatePoly();if(Application.isPlaying)isChange = false;}if (vertexList != null && vertexList.Count > 0){vh.AddUIVertexStream(vertexList, indexList);}}}
}