尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

UGUI无代码搭建多级下拉菜单:Inspector拖拽实现完整交互

UGUI无代码搭建多级下拉菜单:Inspector拖拽实现完整交互 做UI做久了你会发现一个规律越是看着不起眼的控件真要做出来反而越容易翻车多级下拉菜单就是典型。很多人一拿到这个需求就想着写递归脚本、搞树形结构或者直接去Asset Store找现成插件。但实际上UGUI自带的Button、Toggle、ToggleGroup、LayoutGroup这几个组件组合起来就能在Inspector面板里拖出一个多级下拉菜单全程不用写一行代码。这篇文章我会把这套纯Editor方案从事件链路到具体搭建完整拆开来讲。不管你是不太会写代码的策划、UI美术还是想快速验证交互原型的独立开发者这套方法都直接能抄。1. 为什么纯界面搭建也能做出多级下拉菜单1.1 核心思路把“菜单交互”翻译成UGUI组件语言先说结论多级下拉菜单的交互本质上就是“展开”“收起”“互斥”这三件事。而UGUI里刚好有三个组件能一一对应展开/收起由GameObject的SetActive控制UnityEvent可以在Inspector里直接绑定这个方法参数还能静态填好。互斥一个ToggleGroup解决同一组里只能有一个Toggle处于选中状态。点击外部关闭一个全屏透明Button当遮罩点击它就把整个菜单关掉。很多人觉得这事必须写代码是因为习惯性把“菜单”当成一个完整业务模块来设计而不是把它拆成UGUI组件已有的行为。其实你先把交互拆开再映射到组件上很多UI功能根本不需要脚本。1.2 纯Editor方案的边界别拿它硬扛悬停菜单必须先把丑话说在前面这套方案最舒服的场景是“点击展开、点击收起”的交互模式。如果你要的是Windows原生菜单那种“鼠标移入自动展开、移出自动收起”的效果纯Inspector操作也能做个七七八八但会有个比较明显的坑——鼠标从一级菜单项移动到二级菜单时如果中间有一丁点空隙就会触发PointerExit导致二级菜单闪没。这个坑我后面有一节专门讲。所以方案边界先划清楚点击交互纯Editor非常舒服悬停交互能用但有条件限制。还有一个现实问题如果你的项目里UI主题经常变、菜单项数量不固定、数据要动态生成那纯Editor方案就不是最优解了那种情况老老实实写脚本反而更省事。这套方案的真正价值在于前期交互验证、中小型项目、以及让不写代码的同事也能参与UI迭代。2. 事件链路设计Button、Toggle、遮罩的职责分工2.1 入口Button和全屏透明遮罩组成的开关闭环很多新手做下拉菜单会直接用Toggle做入口用IsOn去控制菜单的显隐。这样做不是不行但会陷入一个窘境点开菜单之后如果用户直接点了菜单里的功能项入口Toggle的IsOn状态就停留在true下次点击时会先触发一次false把已经隐藏的菜单再隐藏一遍看起来很卡。我用的方案是入口用普通Button配一个全屏透明遮罩Button形成一个干净的开关闭环。交互逻辑是这样的点击入口Button显示菜单根MenuRoot同时显示透明遮罩。点击透明遮罩隐藏菜单根同时隐藏自己。点击菜单里的功能项隐藏菜单根和遮罩。这样入口Button本身没有状态残留每次点击都是新一轮打开逻辑非常线性。这个“引导遮罩关闭自身”的模式后来我连弹窗、Tooltip、右键菜单都用上了是UGUI无代码交互里最值得记的一个套路。2.2 分类项Toggle和ToggleGroup的展开互斥多级菜单里一级菜单的“分类项”和“功能项”要区分开分类项点击后展开二级菜单需要携带状态展开/收起用Toggle。功能项点击后执行一个动作关闭整个菜单不需要状态用Button。分类项用Toggle之后可以放进同一个ToggleGroup。ToggleGroup的作用是保证同组内只有一个Toggle能处于开启状态也就是说同时只有一个分类项能展开二级菜单。另外记得在ToggleGroup组件上勾选Allow Switch Off这样再次点击已经展开的分类项才能把二级菜单收回去。不勾这个选项的话Toggle一旦被选中就取消不了了你的菜单就只能开不能关。2.3 Inspector里UnityEvent的静态传参逻辑这个方案不需要代码关键在于UnityEvent在Inspector面板里可以直接绑定带参数的方法而且参数可以预先填好。比如GameObject.SetActive(bool)这个方法你在Button的OnClick事件列表里把目标物体拖进去选择GameObject.SetActive下面会出现一个勾选框勾上就是true不勾就是false。Toggle的On Value Changed(bool)同理不过它的参数状态是动态的Toggle处于On状态时传trueOff状态时传false。这就刚好用来把二级菜单容器的SetActive绑上去选中的时候显示取消选中的时候隐藏。整个联动不需要一行代码全靠UnityEvent的参数传递。理解这个机制之后你就能开始在界面上自由组合各种联动了Toggle控制物体显隐、Button控制动画状态切换、Slider控制数值变化都是一个套路。3. 分步搭建从空Canvas到完整二级下拉菜单3.1 先检查地基EventSystem、CanvasScaler、GraphicRaycaster老手也可能在这里翻车场景里压根没有EventSystem或者Canvas上没有GraphicRaycaster导致UI完全点不动还以为是事件绑定问题。第一步先确认三件套EventSystem场景中只能有一个通常Unity创建UI时自动生成包含StandaloneInputModule。GraphicRaycaster挂在Canvas上负责把鼠标/触摸坐标转换成UI射线命中结果。CanvasScaler负责UI自适应建议用Scale With Screen Size模式Reference Resolution填1920x1080之类的主分辨率。这三个组件不齐后面所有操作都是空中楼阁。特别是GraphicRaycaster有时候你从别的工程复制了一个Canvas过来忘了带这个组件UI能显示但点击事件全部失效。3.2 搭入口Button和全屏透明遮罩在Canvas下创建两个顶层节点层级顺序如图Hierarchy越靠上渲染越靠下BgMask全屏透明遮罩ButtonHUD常显UI放入口ButtonMenuRoot初始隐藏放整个菜单系统BgMask的具体配置创建ImageAnchor Presets选Stretch全屏拉伸四边对齐Canvas。Color设为纯黑Alpha为0也就是在Inspector里把Alpha通道拉到0但Raycast Target必须保持勾选。因为透明图片仍然可以参与射线检测点它就是为了接收点击。挂Button组件Transition可以设为None它不需要任何视觉效果只是逻辑上接收一次点击。入口Button放在HUD下面内部子物体放一个Text或者TMP_Text写“菜单”。给它挂Button组件。然后配置两个OnClick监听MenuRoot.SetActive(true)显示菜单根部。BgMask.SetActive(true)把遮罩显示出来。注意UnityEvent列表里每一行都可以添加多个监听别挤在同一个事件里写参数一行一个函数更清晰。配置好之后运行一下点入口Button应该能看到MenuRoot出现。如果没反应先查MenuRoot初始是不是隐藏状态再查Button组件上有无实际挂载可点击的Image。3.3 一级菜单容器VerticalLayoutGroup ContentSizeFitterMenuRoot下面的第一个核心物体叫DropdownContainer它是整个一级菜单的容器。在它身上挂三个组件VerticalLayoutGroup让菜单项纵向自动排列。ContentSizeFitterVertical Fit选PreferredSize让容器高度跟随内容自动撑开。可选ToggleGroup给同一级分类项做互斥。VerticalLayoutGroup的几项配置值得单独说明。首先是Child Control Width和Child Control Height都勾上让子项尺寸受布局控制Child Force Expand Width取消勾选否则子项会被强行拉伸到等宽Spacing按你的观感填2到4像素比较常见Padding建议给个4到8像素不然菜单内容贴边很难看。一级菜单的每一个“分类项”结构都是Toggle作为根节点内部放一个Image背景、一个Text/TMP_Text文字、一个箭头Image。给Toggle设置一个Target Graphic指向背景ImageTransition用ColorTint这样鼠标悬停和选中时背景颜色会变交互反馈清晰。3.4 二级菜单嵌套容器的锚点与定位二级菜单跟一级菜单有一个关键区别二级菜单容器不能放进一级菜单的VerticalLayoutGroup里否则它会被当成一个布局元素挤进列表把整个一级菜单撑变形。我推荐的做法是把二级菜单容器SubContainer作为对应的分类项Toggle的子物体。因为LayoutGroup只计算直接子物体Toggle本身作为一级菜单的一个item参与布局而它内部的SubContainer完全不影响一级菜单的排列就像腰带扣上挂了一个钥匙扣不会改变腰带的形状。SubContainer的RectTransform定位Anchor Presets选Right Top也就是锚定在分类项Toggle的右上角。Pivot设成左上角(0, 1)。PosX填分类项Toggle的宽度再加4像素左右的间距。PosY保持0。这样二级菜单就会从一级菜单的右侧“长出来”符合常见的下拉菜单展开方向。SubContainer内部的结构跟DropdownContainer一样挂VerticalLayoutGroup和ContentSizeFitter里面放二级菜单的功能项Button。SubContainer默认隐藏通过一级分类项Toggle的On Value Changed绑定SetActive来控制显隐。绑定的时候目标选SubContainerFunction选GameObject.SetActive不勾传参框。如果二级菜单还想往右延展三级、四级做法完全一样在二级分类项身上再套一个SubContainer把锚点往右推事件绑定指向同一逻辑操作复制粘贴即可。3.5 功能项Button与整组关闭逻辑二级菜单里的叶子节点是功能项比如“打开文件”“另存为”“退出”。这些用Button组件不参与ToggleGroup不用管选中态。功能项的OnClick需要配置两件事MenuRoot.SetActive(false)整组菜单隐藏。BgMask.SetActive(false)遮罩也一起隐藏。这样点功能项的时候菜单直接收回干净利落。如果你希望“点击分类项时顺带关闭已经展开的其他二级菜单”只要把分类项都放在同一个ToggleGroup里互斥状态就帮你做了不需要额外绑定。至此一个完整的二级下拉菜单已经能跑了点入口展开点功能项执行关闭点外部遮罩关闭一级分类项之间互斥展开。全程没有写一行代码。4. 视觉联动箭头切换、选中态和菜单框自适应4.1 SpriteSwap做箭头与背景的状态变化分类项Toggle展开后最好能给个视觉反馈让用户知道当前哪一项是展开状态。最简单的做法是用Toggle的SpriteSwap把Target Graphic设成背景Image然后准备两张Sprite一张普通背景一张“选中/展开”背景比如颜色更亮或带底边高亮。在Toggle组件的Transition里选Sprite Swap把Active Sprite设成展开态背景其他状态按需配置。展开箭头也可以这样处理。只需要准备两个小箭头Sprite收起状态向右箭头“”展开状态向下箭头“v”。设好SpriteSwap之后Toggle切换状态时箭头会跟着自动换完全不用额外写逻辑。有一点要注意Toggle的SpriteSwap只能作用于一个Target Graphic。如果你既想切背景又想切箭头就把背景和箭头画在同一张Sprite里或者箭头单独放一个子物体用另一套控制。两个方案都用过之后我倾向后者——把背景和箭头分离各挂各的SpriteSwap耦合低美术替换素材也方便。4.2 LayoutElement统一菜单项宽度不使用LayoutElement时菜单项宽度常常会乱文字长的那一项把整个菜单撑宽文字短的项又显得参差不齐。解决办法在每个菜单项的背景Image上加LayoutElement组件设一个Preferred Width比如120。注意是每个菜单项都加整个菜单才会按这个宽度统一。后面如果某个菜单项暂时没有文字内容宽度也不会塌下去菜单框看起来特别整齐。这条经验是我在真实项目里踩出来的最初菜单项清一色用Content Size Fitter自适应宽度结果每次改文案UI都会跳一下后来全换成LayoutElement固定Preferred Width世界安静了。4.3 菜单框内容变化后自动撑高ContentSizeFitter的Vertical Fit设成PreferredSize后菜单容器高度由子项总高度决定子项多就长子项少就短。这个特性配合VerticalLayoutGroup效果很好你增删菜单项菜单框自动伸缩不需要手动拖RectTransform。需要留意的是ContentSizeFitter和LayoutGroup同时存在时RectTransform的尺寸值在Inspector里会显示为灰色不可手动编辑这是正常的。你要是想手动微调位置可以在编辑器里临时把ContentSizeFitter禁用调完再启用。还有个坑如果把菜单框当作其他物体的子物体并且父物体上有RectMask2D或Mask组件ContentSizeFitter算出的尺寸会被裁剪子菜单显示不完整。这个问题在排错章节再细说。5. 排错记录菜单点不开、子菜单被遮挡、分类项收不拢5.1 事件完全没响应先查三件套整套方案如果出现点击入口Button没反应的情况我的排查顺序是EventSystem存在吗缺失的话场景里所有UI事件全部失效。Canvas上有GraphicRaycaster吗入口Button上有可点击的Image吗Button要靠Graphic命中才能触发纯空物体不行。OnClick里是否选对了函数GameObject.SetActive参数勾选框是true还是false这里最容易迷惑的是最后一条在UnityEvent的Function列表里找GameObject.SetActive时Inspector面板会显示一个带名字的参数很多人以为要手动填目标物体的名字其实下面的勾选框才是bool值的输入方式。刚用Unity的时候我在这一步卡了快一下午总觉得传参应该填点啥实际上那个勾选框就是参数。5.2 子菜单被其他UI遮挡渲染层级与Sibling排序二级菜单显示出来了却被后面的UI盖住。UGUI渲染层级的总规则是Hierarchy中越靠后的物体渲染越靠上同一个父物体下的兄弟节点按先后顺序覆盖Canvas与Canvas之间按Sorting Order决定优先级。解决方法按从简到繁把SubContainer的Sibling Index拖到父物体的最末尾让它在同级节点中最后渲染。如果遮挡来自其他Canvas或某些全屏UI给SubContainer补一个Canvas组件勾选Override SortingSorting Order填一个比较大的值比如100。注意加了Canvas之后SubContainer内部的事件和布局逻辑不受影响只要GraphicRaycaster还在父Canvas或自身Canvas上就行。5.3 分类项不能再次收起AllowSwitchOff场景点了一次分类项A二级菜单展开再点一次A二级菜单没反应退出不回来。原因几乎全是ToggleGroup的Allow Switch Off没有勾选。ToggleGroup默认“强制分组内必须有一个选中”你点了A之后A就是选中态再点它系统判定“不能取消到无选中状态”于是吞掉了这次点击。勾上Allow Switch Off之后点击当前选中项会把IsOn设回falseSubContainer也随之隐藏这才是菜单该有的行为。另外如果你希望点击另一个分类项时前一个项及时收起ToggleGroup互斥能自动完成但前提是你能确认所有一级分类项Group属性都指向同一个ToggleGroup组件。5.4 菜单项挤成一团或宽度不一布局组件配置问题菜单项堆叠在一起或者互相重叠优先检查VerticalLayoutGroup的Child Control Height是否勾选。CG不勾的话子项高度不被布局接管就容易互相叠压。宽度不一致的情况先看LayoutElement是否设置再看VerticalLayoutGroup的Child Force Expand Width是不是开着。Force Expand开着的时候子项会被强行拉伸填满容器宽但容器宽度由最宽的item决定所以看起来还是宽窄不一。一个常见的组合建议VerticalLayoutGroupSpacing填2Child Control Width/Height都勾Child Force Expand Width不勾。每个菜单项LayoutElement的Preferred Width统一填120。菜单容器ContentSizeFitter的Vertical Fit选PreferredSize。这个组合能做到菜单框宽度稳定、高度自适应、子项排列整齐是套通用解。6. 扩展思路三级菜单、悬停展开和动画化的边界6.1 三级菜单把二级的搭建步骤再复制一遍三级菜单没有任何新技术就是把二级菜单的搭建逻辑再套一次在二级分类项Toggle下挂SubContainer配置锚点向右偏移把Toggle的On Value Changed指向新的SubContainer。需要注意的点是层级关系会变深如果每层都用Canvas Override Sorting去提升渲染排序多个Canvas之间的事件管理会变得有一点绕。我的做法是同一棵菜单树里的SubContainer统一挂在MenuRoot下面的同一个Canvas组件下只在一个入口处控制渲染层级而不是每层都加Canvas。6.2 纯Editor做悬停展开的边界能实现但别乱来悬停展开确实能用EventTrigger组件无代码实现EventTrigger是UGUI自带的不需要写脚本在Inspector里添加条目选PointerEnter再绑一个SetActive(true)就行。但问题出在PointerExit上。Item局格是鼠标从一级分类项往二级菜单方向移动时如果二级菜单和一级分类项之间有缝隙哪怕只有1像素鼠标会先触发一级分类项的PointerExit再把二级菜单隐藏。即使二级菜单层级上是一级分类项的子物体射线检测的目标在切换物体时仍然会各算各的。想缓解这个情况只能靠布局设计把二级菜单紧贴一级分类项不留下空隙同时给二级菜单的整个容器设置一个较大区域的PointerEnter让鼠标进入二级菜单后“接管”展开状态。但这类方案叠到三层以上很容易出问题维护成本也高。所以我说说结论悬停交互还是建议写一个二三十行的EventTrigger辅助脚本处理进入延迟和离开检测UI体验会稳定得多。纯Editor方案真正的舒适区是点击交互别为了“完全不写代码”这句话硬撑。6.3 什么情况建议直接换成代码方案最后总结一下我个人的选型判断原型验证、中小型系统配置菜单纯Editor方案很好改起来快。菜单项由数据驱动、动态生成必须上脚本Editor里拖不出动态列表。悬停展开、Kiosk模式、复杂度超过三级建议脚本辅助。大型项目多人协作Inspector事件引用容易在Prefab变体中断可维护性会变差这时用代码集中管理事件订阅更稳妥。说一下我在实际使用这套方案时的体会之前三四个版本迭代都用纯Editor搭菜单策划拿到工程也能自己加菜单项不用每次让我写配置效率提升很明显。直到某次需求变成了“菜单内容要按后台返回的权限列表动态生成”我才把那套纯Editor方案整个换成了代码驱动。但前期原型阶段省下的时间已经足够支付这次重构的成本了。这套无代码方案的最大价值不在于省多少行代码而在于把UI交互的调试门槛打了下来让团队里更多角色能参与进来。只要你的场景还在“点击展开、点击收起”的范围内放心按这个思路去搭。数据驱动和悬停交互的硬需求出现时再换代码方案也不迟。
返回列表