Unity UGUI 奇技淫巧:把普通组件玩出花

Unity UGUI 奇技淫巧:把普通组件玩出花 Unity UGUI 奇技淫巧把普通组件玩出花写在前面UGUI 里很多组件的名字很“老实”ScrollRect像是只用来做滚动列表CanvasGroup像是只管透明度Image像是只负责显示图片ToggleGroup像是只为开关服务。但做项目久了会发现组件名字不重要组件真正提供的能力才重要。ScrollRect 拖拽容器 CanvasGroup 输入闸门 Image 进度、遮罩、热区 ToggleGroup 轻量状态机 RawImage RenderTexture 入口 RectMask2D 低成本矩形裁剪器 LayoutElement 自动布局里的局部规则覆盖器这篇不写 UGUI 大百科也不讲“Button 怎么点、Slider 怎么拖”这种基础用法。只分享一些能在项目里少造轮子、少写脆弱代码的组件妙用。技巧总览组件换个角度看可以拿来做什么ScrollRect成熟拖拽容器摇杆、拖拽面板、分页吸附、虚拟列表、下拉刷新CanvasGroup整组输入闸门防连点、Loading 锁屏、新手引导遮罩、弹窗淡入淡出Image可射线、可填充的图形透明热区、CD、血条、转场遮罩ToggleGroup单选状态机页签、筛选、互斥状态、模式切换RawImageRenderTexture 显示器小地图、角色预览、3D 道具展示RectMask2D矩形裁剪器跑马灯、聊天裁剪、局部视窗LayoutElement布局规则覆盖特殊 Item、高度可变消息、局部插队布局子CanvasRebuild 隔离层高频刷新 UI 性能优化1. ScrollRect 做虚拟摇杆这个技巧解决什么问题虚拟摇杆需要处理按下、拖拽、松手、移动范围限制、方向输出。如果完全自己写需要实现一堆事件接口和坐标转换。ScrollRect本来就已经处理好了拖拽输入和content.anchoredPosition的变化所以可以直接复用它的拖拽能力。为什么这个组件能做摇杆和ScrollRect的模型很像摇杆底盘 ScrollRect 节点 摇杆手柄 Content 拖拽偏移 content.anchoredPosition 摇杆方向 content.anchoredPosition / 半径Hierarchy 推荐结构Rocker Image 底盘图 Rocker 继承 ScrollRect Viewport Content 摇杆手柄核心代码usingUnityEngine;usingUnityEngine.EventSystems;usingUnityEngine.UI;publicclassRocker:ScrollRect{publicVector2Direction{get;privateset;}publicfloatMagnitude{get;privateset;}privatefloatradius;protectedoverridevoidStart(){base.Start();inertiafalse;movementTypeMovementType.Unrestricted;radius((RectTransform)transform).sizeDelta.x*0.5f;}publicoverridevoidOnDrag(PointerEventDataeventData){base.OnDrag(eventData);Vector2poscontent.anchoredPosition;if(pos.magnituderadius)pospos.normalized*radius;SetContentAnchoredPosition(pos);Directionradius0f?Vector2.zero:pos/radius;MagnitudeDirection.magnitude;}publicoverridevoidOnEndDrag(PointerEventDataeventData){base.OnEndDrag(eventData);velocityVector2.zero;SetContentAnchoredPosition(Vector2.zero);DirectionVector2.zero;Magnitude0f;}}Inspector 关键设置Horizontal: true Vertical: true Movement Type: Unrestricted Inertia: false Content: 摇杆手柄 Viewport: 可拖拽区域常见坑Rocker已经继承ScrollRect不要再额外挂一个普通ScrollRect。不要在OnDrag里一直Debug.Log移动端拖拽频率高日志会直接拖慢。如果摇杆 UI 能动但角色不动说明只是 UI 输出了Direction还没有让移动系统读取它。适合/不适合场景适合普通固定摇杆、小游戏摇杆、原型期移动控制。不适合复杂多指摇杆、动态摇杆、技能轮盘和方向指示混合的重度控制器。2. ScrollRect 做可拖拽面板这个技巧解决什么问题有些 UI 不是“滚动内容”而是“拖一个面板或图标到某个范围内”比如技能轮盘、战术部署、地图预览、角色摆放。这时仍然可以借ScrollRect的拖拽能力只是把移动范围从滚动区域改成自定义边界。为什么这个组件能做ScrollRect的核心不是列表而是拖拽事件 - 计算偏移 - 移动 Content你可以重写SetContentAnchoredPosition在真正移动前限制位置。Hierarchy 推荐结构DragPanel ScrollRect Viewport Content 被拖拽的面板核心代码usingUnityEngine;usingUnityEngine.UI;publicclassLimitedDragPanel:ScrollRect{publicVector2min;publicVector2max;protectedoverridevoidSetContentAnchoredPosition(Vector2position){position.xMathf.Clamp(position.x,min.x,max.x);position.yMathf.Clamp(position.y,min.y,max.y);base.SetContentAnchoredPosition(position);}}Inspector 关键设置Movement Type: Unrestricted Inertia: false Horizontal / Vertical: 按需要开启常见坑如果 Content 尺寸和 Viewport 尺寸关系不对普通ScrollRect会出现滚动边界干扰。做拖拽面板时通常把MovementType改成Unrestricted边界完全由代码控制。适合/不适合场景适合局部拖拽、编辑器式 UI、地图窗口、预览窗口。不适合需要精确拖拽世界对象的场景世界拖拽最好走射线和坐标转换。3. ScrollRect 做虚拟列表这个技巧解决什么问题背包、排行榜、邮件、聊天记录如果一次生成几百上千个 Item会带来 Instantiate 卡顿、布局重建、内存上涨和滑动掉帧。虚拟列表的思路是数据可以有一万条UI Item 只保留屏幕可见的十几个。为什么这个组件能做ScrollRect提供滚动位移虚拟列表根据content.anchoredPosition算出当前应该显示的数据索引然后复用 Item。滚动偏移 - 起始索引 - 复用 Item - 刷新数据Hierarchy 推荐结构VirtualList ScrollRect Viewport Content Item_0 Item_1 Item_2 ...核心代码privatevoidRefreshVisibleItems(){intfirstIndexMathf.FloorToInt(content.anchoredPosition.y/itemHeight);firstIndexMathf.Clamp(firstIndex,0,data.Count-1);for(inti0;iitems.Count;i){intdataIndexfirstIndexi;if(dataIndexdata.Count){items[i].gameObject.SetActive(false);continue;}items[i].gameObject.SetActive(true);items[i].Rect.anchoredPositionnewVector2(0f,-dataIndex*itemHeight);items[i].Bind(data[dataIndex]);}}Inspector 关键设置ScrollRect 正常配置 Content 高度 数据数量 * itemHeight Item 数量 可见数量 2 到 4 个缓冲常见坑固定高度列表最简单变高 Item 要额外维护高度表。复用 Item 时要重置旧状态比如选中、红点、按钮监听、动画状态。不要让大量 Item 同时参与LayoutGroup自动布局虚拟列表最好自己定位 Item。适合/不适合场景适合背包、邮件、聊天、排行榜、任务列表。不适合数量很少且布局复杂的小列表普通 Instantiate 更简单。4. ScrollRect 做分页吸附这个技巧解决什么问题角色选择、皮肤选择、章节选择、活动页经常需要左右滑动松手后自动吸附到最近一页。为什么这个组件能做ScrollRect负责拖拽分页逻辑只需要在松手时根据 Content 偏移计算最近页码。Hierarchy 推荐结构PageScroll ScrollRect horizontal Viewport Content Page_0 Page_1 Page_2核心代码publicclassPageSnapScroll:ScrollRect{publicfloatpageWidth1080f;publicfloatsnapSeconds0.25f;publicoverridevoidOnEndDrag(UnityEngine.EventSystems.PointerEventDataeventData){base.OnEndDrag(eventData);velocityVector2.zero;intpageMathf.RoundToInt(Mathf.Abs(content.anchoredPosition.x)/pageWidth);floattargetX-page*pageWidth;StopAllCoroutines();StartCoroutine(SnapTo(newVector2(targetX,content.anchoredPosition.y)));}privateSystem.Collections.IEnumeratorSnapTo(Vector2target){Vector2startcontent.anchoredPosition;floatt0f;while(t1f){tTime.unscaledDeltaTime/snapSeconds;content.anchoredPositionVector2.Lerp(start,target,Mathf.SmoothStep(0f,1f,t));yieldreturnnull;}content.anchoredPositiontarget;}}Inspector 关键设置Horizontal: true Vertical: false Inertia: false 或保留一点惯性后手动吸附常见坑如果页面宽度随分辨率变化pageWidth不要写死要取 Viewport 宽度。适合/不适合场景适合页数少、每页宽度一致的横向分页。不适合瀑布流或每页宽度不同的复杂布局。5. ScrollRect 做下拉刷新这个技巧解决什么问题排行榜、好友、邮件、活动公告经常需要下拉刷新或上拉加载。为什么这个组件能做ScrollRect在边界外拖拽时Content 会产生额外偏移。检测这个偏移超过阈值就可以触发刷新。核心代码publicclassPullToRefresh:ScrollRect{publicfloatthreshold100f;publicSystem.ActionOnRefresh;privateboolready;publicoverridevoidOnDrag(UnityEngine.EventSystems.PointerEventDataeventData){base.OnDrag(eventData);readycontent.anchoredPosition.y-threshold;}publicoverridevoidOnEndDrag(UnityEngine.EventSystems.PointerEventDataeventData){base.OnEndDrag(eventData);if(ready)OnRefresh?.Invoke();readyfalse;}}常见坑刷新中要锁住状态避免连续触发多次网络请求。下拉刷新更适合移动端PC 端列表通常用按钮刷新更清楚。6. ScrollRect 嵌套滚动事件分发这个技巧解决什么问题外层页面上下滑内层页签左右滑是最常见的嵌套ScrollRect。如果不处理两个 ScrollRect 会抢拖拽事件。为什么这个组件能做拖拽事件本身带有位移方向。你可以在开始拖拽后判断横向位移更大 - 交给内层 纵向位移更大 - 交给外层核心代码publicclassNestedScrollRouter:MonoBehaviour,UnityEngine.EventSystems.IBeginDragHandler,UnityEngine.EventSystems.IDragHandler,UnityEngine.EventSystems.IEndDragHandler{publicScrollRecthorizontalScroll;publicScrollRectverticalScroll;privateVector2startPos;privateScrollRectactive;publicvoidOnBeginDrag(UnityEngine.EventSystems.PointerEventDataeventData){startPoseventData.position;activenull;}publicvoidOnDrag(UnityEngine.EventSystems.PointerEventDataeventData){if(activenull){Vector2deltaeventData.position-startPos;activeMathf.Abs(delta.x)Mathf.Abs(delta.y)?horizontalScroll:verticalScroll;active.OnBeginDrag(eventData);}active.OnDrag(eventData);}publicvoidOnEndDrag(UnityEngine.EventSystems.PointerEventDataeventData){if(active!null)active.OnEndDrag(eventData);activenull;}}常见坑不要让两个 ScrollRect 同时处理同一次拖拽否则手感会像“粘住”或“斜着乱跑”。7. CanvasGroup 做整组 UI 输入闸门这个技巧解决什么问题网络请求、升级、领取奖励、购买道具时需要临时禁止玩家重复点击。一个个关 Button 很容易漏。为什么这个组件能做CanvasGroup不只是透明度它还有interactable 控制 Selectable 是否可交互 blocksRaycasts 控制是否阻挡 UI 射线核心代码publicstaticclassCanvasGroupUtil{publicstaticvoidSetInput(CanvasGroupgroup,boolenabled){group.interactableenabled;group.blocksRaycastsenabled;}publicstaticvoidSetVisible(CanvasGroupgroup,boolvisible){group.alphavisible?1f:0f;group.interactablevisible;group.blocksRaycastsvisible;}}常见坑alpha 0不等于点不到。真正控制点击的是blocksRaycasts。interactable主要影响Button、Toggle、Slider这类Selectable自定义射线逻辑还要看blocksRaycasts。适合/不适合场景适合弹窗、Loading、网络请求防连点、新手引导遮罩。不适合只想禁用一个按钮的简单场景直接关 Button 更直观。8. Image 做透明点击热区这个技巧解决什么问题有时需要一个看不见但能点击的区域比如弹窗背景关闭、地图热区、扩大按钮点击范围。为什么这个组件能做UGUI 射线检测依赖Graphic。透明Image虽然看不见但只要Raycast Target开着就能被点击。Hierarchy 推荐结构CloseMask Image alpha 0 Button Dialog常见坑透明 Image 很容易误挡下面的按钮。只用于需要接收点击的区域装饰图的Raycast Target要关掉。9. EmptyRaycast 做无渲染点击区域这个技巧解决什么问题透明 Image 可以做点击热区但它仍然是一个 Graphic。大量透明 Image 会让项目里“谁挡了点击”变得很难查。可以写一个不绘制网格、但保留射线能力的Graphic。核心代码usingUnityEngine.UI;publicclassEmptyRaycast:Graphic{protectedoverridevoidOnPopulateMesh(VertexHelpervh){vh.Clear();}}Inspector 关键设置Raycast Target: true Color: 无所谓因为不会绘制常见坑它只是不绘制不代表不会挡点击。使用时要像使用透明 Image 一样谨慎命名。适合/不适合场景适合透明热区、背景遮罩、引导点击区域。不适合需要视觉反馈的按钮。10. Image Filled 做 CD、遮罩、转场这个技巧解决什么问题技能 CD、圆形倒计时、血条、建造进度、转场遮罩本质都是“显示一个百分比”。为什么这个组件能做Image.Type Filled可以用fillAmount控制显示比例。核心代码cooldownImage.fillAmountremainSeconds/totalSeconds;横向血条Type: Filled Fill Method: Horizontal Fill Origin: Left圆形 CDType: Filled Fill Method: Radial360 Fill Origin: Top Clockwise: true进阶玩法两层血条做受击延迟front.fillAmounthpPercent;back.fillAmountMathf.Lerp(back.fillAmount,hpPercent,Time.deltaTime*5f);常见坑fillAmount是 0 到 1不是百分数 0 到 100。如果图片不是适合填充的形状Filled 效果会很怪。11. ToggleGroup 做轻量状态机这个技巧解决什么问题页签、模式切换、分类筛选经常需要“同组只能选一个”。自己维护当前按钮状态很容易写散。为什么这个组件能做ToggleGroup天生就是单选状态机。Hierarchy 推荐结构Tabs Toggle_Bag Toggle_Shop Toggle_Task Panels BagPanel ShopPanel TaskPanel核心代码bagToggle.onValueChanged.AddListener(isOn{if(isOn)ShowPanel(bagPanel);});常见坑onValueChanged会在开和关时都触发所以一定要判断isOn。如果想多选筛选器不要放进ToggleGroup。12. RawImage RenderTexture 做小地图/角色预览这个技巧解决什么问题小地图、角色预览、装备试穿、怪物图鉴都需要把一个摄像机画面放进 UI。为什么这个组件能做RawImage可以直接显示Texture而相机可以渲染到RenderTexture。PreviewCamera - RenderTexture - RawImage.texture核心代码previewCamera.targetTexturerenderTexture;rawImage.texturerenderTexture;常见坑预览相机要用单独 Layer只渲染需要展示的角色或物体。不显示预览时关闭相机避免无意义渲染。RenderTexture 分辨率不要无脑开太高。13. RectMask2D 做跑马灯和轻量裁剪这个技巧解决什么问题公告跑马灯、聊天窗口、局部提示经常需要只显示一个矩形区域内的内容。为什么这个组件能做RectMask2D是轻量矩形裁剪适合矩形视窗。相比Mask它更适合大量普通矩形裁剪场景。Hierarchy 推荐结构MarqueeWindow RectMask2D Text核心代码textRect.anchoredPositionVector2.left*speed*Time.unscaledDeltaTime;if(textRect.anchoredPosition.x-textWidth)textRect.anchoredPositionnewVector2(windowWidth,textRect.anchoredPosition.y);常见坑RectMask2D只能矩形裁剪圆形头像、不规则遮罩还是要用Mask或提前处理图片。14. LayoutElement 反向控制自动布局这个技巧解决什么问题自动布局很方便但有时某个 Item 需要特殊高度或宽度比如聊天消息、奖励条、活动 Banner。为什么这个组件能做LayoutElement可以覆盖父级 LayoutGroup 对单个元素的尺寸计算。核心代码layoutElement.preferredHeightmessage.IsImage?240f:80f;常见坑ContentSizeFitter LayoutGroup LayoutElement嵌套太深时容易频繁 Rebuild。长列表里要谨慎。适合少量动态布局不适合几百个 Item 每帧变化。15. 子 Canvas 隔离高频刷新区域这个技巧解决什么问题倒计时、血条、CD、滚动数字频繁刷新会导致同一个 Canvas 下其他静态 UI 也参与重建。为什么这个组件能做Canvas 是 UGUI 重建和批处理的重要边界。把高频变化区域拆到子 Canvas可以减少无关 UI 被拖下水。推荐结构MainCanvas StaticPanel DynamicHudCanvas ScoreText TimeText SkillCooldowns常见坑Canvas 不是越多越好。Canvas 多了会增加管理成本和 DrawCall 风险。只给高频变化区域拆静态背景不要乱拆。16. GraphicRaycaster 做点击链路定位这个技巧解决什么问题“按钮点不到”是 UGUI 最常见问题。不要先怀疑 Button先查射线链路。为什么这个组件能做UGUI 点击链路大致是EventSystem - GraphicRaycaster - Graphic.raycastTarget - CanvasGroup - Selectable定位顺序1. 场景有没有 EventSystem 2. Canvas 有没有 GraphicRaycaster 3. 目标 Graphic 的 Raycast Target 是否开启 4. 父级 CanvasGroup.blocksRaycasts 是否为 false 5. 上层是否有透明 Image / EmptyRaycast 挡住 6. Button.interactable 是否为 false 7. Canvas sortingOrder 是否被盖住常见坑很多时候不是目标按钮点不到而是上面有一张透明 Image 挡住了。17. UI 对象池复用 Item这个技巧解决什么问题奖励飞字、背包格子、列表 Item、弹幕、伤害数字如果频繁 Instantiate/Destroy会造成卡顿和 GC。为什么这个技巧有用UI 对象生命周期短、数量多非常适合对象池。核心代码publicTSpawnT(Tprefab,Transformparent)whereT:Component{Titempool.Count0?pool.Pop()asT:Instantiate(prefab);item.transform.SetParent(parent,false);item.gameObject.SetActive(true);returnitem;}publicvoidDespawn(Componentitem){item.gameObject.SetActive(false);pool.Push(item);}回池前必须重置文本 图片 按钮监听 选中态 红点 动画状态 CanvasGroup 位置和缩放按钮尤其要清button.onClick.RemoveAllListeners();18. 弹窗栈管理返回逻辑这个技巧解决什么问题手游、微信小游戏、安卓返回键都需要统一返回逻辑。靠每个弹窗自己处理关闭很快会乱。为什么这个技巧有用弹窗天然是栈结构打开 A - 打开 B - 打开 C 返回 - 关闭 C 返回 - 关闭 B 返回 - 关闭 A核心代码privatereadonlyStackUIWindowwindowStacknewStackUIWindow();publicvoidOpen(UIWindowwindow){windowStack.Push(window);window.Open();}publicvoidBack(){if(windowStack.Count0)return;UIWindowtopwindowStack.Pop();top.Close();}常见坑不是所有弹窗都该入栈。Toast、Loading、战斗提示通常不参与返回栈。打开同一个窗口前要决定是复用、置顶还是允许多开。19. Image Material 做局部灰化这个技巧解决什么问题不可用技能、未解锁头像、灰掉的道具常常需要局部灰化。为什么这个组件能做Image可以指定材质配合灰化 Shader 可以只灰某些 UI 图片。核心代码image.materialisLocked?grayMaterial:null;常见坑不同材质会影响合批。列表里大量灰化图标时不要给每个都动态创建材质实例。如果只是简单不可用状态image.color Color.gray更便宜。20. UI 动画用 unscaledDeltaTime这个技巧解决什么问题暂停游戏时Time.timeScale 0如果 UI 动画用Time.deltaTime弹窗动画、Loading 动画也会停住。为什么这个技巧有用UI 动画通常不应该受游戏时间缩放影响。核心代码alphaMathf.MoveTowards(alpha,targetAlpha,Time.unscaledDeltaTime*speed);适合场景暂停菜单 结算面板 Loading Toast 弹窗淡入淡出结语UGUI 的很多组件都不只是“名字上的那个用途”。ScrollRect不只是滚动列表它是成熟的拖拽容器。CanvasGroup不只是透明度它是整组 UI 的输入闸门。Image不只是图片它可以是进度、遮罩、热区。ToggleGroup不只是开关组它是轻量状态机。RawImage不只是贴图它是 RenderTexture 入口。少造轮子复用 UGUI 已经写好的输入、拖拽、裁剪、布局、射线和渲染能力。组件用得巧项目会更稳开发也会轻松很多。