
做Unity界面开发的人迟早都会碰到雷达图需求——角色属性面板、装备对比、数据分析大屏、数字孪生项目里的指标总览几乎每个项目到某个阶段都会问一句“这个雷达图怎么做”。我这次做的是一套可以直接拖进Canvas使用的Unity雷达图组件不是简单贴一张静态图而是用UGUI的底层网格自绘方案支持动态数据、入场动画、多套指标切换用下来比找第三方插件顺手得多。这篇文章就把具体实现思路、完整代码和踩坑记录都放出来适合有一定UGUI基础、想自己控制UI效果的开发同学参考。1. 雷达图用到哪些场景先想清楚需求再动手1.1 三个高频场景角色属性、数据大屏、数字孪生我接触到的雷达图需求基本可以归成三类。第一类是游戏里的角色属性面板一般5到8个属性力量、敏捷、智力、耐力这种数值变化不频繁但要求好看最好带点描边、光晕、渐变效果属于典型的“颜值驱动”需求。第二类是数据大屏上的指标展示指标数量不固定可能是6个也可能是12个数据来源可能是接口轮询或者WebSocket推送刷新频率相对较高这时候对性能就有点要求了。第三类是数字孪生项目里的设备状态总览雷达图往往要叠加在3D场景的UI面板上可能需要半透明、世界空间放置、屏幕空间映射这些额外能力比前两类复杂不少。不同场景对应的技术选型差别很大。如果你只是给一个固定角色面板做效果静态贴图加一个蒙版遮罩就能搞定根本不用上Mesh绘制。但如果做数据大屏指标经常变数据实时刷静态图方案就会很痛苦换一张图或者调整顶点关系都是一堆脏活。我的建议是动手之前先把需求边界问清楚指标数量是否固定、数据刷新频率多高、是否需要动画、是否需要在3D场景里使用、是否需要和用户点击交互。这些问题直接决定你要不要走自绘路线。1.2 三种实现方案选型贴图、第三方插件、自绘Mesh先说贴图方案。美术出一张雷达图的底图包括网格线、轴线、背景渐变这些静态元素然后在前景遮盖一张与雷达范围匹配的三角区域配合mask或者shader根据数值来控制显示范围。这个方案优点是工作量小适合那种完全静态、只展示一次的需求。缺点也很明显数值一旦要变化要么换图要么做复杂的UV映射而且图是位图在4K屏或不同分辨率下拉伸会糊风格定制也要反复找美术切图沟通成本高。第三方插件方案我试过一些GitHub上也有很多开源的Unity雷达图项目功能确实全动画、标签、多系列数据都有。但实际项目里你会发现插件为了兼容各种情况参数特别多真要改到自己项目的视觉风格反而费劲一旦遇到Bug还得去读插件的源码时间成本全砸在逆向理解和适配上了。如果只是Demo演示用插件没问题但正式产品或需要深度定制的项目我不推荐。自绘Mesh方案是我最终选定的路线。核心思路是继承MaskableGraphic重写OnPopulateMesh用VertexHelper直接生成顶点和三角形。这样整个雷达图的网格线、轴线、数据填充区域、数据点全都在一个Mesh里完成顶点数量自己可控颜色、透明度、线宽、动画进度全部可以参数化。代码量其实不大但是对UI的掌控力是前面两种方案完全比不了的。后面我会把完整代码和每一步的原理都拆开讲清楚。2. 雷达图自绘原理极坐标、顶点、三角剖分2.1 极坐标到UI坐标的映射关系雷达图本质上是极坐标下的一组点有一个中心点从中心向外辐射出N条方向每个方向代表一个指标方向上的长度代表指标数值。要自绘第一步就是把极坐标映射到UGUI的本地坐标系里。假设指标数量为N第i个指标的方向角度可以这样算angle i * 360 / N - 90。减去的这90度是让第一个指标从正上方开始画这样视觉上比较符合大多数人的阅读习惯。然后把这个角度转成弧度方向向量就是(Mathf.Cos(rad), Mathf.Sin(rad))。UGUI的坐标系是y轴向上的所以这个方向向量直接就是UI本地坐标下的方向不需要额外翻转。半径部分我是用RectTransform.rect的宽高中较小的一个值乘0.5再乘一个radiusRatio我通常设0.85得到最大半径。为什么取较小的宽高而不是直接用宽度因为雷达图是圆形如果画在一个宽度比高度大很多的RectTransform里直接按宽度算半径图形会超出上下边界看起来像被切掉一块。取较小值并居中可以保证无论面板是什么比例雷达图都是一个漂亮的正圆。中心点直接用rect.center这样就算pivot不是精确的(0.5, 0.5)也不会画偏。2.2 顶点生成与三角剖分从多边形到可渲染的MeshUI渲染本质上是一个三角形集合所以雷达图绘制的核心工作就是把多边形转化成顶点加三角形索引。填充区域我用的方案是三角扇先往VertexHelper里添加一个中心顶点然后按顺序添加多边形边界上的顶点每加一个边界顶点就和中心顶点以及前一个边界顶点组成一个三角形。这样N条边的雷达图只需要N个三角形顶点数也很少性能开销非常低。网格线稍微有点讲究。如果你直接画一圈一圈的多边形轮廓需要把每一圈Polygon的顶点按顺序连成线段。UGUI的UI Shader不会渲染单个像素宽的线所以线宽必须用两个三角形拼成四边形来实现。我的做法是先算出线段的朝向取法线方向沿着法线方向把线段两端各扩充半个线宽得到四个顶点然后连成两个三角形。这个方法通用性很强不仅画网格线能用画数据区域的描边也能用。画轴线就更简单从中心到每个方向的最大半径处画一条线段用同样的四边形线画法就行。这里有一个细节要注意网格线和轴线最好分开配色透明度也要拉开否则整个图看起来会糊成一团。我一般把轴线透明度压到网格线的一半以下让视觉重心落在网格和数值区域上。2.3 数据归一化与边界值处理雷达图画出来好不好看、数据对不对很大程度取决于数值处理。我的做法是给组件暴露minValue和maxValue两个字段然后用Mathf.InverseLerp(minValue, maxValue, values[i])把任意范围的数据映射到0到1之间得到归一化比例再乘最大半径。这样组件就不关心你传入的是百分比、小数还是绝对值它只负责把比例正确画出来。这里必须处理两个边界情况。第一是数值超出范围比如maxValue设了100但传入一个120InverseLerp返回1.2直接画的话数据区域会冲破网格线非常丑。我通常会把归一化结果Mathf.Clamp01一下先保证图形不会越界。如果你确实需要提示数值超标可以单独再做一层颜色变化或者越界标记而不是硬画出去。第二是数值数组长度和指标数量不一致这个问题在编辑器里经常出现我会在OnPopulateMesh入口做一个保护判断长度对不上就直接不绘制同时输出一条Log提醒排查效率会高很多。3. 从零手写RadarChart组件代码可直接用3.1 组件整体设计与公共属性规划先看一下整个组件的结构。继承MaskableGraphic而不是Graphic是为了能享受Mask组件的遮罩功能万一以后要把雷达图放进滚动列表或者做一个局部高亮的遮罩不会被坑。公共属性我按“基本参数、颜色、动画”三组来组织Inspector面板看起来一目了然。基本参数包括radiusRatio半径占比、gridLayer网格层数、indicatorCount指标数量、minValue和maxValue数据范围、values数值数组。颜色组包括gridColor、axisColor、fillColor、borderColor还有一个showDataPoints开关和pointSize用来控制数据点方块的大小。动画方面我留了一个progress字段取值范围0到1后面做入场动画时可以直接驱动这个字段。整个组件最核心的入口就是重写OnPopulateMesh它会在这个Graphic需要重建顶点数据时被UGUI自动调用。你不需要手动管理网格的生成时机只需要在这个方法里把当前状态“画”进VertexHelper里就行。有一点要强调不要在Update里频繁调用SetVerticesDirty性能会有明显损耗后面第四节我会具体讲重绘时机怎么控制。3.2 OnPopulateMesh核心绘制代码逐段拆解直接上核心代码这套代码我实测过可以直接挂到Canvas下的空物体上使用。为了篇幅我把方法内注释写详细一些方便你按段理解。using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; [RequireComponent(typeof(RectTransform))] public class RadarChart : MaskableGraphic { [Header(基本参数)] public float radiusRatio 0.85f; public int gridLayer 4; [Range(3, 12)] public int indicatorCount 6; public float minValue 0f; public float maxValue 100f; public float[] values; [Header(颜色)] public Color gridColor new Color(0.3f, 0.6f, 1f, 0.6f); public Color axisColor new Color(0.3f, 0.6f, 1f, 0.4f); public Color fillColor new Color(0.2f, 0.5f, 1f, 0.35f); public Color borderColor new Color(0.3f, 0.8f, 1f, 1f); public bool showDataPoints true; public float pointSize 6f; [HideInInspector] public float progress 1f; protected override void OnPopulateMesh(VertexHelper vh) { vh.Clear(); if (indicatorCount 3 || values null || values.Length indicatorCount) return; Rect rect rectTransform.rect; Vector2 center rect.center; float maxRadius Mathf.Min(rect.width, rect.height) * 0.5f * radiusRatio; // 画网格层 for (int layer 1; layer gridLayer; layer) { float r maxRadius * layer / gridLayer; DrawPolygonOutline(vh, center, r, indicatorCount, gridColor); } // 画轴线 for (int i 0; i indicatorCount; i) { Vector2 dir GetDirection(i, indicatorCount); DrawLine(vh, center, center dir * maxRadius, axisColor); } // 计算数据点位置 ListVector2 pts new ListVector2(indicatorCount); for (int i 0; i indicatorCount; i) { float t Mathf.InverseLerp(minValue, maxValue, values[i]); t Mathf.Clamp01(t) * progress; pts.Add(center GetDirection(i, indicatorCount) * maxRadius * t); } // 填充数据区域 DrawFilledPolygon(vh, pts, fillColor); // 画数据区域描边 DrawPolygonOutline(vh, pts, borderColor); // 画数据点 if (showDataPoints) { for (int i 0; i pts.Count; i) DrawPoint(vh, pts[i], pointSize, borderColor); } } }这段主流程的逻辑很直观清空顶点缓冲画网格层画轴线算数据点填充数据区域然后描边和数据点。网格层从内到外一层层画每一层的半径比例是layer / gridLayer这样最外层正好是最大半径内部网格均匀分布。轴线从中心直接画到最外层把每个指标的方向线标出来。3.3 网格线、轴线、数据点与标签文字的实现细节接下来是工具方法。GetDirection是前面讲过的极坐标映射DrawPolygonOutline和DrawLine处理轮廓线DrawFilledPolygon用中心三角扇填充区域DrawPoint画一个小方点。这几个方法都比较通用以后做环形进度条、多边形图表也能复用。private Vector2 GetDirection(int index, int count) { float angle index * 360f / count - 90f; float rad angle * Mathf.Deg2Rad; return new Vector2(Mathf.Cos(rad), Mathf.Sin(rad)); } private void DrawPolygonOutline(VertexHelper vh, Vector2 center, float radius, int count, Color color) { ListVector2 pts new ListVector2(count); for (int i 0; i count; i) pts.Add(center GetDirection(i, count) * radius); DrawPolygonOutline(vh, pts, color); } private void DrawPolygonOutline(VertexHelper vh, ListVector2 pts, Color color) { int count pts.Count; for (int i 0; i count; i) { Vector2 p0 pts[i]; Vector2 p1 pts[(i 1) % count]; DrawLine(vh, p0, p1, color); } } private void DrawLine(VertexHelper vh, Vector2 from, Vector2 to, Color color, float thickness 2f) { Vector2 dir (to - from).normalized; Vector2 normal new Vector2(-dir.y, dir.x) * thickness * 0.5f; int start vh.currentVertCount; vh.AddVert(from normal, color, Vector2.zero); vh.AddVert(from - normal, color, Vector2.zero); vh.AddVert(to - normal, color, Vector2.zero); vh.AddVert(to normal, color, Vector2.zero); vh.AddTriangle(start, start 1, start 2); vh.AddTriangle(start, start 2, start 3); } private void DrawFilledPolygon(VertexHelper vh, ListVector2 pts, Color color) { int count pts.Count; if (count 3) return; Vector2 centroid Vector2.zero; for (int i 0; i count; i) centroid pts[i]; centroid / count; vh.AddVert(centroid, color, Vector2.zero); int centerIndex vh.currentVertCount - 1; for (int i 0; i count; i) { vh.AddVert(pts[i], color, Vector2.zero); int cur vh.currentVertCount - 1; int next i count - 1 ? centerIndex 1 : cur 1; vh.AddTriangle(centerIndex, cur, next); } } private void DrawPoint(VertexHelper vh, Vector2 center, float size, Color color) { Vector2 half new Vector2(size * 0.5f, size * 0.5f); int start vh.currentVertCount; vh.AddVert(center - half, color, Vector2.zero); vh.AddVert(center new Vector2(-half.x, half.y), color, Vector2.zero); vh.AddVert(center half, color, Vector2.zero); vh.AddVert(center new Vector2(half.x, -half.y), color, Vector2.zero); vh.AddTriangle(start, start 1, start 2); vh.AddTriangle(start, start 2, start 3); }DrawFilledPolygon里我特意算了一个质心点作为中心顶点而不是直接用rect.center。原因是当数据区域不是一个规则正多边形时质心更靠近点的实际分布中心填充出来视觉上更自然。如果直接用UI面板的中心遇到偏置特别大的数据填充区域会看起来“偏”向一边观感很怪。DrawLine里用了法线方向做线宽这样不管线段是横的还是斜的粗细都是均匀的不会出现横线粗、斜线细的问题。标签文字这块我建议不要往Mesh里硬画直接用独立的Text组件摆位置。因为文字有字体、字号、自动换行这些需求硬画进Mesh会非常麻烦还要自己处理字体图集。我的做法是在外部准备好一个Text数组写一个RefreshLabels方法遍历计算每个指标方向上的外扩位置把Text摆过去。需要注意的是这个外扩距离要比最大半径再大出一段给文字留呼吸空间。如果是普通Canvas Overlay模式直接设置rectTransform.anchoredPosition就行如果是Screen Space Camera模式可能需要转换一下世界坐标。这个细节后面在踩坑部分会展开。public void RefreshLabels(Text[] labels, float textGap 30f) { if (labels null) return; Rect rect rectTransform.rect; float maxRadius Mathf.Min(rect.width, rect.height) * 0.5f * radiusRatio; for (int i 0; i labels.Length i indicatorCount; i) { Vector2 dir GetDirection(i, indicatorCount); Vector2 localPos rect.center dir * (maxRadius textGap); labels[i].rectTransform.anchoredPosition localPos; } }3.4 使用方式与参数调整建议在Canvas下创建一个空物体RectTransform大小设成你想要的雷达图尺寸比如400乘400把RadarChart脚本挂上去。然后在Inspector里设置指标数量填上values数组雷达图马上就出来了。如果你用的是Unity 2021以上版本新增的UI Toolkit和UGUI并不冲突这个组件依然可以直接用只是你要确保所在Canvas的渲染模式符合预期。调整参数时我建议按这个顺序来。先定最大半径也就是radiusRatio一般0.8到0.9比较舒服留出的边距正好放标签文字。然后调网格层数4到5层最耐看太多了会很密少了又缺少刻度感。最后才是调颜色网格和轴线走冷色系半透明数据区域用明亮的填充色透明度控制在0.3左右最保险。如果做的是大屏暗色背景主题色选青色系会很出效果如果是浅色面板就要降低所有颜色的透明度避免颜色堆叠发脏。4. 让雷达图动起来数据刷新与动画播放4.1 数据变更后的重绘时机管理雷达图的动画和刷新逻辑本质上就是控制progress字段和values数组然后触发顶点重建。这里最核心的API是SetVerticesDirty()它告诉UGUI这个Graphic的顶点数据过期了你需要重新调用OnPopulateMesh。注意不要用SetAllDirty()那会把材质和布局也全部标记为脏开销大很多纯属浪费。我在组件里暴露了SetValues和Refresh方法。业务侧拿到新数据后调用SetValues把新数组赋进来同时自动同步indicatorCount再调一次SetVerticesDirty雷达图就更新了。这里有个老坑如果你直接改values数组里的元素比如chart.values[0] 80fUGUI是感知不到的画面上不会有任何变化。必须显式调用刷新方法UGUI才会重建Mesh。我见过不少同事在这个问题上卡了很久去排查材质、射线检测最后发现只是没触发重绘。4.2 用DOTween做数值从小到大的入场动画入场动画我推荐用DOTween来做简单、干净、可控。思路是给组件加一个PlayAnim方法用DOTween.To从0补间到1赋值给progress字段然后在OnPopulateMesh里把progress乘到每个点的半径上。这样开始时所有数据点都收缩在中心动画结束后才展开到真实数值位置视觉上非常顺。public void PlayAnim(float duration 0.8f) { DOTween.To(() progress, x { progress x; SetVerticesDirty(); }, 1f, duration) .SetEase(Ease.OutCubic); }动画期间的性能要注意progress每次变化都会调SetVerticesDirty如果一帧里变化很多次会频繁重建Mesh。但DOTween默认在Update中驱动每帧也就一次到两次完全可以接受。如果你有大量雷达图同时播放入场动画优化方式是把动画驱动统一放到一个管理器里手动控制刷新频率而不是让每个图都各自触发重建。这个在后面性能优化小节再具体讲。4.3 多套数据切换与业务场景对接实际项目里雷达图经常要做多套角色的数据对比或者同一个角色切换不同形态时数值完全变化。我的做法是把多套数据存成一个float[][]或者一个DataEntry结构暴露一个切换方法内部先重置动画进度再把目标数据交给SetValues。public void SwitchData(float[] newValues, bool playAnimation true, float duration 0.8f) { SetValues(newValues); if (playAnimation) { progress 0f; PlayAnim(duration); } else { progress 1f; SetVerticesDirty(); } }业务侧对接时需要注意数据和指标顺序的对应关系。比如服务端返回的指标顺序是乱序的你要在解析层先做一次映射保证传入组件的数组顺序和UI上的显示顺序一致。这个问题很隐蔽一旦顺序错乱整个雷达图形状会变得莫名其妙排查起来又费时间又费眼神。所以我建议在解析完数据后先打印一次数组人工核对一下顺序确认没问题再接组件。5. 踩坑记录与性能优化帮你在真实项目里少走弯路5.1 常见问题速查表自绘UI组件最容易出的问题不是绘制逻辑而是对UGUI生命周期不熟悉导致的“看起来没反应”。我把这些年在项目里踩过的坑整理成一个速查表每一条都是真实遇到过的。问题现象可能原因排查与解决思路挂上脚本后画面空白values数组为空或数组长度小于indicatorCount检查Inspector中数组是否已赋值长度是否匹配修改数值后画面不变直接改了数组元素没有触发重绘调用Refresh()或SetValues()雷达图被拉伸变形RectTransform宽高比和正圆不匹配使用Mathf.Min(rect.width, rect.height)计算半径网格线粗得离谱RectTransform尺寸很小而线宽是固定像素把线宽改为按半径百分比计算图形边缘有锯齿UI没有启用抗锯齿在Canvas上开启MSAA或者把顶点位置做像素对齐动画播放时卡顿多张雷达图同时频繁SetVerticesDirty统一管理器驱动降低刷新频率标签位置偏得离谱Canvas渲染模式和坐标空间换算没做对Overlay用anchoredPositionCamera模式需要转换坐标图形半透明时颜色发灰开启了UI的Maskable但Mask没有正确设置检查Mask组件的Show Mask Graphic选项这里面“坐标空间换算”值得多说两句。如果你的Canvas是Screen Space OverlayUI元素的anchoredPosition就是屏幕空间下的本地坐标直接设置Text的位置没问题。但如果是Screen Space Camera模式UI元素实际上放在世界空间的一个平面上TransformPoint才是正确姿势。我一开始图省事在所有Canvas模式下都用同一套位置计算结果在相机模式里标签全部飞到了画面角落改了半小时才反应过来是这个原因。5.2 性能优化顶点数量控制、合批与内存自绘Mesh最大的好处就是顶点可控。以6个指标、4层网格为例整个雷达图的顶点数量大概是网格轮廓线每层6条线段乘每条4个顶点再乘4层算上轴线6条线段、数据填充区域6个三角形加数据点6个小方块全部加起来也就一百个顶点出头。这个量级对于UGUI来说非常轻松做30个雷达图同时显示都能扛得住。但要注意几个隐形开销。第一个是ListVector2的临时分配OnPopulateMesh每帧刷新都会产生GC如果雷达图数量多内存碎片会持续积累。我的优化手段是组件内缓存一个ListVector2复用避免每次new。第二个是Canvas合批雷达图如果和很多其他UI元素混在一起频繁重建顶点会打断批处理造成额外的DrawCall。我一般会把雷达图放在独立的Canvas或者独立的层级里减少批处理被打断的概率。第三个是Overdraw数据填充区域使用的是半透明颜色如果雷达图下面还叠了别的UI就可能产生多余的像素填充。5.3 从UI到3D雷达图在数字孪生项目里的扩展数字孪生项目里我经常遇到一个需求在3D场景的设备旁悬浮一个半透明的雷达图展示设备当前状态。这个情况下直接用UGUI的Screen Space Canvas会显得很假因为雷达图不跟随相机旋转也没有空间感。我的做法是把雷达图放在World Space Canvas下面设置合理的缩放比例然后让Canvas或者雷达图本身始终进行相机朝向的Billboard旋转。有一个细节要注意World Space Canvas下的雷达图rectTransform.rect的单位不是像素而是世界单位。如果你的雷达图打算显示成1米大小那RectTransform就设成1米乘以1米半径计算逻辑不用变但pointSize和线宽这种像素级参数就要按比例放大。我的习惯是加一个displayScale公共字段在OnPopulateMesh里把所有像素级参数乘上这个缩放这样同一个组件既能在Overlay Canvas下用也能在World Space下用不用改代码。如果你对性能有极致要求还可以考虑用Shader直接画雷达图。思路是在片元着色器里根据当前片元所处的极坐标角度和半径判断是否属于雷达填充区域用SDF有向距离场做边缘抗锯齿。这种方案顶点数几乎为零渲染由GPU完成在大量雷达图同屏的时候优势巨大。但缺点是需要写自定义Shader还要处理UI和3D环境的融合开发成本比UGUI自绘高不少适合那种“百个雷达图同时存在”的极端场景。6. 从一次实践到一套方法边界情况与后续扩展前面把雷达图的实现拆得很细了这节我想聊聊实战中容易被忽略的边界情况和扩展思路。这些都来自我自己的项目经验不是理论推演。6.1 编辑器实时预览与OnValidate写这种自绘组件编辑器里的开发体验特别影响效率。如果你直接改Inspector里的颜色、数值组件不刷新你得每次手动运行游戏才能看效果那太痛苦了。解决方式是在脚本里重写OnValidate让它感知Inspector变化时自动调用SetVerticesDirty。protected override void OnValidate() { base.OnValidate(); if (values ! null values.Length ! indicatorCount) System.Array.Resize(ref values, indicatorCount); SetVerticesDirty(); }这段代码还有一个隐藏作用Array.Resize保证数组长度始终和指标数量同步。你在Inspector里把indicatorCount从6改成8数组会自动扩到8个元素新元素默认是0图形会有一个凹进去的边你填上数值就能看到效果。这个体验对调UI非常重要相当于实时预览省去了反复代码改参数的环节。6.2 数值为0时的显示策略雷达图有个很常见的业务需求某个属性当前值是0要不要画出来如果直接按0处理这个点在中心填充区域会凹进去整个图形看起来像缺了一块。有些项目希望值0时显示一条很短的“基线”提示这个指标存在但当前没有数值有些项目希望直接不画这个点用虚线表示“未解锁”。我倾向于加一个zeroAsBase开关。开启时归一化结果会从0变成0.05f左右也就是给每个指标一个最小可见半径。关闭时则保持真实0值。这个开关在角色属性面板里特别有用因为很多角色初始属性是0但你不想让雷达图初始状态是“一个点”。不要小看这个小功能它对产品验收时的主观体验影响挺大。6.3 从雷达图到任意多边形图表一旦你把OnPopulateMesh这套思路跑通了其实不止是雷达图很多自定义UI图表都能用同一套方法来实现。环形进度条、词云分布、折线图、柱状图底层都是“极坐标映射 顶点生成 三角剖分”的组合。我后续就把同一个基础类扩展成了好几个组件比如把填充区域改成环形用多个同心的雷达图叠加做多层指标对比甚至在数据点上挂交互检测来响应点击事件。交互检测这块也值得提一句因为雷达图业务经常需要点击某个属性查看详情。UGUI的默认射线检测是按矩形的对雷达图这种不规则图形不准确。我的实现是重写Graphic.Raycast在射线命中UI元素后再追加一层“点到原点距离是否小于最大半径”的判断。如果只想让填充区域可点就进一步判断点是否在多边形内部这个算法网上能搜到现成的叫射线法或者回转数法代码量不大。最后说点个人体会我一开始也图省事想找现成插件试过几个之后发现这类插件在Demo里怎么都好一上正式项目涉及风格统一、字体、描边、动画节奏、性能指标全都得自己调。自己写自绘组件代码量其实不大但对UI的掌控力完全不一样。雷达图这个组件也只是一个起点同样的方法还能做环形进度条、词云、各种不规则多边形图表底层的极坐标映射和Mesh绘制逻辑都是相通的。希望这篇实践记录能帮你少走一点弯路做出来的雷达图比我的还漂亮。