首页 / 资讯中心 / 文章详情

Unity UI与Sprite遮挡关系详解:从Sorting Layer到Canvas层级控制

Unity UI与Sprite遮挡关系详解:从Sorting Layer到Canvas层级控制 ★ FEATURED ARTICLE
1. 为什么UI和Sprite的遮挡关系会乱做Unity开发的朋友几乎都会碰到一个让人抓狂的问题明明某个界面或者图片应该显示在最上面运行起来却被其他东西盖住了或者反过来——一个应该藏在后面的Sprite跑到了UI上面把整个界面糊住了。这个问题说白了就是显示层级也就是遮挡关系没控制住。它的底层逻辑并不复杂Unity本质上是个“画家算法”式的渲染器谁后画谁就盖在谁上面。但这套顺序不是按代码执行顺序来的而是由每个渲染对象的几个关键属性共同决定。搞懂这几个属性基本上就能解决90%的层级问题。先明确一个基本分类Unity里的可见对象大体分两拨——UI体系Canvas下的所有元素和世界空间里的SpriteSpriteRenderer组件控制的图片、粒子、3D物体的贴图等。它们的渲染顺序都要经过“CPU排单、GPU执行”的过程但排单的依据不一样所以UI和Sprite混在一起时最容易出乱子。这篇文章适合两类人看一类是刚入坑Unity、被UI层级折磨过的新手一类是已经能做完整游戏但碰到“某个特效要盖在角色上又被UI挡住”这类特殊需求时需要系统性思路的开发者。我会把判断层级的所有决定因素、常见坑、以及几种真正能落地的控制方案都讲清楚。2. 层级控制的底层逻辑到底是谁在决定谁盖谁2.1 渲染顺序的决定因素Camera、Canvas与Sorting Layer任意一个渲染对象最终被画到屏幕上走的流程是先被某个Camera“看见”然后这个Camera把场景里的所有可见对象按规则排序最后一层层画出来。这里有两层意义很多人只理解了一半。第一层是Camera本身的绘图顺序。场景里可以存在多个摄像机每个摄像机的Depth深度值决定了谁先渲染。Depth越小越先画也就是越靠底Depth大的摄像机后画画面会盖在之前所有内容上面。主摄像机通常设成0或-1而带UI的摄像机习惯上会设成比主摄像机大否则UI会显示在主场景下面——这是新手第一个容易踩的坑。第二层才是关键即在同一个Camera内部对象怎么排序。3D物体主要看Transform.Z轴距离离相机越近越先画远处反倒被挡住是深度缓冲的功劳而UI和Sprite则不看Z轴它们看的是两个专属属性Sorting Layer排序层和Order in Layer层内顺序。排序层优先级更高先比较Sorting Layer相同排序层里再比Order in Layer数字大的盖在上方。2.2 为什么同一个屏幕里UI能挡住SpriteSprite也能挡住UI很多开发者默认“UI永远显示在最上面”这个惯性思维害了不少人。UI能不能被Sprite挡住取决于UI的渲染方式和Courier的选择。把Canvas的Render Mode设置为Screen Space - Overlay时UI专用渲染管线会跳过常规排序直接绘制在屏幕之上这时候普通Sprite几乎不可能盖住UI。但一旦把UI改成Screen Space - Camera或World Space模式UI就变成了一个“被Camera渲染的世界对象”它也要参与排序。这时如果UI所在的Canvas sortingOrder比较低而一个Sprite的Sorting Layer更高Sprite就能结结实实地压在UI上面。所以不是“UI一定在最上面”而是“Overlay模式的UI一定在最上面”。这是理解整个层级问题的分水岭。围绕这个分水岭后续所有方案都能推导出来。2.3 用一个类比搞清楚Order、Layer、Z轴的关系我把渲染顺序类比成食堂排队打饭Sorting Layer是“年级”Order in Layer是“学号”。高一高二整体排在高三年级前面同年级里再按学号排队学号大的后面但无论学号多大高一的干不过在后面的高三——因为年级已经定死了优先级。Z轴则是另一个维度的概念只影响3D和粒子这类带透视关系的渲染对象。它和Sorting Layer之间没有直接的可比性甚至取决于摄像机的投影模式。所以在2D游戏里我建议尽量不要依赖Z轴去控制UI和Sprite的遮挡因为一旦你调整摄像机角度或者引入多层摄像机Z轴一乱Premiere式的层级夹层问题就会出现排查起来成本极高。3. UI层的层级实战从Canvas到嵌套Canvas3.1 Render Mode三种模式对层级的影响先看Canvas的Render Mode这一项直接决定了UI系统的“身份归属”。Screen Space - Overlay所有UI元素脱离世界渲染永远盖住3D和Sprite画面。设置面板、弹窗、Loading条这类必须置顶的内容放这个模式最稳。缺点是不能和世界空间对象做透视穿插所以要给3D模型标注名字那种需求得换模式。Screen Space - CameraUI被渲染到指定Camera的平面上像“贴纸”一样贴在屏幕上既能保证UI清晰又能和场景里的3D/Ice出现在同一个成像过程中方便配合后期特效。但它参与层级排序容易和Sprite抢先后。World SpaceUI变成了世界里的3D对象可以在场景里移动旋转头盔UI、血条、告示牌都常用这个。它同样参与常规排序而且受摄像机角度影响。我的建议很简单能用Overlay就用Overlay需要特殊效果时才降级为Camera模式。减少一个可变量就减少一类Bug。3.2 SortingOrder同一个Canvas内和多个Canvas之间同一Canvas内部子物体的层级优先级是父物体靠下的子物体绘制在上面同层级的子物体越靠下的越后绘制。换句话说Hierarchy面板里排最后一个的实际显示在最上面。这和美术同学“最下面一层是背景”的习惯相反需要适应一下。但多个Canvas之间时规则变成比较每个Canvas组件的SortingOrder。就算两个Canvas下面都有层级关系很复杂的UI树Canvas的SortingOrder数值大的整体盖在上面。所以我的习惯是给项目规定一个SortingOrder分配表例如用途SortingOrder说明背景UI/次级面板0不参与主要交互主界面10常规页面弹窗50Modal窗口Loading/全屏遮罩100吸顶提示Debug调试信息200永远可视这套数字之间留足空隙方便后续临时插入一个层级比如新手引导要压在弹窗和主界面之间给个25就行不会动不动就动用几十几百的重构。3.3 动态修改UI层级两个实用路径运行时动态调整UI层级的场景非常常见点击一个物品弹出详情面板、打开商店再叠一个购买确认框、排行榜弹出后再弹一个分享面板……层级的增减不能用“反复改Hierarchy顺序”这种硬办法因为那样做既慢又容易出错。第一个路径是直接修改Canvas.sortingOrder。只要元素携带Canvas组件就能用代码控制它跨Canvas级别的优先级Canvas canvas targetUI.GetComponentCanvas(); if (canvas null) { canvas targetUI.gameObject.AddComponentCanvas(); } canvas.sortingOrder 100;注意一个细节给普通UI对象临时挂Canvas组件时Unity会自动要求你同时挂GraphicRaycaster否则按钮无法接收点击。你手写AddComponent()时不会自动补上GraphicRaycaster所以要手动检查。这是常见的“能显示但点不到”问题的根源之一。第二个路径是调整物体在Hierarchy里的前后位置适合同一个Canvas内部的顺序调整transform.SetAsLastSibling(); transform.SetAsFirstSibling();这个路径快但影响面比较大——如果这个Canvas里有几千个元素等于把整个Canvas的绘制顺序全部打乱可能引发脏标记和重建。所以高频调用时我会用分隔Canvas的方案把“会频繁变化的元素”单独放进一个子Canvas让它的SortingOrder高于其他部分。3.4 嵌套Canvas的正确打开方式嵌套Canvas是Unity提供的一个隐藏强大功能子物体上的Canvas组件可以“覆盖”父级的排序结果重新定义该子树在全局的显示层级。什么意思呢比如你有一个主CanvasSortingOrder是10里面有大量UI。现在你要做一个通用Toast提示。如果你直接把Toast塞进主Canvas的根部它得排在最下面才能显示到最顶层但一旦有新手引导需要比Toast更高你又得改排序。更聪明的做法是单独建一个“ToastRoot”空物体挂Canvas组件SortingOrder设成300这样Toast永远显示在最顶。而且因为它是独立Canvas它内部的变化不会触发主Canvas重建性能上也有优势。把随时间频繁变化、不高频互动的元素拆分到独立Canvas是优化UI卡顿的一个好思路。4. Sprite层的遮挡控制Sorting Layer与Order in Layer的配合4.1 Sorting Layer的命名策略与顺序Sprite的层级控制集中在SpriteRenderer上。每个SpriteRenderer带两个参数Sorting Layer和Order in Layer。Sorting Layer需要在Project Settings的Tags and Layers里预先定义好引擎会按从上到下的顺序排列优先级——排在上面的属于底层。我建议所有2D项目都统一规划一套Sorting Layer而不是只用默认的Default。比如BackGround地图、远景Ground地面Character角色Effect特效UI世界空间UI、飘字Foreground前景遮挡物比如树叶、栅栏把常用对象归入这些层后角色永远是盖在Gound上、又可以被前景挡住、特效可以压住角色逻辑一目了然。最忌讳的是所有Sprite都放在Default层靠Order in Layer的数字硬调项目小了还行项目一大一定乱。4.2 Order in Layer的计算习惯留白比精确更省心Sorting Layer确定后每个SpriteRenderer里的Order in Layer负责层内排序。这个值不需要连续递增我习惯按10的倍数分配。比如角色层的Order普通NPC给0玩家给10玩家脚底特效给20这样一个新怪物入场时给5就能插在NPC和玩家之间不用全局检索改数值。这个方法本质上是给未来的“中间层”留了缝。如果你现在就把1、2、3、4全部占满后期想插入一个介于2和3之间的效果就得同时改好几个对象。4.3 Sprite与UI的混合遮挡两个方案对比前面提到Overlay模式UI永远在最顶层这在大多数情况下是好用的。但如果我要做一个2D游戏角色在地图上行走脚下踩着一个光环特效UI上有一个商店面板。商店面板应该盖住整个游戏画面这是常规需求——Overlay模式直接满足。可是如果我的需求是“角色走进大树后面大树要把角色挡住但大树不能挡住UI”这时候UI用Overlay照样OK大树和角色都归世界空间管Sorting Layer把大树放在前景即可。第二种需求麻烦一点有一种解密游戏世界里有可交互的开关按钮是Sprite不是UI但这个按钮需要盖住某些UI文字或者UI的弹窗要被某个巨大的怪物Sprite穿插入场挡住一半再移开。这时Overlay模式就不行了——要么怪物永远在UI下面要么怪物把UI全挡住。解决方案是把UI改成Screen Space - Camera模式然后把Canvas的SortingLayer和SortingOrder设为与Sprite同一体系比如UI层设在Character之上、Effect之下这样怪物Effect层就能从UI前面穿过去了。这两种方案的取舍原则是如果只是“角色/特效被UI盖上”用Overlay如果是“Sprite要穿过UI之间形成景深遮挡”必须改成Camera模式并参与同一排序。4.4 粒子系统、LineRenderer的层级坑粒子和LineRenderer默认也算世界空间里的一类渲染对象它们的层级控制和Sprite类似但有几个容易踩的坑。粒子系统ParticleSystem的渲染顺序按照Renderer里的Sorting Layer和Order in Layer走。但ParticleSystem组件上只有一个Renderer模块有时候你不小心没勾选Renderer模块粒子就显示不出来还以为是层级问题。检查顺序是先确认Renderer模块开启再查层级参数。LineRenderer同理它也有Sorting Layer和Order in Layer。但LineRenderer的Position数组是按世界坐标排列的如果需要它“跟随”某个UI元素需要每帧转换坐标容易造成性能损耗和卡顿。所以能用UI的LineRenderer就尽量用UI的Image做一像素拉伸能不用LineRenderer就不用。还有TextMeshPro世界空间文字它是自己做排序的——挂在Canvas下时用UICanvas的层级作为世界对象时用TextMeshPro的Sorting Layer和Order in Layer。很多开发者把TextMeshPro当Sprite用结果发现层级怎么调都比Sprite低就是因为忘了TMP有自己的排序属性。找到它的Sorting Order字段改大就行。5. 综合案例一个2D动作游戏的界面层级控制方案5.1 需求拆解与排序结构设计我现在拿一个实际做过的项目来复盘2D横版动作游戏玩法是角色在地图里打怪怪物会掉落装备界面左侧有角色血条和技能按钮顶部有金币数量底部有经验条。打怪时怪物被击中要飘伤害数字同时触发一个全屏的冲击波特效。怪物死亡时地面会出现拾取框弹出一个拾取确认面板。它的层级需求可以拆成这样最底地图背景装饰其次地面上的掉落物再上角色和怪物再上角色脚底特效、冲击波特效再上飘字伤害数字世界空间再上UI主界面血条、技能、金币、经验最顶拾取确认弹窗、暂停菜单这个排序如果全部靠Overlay UI SortingLayer搞定其实不难。但要求是“冲击波特效要盖在角色上方”同时“拾取确认面板不能出现在冲击波前面”。所以我的方案是UI用Screen Space - Overlay主Canvas SortingOrder10弹窗单独CanvasSortingOrder50世界空间的Sprite和特效都用同一套Sorting LayerBackGround0DropItem10Character20Effect30WorldText40飘字用TextMeshProSortingOrder设为41这样冲击波属于Effect层30在角色上20飘字在40压在冲击波上UI 10在Overlay最顶弹窗50压主界面。整个层级在逻辑上是严丝合缝的。5.2 代码实现动态挂载与参数设置关键代码方面有两个地方要重点说明。第一个是飘字功能。伤害数字是动态生成的每次打怪都可能创建好几个。如果每次都new一个TextMeshPro对象并设置SortingOrder运行久了对象数量膨胀GC压力和合批都会受影响。我用的方案是对象池缓存配置public class DamageText : MonoBehaviour { private TextMeshPro tmp; private Canvas canvas; private void Awake() { tmp GetComponentTextMeshPro(); canvas GetComponentCanvas(); } public void Show(string value) { tmp.text value; // 动态排序让最新的飘字盖住旧的 GetComponentCanvas().sortingOrder 41 Random.Range(0, 3); } }一个常见的坑是TextMeshPro组件直接挂在场景里时排序靠SortingOrder是生效的。但如果你像上面一样额外挂了一个Canvas那么SortingOrder就得在Canvas上设置TextMeshPro自身的SortingOrder会被忽略。很多新手在这两者之间反复试错最后发现是Canvas和TMP排序属性打架了。第二个是冲击波特效。它既可能是粒子也可能是Sprite动画。我用的是一个Sprite序列帧播放器挂在特效对象上将它的SortingLayer设为“Effect”Order in Layer设为10。代码只负责播放完自动回收层级设置全部在预制体里预先配好不运行时改。这样角色和怪物的各种通用特效可以复用同一套预制体不容易在层级上互相抢占。5.3 动态排序的全局管理不硬编码项目大了以后硬编码会变成灾难。我在这个项目里做了一个轻量级的枚举映射表public enum RenderLayerType { BackGround 0, DropItem 10, Character 20, Effect 30, WorldText 40, UI 1000, Popup 1050 }然后在所有需要设置层级的对象初始化处统一走一个静态方法避免手写字符常量public static class RenderLayerHelper { public static void SetSpriteLayer(GameObject go, RenderLayerType layer, int order 0) { var sr go.GetComponentSpriteRenderer(); if (sr ! null) { sr.sortingLayerName Default; // 根据项目配置 sr.sortingOrder (int)layer order; } } }这种做法带来的直接好处是当后期要把怪物特效整体提升一层时只需要改枚举全项目自动生效不用到处找硬编码的数字。这个习惯我沿用到了后续所有Unity项目里每次都能省下至少一晚上的排查时间。6. 特殊场景与极端情况的处理6.1 多摄像机合屏时的层级策略有些项目里主场景用一个摄像机UI用另一个专用摄像机特效再用一个摄像机最后通过多个Camera叠加显示。这时要掌握的核心原则是不同摄像机之间的排序优先级优先于任何一个摄像机内部的Sorting Layer。也就是说如果带UI的摄像机Depth是10而带特效的摄像机Depth是5那么无论特效在场景里SortingOrder调得多大它始终显示在UI摄像机之下。想用深度值制造“UI在特效前、普通场景在后”的效果就要细心管理每个摄像机的Depth顺序并让UI摄像机只看到UI层。一个常见做法是通过Culling Mask控制摄像机能看到哪些层。比如世界摄像机只渲染World层UI摄像机只渲染UI层特效摄像机只渲染FX层然后按Depth从小到大排世界、特效、UI。这样排序逻辑最清晰而且可以独立控制后处理效果。代价是每个摄像机都会多一次场景绘制移动端上有性能消耗需要权衡。6.2 阴影显示与遮挡的关联热词里提到“unity阴影问题”这个和层级还真有关联。当Sprite之间出现遮挡关系后阴影的渲染方式和光源的阴影参数会受Sorting Layer影响。在2D光照系统里阴影投射与接受是按SpriteRenderer的Sorting Layer来区分优先级的如果一个Sprite的Sorting Layer设得比另一个高它在光照下会产生“盖住”另一个Sprite阴影的效果。排查时先确认两个Sprite的Sorting Layer和Order in Layer是否正确再看Light组件的Sorting Layer和Sorting Order。很多“阴影忽然没了”的案例其实只是那个Sprite的Sorting Layer被某个脚本意外改了。6.3 微信小游戏小程序中的视频播放层级碰过Unity微信小游戏开发的应该都懂视频播放是多一个大坑。微信环境里视频是原生控件天然显示在所有WebGL画面之上Unity的UI层级约束不了它。你辛辛苦苦把UI排序做得干干净净结果原生视频播放器一出现什么Canvas、SortingLayer全在它底下。现阶段比较可行的处理方式是把游戏内“需要被UI遮挡的视频”拆成两种思路一种是视频作为全屏或区域播放时设计成“播放期间不接受UI叠加操作”或者UI在视频播放时自动隐藏另一种是视频画面通过RenderTexture转成Texture再贴到UIImage上用Unity自己渲染这样层级完全可控。后者性能开销大一些但换来了完整的层级自由。需要说明的是这种原生控件与游戏画面叠加的冲突是平台特性做小游戏适配时要提前在UI设计上留出规避空间。6.4 加载界面、遮挡过渡、动态特效的多个注意点加载界面是典型的“所有内容都隐藏只有加载图最顶”的场景。我建议加载界面单独做成一个Scene的UI或者一个最高SortingOrder的Canvas切场景时用DontDestroyOnLoad挂住保证它能在新旧场景之间无缝显示。它的Canvas Group可以瞬间设置BlocksRaycaststrue防止玩家在加载期间点击底层UI。全屏遮罩过渡比如“白屏闪进战斗”或“黑屏切关卡”也有同样的要求。黑屏要盖住所有现有UI同时生成黑屏的Canvas不能被切换场景时的清场景逻辑带走否则过渡到一半画面就暴露了。所以这类过渡对象也用DontDestroyOnLoad独立Canvas。特效方面比如“人物闪红”这种受击表现它们要盖在角色身上但又要被UI盖住。如果UI是Overlay模式特效对象只要在世界空间的SortingLayer上高于角色就行。如果是“全屏冻结冲击波”那种要盖住所有世界画面但不能盖UI的特效放Effect层UI Overlay自动压制它各安其事。7. 常见问题排查与避坑速查7.1 高频Bug对照表现象可能原因解决方案UI按钮点不到但显示正常缺GraphicRaycaster或上一级遮挡物BlocksRaycaststrue给Canvas/物体补GraphicRaycaster检查遮罩层UI显示在3D物体下面Canvas不是Overlay模式且sortingOrder过低改用Overlay或调高Canvas SortingOrderSprite盖住了UIUI模式为Camera/WorldSpace且Sprite的SortingLayer高于UI把UI加回Overlay或调整Sprite所在层的SortingLayer粒子特效在UI前面粒子SortingLayer被误设为UI层检查粒子Renderer里的SortingLayer与UI分层阴影消失了SpriteRenderer的SortingLayer被改或Light的排序异常检查SpriteRenderer和Light的Layer设置切换场景后弹窗没了弹窗Canvas挂在场景物体上被销毁用DontDestroyOnLoad或全局UI管理单例UI界面卡顿大Canvas频繁变更层级拆分Canvas动态元素独立避免SetAsLastSibling高频调用VideoPlayer黑屏但有声音平台原生视频和Unity渲染层冲突换RenderTexture方案或调整视频控件层级7.2 层级问题排查的四步法遇到“某某显示不对”的Bug我推荐按固定顺序排查不跳跃第一步确认对象属于UI还是Sprite/世界物体。看Inspector里有没有Canvas组件或者SpriteRenderer组件这一步决定了后面的排查方向。第二步区分是跨Canvas/世界对象的问题还是同一个Canvas内部的问题。跨层级的看Canvas.sortingOrder和Sorting Layer同层级的看Hierarchy顺序和Order in Layer。第三步检查摄像机的Depth和Culling Mask。一次只保留一个Camera显示逐个开启排查看哪一个把画面抢走了。第四步检查有没有隐藏的设置冲突。比如TextMeshPro的Canvas排序覆盖、粒子Renderer模块的默认值、CanvasGroup的alpha和raycast属性等。这个四步法基本能覆盖八成以上的层级问题排查一遍大概五分钟。我在培训新同事时也强调这套流程上手速度明显比直接搜代码快。7.3 三个最容易忽略的隐性影响源第一个是CanvasGroup组件。它除了影响透明度外还会控制下面的子物体是否接受射线。有时候你为了做一个渐隐效果给父物体挂了CanvasGroup且alpha降到0但子物体依然存在且rayscale仍然开着就会挡住下层按钮。第二个是EventSystem的Raycaster。UI上很多“透明但占用”的Image带着GraphicRaycaster即使Image的Color alpha是0它照样可以挡住后面的按钮点击。如果发现问题永远“点到底下”检查这个区域有没有透明的拦截物体。第三个是屏幕后处理或全屏Shader。有些后处理效果比如Bloom、全屏模糊会让整个画面像一个图层一样“盖”在UI上面或下面这个和SortingLayer无关但视觉上会让人觉得层级错了。遇到“所有东西颜色发灰”“边缘发光特效盖过UI”的情况先检查后处理配置别一股脑调排序。8. 性能优化排序操作对UI卡顿的影响热词里反复出现“ui界面卡顿”UI卡顿很大一部分原因是Canvas重建。任何UI元素的层级变化都可能触发Canvas的脏标记导致这个Canvas全部顶点重新计算一次。UI层级越复杂、元素越多重建开销越大。所以高频变动的元素比如伤害飘字、进度条、加载动画不要全部塞在一个大Canvas里。拆成独立Canvas后它的重建只影响自己不会让整个主界面卡一下。这是我的老生常谈但确实是最有效、最容易被忽视的性能优化手段。代码上的另一个建议是将连续多次的层级调整合并。比如同时要改父物体排序、再改子物体位置、再改一个Image的enabled尽量在一次Update里完成不要分散到三个协程或者被多个事件反复触发。触发一次Canvas重建还好触发三次就是浪费。SortingLayer和SortingOrder的修改速度也有讲究。运行中改SpriteRenderer.sortingLayerName要先查字符串比设置sortingOrder慢。如果仅仅是层层顺序往前移几位用sortingOrder就够了不要重设sortingLayerName。字符串比较在移动端上更容易察觉能省则省。最后再提醒一下Unity的Profiler面板里有一个Canvas.SendWillRenderCanvases节点它的耗时直接反映UI重建频率。做层级优化前后可以对比这个数值效果非常直观。我自己优化UI卡顿的项目里基本上都是把这个节点的时间压下去以后帧率才真正稳定下来。9. 个人经验层级方案在项目早期就要定好经过这几个项目的折腾我最大的体会是层级控制本身并不难难点在于“一开始就没定规矩”。很多团队开发到中后期才意识到层级混乱这时候再改Sorting Layer的命名、再调整Canvas结构成本极高。改一个Sorting Layer名字所有预制体里的引用全要更新一遍把UI从Overlay改成Camera模式所有弹窗的动画和坐标可能有微调拆分Canvas后挂在上面的脚本和动态加载逻辑都要配合调整。所以我的建议是任何2D/AR/UI重的Unity项目开工第一周就把这篇文章里的表格、枚举、规范定下来。哪怕是demo阶段也要按照最终方案的去搭UI结构。宁可前期多花小半天时间把层级设计方案写下来也不要后期加班排查几十上百个“显示异常”。每次项目结束回看那些在初期把层级方案做扎实的项目后期加功能、插新UI都快很多UI相关的Bug数量基本是个位数。反之匆匆开工的项目几乎没有例外地会在UI层级上栽跟头。层级控制是Unity里少有的“靠规划而不是靠技巧”的课题。把每一个对象的归属层、顺序值、遮挡意图想清楚它就是一件极其简单的事只要想偷懒它马上会变成最容易让你熬夜的事。
阅读完成 · 觉得有帮助?
咨询建站