ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Unity UI与触摸实战:Canvas优化、手势识别与按键反馈

Unity UI与触摸实战:Canvas优化、手势识别与按键反馈 翻了翻我之前给项目组做内部培训时留下的那份笔记标题就是“Unity游戏基础9UI再探讨触摸处理按键可视化响应”。说实话这一章才是很多新手从“能用UI”走到“UI真正好用”的分水岭。按钮、文本框、Image这些基础控件谁都会拖但一旦涉及到真机触摸、多指操作、按键反馈、界面卡顿这几件事没有真正上过移动端项目的人基本都会在这里翻车。我自己也是在一款休闲手游上线之后才把这几块内容彻底串起来的。这篇内容适合谁看两类人。第一类是刚做完几个Unity小Demo、准备上真机做商业项目的初级开发者第二类是已经在做独立游戏但总被UI交互手感、触摸穿透、界面卡顿折磨的中级开发者。我会把UI重新讲一遍重点放在Canvas结构、布局自适应和性能然后讲触摸处理的底层逻辑包括新旧输入系统怎么选最后讲按键可视化响应从Unity Button过渡机制到自定义按住反馈再到移动端虚拟按键的坐标映射。每部分都有可复现的代码和参数也会把我踩过的坑直接摊开说。1. UI再探讨把Canvas和布局问题彻底理顺1.1 Canvas三件套里容易踩的坑Unity的UI系统核心是Canvas但很多人对Canvas的认知只停留在“把UI放进去就行”。我在面试里经常问一个问题Canvas的Render Mode三种模式分别用于什么场景能答清楚的初级开发真的不多。Screen Space - Overlay是最常用的模式UI直接绘制在屏幕最上层不需要Camera。优点是部署快、新手友好缺点是它对Canvas重建的开销比较敏感尤其在移动端Overlay模式下的所有UI元素在尺寸变化、位置变化、文本内容变化时都可能触发Canvas重建这一块在低端安卓机上非常容易卡顿。Screen Space - Camera模式是把Canvas挂在一台UICamera上通过Plane Distance控制UI离相机的距离。这种模式在项目里最大的价值是让UI和3D场景能产生正确的遮挡关系比如角色站到UI按钮前面时按钮会被挡住而不是永远浮在最上层。做MMO、AR、VR类项目几乎必选。代价是多一台相机多一次渲染需要做好Culling Mask和Depth排序。World Space模式则是把UI放进3D世界坐标里常用于血条、名字浮标、VR里的面板交互。这里最容易踩的坑是缩放比例问题。很多人把World Space Canvas的RectTransform设成和屏幕分辨率一样的宽高结果放进场景里发现字大得离谱。正确做法是先定一个“世界单位下的UI尺寸”比如一张公告牌宽度设计成2.5米再把Canvas上的Reference Resolution按比例换算过去字体的Font Size也要跟着世界单位调整。还有一个经常被忽略的点每个Canvas都会连带一个Graphic Raycaster组件EventSystem通过它做射线检测。如果你的UI全部堆在一个Canvas里那么每帧都要对这个Canvas下所有Graphic做一次射线求交。UI元素数量到几百个以后这个开销在真机上会明显影响触摸响应速度。我的建议是项目里至少拆三个Canvas常驻UI顶栏、底栏、动态UI弹窗、飘字、战斗UI血条、技能按钮三者按Screen Space - Camera模式挂在不同Depth上。这样既方便管理层级也能隔离重建范围不至于一个弹窗动画把整个界面的帧率拖下来。1.2 Scaler、锚点与自适应布局的实战套路Canvas Scaler是另一个被严重低估的组件。默认的Constant Pixel Size在小屏手机上会直接把UI挤成一团所以大多数项目会切到Scale With Screen Size模式。这里有个关键参数Reference Resolution它决定了所有UI布局的基准。选参考分辨率时别拿自己开发机的分辨率硬套。你要先想清楚游戏是横屏还是竖屏主流的适配比例是什么。竖屏游戏我一般用1080 x 1920做基准横屏游戏用1920 x 1080。但真正影响适配效果的是Match Width or Height这个滑条0表示完全按宽度缩放1表示完全按高度缩放0.5表示宽度高度各占一半。实际项目里没有万能值我一般是根据UI里“长度敏感”和“高度敏感”元素的比例来调。如果你的UI上下栏固定高度、左右内容需要留白可以偏高度匹配如果所有界面都希望保持整体放大缩小就偏宽度匹配。拿0.5起步然后到不同比例的机器上跑一圈再微调。锚点Anchor体系是RectTransform的灵魂。很多新手做界面时习惯把元素放在一个固定位置换台设备就全乱套。正确套路是顶部标题锚点放在Top Center底部按钮组锚点放在Bottom Center侧边栏锚点放在Left Stretch或Right Stretch。用代码动态创建UI时也一定要先设anchorMin、anchorMax再设anchoredPosition否则你会发现坐标怎么算都不对。动态列表的适配是我在实际项目里花时间最多的地方。如果你用Layout Group Content Size Fitter做背包列表每次增删物品后都要手动调用LayoutRebuilder.ForceRebuildLayoutImmediate(rectTransform)否则新加进来的元素位置是错的这个操作还要放在当前帧布局修改之后。另外Content Size Fitter和Layout Group同时挂时尽量避免再对同一节点做Scale动画lattice重算会把动画打得乱七八糟。还有一个安全区问题iPhone的刘海屏、安卓的挖孔屏都会遮挡UI。Unity 2020之后的版本提供了Screen.safeArea可以在脚本里把根Canvas下的SafeAreaPanel的RectTransform按安全区范围重新设置一遍。这个组件建议做成独立的工具类放在项目公共代码库里所有界面统一挂在SafeAreaPanel下面而不是一个界面一个界面地手动留边距那样迟早会漏掉一两台机器。1.3 不要让UI卡顿毁掉前面的所有工作我在热词列表里看到了“UI界面卡顿”这个词几乎是每个Unity项目的必经之痛。UI卡顿的根源八成不是GPU渲染而是Canvas重建和布局重建。Canvas重建是什么就是Canvas下所有Graphic顶点、材质属性的数据被CPU重新计算然后重新提交给GPU。在Profiler的CPU模块里你会在Canvas.SendWillRenderCanvases这个函数下面看到一大片红色耗时。避免卡顿的第一原则动静分离。持续移动的UI元素飘字、滑动列表、加载转圈单独放一个Canvas静止的界面元素放另一个Canvas。为什么要这样因为一个Canvas里只要有一个元素需要重建Unity为了保证渲染排序正确经常会把这个Canvas下大量相邻元素一起重建。把动态元素隔离出去之后静态Canvas几乎不会有重建开销性能天差地别。第二个原则不要每帧修改会触发布局的属性。最常见的就是每帧修改Text.text显示帧率或者倒计时。Text内容一变UGUI会重新生成顶点数据还会标记父节点布局脏一连串消耗全来了。正确做法是降低更新频率比如每秒更新4次足够或者用TextMeshPro的m_Text属性直接改字符串仍然会重建所以最好的方案是少做实时文本改成高频事件驱动更新。第三个原则Mask和RectMask2D别乱用。老式Mask组件会开启模板缓冲Stencil Buffer它会影响整个UI绘制链一个列表里有10个MaskDrawCall和填充率都会成倍上升。大部分“显示图片的一部分”的需求用RectMask2D就够了它是通过裁剪矩形来处理的性能开销比Mask低一个量级。我在项目里甚至会把RectMask2D也当成需要优化的对象能用Layout Group控制显示区域的就不挂裁剪组件。图上性能优化还要提到图集。UI的Image sprite尽量全部打进SpriteAtlas按模块拆图集公共图集、主界面图集、弹窗图集、战斗图集。图集太大反而增加加载和显存压力单个图集约2048x2048就够用。DrawCall正常情况下UGUI会自动合批前提是相同材质、相同图集的元素在层级上相邻所以把相同图集的按钮、图标、分割线放在相邻节点也是一种无代码就能拿到的优化。2. 触摸处理从Input.touches到输入系统的升级2.1 touches数组、GetTouch和点按相位触摸处理是所有移动端项目躲不开的硬骨头。Unity老旧的Input类里有两个获取触摸数据的方式Input.touches直接返回一个Touch数组Input.GetTouch(index)则按索引返回单个Touch。从接口上看差不多但细节上有讲究。Touch是一个结构体包含了fingerId、position、deltaPosition、deltaTime、tapCount、phase这几个核心字段。fingerId是手指唯一标识多指操作时靠它区分是哪根手指在动。position是屏幕坐标左下角为原点注意和UI的RectTransform坐标不是一回事。所以实战里做坐标转换前先想清楚这个坐标是屏幕坐标还是Canvas坐标混用是最常见的低级错误。phase是TouchPhase枚举有Began、Moved、Stationary、Ended、Canceled五个值。一根手指按下时phase是Began移动时是Moved按住不动是Stationary抬起是Ended被系统打断来电、下拉控制中心是Canceled。判断手势时一定要同时看phase和时间戳不能只凭position变化。这里有个坑很多新手在Update里写一个foreach (Touch t in Input.touches)就开始处理逻辑简单时没问题但如果你要在多指手势里追踪手指ID用for循环加GetTouch会清晰很多void Update() { for (int i 0; i Input.touchCount; i) { Touch touch Input.GetTouch(i); switch (touch.phase) { case TouchPhase.Began: // 记录起始位置和时间 break; case TouchPhase.Moved: // 处理拖拽增量 break; case TouchPhase.Ended: // 判断是点击还是甩动 break; } } }用GetTouch的好处是循环次数和触摸数量严格一致索引稳定不容易因为数组变化产生错乱。还有一个容易被忽略的开关Input.multiTouchEnabled默认是开的但如果你只做单指点击类游戏可以在启动时把它关掉减少误触和性能开销。同样Input.simulateMouseWithTouches在纯触摸项目里可以关闭避免触摸事件被同时转成鼠标事件导致某些UI控件收到双份输入。2.2 手势识别的几个判断模板点击、滑动、双指缩放手势识别的本质是“时间 位移 速度”的组合判断。拿点击来说手指从按下到抬起时间小于0.2秒、位移小于一定像素阈值才认为是一次点击。那个阈值多少合适不能写死因为不同设备DPI不一样。我在项目里会把阈值换算成“相对于屏幕宽度的百分比”比如屏幕宽度的1.5%大约在1080p下是16到20像素。这样在iPad和安卓小屏手机上表现一致。判断滑动手势则需要记录手指的起始位置和当前位移同时看速度。Unity的Touch本身就带deltaPosition但它是“这一帧相对上一帧”的增量直接累加即可。滑动距离超过阈值后再结合水平和垂直方向的位移差判断是横滑还是竖滑。这个逻辑在UGUI的ScrollRect里已经被封装好了但如果你要做战斗操作中的滑动释放技能ScrollRect就帮不上忙只能自己写private Vector2 startPos; private float startTime; private bool isSwiping; void OnTouchBegan(Touch touch) { startPos touch.position; startTime Time.unscaledTime; isSwiping false; } void OnTouchMoved(Touch touch) { float dist Vector2.Distance(startPos, touch.position); if (dist swipeThreshold !isSwiping) { isSwiping true; // 触发一次滑动开始逻辑 } }双指缩放是另一个高频需求。核心是计算两指之间的向量长度用当前距离除以上一帧距离得到缩放比例。注意每一帧都要更新基准距离否则缩放会越来越快private float lastPinchDist; void Update() { if (Input.touchCount 2) { Touch touch1 Input.GetTouch(0); Touch touch2 Input.GetTouch(1); float dist Vector2.Distance(touch1.position, touch2.position); if (touch1.phase TouchPhase.Began || touch2.phase TouchPhase.Began) { lastPinchDist dist; } else { float scaleFactor dist / lastPinchDist; // 用scaleFactor缩放目标 lastPinchDist dist; } } }这套方案的问题在于它没有区分“哪根手指是新按下的”所以如果三根手指切到两根计算会突然跳变。更稳的做法是记录每根手指的fingerId只有当参与缩放的恰好是当前记录的那两根手指时才更新距离。另外在多指手势里一定不要用Input.GetTouch(0)、GetTouch(1)的索引去固定“这是哪根手指”因为索引和手指没有绑定关系必须用fingerId做映射。2.3 EventSystem事件接口与新输入系统的取舍前面讲的是“底层坐标流”但游戏里更多时候我们要的是“语义化事件”按下、抬起、拖拽、点击。Unity的EventSystem把这一层封装好了你只需要在MonoBehaviour上实现IPointerDownHandler、IPointerUpHandler、IDragHandler这些接口Unity就会在合适的时机回调。这套事件系统的底层是GraphicRaycaster它把触摸坐标转成UI射线命中哪个元素就把事件发给哪个元素。直接用事件接口的好处是省去自己维护一堆触摸状态。比如实现一个虚拟摇杆主要逻辑在IDragHandler里public class VirtualJoystick : MonoBehaviour, IDragHandler, IPointerDownHandler, IPointerUpHandler { public void OnDrag(PointerEventData eventData) { // eventData.position是当前屏幕坐标 // eventData.delta是相对上一帧的位移 RectTransformUtility.ScreenPointToLocalPointInRectangle( bgRect, eventData.position, eventData.pressEventCamera, out Vector2 localPos); // 计算摇杆偏移量并限制半径 } }这套事件系统在处理多指时也有机制每个触摸点会生成一个独立的PointerEventData通过pointerId区分。所以你按着左边的摇杆再按下右边的技能按钮两边事件是隔离的不会串。多数情况下你不需要自己碰touch数组直接用事件接口更省心。到了Unity 2019以后官方推荐新输入系统Input System Package。它把触摸、鼠标、键盘、手柄统一成了ActionAsset还支持EnhancedTouch模式。如果你要做跨平台游戏手柄触摸键鼠新输入系统的确更科学输入设备只负责事件源逻辑层监听Action不用关心玩家用的是手柄还是键盘。新旧输入系统的切换有个特别烦人的坑如果你在Player Settings里把Active Input Handling设成“Input System Package”那么旧的Input类就会报错如果你把它设成“Both”老代码里Input.GetTouch仍然能用新输入系统的代码也能跑但会有双份输入事件同时触发的风险。举个例子你用新输入系统写触摸监听同时项目里的UGUI还是走旧Input的模拟鼠标事件屏幕可能会收到两次点击。所以实战策略是新项目直接全部切新输入老项目继续用旧Input不到万不得已别在中间状态卡太久。EnhancedTouch是新输入系统里做触摸识别最舒服的API它会维护一个activeTouches列表每根手指都有对应的Touch对象和press/release事件using UnityEngine.InputSystem.EnhancedTouch; void OnEnable() { TouchSimulation.Enable(); EnhancedTouchSupport.Enable(); Touch.onPress OnTouchPress; Touch.onRelease OnTouchRelease; }用EnhancedTouch的另一个好处是它自带“触摸历史记录”可以很方便地算滑动速度和单击双击不需要自己维护一堆状态变量。不过在移动端的UI事件响应上新输入系统的GraphicRaycaster需要通过StandaloneInputModule的替换组件InputSystemUIInputModule来驱动别漏配否则UI点了没反应。3. 按键可视化响应手感就是这么一点一点调出来的3.1 Transition三兄弟ColorTint、SpriteSwap和Animation该怎么选按键可视化响应翻译成大白话就是玩家手指按下去的那一刻屏幕上要有立即的视觉反馈。这个反馈做得好不好直接决定玩家评价“手感”是薄还是厚。Unity Button组件自带Transition选项一共三种。ColorTint是最常用、成本最低的方案。它在不同状态下把targetGraphic的CanvasRenderer颜色设置成指定色Normal、Highlighted、Pressed、Selected、Disabled。实践要点是Pressed Color别调成纯黑或纯白那会让按钮看起来像坏了一样一般按下的颜色调成正常色往暗走一个梯度比如白色按钮按下去变#C8C8C8手感就很自然。Highlighted悬停在移动端几乎不会被触发但PC端需要保留颜色比Normal亮一点点即可。SpriteSwap适合需要替换贴图的按钮比如按下变成“凹陷”状态的图片。这里最大的坑是Sprite需要预先准备多张图一旦做图集合批这些状态图必须在同一个图集里并且遵守对应命名规范否则会额外产生DrawCall。如果你的项目用的是动态生成图集SpriteAtlas运行时不勾选Include in BuildSpriteSwap很容易在真机上出现纹理丢失因为我遇到过太多次了。Animation是三种里最重的方案它会给Button对象生成一个Animator Controller通过AnimationClip播放状态变化。优点是表现力强可以做复杂的缩放、旋转、颜色组合动画缺点是一个按钮一个Controller资源量和管理复杂度都会上涨。项目里如果只有三五个需要特殊反馈的按钮用Animation没问题但整个UI几十个按钮都用它建议先考虑性能。我的通用准则是普通界面按钮用ColorTint主界面的核心操作按钮用SpriteSwap或纯代码做Scale回弹动画。为什么核心按钮推荐Scale回弹因为人眼对物体大小变化的敏感度远高于颜色变化按下时按钮缩小到0.9倍再弹回原状比变色要“有肉”得多。这个效果用DoTween或协程几行代码就能实现我在3.2节会给出可直接用的代码。3.2 自定义按住反馈接口与协程Unity Button自带的是“一次性点击”反馈但游戏里很多操作是“按住持续生效”的比如移动方向键、加速按钮、蓄力攻击。这时用Button组件就没法直接满足需要自己实现一套按住按钮。我的实现思路是这样做一个HoldButton脚本继承MonoBehaviour并实现IPointerDownHandler、IPointerUpHandler、IPointerExitHandler、IPointerEnterHandler四个接口。按下时开始触发抬起时停止触发手指按住不放移出按钮区域时也停止避免玩家手指滑出去还误触发public class HoldButton : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IPointerExitHandler, IPointerEnterHandler { public UnityEvent onHold; private bool isHolding; public void OnPointerDown(PointerEventData eventData) { isHolding true; StartCoroutine(HoldLoop()); } public void OnPointerUp(PointerEventData eventData) { isHolding false; } public void OnPointerExit(PointerEventData eventData) { isHolding false; } public void OnPointerEnter(PointerEventData eventData) { if (Input.GetMouseButton(0)) // 拖动进入时恢复 isHolding true; } private IEnumerator HoldLoop() { while (isHolding) { onHold.Invoke(); yield return new WaitForSecondsRealtime(0.1f); } } }这里有个关键细节我用了WaitForSecondsRealtime而不是WaitForSeconds。因为游戏暂停时Time.timeScale会变成0WaitForSeconds在这种状态下直接冻结而WaitForSecondsRealtime不受影响。你暂停游戏时还能继续触发按住逻辑在某些场景比如暂停界面调节音量很有用。配合手感还有一个细节按下瞬间的视觉反馈延迟。很多新手把动画时长设到0.2秒以上结果手指按下去按钮半天没反应玩家会觉得“按钮发闷”。我的参数习惯是按下动画0.05到0.08秒回弹动画0.1到0.15秒整体控制在0.25秒以内。超过0.3秒就会明显觉得“按键迟滞”。长按和普通点击的冲突处理也值得说。一个按钮既要响应普通点击比如打开详情又要支持长按比如连续使用道具实现上通常是记录按下时间和位置在抬起时根据按下总时长判断是短按还是长按短按触发点击逻辑长按进入长按循环。注意当长按条件满足时要标记“本次触摸已消费”否则抬起时还会误触一次短按逻辑。3.3 移动端虚拟按钮的坐标映射与多指协同移动端没有键盘鼠标玩家通过屏幕上的虚拟按键操作。虚拟按键的坐标映射是个隐藏问题Button的点击判定由EventSystem负责只要按钮UI在屏幕上点击就能命中但如果你要写自定义拖拽、绘制轨迹、或者把屏幕坐标传给3D对象InverseTransformDirection就必然涉及坐标变换。最常见的变换是屏幕坐标转Canvas局部坐标核心API是RectTransformUtility.ScreenPointToLocalPointInRectangleRectTransformUtility.ScreenPointToLocalPointInRectangle( canvasRect, screenPos, uiCamera, out Vector2 localPos);其中uiCamera参数要传你的UICamera。如果Canvas是Overlay模式这个参数传null即可如果是Screen Space - Camera模式必须传对应的UICamera否则坐标会偏。Canvas带Canvas Scaler做缩放时这个API会按Scaler的缩放比例帮你换算所以比自己去除缩放系数要可靠得多。多指协同按键则是移动端手感里最容易被低估的部分。FPS手游的左手移动摇杆、右手瞄准射击两根手指在不同UI区域按下互不干扰这依赖于EventSystem对每个触摸点独立分配PointerEventData。但如果你是绕开EventSystem自己读触摸做响应比如某些独立游戏项目就一定要用fingerId去建立索引而不是遍历全部触摸挨个处理。我做一个多按钮虚拟键盘时踩过一个大坑手指从按钮A滑到按钮B再抬起Button A收到了OnPointerUp但Button B收不到OnPointerDown因为手指进入B时鼠标键已经处于按下状态。解决方案是在OnPointerEnter里判断当前是否处于按下状态然后手动切换高亮和触发逻辑。事件接口并不难难的是把所有边界状态想全按下不抬起直接拖出边界、拖回边界、从A滑到B再滑回A。按压反馈里还有一个体验层面的点震动。移动端游戏为了强化打击感会在关键操作时触发手机震动。Unity官方没有跨平台的震动APIAndroid上我用AndroidJavaObject调用Vibrator服务iOS则需要接原生插件或使用第三方的SimpleHaptic插件。注意震动频率别太高连续高频震动不仅耗电玩家也会觉得很烦。我个人的经验是只有“确认、攻击、成功”这类正向事件才配震动普通UI点击最多给一点微弱的Haptic或者干脆不加。4. 常见问题与排查技巧实录真机上的那些坑4.1 透明物体挡了触摸Raycast Target的隐藏杀手这是我排查过最多的问题之一某个透明的Image或者Text明明不显示了却还在挡住后面的按钮点击。表现形式是界面某一整块区域点了没反应排查半天发现那块区域上面压了一层透明的Image。为什么会这样UGUI的Graphic组件上有个Raycast Target勾选项。它跟图片是否透明完全无关只要勾选了这个Graphic就会参与GraphicRaycaster的射线检测。很多UI框架在生成背景遮罩或装饰元素时Image设置成纯透明色Alpha0但忘了关Raycast Target结果就是整块区域都“被挡住”。排查方法很简单在Editor里选中问题节点看EventSystem当前选中的GameObject是什么或者直接打开场景视图里Overdraw调试模式一层层找。改的话不要只改一个要通查所有装饰性的Image、Text、ScrollRect的content节点。我做项目有个规定所有纯装饰元素创建的ImageRaycast Target一律关掉需要响应事件的才开。这样从源头杜绝问题。还有一个相关坑CanvasGroup的alpha设成0来隐藏UI这个UI仍然会接收射线。很多人以为alpha0就“看不见摸不着”了其实触摸还是会被拦截。隐藏UI应该用SetActive(false)或者同时把CanvasGroup的blocksRaycasts设成false。如果你只是因为动画需要把透明度降到0但还要保留节点记得禁用blocksRaycasts否则它是真真切切的“隐形墙”。4.2 UI卡顿自查清单从帧率曲线到Canvas拆分UI卡顿的排查思路至少比“项目卡了”要具体得多。我在项目里排UI卡顿时的固定流程是这样的先开Profiler看CPU模块的Canvas.SendWillRenderCanvases、Layout.Rebuild、Graphic.Rebuild这三个函数的耗时然后开FrameDebugger看渲染管线的DrawCall和Overdraw最后用真机Profiler跑5分钟记录帧率曲线。如果Canvas.SendWillRenderCanvases耗时高说明Canvas重建频繁。常规修复就是我前面说的“动静分离”把飘字、转圈、滑动列表放到独立Canvas下。如果Layout.Rebuild耗时高说明布局系统在被频繁标记dirty最常见的元凶是Content Size Fitter搭配动态内容或者频繁修改Text内容。后者可以改用对象池 预生成文本块而不是每次都创建一个新的Text实例。如果这两个都不是再看Overdraw。Overdraw指同一像素被绘制多次UI上最常见的原因是半透明大图层层叠叠。一个按钮正常情况是“一张背景图 一个文本”两层但美术资源不规范时按钮会被拆成阴影层、描边层、渐变层、高光层每一层都是半透明贴图叠加起来GPU填充率直接爆炸。移动端UI的Overdraw控制在2到3倍以内还算健康超过5倍就该找美术同事聊聊了。还有一个小技巧在Profiler里看到Canvas.RecreateBatch耗时高多半是Canvas里的元素顺序被频繁改变SetAsLastSibling、SetActive true再false每变一次都会让UGUI重新生成批次。尽量避免在帧中反复调整UI节点的SiblingIndex。用对象池隐藏/显示元素时也尽量在同一个父节点下操作不要让一个物品的显示导致整个列表重新合批。4.3 打断与误触菜单切后台、ScrollRect和Button抢事件真机上的触摸和编辑器里最大的差别是什么是“被打断”和“误触”。这两点不做防护游戏上线后会被玩家骂死。打断是指打电话、下拉通知栏、切后台这类系统操作。Unity里触摸会收到一个TouchPhase.Canceled同时OnApplicationPause和OnApplicationFocus会被调用。很多新手只处理了Began和Ended忘了Canceled结果手指被系统打断后游戏以为手指还按着角色一直朝一个方向跑。处理方法是把所有触摸状态机在Canceled、OnApplicationPause、OnApplicationFocus(false)里统一重置清空手指数、停止所有按住协程、把所有Toggle状态复位。误触方面最经典的是ScrollRect和Button抢事件。一个按钮放在ScrollView里玩家本来想滚动列表结果滚到一半把按钮触发了。Unity原生方案是EventSystem的Drag Threshold默认是10像素也就是说拖拽超过10像素后才知道这是“拖动”而不是“点击”。但不同设备DPI不同10像素在iPad上可能只有2毫米在安卓小屏手机上却是6毫米手感差异很大。我的做法是写一个自适应DragThreshold脚本在启动时把EventSystem.current.pixelDragThreshold乘上一个与屏幕DPI相关的系数。判断拖拽还是点击在自定义手势里也别用“位移为0才叫点击”。手指按压时轻微抖动几像素是非常常见的阈值设得太小会导致玩家单击经常变成拖动反思一下你的应用场景。用手势状态机管理按下时先假定是一次“潜在点击”只有位移超过阈值才升级为“拖动”这样能让点击和滚动共存比在事件回调里去烦恼交互要聪明得多。ScrollRect还有一个坑当子物体的锚点不是以ScrollRect的Content为中心时拖动偏移会不稳定。如果你用了Stretch锚点加content的pivot一开始不在0.5, 0.5拖拽和Snap功能都会出问题。建议Content上永远使用pivot (0.5, 0.5)想要偏移就对整个Content加Padding。不要硬改pivot去适配边距Compensation计算会让人崩溃。最后聊点个人的经验。这套内容我前后在三个项目里落地过每次真正决定一款游戏“手感好不好”的不是代码写得多花哨而是那些细节参数有没有人愿意一遍遍拿真机去试。触摸阈值、按住触发的间隔、反馈动画的时长、震动的强度每一样都必须在目标设备矩阵上实测不能只在编辑器里拍脑袋。如果你刚接手一个移动端Unity项目我强烈建议立项阶段就把这三块当成一等公民来设计UI铺完再去补触摸反馈和性能优化那真的是拆东墙补西墙的活儿。做个测试面板把这些参数全部暴露出来让策划和测试在真机上自己拖比你一个人埋头调半天空想有效得多。
返回列表