做滑动类交互方向判断永远是绕不开的一块。不管是手游里的手势出招、卡牌翻页、地图拖拽还是某个内部工具的拖拽面板都要回答同一个问题鼠标或手指到底朝哪个方向拖了Unity里判断鼠标上下左右拖动方向看起来是个小功能实际落地时却能牵扯出坐标系、阈值、事件接口、多点触控一系列坑。这篇围绕标题展开把设计思路、坐标陷阱、移动端差异、调试技巧一次性梳理清楚用的是我平时项目里验证过的写法直接能搬运。1. 先把需求说清楚你要的是“一次判定”还是“持续反馈”1.1 判定时机决定了代码结构很多刚接触的人拿到这个需求第一反应就是写一个“按下时记录坐标松手时算差值”的流程。这确实是最常见的形态适合“滑动结束后给出方向”的场景比如技能出招、页面翻页、手势密码这一类。玩家按下、拖动一段距离、松手系统根据起点和终点的向量给出一个方向结果整个过程只需要在MouseUp或者EndDrag事件里计算一次。但还有另一类需求是要在拖动过程中持续反馈方向。典型的例子是虚拟摇杆、小地图拖拽、或者某些3D场景里的视角旋转。这时候不能等松手才算要在每一帧拖动过程中不断输出方向信息并且还要处理“方向抖动”的问题——手稍微一晃方向来回跳交互体验就很难受。这两种需求写法的差异非常大。前者简单粗暴把事件挂在松手那一下就行后者要考虑事件触发的频率、方向锁定的策略、以及阈值和灵敏度配合。先想清楚自己要哪一种再去选方案能省掉后面至少一半的返工。1.2 三种主流方案直接比较、点积、角度映射我在项目里反复用过三种方案各有各的适用场景谁也别瞧不起谁。第一种是直接比较法核心逻辑就是取起点到终点的向量比较纵向和横向的绝对值。|delta.y| |delta.x|就是上下方向否则是左右方向再根据正负确定具体是上还是下、左还是右。这个方案代码量最少性能几乎为零开销而且非常直观。它回答的问题是“整体更偏哪个方向”对于纯粹的上下左右分类来说这已经足够了。第二种是点积方案。思路是把拖动向量归一化然后和基准方向做点积比如和Vector2.up做点积点积值越接近1说明拖动方向越接近上方。有人喜欢用点积来判断四方向我个人不太推荐新手走这条路因为四方向分类用绝对值比较就够了点积反而绕了一圈。但点积真正的优势在于“对比参考方向”比如角色有一个朝前的方向你想判断玩家拖动的方向是否大致和角色朝向一致这时候点积就非常合适。如果是判断“朝哪个方向拖”这种场景点积并不是最高效的。第三种是角度映射法。用Mathf.Atan2把拖动向量直接换算成角度然后根据角度落在哪个区间来映射方向。这个方案代码量居中但扩展性最强。45到135度判定为上负45到45度判定为右以此类推想切成八方向或者十六方向改改区间就行。如果要做一个扇形技能手势需要一个精确的角度值传给技能系统那角度映射法就是唯一比较自然的选择。三种方案没有绝对的优劣我自己的选型习惯是纯上下左右四方向直接用第一种需要连续角度或者八方向的用第三种需要和某个基准方向对比的用第二种。这样挑下来代码一直保持在一个可控的复杂度内。方案优点缺点适用场景绝对值比较简单、性能最好无法扩展为八方向或角度输出上下左右四方向判定点积比较灵活处理“与基准方向一致”四方向分类上不如绝对值直接与朝向、基准向量对比角度映射可输出精确角度、可扩展方向数边界处理要小心手势技能、八方向、扇形判定2. 决定结果质量的三个细节阈值、坐标系、多指输入2.1 阈值不是参数是防误触的第一道防线“轻点一下也触发方向”这个现象我见过太多次了本质上就是没有最小判定距离。玩家本来只想点一下按钮结果手指挪动了几个像素系统就当成了一次滑动。解决方式是在计算方向之前先判断拖动距离小于阈值就直接返回不做方向判定。阈值的大小不能拍脑袋定成固定像素值。不同分辨率的设备上固定像素对应的物理尺寸完全不一样。手机高分屏上300像素可能才一厘米PC端显示器上30像素就能顶一厘米。我的做法是用屏幕高度作为参考系minDragDistance Screen.height * 0.015f也就是屏幕高度的1.5%作为默认阈值。这个值在绝大多数设备上都能获得体感一致的结果也方便策划同学在配置表里做微调。针对不同的使用场景阈值还得做二次调整。如果是技能手势1%到2%之间手感比较合适太高手势就变成“要拖很远才生效”如果是翻页类交互3%到5%也没问题因为这种场景本来就不希望响应过于灵敏。改参数的时候注意每次修改都要在真机上验证一遍模拟器上的尺寸和真机永远有差距。2.2 坐标系混用是方向反了的元凶屏幕坐标、UI坐标、世界坐标这三套坐标系混在一起是方向判定最容易出错的地方。Input.mousePosition返回的是屏幕坐标原点在屏幕左下角y轴向上。而Canvas的RectTransform默认锚点位置在中心pivot也在中心。如果你直接拿屏幕坐标去和UI坐标做加减方向完全可能错乱甚至出现“明明往右上方拖系统判断成左下”的结果。正确做法是用RectTransformUtility.ScreenPointToLocalPointInRectangle把屏幕坐标转换成指定RectTransform上的本地坐标然后再用本地坐标做差值。这个函数在UGUI的拖拽场景里几乎是标配。很多UI拖拽判断之所以出现莫名其妙的偏移十有八九是没做这一步直接拿屏幕像素坐标和收藏的UI位置做了比较。这个坐标系问题可以类比成两个人都拿着地图但一个人用的是“以自己为中心”的坐标另一个人用的是“以地图中心为原点”的坐标两人对着同一个地标报的坐标永远对不上。2.3 移动端触摸不能简单套用鼠标逻辑Unity老版本默认会把第一根触摸模拟成鼠标事件于是很多人在手机上直接写Input.GetMouseButtonDown也能跑起来。但一旦涉及多指操作这套逻辑就不成立了。真实的触摸处理必须走Input.touches根据每个触摸的fingerId来跟踪在Began阶段记录起始点在Moved阶段更新位置在Ended或者Canceled阶段做最终判定。不能假设第0根手指一直是玩家主要操作的那根手指因为玩家可能先放一根手指在屏幕上再放另一根开始操作。UI层面其实有更省心的选择就是EventSystem提供的那一套接口。IPointerDownHandler、IDragHandler、IEndDragHandler天然支持多指区分PointerEventData里的pointerId会告诉你每一根手指对应的事件EventSystem内部已经帮你做了筛选。在UI上做手势判断优先考虑这套接口手写触摸遍历反而容易出各种边界问题。3. 实操从零实现一个可复用的方向判定组件3.1 一个能直接用的DragDirectionDetector下面这个组件是我平时用来做手势方向判定的基础模板挂在任意UI对象上就能用。它基于事件接口实现不需要在Update里轮询也不用手动判断点击穿透。using System; using UnityEngine; using UnityEngine.EventSystems; public enum DragDirection { None, Up, Down, Left, Right } public class DragDirectionDetector : MonoBehaviour, IPointerDownHandler, IDragHandler, IEndDragHandler { [Header(灵敏度屏幕高度的百分比建议0.01~0.02)] [Range(0.005f, 0.05f)] public float minDragPercent 0.015f; public event ActionDragDirection OnDirectionDetected; public event ActionDragDirection, float OnDirectionWithDistance; private Vector2 startScreenPos; private RectTransform cachedRectTransform; private void Awake() { cachedRectTransform GetComponentRectTransform(); } public void OnPointerDown(PointerEventData eventData) { startScreenPos eventData.position; } public void OnDrag(PointerEventData eventData) { // 拖动过程中想要实时反馈的话可以在这里做持续判断 // 但大多数四方向判定放在松手时做避免抖动 } public void OnEndDrag(PointerEventData eventData) { float minDragDistance Screen.height * minDragPercent; // 起点终点都转换到当前UI对象的本地坐标避免坐标系不一致 RectTransformUtility.ScreenPointToLocalPointInRectangle( cachedRectTransform, startScreenPos, eventData.pressEventCamera, out Vector2 startLocal ); RectTransformUtility.ScreenPointToLocalPointInRectangle( cachedRectTransform, eventData.position, eventData.pressEventCamera, out Vector2 endLocal ); Vector2 delta endLocal - startLocal; if (delta.magnitude minDragDistance) { OnDirectionDetected?.Invoke(DragDirection.None); return; } DragDirection result JudgeDirection(delta); OnDirectionDetected?.Invoke(result); OnDirectionWithDistance?.Invoke(result, delta.magnitude); } private DragDirection JudgeDirection(Vector2 delta) { if (Mathf.Abs(delta.y) Mathf.Abs(delta.x)) { return delta.y 0 ? DragDirection.Up : DragDirection.Down; } else { return delta.x 0 ? DragDirection.Right : DragDirection.Left; } } }这段代码有几个细节值得说明。第一计算方向用的是累计位移也就是终点减起点不是当前帧位移。Unity事件接口里的eventData.delta表示的是当前帧的位移量分辨率高的设备上同一段拖动会触发很多次事件拿单帧位移做判定基本都是一小段碎片路径反而会得到错误的分类。第二我做了两次坐标转换把起点和终点都转到同一个本地坐标空间这样既不会受Canvas渲染模式影响也不会出现UI缩放导致的方向判断偏差。事件的使用方式也很简单在脚本里直接订阅private void Start() { DragDirectionDetector detector GetComponentDragDirectionDetector(); detector.OnDirectionDetected HandleSwipeDirection; } private void HandleSwipeDirection(DragDirection direction) { Debug.Log(检测到方向: direction); }选中事件驱动而不是Update轮询是因为它天然契合UI交互的节奏OnEndDrag只在松手那一下触发一次逻辑开销可以忽略不计而且不需要手动去判断鼠标是否还按住、是否拖出了哪个区域EventSystem会把这些状态帮我们维护好。3.2 进阶处理手抖和方向锁定实际使用的时候会发现一个很头疼的问题就是手抖。玩家的手指在斜上方轻微左右晃方向在Up和Right之间反复跳。如果不做任何处理最终结果完全取决于松手那一瞬间的微小位移有时候明明整体是向上的手势却判定成了右。一个有效的处理思路是做方向锁定。拖动过程中在第一次检测到有效方向之后把这个方向保存下来后续拖动不管怎么晃都不再更改直到松手时直接用这个锁定的方向作为结果。这个方案对“手势出招”这类要求启动方向明确的场景非常合适但对“拖动地图、拖动视角”这类需要实时响应的场景就不适用如果做实时响应需要的是后文的方向置信度累积方案。方向锁定的实现很简单在OnDrag里增加一次累计位移判断private bool directionLocked; private DragDirection lockedDirection; public void OnDrag(PointerEventData eventData) { if (directionLocked) return; RectTransformUtility.ScreenPointToLocalPointInRectangle( cachedRectTransform, startScreenPos, eventData.pressEventCamera, out Vector2 startLocal ); RectTransformUtility.ScreenPointToLocalPointInRectangle( cachedRectTransform, eventData.position, eventData.pressEventCamera, out Vector2 endLocal ); Vector2 delta endLocal - startLocal; float minDragDistance Screen.height * minDragPercent; if (delta.magnitude minDragDistance) return; lockedDirection JudgeDirection(delta); directionLocked true; }方向锁定有一个很重要的配套操作在OnEndDrag结束时要记得复位。很多人写完锁定逻辑第二次手势发现方向永远沿用上一次的结果就是因为directionLocked没有在OnEndDrag里设置为false。这个坑我踩过一次检查半天才发现是少了复位。如果要做实时反馈又不希望方向抖动可以用“帧数累积”的方式。连续几帧的方向都指向同一个方向才把这个方向作为有效方向输出。方向不一致时累积计数清零。这个方案比单纯锁定复杂一些需要在OnDrag里做逐帧计算但对摇杆这类场景很管用。3.3 实战案例手势技能的方向判定把上面的组件接进一个手势技能系统是之前一个模拟项目X里的实际任务。项目需求很简单上下左右滑动分别触发不同的技能上滑跳跃、下滑蹲、左滑位移、右滑冲刺。技能系统的代码大概是这个逻辑public class GestureSkillController : MonoBehaviour { [SerializeField] private DragDirectionDetector gestureDetector; private void Awake() { gestureDetector.OnDirectionDetected OnGestureDetected; } private void OnGestureDetected(DragDirection direction) { if (direction DragDirection.None) return; switch (direction) { case DragDirection.Up: Debug.Log(触发跳跃技能); break; case DragDirection.Down: Debug.Log(触发下蹲); break; case DragDirection.Left: Debug.Log(触发向左位移); break; case DragDirection.Right: Debug.Log(触发冲刺); break; } } }这套接线的稳定性很高最主要的原因是把“如何判断方向”和“方向对应的业务逻辑”完全拆开了。方向判定组件只负责输出结果不管这个结果是用来触发技能还是翻页业务侧只关心拿到什么样的方向至于方向怎么来的完全不用理会。想改手势规则比如把左右手势对调只需要改方向判定的映射逻辑技能系统代码一行都不用动。距离信息也可以一起传出来。比如技能强度需要按拖动距离缩放就利用OnDirectionWithDistance事件里的float参数做处理这个参数实际上是起点到终点的累计位移量不受拖动速度影响可以放心用作强度变化。4. 常见问题与调试技巧实录4.1 高频问题速查表现象原因解决方案轻点一下也触发方向没有设置最小判定距离或阈值过小增加minDragDistance用屏幕高度百分比判断方向与实际滑动方向相反UI坐标系转换遗漏或Canvas缩放不一致用ScreenPointToLocalPointInRectangle转换到本地坐标UI上拖动没反应Input.GetMouseButton被UI层吞掉了点击改用EventSystem的IDragHandler系列接口双指操作方向判断混乱只跟踪了第一根手指手指切换后信息丢失按fingerId记录触摸状态改分辨率后灵敏度剧烈变化阈值写死了固定像素值阀值改用屏幕高度百分比第二次手势沿用上一次方向OnEndDrag忘记复位lockedDirection或startPos在OnEndDrag末尾重置状态标志和记录点4.2 调试方向判定的两个实用小工具排查方向问题最怕“凭感觉”。我推荐两个可视化的调试方式。第一个是在Scene视图里绘制从起点到当前点的矢量线颜色根据判断结果切换这样一眼就能看出起终点位置对不对、判定结果和视觉方向是否匹配。实现方式不复杂挂一个空物体在OnDrawGizmos里用Debug.DrawLine画线就可以。第二个技巧是把方向判定结果实时显示到屏幕上最简单的办法是使用OnGUI或者在UI上挂一个Text组件实时更新当前角度值。这个技巧在处理角度映射方案时特别有用你能看到实际的Atan2角度是多少然后再对照映射区间马上能找到是不是边界处理的问题。4.3 新旧输入系统切换的注意点如果项目启用了新的Input SystemInput.mousePosition会在编辑器里报警告或者直接失效。这时候要用Mouse.current.position.ReadValue()来读取鼠标位置。这个API返回值是Vector2用法和Input.mousePosition基本一致但要注意它是从屏幕左上角还是左下角开始计数的坐标这和当前设置里的坐标原点选项有关。UI事件接口PointerEventData对新旧输入系统都兼容这也是我推荐优先使用事件接口的原因之一。很多情况下同一个项目里新旧输入系统混搭所有代码都基于诸如动画片段带关键帧位移的系统不会因为输入方案切换就崩掉。顺带提一句如果一个项目同时勾选了新旧两套输入系统的支持偶尔会出现输入被重复处理的情况界面上的点击事件被吞掉这通常是工程配置层面的问题优先检查项目设置代码里不用绕。调试方向问题时多做真机验证也有很实际的意义。模拟器上不敢说所有的坐标系统都100%一致尤其在Android设备的刘海屏区域屏幕坐标和UI实际渲染位置会存在偏移用ScreenPointToLocalPointInRectangle能最大程度规避这类问题。我建议每个方向判定的组件在接版本前的自测清单里加上“真机手势方向”“真机点击不触发”“平板分屏改分辨率”这三项测完这三项上线前基本能放心。最后再分享一个小细节文本上面提到的OnEndDrag里方向锁的复位实际操作中还是容易漏掉。我后来养成了一个习惯所有手势类的组件在EndDrag和PointerDown里各打一个状态日志日志里输出当前锁定的方向和起点位置。这样一旦出现状态没复位的问题日志能直接定位到是哪个环节断了逻辑不用一行行排查。这个习惯帮助我在后来的多次项目迭代里省掉了不少排查时间。